@msb235/blits-rapier 0.4.0 → 0.6.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,44 @@ 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.6.0
8
+
9
+ ### Breaking
10
+
11
+ - `@msb235/blits` is a peer at `^1.0.0`, where it was `^0.20.0`. Built and tested against engine
12
+ 1.0.0; nothing here uses what that release changed.
13
+
14
+ ## 0.5.0
15
+
16
+ ### Breaking
17
+
18
+ - `@msb235/blits` is a peer at `^0.20.0`, where it was `^0.19.0`. Built and tested against engine
19
+ 0.20.0; a physics mix on a transport reads its `Transport.history`.
20
+
21
+ ### Added
22
+
23
+ - A physics mix plays on a shared `transport`: the host syncs the transport and calls the new
24
+ `sim.step()`, and the world seeks with the transport's other mixes. `physicsStore(store)` wraps
25
+ the transport's history store so the world's log of pushes pages through it. `sim.sync` on a
26
+ transport throws, as does `physics` on a transport whose store is not wrapped.
27
+ - The mix is named `'physics'` when `physics` is given no `name`, so the debug panel lists it.
28
+
29
+ ### Fixed
30
+
31
+ - A seek forward after playing on from a seek back replays the world with the pushes logged the
32
+ first time. Playing on dropped every frame logged ahead, so the world replayed that stretch with
33
+ one frame's push.
34
+ - An `impulse`, `drop` or `body` call made after a seek lets go of the calls logged ahead of it
35
+ before the seek. A host making its calls again on the way did each twice, and a call it did not
36
+ make again still happened.
37
+ - A body shows the time sought as soon as a seek lands, and `sim.world()` returns that world. It
38
+ showed the world from before the seek until the next frame, and on that frame too if no time
39
+ had passed.
40
+ - A mix given no `history` keeps the one a debugger's `watched().history` gives it. The world
41
+ logged only one frame of pushes, so the debugger's seek replayed every step with that frame's.
42
+ - Without a history `store`, the log of `impulse`, `drop` and `body` calls lets go of what history
43
+ can no longer reach. It grew for the session, holding every body's Rapier description.
44
+
7
45
  ## 0.4.0
8
46
 
9
47
  ### Breaking
package/README.md CHANGED
@@ -56,7 +56,10 @@ const { position, rotation } = sim.mix.probe(box);
56
56
  role adds on top of wherever physics has it.
57
57
  - **`body.impulse(v)`** applies an impulse once, on the step after the mix's current time.
58
58
  **`body.drop()`** takes the body out of the world then. Both are logged at that time, so a seek
59
- back before it undoes them and playing forward through it does them again.
59
+ back before it undoes them and playing forward through it does them again. One of these calls,
60
+ or `sim.body`, made after a seek lets go of the calls logged ahead of it before the seek, as a
61
+ call on the mix's tape starts a new future: a host that makes its calls again on the way does
62
+ each once.
60
63
  - **`sim.world()`** is the Rapier world as the last `sync` left it, for reading: raycasts, contact
61
64
  queries, debugging draws. A change made to it directly is lost at the next seek.
62
65
 
@@ -96,9 +99,15 @@ interpolate, the facing the short way round.
96
99
 
97
100
  ## The clock and seeking
98
101
 
99
- `physics` makes the mix, with `stepMs` 1000 / 60 unless you set it; the world takes one Rapier step
100
- per interval, so it runs at the same rate whatever the frame rate. Pass `history` and a `tape` to
101
- seek. The world lives in the mix as the state of a stateful patch, so blits' history keeps a copy
102
+ `physics` makes the mix, named `'physics'` unless you name it, with `stepMs` 1000 / 60 unless you
103
+ set it; the world takes one Rapier step per interval of mix time, so it runs at the same rate
104
+ whatever the frame rate or the mix's `rate`. Each frame reads every body's controls once, and the
105
+ steps that frame takes all push by what it read, so a force that changes faster than the frame
106
+ rate is sampled at it. A long frame, such as the first after a tab was hidden, catches up every
107
+ step it owes, unless the mix's `maxDt` caps how far a frame steps: two seconds take 120 steps
108
+ without it and 6 under `maxDt: 100`, the rest dropped rather than owed. Call `mix.rebase()` when a
109
+ tab comes back to leave the hidden time out instead. Pass `history` and a `tape` to
110
+ seek, or leave `history` out and a debugger's `watched().history` is kept as the mix's. The world lives in the mix as the state of a stateful patch, so blits' history keeps a copy
102
111
  every `every` ms and a seek replays from the nearest one. A copy is Rapier's snapshot, kept as
103
112
  bytes and rebuilt into a world only when a replay needs it, so history costs memory in proportion
104
113
  to the world's size and `ms / every`. Each frame's pushes are logged beside it for the replay, back
@@ -109,9 +118,35 @@ to the copy a replay at the edge of memory would start from.
109
118
  no `keyOf`. The log of each frame's pushes pages out with it, one `host` record a frame under the
110
119
  subject `'blits-rapier frames'`, and `prepare` loads it back for a seek past memory.
111
120
 
121
+ On a shared `transport`, the world plays on the transport's clock and history beside its other
122
+ mixes: sync the transport, then call `sim.step()` each frame, where a mix alone calls
123
+ `sim.sync(now)`. If the transport keeps a history `store`, make it with `physicsStore(store)`, so
124
+ the log of pushes pages through it; `physics` throws for a transport whose store is not wrapped.
125
+
126
+ ```ts
127
+ const t = transport({ history: { ms: 10_000, tape, store: physicsStore(store) } });
128
+ const sim = physics(kit<Pose3>(channels3), { rapier: RAPIER, transport: t, name: 'world' });
129
+ // each frame
130
+ t.sync(now);
131
+ sim.step();
132
+ ```
133
+
112
134
  Replay matches the first pass exactly on one machine. To match across machines, pass Rapier's
113
135
  deterministic build.
114
136
 
115
- ## Not yet
116
-
137
+ ## Not handled
138
+
139
+ - **Reads at another time.** `mix.project(t)` and `mix.trace` show a body where the world was at
140
+ the last `sync`, not at `t`, since the world is a voice on a subject of its own. A seek is
141
+ shown at once, before the next `sync`.
142
+ - **Picking another future.** The tape's `switchBranch` brings back the mix's own calls, not
143
+ `impulse`, `drop` or `body` calls a later one let go of. A call on the mix itself, such as a
144
+ `cue`, made after a seek does not let go of those calls logged ahead.
145
+ - **A seek forward past memory after playing on from a seek back**, with a `store`: the seek back
146
+ cuts the pushes paged after it from the store, so the world replays that stretch with the push
147
+ of the first frame it still holds. Within memory it replays exactly.
148
+ - **Growth with a store.** Each `impulse`, `drop` and `body` call is kept for the session, since
149
+ a seek past memory replays it and a body's description is a Rapier object, not data a store
150
+ takes. Without a store they go once history can no longer reach them. Every body made keeps its
151
+ voices in the mix for the session, dropped or not.
117
152
  - Soft bodies, cloth and ropes.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { channels2, channels3, type Kinematic2, type Kinematic3, kinematic2, kinematic3, type Pose2, type Pose3, } from './channels.js';
2
- export { type Body, type Physics, type PhysicsOptions, physics } from './physics.js';
2
+ export { type Body, type Physics, type PhysicsOptions, physics, physicsStore } from './physics.js';
3
3
  export type { RapierBody, RapierModule, RapierVector, RapierWorld } from './rapier.js';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export { channels2, channels3, kinematic2, kinematic3, } from './channels.js';
2
- export { physics } from './physics.js';
2
+ export { physics, physicsStore } from './physics.js';
package/dist/physics.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Handle, type Kit, type Mix, type MixOptions, type Signal, type VoiceSpec } from '@msb235/blits';
1
+ import { type Handle, type HistoryStore, 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
4
  /**
@@ -42,10 +42,21 @@ export interface Physics<O, H> {
42
42
  * the first step before the mix has synced.
43
43
  */
44
44
  body(desc: unknown, ...colliders: unknown[]): Body<O, H>;
45
- /** Syncs the mix to `now`, reads every body's controls, and steps the world. Call it in place of `mix.sync`. */
45
+ /** Syncs the mix to `now`, then calls `step`. Call it in place of `mix.sync`; on a transport, call `step`. */
46
46
  sync(now: number): void;
47
+ /**
48
+ * Reads every body's controls and steps the world to the mix's time. On a transport, call it each
49
+ * frame after the transport's `sync`.
50
+ */
51
+ step(): void;
47
52
  /** The world as it stands after the last `sync`, for reading. A change made to it is lost at the next seek. */
48
53
  world(): RapierWorld | undefined;
49
54
  }
55
+ /**
56
+ * Wraps a history store so the worlds of physics mixes on a transport page their log of each
57
+ * frame's pushes through it, and get it back on `prepare`. Make the transport with
58
+ * `history: { ..., store: physicsStore(store) }`; a mix made alone wraps its own store.
59
+ */
60
+ export declare function physicsStore(store: HistoryStore): HistoryStore;
50
61
  /** Makes a mix that holds a Rapier world, stepped on its `stepMs` clock and replayed by its history. */
51
62
  export declare function physics<O extends Pose2 | Pose3, H extends object = Record<string, never>>(kit: Kit<O>, opts: PhysicsOptions<H>): Physics<O, H>;
package/dist/physics.js CHANGED
@@ -1,19 +1,51 @@
1
- import { mix, patch, } from '@msb235/blits';
1
+ import { mix, patch, watched, } from '@msb235/blits';
2
2
  import { dimsOf } from './rapier.js';
3
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
6
  /** The subject key the world's frame records go under in a history store, on the `host` stream. */
7
7
  const FRAMES = 'blits-rapier frames';
8
+ const paging = new WeakMap();
9
+ /**
10
+ * Wraps a history store so the worlds of physics mixes on a transport page their log of each
11
+ * frame's pushes through it, and get it back on `prepare`. Make the transport with
12
+ * `history: { ..., store: physicsStore(store) }`; a mix made alone wraps its own store.
13
+ */
14
+ export function physicsStore(store) {
15
+ const worlds = new Set();
16
+ const wrapped = {
17
+ page: (out) => store.page(out),
18
+ cut: (seq) => store.cut(seq),
19
+ async load(t) {
20
+ const all = await store.load(t);
21
+ for (const w of worlds) {
22
+ // A replay to t starts from the world's latest copy at or before it, which needs the frames from there.
23
+ let from = t;
24
+ for (const r of all)
25
+ if (r.mix === w.name && r.stream === 'snap' && r.subject === 'world' && r.at < from)
26
+ from = r.at;
27
+ w.restore(from < t ? await store.load(from) : all, from);
28
+ }
29
+ return all.filter((r) => !(r.stream === 'host' && r.subject === FRAMES));
30
+ },
31
+ };
32
+ paging.set(wrapped, worlds);
33
+ return wrapped;
34
+ }
8
35
  /** Makes a mix that holds a Rapier world, stepped on its `stepMs` clock and replayed by its history. */
9
36
  export function physics(kit, opts) {
10
37
  const { rapier, gravity, weight, ...rest } = opts;
11
38
  const dims = dimsOf(rapier);
12
39
  const g = gravity ?? (dims.n === 2 ? [0, -9.81] : [0, -9.81, 0]);
13
40
  const stepMs = rest.stepMs ?? 1000 / 60;
14
- const name = rest.name ?? '';
15
- const history = rest.history;
16
- const store = history?.store;
41
+ const name = rest.name ?? 'physics';
42
+ const transport = rest.transport;
43
+ // A mix naming no history keeps the one a debugger gives mixes made from now on.
44
+ const history = transport === undefined ? (rest.history ?? watched().history) : transport.history;
45
+ const outer = history?.store;
46
+ if (outer !== undefined && transport !== undefined && !paging.has(outer))
47
+ throw new Error("blits-rapier: a physics mix on a transport with a history store needs the transport's store made with physicsStore(store), so the world's frames page with it");
48
+ const store = outer === undefined ? undefined : transport === undefined ? physicsStore(outer) : outer;
17
49
  // Each frame's pushes at its mix time. A seek replays steps on what the host reads now, so the
18
50
  // world reads the pushes of the frame that caused each step from here instead: the first frame
19
51
  // at or after the step's end, looked up half a step early since replayed times can round apart.
@@ -38,26 +70,17 @@ export function physics(kit, opts) {
38
70
  .map((r) => ({ ...r.data, at: r.at, seq: r.seq }));
39
71
  pushLog.unshift(...older);
40
72
  };
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
- };
73
+ if (store !== undefined)
74
+ paging.get(store)?.add({ ours, restore, name });
55
75
  // Every subject is the driver's own: the world, each body and each body's controls.
56
76
  const keys = new WeakMap();
57
77
  const m = mix(kit, {
58
78
  ...rest,
59
- ...(paging ? { history: { ...history, store: paging } } : {}),
79
+ ...(store !== undefined && transport === undefined
80
+ ? { history: { ...history, store } }
81
+ : {}),
60
82
  stepMs,
83
+ name,
61
84
  keyOf: (subject) => keys.get(subject),
62
85
  });
63
86
  const log = [];
@@ -138,7 +161,10 @@ export function physics(kit, opts) {
138
161
  const WORLD = {};
139
162
  keys.set(WORLD, 'world');
140
163
  let started = false;
141
- const simulated = patch(0, (_p, subject) => {
164
+ const simulated = patch(0, (_p, subject, setting) => {
165
+ // A read at another time sees the world as it stands; see the README.
166
+ if (setting.timestamp === m.now)
167
+ fresh();
142
168
  const handle = live?.handles.get(subject.key);
143
169
  if (live === undefined || handle === undefined)
144
170
  return {};
@@ -182,12 +208,116 @@ export function physics(kit, opts) {
182
208
  return holds;
183
209
  };
184
210
  let next = 0;
211
+ // The frame, mix time and host timestamp of the last `step`, the first frame after a seek it
212
+ // noticed, and the frame `live` was read in. A seek comes and goes without the driver hearing.
213
+ let lastSeq;
214
+ let lastAt = Number.NEGATIVE_INFINITY;
215
+ let lastTs = Number.NaN;
216
+ let soughtSeq = Number.NEGATIVE_INFINITY;
217
+ let liveSeq = Number.NaN;
218
+ let startAt = Number.NaN;
219
+ const sought = () => lastSeq !== undefined && history?.tape !== undefined && (m.seq > lastSeq + 1 || m.now < lastAt);
220
+ /** Reads the world at the time sought after seeks since the last step, with no sync since. */
221
+ const fresh = () => {
222
+ if (lastSeq === undefined ||
223
+ history?.tape === undefined ||
224
+ liveSeq === m.seq ||
225
+ m.timestamp !== lastTs)
226
+ return;
227
+ m.probe(WORLD);
228
+ live = seen;
229
+ liveSeq = m.seq;
230
+ };
231
+ /** Logs a call, letting go of the calls logged ahead of it before a seek, as the mix's tape does. */
232
+ const call = (entry) => {
233
+ if (sought())
234
+ soughtSeq = lastSeq + 1;
235
+ const from = entry.at - stepMs / 2;
236
+ for (let i = log.length - 1; i >= 0 && log[i].at >= from; i--)
237
+ if (log[i].seq < soughtSeq)
238
+ log.splice(i, 1);
239
+ insert(log, entry);
240
+ };
241
+ const step = () => {
242
+ // Cued before the mix has a time, the world would catch up from mix time 0 on its first step.
243
+ if (!started) {
244
+ started = true;
245
+ m.cue({ patch: world, subjects: [WORLD] });
246
+ }
247
+ const pushes = {};
248
+ const goals = {};
249
+ for (const body of driven) {
250
+ const p = m.probe(body.controls, out);
251
+ const torque = typeof p.torque === 'number' ? [p.torque] : p.torque;
252
+ if (p.force.some((x) => x !== 0) || torque.some((x) => x !== 0))
253
+ pushes[body.key] = [...p.force, ...torque];
254
+ if (p.goal !== undefined || p.facing !== undefined) {
255
+ const g = {};
256
+ if (p.goal !== undefined)
257
+ g.position = [...p.goal];
258
+ if (p.facing !== undefined)
259
+ g.rotation = copyOf(p.facing);
260
+ goals[body.key] = g;
261
+ }
262
+ }
263
+ const at = m.now;
264
+ const seq = m.seq;
265
+ if (Number.isNaN(startAt))
266
+ startAt = at;
267
+ // This frame stands for the frames logged after where the mix stood before it, up to its own
268
+ // time. Those ahead stay for a seek forward, which replays them as the mix's tape does its calls.
269
+ let before = lastAt;
270
+ if (sought()) {
271
+ before = m.project({ frame: seq - 1 }).timestamp;
272
+ soughtSeq = lastSeq + 1;
273
+ holding = {};
274
+ held.clear();
275
+ }
276
+ let end = pushLog.length;
277
+ while (end > 0 && pushLog[end - 1].at > at)
278
+ end--;
279
+ let start = end;
280
+ while (start > 0 &&
281
+ (pushLog[start - 1].at > before || pushLog[start - 1].at === at))
282
+ start--;
283
+ pushLog.splice(start, end - start, { at, seq, pushes, holds: holding, goals });
284
+ lastSeq = seq;
285
+ lastAt = at;
286
+ lastTs = m.timestamp;
287
+ let old = 0;
288
+ while (old < pushLog.length - 1 && pushLog[old + 1].at < at - keep)
289
+ old++;
290
+ const gone = old > 0 ? pushLog.splice(0, old) : undefined;
291
+ if (gone !== undefined && store !== undefined)
292
+ store.page(gone.map(({ at, seq, pushes, holds, goals }) => ({
293
+ mix: name,
294
+ stream: 'host',
295
+ subject: FRAMES,
296
+ at,
297
+ seq,
298
+ data: { pushes, holds, goals },
299
+ })));
300
+ m.probe(WORLD);
301
+ live = seen;
302
+ liveSeq = seq;
303
+ // Without a store, calls older than any copy a replay starts from are in every copy already,
304
+ // once history no longer reaches the world's first step, which a fresh world replays from.
305
+ const reach = Math.min(at, live?.t ?? at) - keep - stepMs;
306
+ if (store === undefined && reach > startAt) {
307
+ let n = 0;
308
+ while (n < log.length && log[n].at < reach)
309
+ n++;
310
+ if (n > 0)
311
+ log.splice(0, n);
312
+ }
313
+ holding = holdsAt(at);
314
+ };
185
315
  return {
186
316
  mix: m,
187
317
  role: ROLE,
188
318
  body(desc, ...colliders) {
189
319
  const key = next++;
190
- insert(log, { at: now(), key, kind: 'add', desc, colliders });
320
+ call({ at: now(), seq: m.seq, key, kind: 'add', desc, colliders });
191
321
  const controls = {};
192
322
  keys.set(controls, `controls ${key}`);
193
323
  const own = (spec, subject) => {
@@ -209,10 +339,8 @@ export function physics(kit, opts) {
209
339
  driven.add(body);
210
340
  return own(spec, controls);
211
341
  },
212
- impulse: (v) => insert(log, { at: now(), key, kind: 'impulse', v: [...v] }),
213
- drop: () => {
214
- insert(log, { at: now(), key, kind: 'drop' });
215
- },
342
+ impulse: (v) => call({ at: now(), seq: m.seq, key, kind: 'impulse', v: [...v] }),
343
+ drop: () => call({ at: now(), seq: m.seq, key, kind: 'drop' }),
216
344
  };
217
345
  keys.set(body, `body ${key}`);
218
346
  body.simulated = m.cue({
@@ -224,57 +352,15 @@ export function physics(kit, opts) {
224
352
  return body;
225
353
  },
226
354
  sync(t) {
355
+ if (transport !== undefined)
356
+ throw new Error('blits-rapier: this physics plays on a transport: sync the transport, then call step()');
227
357
  m.sync(t);
228
- // Cued before the mix has a time, the world would catch up from mix time 0 on its first step.
229
- if (!started) {
230
- started = true;
231
- m.cue({ patch: world, subjects: [WORLD] });
232
- }
233
- const pushes = {};
234
- const goals = {};
235
- for (const body of driven) {
236
- const p = m.probe(body.controls, out);
237
- const torque = typeof p.torque === 'number' ? [p.torque] : p.torque;
238
- if (p.force.some((x) => x !== 0) || torque.some((x) => x !== 0))
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
- }
248
- }
249
- const at = m.now;
250
- let back = false;
251
- while (pushLog.length > 0 && pushLog[pushLog.length - 1].at >= at) {
252
- pushLog.pop();
253
- back = true;
254
- }
255
- // A seek back leaves the holds read before it, and their speeds, behind.
256
- if (back) {
257
- holding = {};
258
- held.clear();
259
- }
260
- pushLog.push({ at, seq: m.seq, pushes, holds: holding, goals });
261
- let old = 0;
262
- while (old < pushLog.length - 1 && pushLog[old + 1].at < at - keep)
263
- 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
- })));
274
- m.probe(WORLD);
275
- live = seen;
276
- holding = holdsAt(at);
358
+ step();
359
+ },
360
+ step,
361
+ world: () => {
362
+ fresh();
363
+ return live === undefined ? undefined : worldOf(live, rapier);
277
364
  },
278
- world: () => (live === undefined ? undefined : worldOf(live, rapier)),
279
365
  };
280
366
  }
package/dist/world.d.ts CHANGED
@@ -11,23 +11,24 @@ export interface State {
11
11
  /** The mix time of the last step, NaN before the first. */
12
12
  t: number;
13
13
  }
14
- /** Something done to the world at a mix time, applied by whichever step reaches it, on every replay. */
14
+ /**
15
+ * Something done to the world at a mix time, applied by whichever step reaches it, on every
16
+ * replay. `seq` is the frame the host made the call in.
17
+ */
15
18
  export type Entry = {
16
19
  at: number;
20
+ seq: number;
17
21
  key: number;
22
+ } & ({
18
23
  kind: 'add';
19
24
  desc: unknown;
20
25
  colliders: readonly unknown[];
21
26
  } | {
22
- at: number;
23
- key: number;
24
27
  kind: 'drop';
25
28
  } | {
26
- at: number;
27
- key: number;
28
29
  kind: 'impulse';
29
30
  v: readonly number[];
30
- };
31
+ });
31
32
  /** The state's world, rebuilt from its snapshot where it has none yet. */
32
33
  export declare function worldOf(state: State, rapier: RapierModule): RapierWorld;
33
34
  /** A copy sharing nothing with `state`, holding the world as a snapshot. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@msb235/blits-rapier",
3
- "version": "0.4.0",
3
+ "version": "0.6.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.19.0",
41
+ "@msb235/blits": "^1.0.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.19.0"
48
+ "@msb235/blits": "1.0.0"
49
49
  },
50
50
  "peerDependenciesMeta": {
51
51
  "@dimforge/rapier2d-compat": {