@msb235/blits-rapier 0.4.0 → 0.5.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 +31 -0
- package/README.md +41 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/physics.d.ts +13 -2
- package/dist/physics.js +161 -75
- package/dist/world.d.ts +7 -6
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@ 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.5.0
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
|
|
11
|
+
- `@msb235/blits` is a peer at `^0.20.0`, where it was `^0.19.0`. Built and tested against engine
|
|
12
|
+
0.20.0; a physics mix on a transport reads its `Transport.history`.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- A physics mix plays on a shared `transport`: the host syncs the transport and calls the new
|
|
17
|
+
`sim.step()`, and the world seeks with the transport's other mixes. `physicsStore(store)` wraps
|
|
18
|
+
the transport's history store so the world's log of pushes pages through it. `sim.sync` on a
|
|
19
|
+
transport throws, as does `physics` on a transport whose store is not wrapped.
|
|
20
|
+
- The mix is named `'physics'` when `physics` is given no `name`, so the debug panel lists it.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- A seek forward after playing on from a seek back replays the world with the pushes logged the
|
|
25
|
+
first time. Playing on dropped every frame logged ahead, so the world replayed that stretch with
|
|
26
|
+
one frame's push.
|
|
27
|
+
- An `impulse`, `drop` or `body` call made after a seek lets go of the calls logged ahead of it
|
|
28
|
+
before the seek. A host making its calls again on the way did each twice, and a call it did not
|
|
29
|
+
make again still happened.
|
|
30
|
+
- A body shows the time sought as soon as a seek lands, and `sim.world()` returns that world. It
|
|
31
|
+
showed the world from before the seek until the next frame, and on that frame too if no time
|
|
32
|
+
had passed.
|
|
33
|
+
- A mix given no `history` keeps the one a debugger's `watched().history` gives it. The world
|
|
34
|
+
logged only one frame of pushes, so the debugger's seek replayed every step with that frame's.
|
|
35
|
+
- Without a history `store`, the log of `impulse`, `drop` and `body` calls lets go of what history
|
|
36
|
+
can no longer reach. It grew for the session, holding every body's Rapier description.
|
|
37
|
+
|
|
7
38
|
## 0.4.0
|
|
8
39
|
|
|
9
40
|
### 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,
|
|
100
|
-
per interval, so it runs at the same rate
|
|
101
|
-
|
|
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
|
|
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`,
|
|
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
|
|
16
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
/**
|
|
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.
|
|
3
|
+
"version": "0.5.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.
|
|
41
|
+
"@msb235/blits": "^0.20.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.
|
|
48
|
+
"@msb235/blits": "0.20.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependenciesMeta": {
|
|
51
51
|
"@dimforge/rapier2d-compat": {
|