@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 +33 -0
- package/README.md +30 -7
- package/dist/channels.d.ts +22 -0
- package/dist/channels.js +13 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/physics.d.ts +5 -2
- package/dist/physics.js +84 -9
- package/dist/rapier.d.ts +9 -0
- package/dist/rapier.js +16 -0
- package/dist/world.d.ts +4 -0
- package/dist/world.js +22 -0
- package/package.json +3 -3
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
|
|
83
|
-
|
|
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.
|
package/dist/channels.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
5
|
-
|
|
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
|
|
12
|
-
const
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
-
|
|
198
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
48
|
+
"@msb235/blits": "0.19.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependenciesMeta": {
|
|
51
51
|
"@dimforge/rapier2d-compat": {
|