@msb235/blits-rapier 0.1.0 → 0.2.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 +19 -0
- package/README.md +8 -5
- package/dist/physics.d.ts +7 -2
- package/dist/physics.js +64 -7
- package/dist/rapier.d.ts +11 -0
- package/dist/rapier.js +36 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ 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.2.0
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
|
|
11
|
+
- `@msb235/blits` is a peer at `^0.17.0`, where it was `^0.16.1`. Built and tested against engine
|
|
12
|
+
0.17.0; nothing here uses what that release changed.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `physics` takes `weight`, a number or a signal, for every body's simulated voice. A signal hands
|
|
17
|
+
bodies between physics and authored poses with nothing written from the frame loop. Fading
|
|
18
|
+
`simulated` out ended it for good, so a body could not be handed back.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- A body handed back to physics starts where it was shown, at the speed it was shown moving. The
|
|
23
|
+
world went on simulating it while an authored voice held it, so it jumped back to wherever the
|
|
24
|
+
simulation had taken it, such as against a wall that a push had sent it to.
|
|
25
|
+
|
|
7
26
|
## 0.1.0
|
|
8
27
|
|
|
9
28
|
### Added
|
package/README.md
CHANGED
|
@@ -46,11 +46,14 @@ const { position, rotation } = sim.mix.probe(box);
|
|
|
46
46
|
are read before the world steps and the pose after, which is why they are two subjects.
|
|
47
47
|
- **`body.drive(spec)`** cues a voice on the controls; its weight scales the push.
|
|
48
48
|
**`body.cue(spec)`** cues one on the pose. Neither spec names `subjects`.
|
|
49
|
-
- **`body.simulated`** is the `Handle` of the voice that shows the body where the world puts it,
|
|
50
|
-
|
|
51
|
-
and the two crossfade by weight
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
- **`body.simulated`** is the `Handle` of the voice that shows the body where the world puts it, in
|
|
50
|
+
the role `sim.role`, at the weight `physics` was given (default 1). A voice cued in that role is
|
|
51
|
+
an alternative to the simulation, and the two crossfade by weight. To hand bodies to authored
|
|
52
|
+
poses and back, give `physics` a `weight` signal and the authored voices its complement, such as
|
|
53
|
+
`share(k, 0, 2)` and `share(k, 1, 2)` of one `level`. While a body's simulated voice is at weight 0
|
|
54
|
+
and a voice in the role holds it, the world puts the body where it is shown, moving as fast as
|
|
55
|
+
the shown pose moves, so handing it back starts the simulation from there. A voice outside the
|
|
56
|
+
role adds on top of wherever physics has it.
|
|
54
57
|
- **`body.impulse(v)`** applies an impulse once, on the step after the mix's current time.
|
|
55
58
|
**`body.drop()`** takes the body out of the world then. Both are logged at that time, so a seek
|
|
56
59
|
back before it undoes them and playing forward through it does them again.
|
package/dist/physics.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Handle, type Kit, type Mix, type MixOptions, type VoiceSpec } from '@msb235/blits';
|
|
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
4
|
/** What `physics` takes: a mix's options, the Rapier module, and the world's gravity. */
|
|
@@ -7,13 +7,18 @@ export interface PhysicsOptions<H> extends MixOptions<H> {
|
|
|
7
7
|
rapier: RapierModule;
|
|
8
8
|
/** Default: `[0, -9.81]` or `[0, -9.81, 0]`. */
|
|
9
9
|
gravity?: readonly number[];
|
|
10
|
+
/**
|
|
11
|
+
* The weight of every body's simulated voice, a signal reading each body. A signal hands a body
|
|
12
|
+
* between physics and authored motion with no weight written from the frame loop. Default 1.
|
|
13
|
+
*/
|
|
14
|
+
weight?: number | Signal<object, H>;
|
|
10
15
|
}
|
|
11
16
|
/** A rigid body: the mix's subject for its pose, with its controls beside it. */
|
|
12
17
|
export interface Body<O, H> {
|
|
13
18
|
readonly key: number;
|
|
14
19
|
/** The subject whose `force` and `torque` push the body, read before the world steps. */
|
|
15
20
|
readonly controls: object;
|
|
16
|
-
/** The voice that shows the body where the world puts it, in `Physics.role
|
|
21
|
+
/** The voice that shows the body where the world puts it, in `Physics.role`, at `PhysicsOptions.weight`. */
|
|
17
22
|
readonly simulated: Handle<object>;
|
|
18
23
|
/** Cues a voice on the body's pose. In `Physics.role` it is an alternative to the simulation; outside it, it adds on top. */
|
|
19
24
|
cue(spec: Omit<VoiceSpec<object, O, H>, 'subjects'>): Handle<object>;
|
package/dist/physics.js
CHANGED
|
@@ -2,9 +2,10 @@ import { mix, patch, } from '@msb235/blits';
|
|
|
2
2
|
import { dimsOf } from './rapier.js';
|
|
3
3
|
import { applyLog, copy, insert, worldOf } from './world.js';
|
|
4
4
|
const ROLE = 'physics';
|
|
5
|
+
const copyOf = (r) => typeof r === 'number' ? r : [...r];
|
|
5
6
|
/** Makes a mix that holds a Rapier world, stepped on its `stepMs` clock and replayed by its history. */
|
|
6
7
|
export function physics(kit, opts) {
|
|
7
|
-
const { rapier, gravity, ...rest } = opts;
|
|
8
|
+
const { rapier, gravity, weight, ...rest } = opts;
|
|
8
9
|
const dims = dimsOf(rapier);
|
|
9
10
|
const g = gravity ?? (dims.n === 2 ? [0, -9.81] : [0, -9.81, 0]);
|
|
10
11
|
const m = mix(kit, { ...rest, stepMs: rest.stepMs ?? 1000 / 60 });
|
|
@@ -14,7 +15,7 @@ export function physics(kit, opts) {
|
|
|
14
15
|
// at or after the step's end, looked up half a step early since replayed times can round apart.
|
|
15
16
|
const pushLog = [];
|
|
16
17
|
const keep = rest.history?.ms ?? 0;
|
|
17
|
-
const
|
|
18
|
+
const frameAt = (t) => {
|
|
18
19
|
let lo = 0;
|
|
19
20
|
let hi = pushLog.length;
|
|
20
21
|
while (lo < hi) {
|
|
@@ -24,7 +25,7 @@ export function physics(kit, opts) {
|
|
|
24
25
|
else
|
|
25
26
|
hi = mid;
|
|
26
27
|
}
|
|
27
|
-
return
|
|
28
|
+
return pushLog[lo] ?? pushLog[pushLog.length - 1];
|
|
28
29
|
};
|
|
29
30
|
let seen;
|
|
30
31
|
let live;
|
|
@@ -49,7 +50,15 @@ export function physics(kit, opts) {
|
|
|
49
50
|
const w = worldOf(s, rapier);
|
|
50
51
|
w.timestep = dt / 1000;
|
|
51
52
|
applyLog(s, w, log, setting.timestamp, dt / 2, dims);
|
|
52
|
-
const
|
|
53
|
+
const frame = frameAt(setting.timestamp - dt / 2);
|
|
54
|
+
const pushes = frame?.pushes ?? {};
|
|
55
|
+
const holds = frame?.holds ?? {};
|
|
56
|
+
for (const k in holds) {
|
|
57
|
+
const handle = s.handles.get(+k);
|
|
58
|
+
const h = holds[k];
|
|
59
|
+
if (handle !== undefined && h !== undefined)
|
|
60
|
+
dims.place(w.getRigidBody(handle), h.now, h.before, h.dt);
|
|
61
|
+
}
|
|
53
62
|
const pushed = [];
|
|
54
63
|
for (const k in pushes) {
|
|
55
64
|
const handle = s.handles.get(+k);
|
|
@@ -82,7 +91,40 @@ export function physics(kit, opts) {
|
|
|
82
91
|
}, { writes: ['position', 'rotation'] });
|
|
83
92
|
const now = () => (Number.isNaN(m.timestamp) ? Number.NEGATIVE_INFINITY : m.timestamp);
|
|
84
93
|
const driven = new Set();
|
|
94
|
+
// Each body's voices in the simulation's role besides its own, and its pose the last frame they held it.
|
|
95
|
+
const authored = new Map();
|
|
96
|
+
const held = new Map();
|
|
85
97
|
const out = m.pose();
|
|
98
|
+
const shown = m.pose();
|
|
99
|
+
// Read at the end of each frame, for the next frame's steps to apply.
|
|
100
|
+
let holding = {};
|
|
101
|
+
const holdsAt = (at) => {
|
|
102
|
+
const holds = {};
|
|
103
|
+
for (const [body, voices] of authored) {
|
|
104
|
+
const playing = voices.filter((v) => v.state !== 'done');
|
|
105
|
+
if (playing.length === 0) {
|
|
106
|
+
authored.delete(body);
|
|
107
|
+
held.delete(body);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (playing.length < voices.length)
|
|
111
|
+
authored.set(body, playing);
|
|
112
|
+
const p = m.probe(body, shown);
|
|
113
|
+
// Shown with none of the simulation, the body is put where it is shown, so the simulation
|
|
114
|
+
// takes it back from there.
|
|
115
|
+
if (!live?.handles.has(body.key) ||
|
|
116
|
+
body.simulated.weightOf(body) > 0 ||
|
|
117
|
+
!playing.some((v) => v.weightOf(body) > 0)) {
|
|
118
|
+
held.delete(body);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const now = { position: [...p.position], rotation: copyOf(p.rotation) };
|
|
122
|
+
const last = held.get(body);
|
|
123
|
+
holds[body.key] = { now, before: last?.pose, dt: last === undefined ? 0 : at - last.at };
|
|
124
|
+
held.set(body, { pose: now, at });
|
|
125
|
+
}
|
|
126
|
+
return holds;
|
|
127
|
+
};
|
|
86
128
|
let next = 0;
|
|
87
129
|
return {
|
|
88
130
|
mix: m,
|
|
@@ -100,7 +142,12 @@ export function physics(kit, opts) {
|
|
|
100
142
|
key,
|
|
101
143
|
controls,
|
|
102
144
|
simulated: undefined,
|
|
103
|
-
cue: (spec) =>
|
|
145
|
+
cue: (spec) => {
|
|
146
|
+
const h = own(spec, body);
|
|
147
|
+
if (spec.role === ROLE)
|
|
148
|
+
authored.set(body, [...(authored.get(body) ?? []), h]);
|
|
149
|
+
return h;
|
|
150
|
+
},
|
|
104
151
|
drive: (spec) => {
|
|
105
152
|
driven.add(body);
|
|
106
153
|
return own(spec, controls);
|
|
@@ -114,6 +161,7 @@ export function physics(kit, opts) {
|
|
|
114
161
|
patch: simulated,
|
|
115
162
|
subjects: [body],
|
|
116
163
|
role: ROLE,
|
|
164
|
+
...(weight === undefined ? {} : { weight }),
|
|
117
165
|
});
|
|
118
166
|
return body;
|
|
119
167
|
},
|
|
@@ -132,9 +180,17 @@ export function physics(kit, opts) {
|
|
|
132
180
|
pushes[body.key] = [...p.force, ...torque];
|
|
133
181
|
}
|
|
134
182
|
const at = m.timestamp;
|
|
135
|
-
|
|
183
|
+
let back = false;
|
|
184
|
+
while (pushLog.length > 0 && pushLog[pushLog.length - 1].at >= at) {
|
|
136
185
|
pushLog.pop();
|
|
137
|
-
|
|
186
|
+
back = true;
|
|
187
|
+
}
|
|
188
|
+
// A seek back leaves the holds read before it, and their speeds, behind.
|
|
189
|
+
if (back) {
|
|
190
|
+
holding = {};
|
|
191
|
+
held.clear();
|
|
192
|
+
}
|
|
193
|
+
pushLog.push({ at, pushes, holds: holding });
|
|
138
194
|
let old = 0;
|
|
139
195
|
while (old < pushLog.length - 1 && pushLog[old + 1].at < at - keep)
|
|
140
196
|
old++;
|
|
@@ -142,6 +198,7 @@ export function physics(kit, opts) {
|
|
|
142
198
|
pushLog.splice(0, old);
|
|
143
199
|
m.probe(WORLD);
|
|
144
200
|
live = seen;
|
|
201
|
+
holding = holdsAt(at);
|
|
145
202
|
},
|
|
146
203
|
world: () => (live === undefined ? undefined : worldOf(live, rapier)),
|
|
147
204
|
};
|
package/dist/rapier.d.ts
CHANGED
|
@@ -19,6 +19,10 @@ export interface RapierBody {
|
|
|
19
19
|
applyImpulse(impulse: RapierVector, wake: boolean): void;
|
|
20
20
|
resetForces(wake: boolean): void;
|
|
21
21
|
resetTorques(wake: boolean): void;
|
|
22
|
+
setTranslation(v: RapierVector, wake: boolean): void;
|
|
23
|
+
setRotation(r: never, wake: boolean): void;
|
|
24
|
+
setLinvel(v: RapierVector, wake: boolean): void;
|
|
25
|
+
setAngvel(v: never, wake: boolean): void;
|
|
22
26
|
}
|
|
23
27
|
/** What blits-rapier uses of a Rapier world, in either build. */
|
|
24
28
|
export interface RapierWorld {
|
|
@@ -50,6 +54,13 @@ export interface Dims {
|
|
|
50
54
|
rotation(body: RapierBody): number | number[];
|
|
51
55
|
/** Adds `torque`, a number in 2D and three numbers in 3D. */
|
|
52
56
|
torque(body: RapierBody, torque: number | readonly number[]): void;
|
|
57
|
+
/** Puts the body at a pose, moving at the speed it took to get there from `before` over `dt` ms. */
|
|
58
|
+
place(body: RapierBody, now: Placed, before: Placed | undefined, dt: number): void;
|
|
59
|
+
}
|
|
60
|
+
/** A body's position and rotation as the channels hold them. */
|
|
61
|
+
export interface Placed {
|
|
62
|
+
position: readonly number[];
|
|
63
|
+
rotation: number | readonly number[];
|
|
53
64
|
}
|
|
54
65
|
/** The 3D build where the module has `Vector3`, else the 2D. */
|
|
55
66
|
export declare function dimsOf(rapier: RapierModule): Dims;
|
package/dist/rapier.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const speed = (now, before, dt) => now.map((x, i) => (before === undefined || dt <= 0 ? 0 : (x - (before[i] ?? x)) / (dt / 1000)));
|
|
1
2
|
const two = {
|
|
2
3
|
n: 2,
|
|
3
4
|
vector: (v) => ({ x: v[0] ?? 0, y: v[1] ?? 0 }),
|
|
@@ -7,6 +8,15 @@ const two = {
|
|
|
7
8
|
},
|
|
8
9
|
rotation: (b) => b.rotation(),
|
|
9
10
|
torque: (b, t) => b.addTorque((typeof t === 'number' ? t : (t[0] ?? 0)), true),
|
|
11
|
+
place: (b, now, before, dt) => {
|
|
12
|
+
const r = now.rotation;
|
|
13
|
+
b.setTranslation(two.vector(now.position), true);
|
|
14
|
+
b.setRotation(r, true);
|
|
15
|
+
b.setLinvel(two.vector(speed(now.position, before?.position, dt)), true);
|
|
16
|
+
let turn = before === undefined ? 0 : r - before.rotation;
|
|
17
|
+
turn -= 2 * Math.PI * Math.round(turn / (2 * Math.PI));
|
|
18
|
+
b.setAngvel((dt > 0 ? turn / (dt / 1000) : 0), true);
|
|
19
|
+
},
|
|
10
20
|
};
|
|
11
21
|
const three = {
|
|
12
22
|
n: 3,
|
|
@@ -20,7 +30,33 @@ const three = {
|
|
|
20
30
|
return [q.x, q.y, q.z, q.w];
|
|
21
31
|
},
|
|
22
32
|
torque: (b, t) => b.addTorque(three.vector(typeof t === 'number' ? [0, 0, t] : t), true),
|
|
33
|
+
place: (b, now, before, dt) => {
|
|
34
|
+
const [x, y, z, w] = now.rotation;
|
|
35
|
+
b.setTranslation(three.vector(now.position), true);
|
|
36
|
+
b.setRotation({ x, y, z, w }, true);
|
|
37
|
+
b.setLinvel(three.vector(speed(now.position, before?.position, dt)), true);
|
|
38
|
+
b.setAngvel(three.vector(spin(now.rotation, before?.rotation, dt)), true);
|
|
39
|
+
},
|
|
23
40
|
};
|
|
41
|
+
/** The angular velocity turning `before` into `now` over `dt` ms, both `[x, y, z, w]`. */
|
|
42
|
+
function spin(now, before, dt) {
|
|
43
|
+
if (before === undefined || dt <= 0)
|
|
44
|
+
return [0, 0, 0];
|
|
45
|
+
const [ax, ay, az, aw] = now;
|
|
46
|
+
const [bx, by, bz, bw] = before;
|
|
47
|
+
// now · conj(before), taken the short way round.
|
|
48
|
+
let x = bw * ax - bx * aw - by * az + bz * ay;
|
|
49
|
+
let y = bw * ay - by * aw - bz * ax + bx * az;
|
|
50
|
+
let z = bw * az - bz * aw - bx * ay + by * ax;
|
|
51
|
+
let w = bw * aw + bx * ax + by * ay + bz * az;
|
|
52
|
+
if (w < 0)
|
|
53
|
+
[x, y, z, w] = [-x, -y, -z, -w];
|
|
54
|
+
const s = Math.hypot(x, y, z);
|
|
55
|
+
if (s < 1e-12)
|
|
56
|
+
return [0, 0, 0];
|
|
57
|
+
const k = (2 * Math.atan2(s, w)) / s / (dt / 1000);
|
|
58
|
+
return [x * k, y * k, z * k];
|
|
59
|
+
}
|
|
24
60
|
/** The 3D build where the module has `Vector3`, else the 2D. */
|
|
25
61
|
export function dimsOf(rapier) {
|
|
26
62
|
return 'Vector3' in rapier ? three : two;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@msb235/blits-rapier",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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.17.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.17.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependenciesMeta": {
|
|
51
51
|
"@dimforge/rapier2d-compat": {
|