@irtio/client 0.5.2 → 0.7.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/dist/{physics-H2VDQLAU.js → chunk-DABQDR3S.js} +417 -269
- package/dist/{chunk-7UTJ7RSF.js → chunk-XWVXZRBS.js} +73 -8
- package/dist/index.d.ts +1496 -179
- package/dist/index.js +1417 -47
- package/dist/physics-RT5T36P5.js +219 -0
- package/dist/physics2d-HOFMWPZV.js +255 -0
- package/package.json +15 -6
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Predictor
|
|
3
|
+
} from "./chunk-DABQDR3S.js";
|
|
4
|
+
import "./chunk-XWVXZRBS.js";
|
|
5
|
+
|
|
6
|
+
// src/physics.ts
|
|
7
|
+
function readPose(body, into) {
|
|
8
|
+
const t = body.translation();
|
|
9
|
+
const r = body.rotation();
|
|
10
|
+
const v = body.linvel();
|
|
11
|
+
const w = body.angvel();
|
|
12
|
+
into.t.x = t.x;
|
|
13
|
+
into.t.y = t.y;
|
|
14
|
+
into.t.z = t.z;
|
|
15
|
+
into.r.x = r.x;
|
|
16
|
+
into.r.y = r.y;
|
|
17
|
+
into.r.z = r.z;
|
|
18
|
+
into.r.w = r.w;
|
|
19
|
+
into.v.x = v.x;
|
|
20
|
+
into.v.y = v.y;
|
|
21
|
+
into.v.z = v.z;
|
|
22
|
+
into.w.x = w.x;
|
|
23
|
+
into.w.y = w.y;
|
|
24
|
+
into.w.z = w.z;
|
|
25
|
+
}
|
|
26
|
+
var engine;
|
|
27
|
+
var loading;
|
|
28
|
+
async function loadEngine() {
|
|
29
|
+
if (engine) return engine;
|
|
30
|
+
loading ??= (async () => {
|
|
31
|
+
const mod = await import("@dimforge/rapier3d-compat");
|
|
32
|
+
const ns = mod.default ?? mod;
|
|
33
|
+
await ns.init();
|
|
34
|
+
engine = ns;
|
|
35
|
+
return ns;
|
|
36
|
+
})();
|
|
37
|
+
return loading;
|
|
38
|
+
}
|
|
39
|
+
function planarLockWarning(spec) {
|
|
40
|
+
const boxed = (spec.colliders ?? []).some(
|
|
41
|
+
(c) => c.shape.type === CUBOID_SHAPE || c.shape.type === ROUND_CUBOID_SHAPE
|
|
42
|
+
);
|
|
43
|
+
if (!boxed) return void 0;
|
|
44
|
+
const b = spec.body;
|
|
45
|
+
const t = [b.translationsEnabledX, b.translationsEnabledY, b.translationsEnabledZ];
|
|
46
|
+
const r = [b.rotationsEnabledX, b.rotationsEnabledY, b.rotationsEnabledZ];
|
|
47
|
+
const axes = ["x", "y", "z"];
|
|
48
|
+
for (let i = 0; i < 3; i++) {
|
|
49
|
+
if (t[i]) continue;
|
|
50
|
+
if (r[(i + 1) % 3] || r[(i + 2) % 3]) continue;
|
|
51
|
+
const free = axes[i] === "z" ? "enabledRotations(true, false, true)" : "one of the other two rotations";
|
|
52
|
+
return `locks translation on ${axes[i]} and both rotations across that plane on a box-shaped body. That removes friction entirely: the box slides at a constant speed until it hits something. Free one out-of-plane rotation \u2014 in the xy plane that is ${free}, and it costs nothing, because geometry symmetric about the plane generates no torque about it.`;
|
|
53
|
+
}
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
var CUBOID_SHAPE = 1;
|
|
57
|
+
var ROUND_CUBOID_SHAPE = 12;
|
|
58
|
+
var RapierAdapter = class {
|
|
59
|
+
constructor(options) {
|
|
60
|
+
this.options = options;
|
|
61
|
+
}
|
|
62
|
+
options;
|
|
63
|
+
engineName = "@dimforge/rapier3d-compat";
|
|
64
|
+
optionName = "physics";
|
|
65
|
+
rapier;
|
|
66
|
+
world;
|
|
67
|
+
get timestep() {
|
|
68
|
+
return this.options.timestep;
|
|
69
|
+
}
|
|
70
|
+
async start(timestepSeconds) {
|
|
71
|
+
const rapier = await loadEngine();
|
|
72
|
+
this.rapier = rapier;
|
|
73
|
+
const world = new rapier.World({ ...this.options.gravity });
|
|
74
|
+
world.timestep = timestepSeconds;
|
|
75
|
+
if (this.options.setup) this.options.setup(world, rapier);
|
|
76
|
+
this.world = world;
|
|
77
|
+
}
|
|
78
|
+
free() {
|
|
79
|
+
this.world?.free();
|
|
80
|
+
this.world = void 0;
|
|
81
|
+
}
|
|
82
|
+
hasFactory(collection) {
|
|
83
|
+
return this.options.bodies?.[collection] !== void 0;
|
|
84
|
+
}
|
|
85
|
+
createBody(desc, id, record, warn) {
|
|
86
|
+
const world = this.world;
|
|
87
|
+
const rapier = this.rapier;
|
|
88
|
+
const factory = this.options.bodies?.[desc.name];
|
|
89
|
+
if (!world || !rapier || !factory) return void 0;
|
|
90
|
+
const spec = factory(rapier, record, id);
|
|
91
|
+
if (!spec || !spec.body) return void 0;
|
|
92
|
+
const planar = planarLockWarning(spec);
|
|
93
|
+
if (planar !== void 0) {
|
|
94
|
+
warn(`planar:${desc.name}`, `irtio: physics.bodies.${desc.name} ${planar}`);
|
|
95
|
+
}
|
|
96
|
+
const body = world.createRigidBody(spec.body);
|
|
97
|
+
for (const collider of spec.colliders ?? []) world.createCollider(collider, body);
|
|
98
|
+
return body;
|
|
99
|
+
}
|
|
100
|
+
removeBody(body) {
|
|
101
|
+
this.world?.removeRigidBody(body);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* D71: a proxy is a `KinematicPositionBased` body — driven by the position it is told to be at
|
|
105
|
+
* next, never by a force, an impulse or a contact. Rapier derives the velocity its contacts see
|
|
106
|
+
* from the move itself, which is what carries a predicted body standing on a moving platform.
|
|
107
|
+
*
|
|
108
|
+
* The body is *switched* rather than built kinematic, so its collider, mass properties and axis
|
|
109
|
+
* locks are the ones the shared factory produced. A proxy has to be the server's body, and the
|
|
110
|
+
* only way to keep that true is to run the same factory call the predicted path runs.
|
|
111
|
+
*/
|
|
112
|
+
makeKinematic(handle) {
|
|
113
|
+
const rapier = this.rapier;
|
|
114
|
+
if (!rapier) return;
|
|
115
|
+
handle.setBodyType(rapier.RigidBodyType.KinematicPositionBased, true);
|
|
116
|
+
}
|
|
117
|
+
moveKinematic(handle, pose) {
|
|
118
|
+
const body = handle;
|
|
119
|
+
body.setNextKinematicTranslation(pose.t);
|
|
120
|
+
body.setNextKinematicRotation(pose.r);
|
|
121
|
+
}
|
|
122
|
+
step() {
|
|
123
|
+
this.world?.step();
|
|
124
|
+
}
|
|
125
|
+
/** Server record → body channels (the same mapping the runtime's sync uses, inverted). */
|
|
126
|
+
applyRecord(handle, channels, record) {
|
|
127
|
+
const body = handle;
|
|
128
|
+
const t = { ...body.translation() };
|
|
129
|
+
const r = { ...body.rotation() };
|
|
130
|
+
const v = { ...body.linvel() };
|
|
131
|
+
const w = { ...body.angvel() };
|
|
132
|
+
let setT = false;
|
|
133
|
+
let setR = false;
|
|
134
|
+
let setV = false;
|
|
135
|
+
let setW = false;
|
|
136
|
+
for (const [channel, field] of channels) {
|
|
137
|
+
const raw = record[field];
|
|
138
|
+
if (typeof raw !== "number") continue;
|
|
139
|
+
switch (channel) {
|
|
140
|
+
case "x":
|
|
141
|
+
case "y":
|
|
142
|
+
case "z":
|
|
143
|
+
t[channel] = raw;
|
|
144
|
+
setT = true;
|
|
145
|
+
break;
|
|
146
|
+
case "qx":
|
|
147
|
+
r.x = raw;
|
|
148
|
+
setR = true;
|
|
149
|
+
break;
|
|
150
|
+
case "qy":
|
|
151
|
+
r.y = raw;
|
|
152
|
+
setR = true;
|
|
153
|
+
break;
|
|
154
|
+
case "qz":
|
|
155
|
+
r.z = raw;
|
|
156
|
+
setR = true;
|
|
157
|
+
break;
|
|
158
|
+
case "qw":
|
|
159
|
+
r.w = raw;
|
|
160
|
+
setR = true;
|
|
161
|
+
break;
|
|
162
|
+
case "vx":
|
|
163
|
+
v.x = raw;
|
|
164
|
+
setV = true;
|
|
165
|
+
break;
|
|
166
|
+
case "vy":
|
|
167
|
+
v.y = raw;
|
|
168
|
+
setV = true;
|
|
169
|
+
break;
|
|
170
|
+
case "vz":
|
|
171
|
+
v.z = raw;
|
|
172
|
+
setV = true;
|
|
173
|
+
break;
|
|
174
|
+
case "wx":
|
|
175
|
+
w.x = raw;
|
|
176
|
+
setW = true;
|
|
177
|
+
break;
|
|
178
|
+
case "wy":
|
|
179
|
+
w.y = raw;
|
|
180
|
+
setW = true;
|
|
181
|
+
break;
|
|
182
|
+
case "wz":
|
|
183
|
+
w.z = raw;
|
|
184
|
+
setW = true;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (setT) body.setTranslation(t, true);
|
|
189
|
+
if (setR) body.setRotation(r, true);
|
|
190
|
+
if (setV) body.setLinvel(v, true);
|
|
191
|
+
if (setW) body.setAngvel(w, true);
|
|
192
|
+
}
|
|
193
|
+
readPose(body, into) {
|
|
194
|
+
readPose(body, into);
|
|
195
|
+
}
|
|
196
|
+
applyIntent(collection, body, instance) {
|
|
197
|
+
const hook = this.options.intents?.[collection];
|
|
198
|
+
const world = this.world;
|
|
199
|
+
const rapier = this.rapier;
|
|
200
|
+
if (!hook || !world || !rapier) return;
|
|
201
|
+
hook(body, instance, rapier, world);
|
|
202
|
+
}
|
|
203
|
+
/** Rapier velocities are per second, so one tick of error is `epsilon` when `dv = eps / dt`. */
|
|
204
|
+
velocityTolerance(epsilon, timestepSeconds) {
|
|
205
|
+
return epsilon / timestepSeconds;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
var PhysicsPredictor = class extends Predictor {
|
|
209
|
+
constructor(ext, store, options, meOf, rttOf, tickIntervalOf, log = (m) => console.warn(m)) {
|
|
210
|
+
super(ext, store, new RapierAdapter(options), options, meOf, rttOf, tickIntervalOf, log);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
function createRapierAdapter(options) {
|
|
214
|
+
return new RapierAdapter(options);
|
|
215
|
+
}
|
|
216
|
+
export {
|
|
217
|
+
PhysicsPredictor,
|
|
218
|
+
createRapierAdapter
|
|
219
|
+
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Predictor
|
|
3
|
+
} from "./chunk-DABQDR3S.js";
|
|
4
|
+
import "./chunk-XWVXZRBS.js";
|
|
5
|
+
|
|
6
|
+
// src/physics2d.ts
|
|
7
|
+
import {
|
|
8
|
+
angleFrom2d,
|
|
9
|
+
applyChannel2d,
|
|
10
|
+
channelOf2d
|
|
11
|
+
} from "@irtio/schema";
|
|
12
|
+
var MATTER_BASE_DELTA = 1 / 60;
|
|
13
|
+
var engine;
|
|
14
|
+
var loading;
|
|
15
|
+
async function loadEngine() {
|
|
16
|
+
if (engine) return engine;
|
|
17
|
+
loading ??= (async () => {
|
|
18
|
+
const mod = await import("matter-js");
|
|
19
|
+
const ns = mod.default ?? mod;
|
|
20
|
+
engine = ns;
|
|
21
|
+
return ns;
|
|
22
|
+
})();
|
|
23
|
+
return loading;
|
|
24
|
+
}
|
|
25
|
+
var MatterAdapter = class {
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this.options = options;
|
|
28
|
+
}
|
|
29
|
+
options;
|
|
30
|
+
engineName = "matter-js";
|
|
31
|
+
optionName = "physics2d";
|
|
32
|
+
matter;
|
|
33
|
+
engine;
|
|
34
|
+
/** The fixed delta `Engine.update` is given, in milliseconds — the room's, never a frame's. */
|
|
35
|
+
stepMs = 1e3 / 30;
|
|
36
|
+
timestepSeconds = 1 / 30;
|
|
37
|
+
/** A factory's constraints, so `removeBody` takes them out with the body they pinned. */
|
|
38
|
+
constraints = /* @__PURE__ */ new WeakMap();
|
|
39
|
+
/** Reused by `readPose` and `applyRecord`, which run per body per step and must not allocate. */
|
|
40
|
+
state = {
|
|
41
|
+
x: 0,
|
|
42
|
+
y: 0,
|
|
43
|
+
angle: 0,
|
|
44
|
+
vx: 0,
|
|
45
|
+
vy: 0,
|
|
46
|
+
angularVelocity: 0
|
|
47
|
+
};
|
|
48
|
+
/** `applyChannel2d`'s target shape, likewise reused. */
|
|
49
|
+
channels = { x: 0, y: 0, qz: 0, qw: 1, vx: 0, vy: 0, wz: 0 };
|
|
50
|
+
/** `moveKinematic`'s position argument, reused: it runs per proxy per step (D71). */
|
|
51
|
+
moveTarget = { x: 0, y: 0 };
|
|
52
|
+
get timestep() {
|
|
53
|
+
return this.options.timestep;
|
|
54
|
+
}
|
|
55
|
+
async start(timestepSeconds) {
|
|
56
|
+
const matter = await loadEngine();
|
|
57
|
+
this.matter = matter;
|
|
58
|
+
this.timestepSeconds = timestepSeconds;
|
|
59
|
+
this.stepMs = timestepSeconds * 1e3;
|
|
60
|
+
const engine2 = matter.Engine.create();
|
|
61
|
+
engine2.gravity.x = this.options.gravity.x;
|
|
62
|
+
engine2.gravity.y = this.options.gravity.y;
|
|
63
|
+
if (this.options.setup) this.options.setup(engine2, matter);
|
|
64
|
+
this.engine = engine2;
|
|
65
|
+
}
|
|
66
|
+
free() {
|
|
67
|
+
if (this.matter && this.engine) this.matter.Engine.clear(this.engine);
|
|
68
|
+
this.engine = void 0;
|
|
69
|
+
}
|
|
70
|
+
hasFactory(collection) {
|
|
71
|
+
return this.options.bodies?.[collection] !== void 0;
|
|
72
|
+
}
|
|
73
|
+
createBody(desc, id, record, warn) {
|
|
74
|
+
const matter = this.matter;
|
|
75
|
+
const engine2 = this.engine;
|
|
76
|
+
const factory = this.options.bodies?.[desc.name];
|
|
77
|
+
if (!matter || !engine2 || !factory) return void 0;
|
|
78
|
+
const spec = factory(matter, record, id);
|
|
79
|
+
if (!spec || !spec.body) return void 0;
|
|
80
|
+
if (spec.body.isStatic && this.options.settle?.[desc.name] !== void 0) {
|
|
81
|
+
warn(
|
|
82
|
+
`settle-static:${desc.name}`,
|
|
83
|
+
`irtio: physics2d.settle.${desc.name} runs on static bodies, which matter.js never integrates, so the force does nothing. Drop the settle hook for that collection.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const constraints = spec.constraints ?? [];
|
|
87
|
+
matter.Composite.add(engine2.world, [spec.body, ...constraints]);
|
|
88
|
+
if (constraints.length > 0) this.constraints.set(spec.body, constraints);
|
|
89
|
+
return spec.body;
|
|
90
|
+
}
|
|
91
|
+
removeBody(handle) {
|
|
92
|
+
const matter = this.matter;
|
|
93
|
+
const engine2 = this.engine;
|
|
94
|
+
if (!matter || !engine2) return;
|
|
95
|
+
const body = handle;
|
|
96
|
+
const pinned = this.constraints.get(body);
|
|
97
|
+
matter.Composite.remove(engine2.world, pinned ? [body, ...pinned] : [body]);
|
|
98
|
+
this.constraints.delete(body);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* D71: matter.js has no kinematic body type. Static is the only kind it never integrates and
|
|
102
|
+
* never moves from an impulse, so that is what a proxy is.
|
|
103
|
+
*
|
|
104
|
+
* `Body.setStatic` also overwrites the surface properties the factory set — `friction` to 1 and
|
|
105
|
+
* `restitution` to 0, on every part, and on a body that was *already* static it does that without
|
|
106
|
+
* even stashing the old values in `_original`. A proxy has to be the server's body rather than a
|
|
107
|
+
* stickier copy of it (matter pairs friction as the minimum of the two, so a proxy at friction 1
|
|
108
|
+
* is only invisible while the other body is the grippier one), so the factory's values are
|
|
109
|
+
* snapshotted here and put back. Mass and inertia keep the infinities that make it immovable.
|
|
110
|
+
*/
|
|
111
|
+
makeKinematic(handle) {
|
|
112
|
+
const matter = this.matter;
|
|
113
|
+
if (!matter) return;
|
|
114
|
+
const body = handle;
|
|
115
|
+
const parts = body.parts;
|
|
116
|
+
const surfaces = parts.map((p) => ({ friction: p.friction, restitution: p.restitution }));
|
|
117
|
+
matter.Body.setStatic(body, true);
|
|
118
|
+
for (let i = 0; i < parts.length; i++) {
|
|
119
|
+
const part = parts[i];
|
|
120
|
+
const surface = surfaces[i];
|
|
121
|
+
part.friction = surface.friction;
|
|
122
|
+
part.restitution = surface.restitution;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* `updateVelocity: true` is the whole mechanism. It leaves `positionPrev` exactly one move behind
|
|
127
|
+
* the new position, and matter's resolver reads a body's velocity as precisely that difference
|
|
128
|
+
* (`Resolver.solveVelocity`: `position - positionPrev`), so a predicted body resting on the proxy
|
|
129
|
+
* is carried by friction the way it is on the server. Given the same pose twice the difference is
|
|
130
|
+
* zero and the proxy is at rest, which is what holds it still through a rebase's re-steps.
|
|
131
|
+
*
|
|
132
|
+
* The third parameter is real in matter-js 0.20 (`src/body/Body.js`) and missing from
|
|
133
|
+
* `@types/matter-js@0.20.2` on both setters, which is what the two casts are for.
|
|
134
|
+
*/
|
|
135
|
+
moveKinematic(handle, pose) {
|
|
136
|
+
const matter = this.matter;
|
|
137
|
+
if (!matter) return;
|
|
138
|
+
const body = handle;
|
|
139
|
+
const setPosition = matter.Body.setPosition;
|
|
140
|
+
const setAngle = matter.Body.setAngle;
|
|
141
|
+
const to = this.moveTarget;
|
|
142
|
+
to.x = pose.t.x;
|
|
143
|
+
to.y = pose.t.y;
|
|
144
|
+
setPosition(body, to, true);
|
|
145
|
+
setAngle(body, angleFrom2d(pose.r.z, pose.r.w), true);
|
|
146
|
+
}
|
|
147
|
+
step() {
|
|
148
|
+
if (!this.matter || !this.engine) return;
|
|
149
|
+
this.matter.Engine.update(this.engine, this.stepMs);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Server record → body state, in `applyRecordToBody`'s order.
|
|
153
|
+
*
|
|
154
|
+
* Position before velocity and angle before spin, matching the server. In matter-js 0.20 the
|
|
155
|
+
* two orders happen to agree — `setPosition` shifts `positionPrev` by the same delta, so it
|
|
156
|
+
* preserves the velocity either way — but the server's order is the one to keep: it is the
|
|
157
|
+
* order the runtime's `applyRecordToBody` uses, and a version that stopped agreeing would break
|
|
158
|
+
* both sides the same way rather than only this one.
|
|
159
|
+
*
|
|
160
|
+
* Then the body is woken. A rebase writes state a sleeping body would ignore, the same reason
|
|
161
|
+
* Rapier's setters are called with `wakeUp = true`. matter's `enableSleeping` is off by default
|
|
162
|
+
* on both sides, so this is usually a no-op, and it is here for the room that turns it on.
|
|
163
|
+
*/
|
|
164
|
+
applyRecord(handle, channels, record) {
|
|
165
|
+
const matter = this.matter;
|
|
166
|
+
if (!matter) return;
|
|
167
|
+
const body = handle;
|
|
168
|
+
const t = this.channels;
|
|
169
|
+
t.x = body.position.x;
|
|
170
|
+
t.y = body.position.y;
|
|
171
|
+
t.qz = Math.sin(body.angle / 2);
|
|
172
|
+
t.qw = Math.cos(body.angle / 2);
|
|
173
|
+
t.vx = body.velocity.x;
|
|
174
|
+
t.vy = body.velocity.y;
|
|
175
|
+
t.wz = body.angularVelocity;
|
|
176
|
+
for (const [channel, field] of channels) {
|
|
177
|
+
const raw = record[field];
|
|
178
|
+
if (typeof raw !== "number") continue;
|
|
179
|
+
applyChannel2d(channel, raw, t);
|
|
180
|
+
}
|
|
181
|
+
matter.Body.setPosition(body, { x: t.x, y: t.y });
|
|
182
|
+
matter.Body.setAngle(body, angleFrom2d(t.qz, t.qw));
|
|
183
|
+
matter.Body.setVelocity(body, { x: t.vx, y: t.vy });
|
|
184
|
+
matter.Body.setAngularVelocity(body, t.wz);
|
|
185
|
+
matter.Sleeping.set(body, false);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Body state → a 3D pose, through `channelOf2d` so the plane-to-channel mapping stays written
|
|
189
|
+
* once, in `@irtio/schema`: `t = (x, y, 0)`, `r` a quaternion about Z, `v = (vx, vy, 0)`,
|
|
190
|
+
* `w = (0, 0, spin)`.
|
|
191
|
+
*/
|
|
192
|
+
readPose(handle, into) {
|
|
193
|
+
const body = handle;
|
|
194
|
+
const s = this.state;
|
|
195
|
+
s.x = body.position.x;
|
|
196
|
+
s.y = body.position.y;
|
|
197
|
+
s.angle = body.angle;
|
|
198
|
+
s.vx = body.velocity.x;
|
|
199
|
+
s.vy = body.velocity.y;
|
|
200
|
+
s.angularVelocity = body.angularVelocity;
|
|
201
|
+
const b = s;
|
|
202
|
+
into.t.x = channelOf2d("x", b);
|
|
203
|
+
into.t.y = channelOf2d("y", b);
|
|
204
|
+
into.t.z = channelOf2d("z", b);
|
|
205
|
+
into.r.x = channelOf2d("qx", b);
|
|
206
|
+
into.r.y = channelOf2d("qy", b);
|
|
207
|
+
into.r.z = channelOf2d("qz", b);
|
|
208
|
+
into.r.w = channelOf2d("qw", b);
|
|
209
|
+
into.v.x = channelOf2d("vx", b);
|
|
210
|
+
into.v.y = channelOf2d("vy", b);
|
|
211
|
+
into.v.z = channelOf2d("vz", b);
|
|
212
|
+
into.w.x = channelOf2d("wx", b);
|
|
213
|
+
into.w.y = channelOf2d("wy", b);
|
|
214
|
+
into.w.z = channelOf2d("wz", b);
|
|
215
|
+
}
|
|
216
|
+
applyIntent(collection, body, instance) {
|
|
217
|
+
this.run(this.options.intents?.[collection], body, instance);
|
|
218
|
+
}
|
|
219
|
+
settle(collection, body, instance) {
|
|
220
|
+
this.run(this.options.settle?.[collection], body, instance);
|
|
221
|
+
}
|
|
222
|
+
run(hook, body, instance) {
|
|
223
|
+
const matter = this.matter;
|
|
224
|
+
const engine2 = this.engine;
|
|
225
|
+
if (!hook || !matter || !engine2) return;
|
|
226
|
+
hook(body, instance, matter, engine2, this.timestepSeconds);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* What one step of a velocity disagreement moves the body by, inverted.
|
|
230
|
+
*
|
|
231
|
+
* Rapier's velocities are per second, so its answer is `epsilon / dt`. matter's are neither per
|
|
232
|
+
* second nor, strictly, per step: `Body.updateVelocities` normalises `body.velocity` against
|
|
233
|
+
* `Body._baseDelta` at the end of every `Engine.update`, and `Body.setVelocity` reads the same
|
|
234
|
+
* units back, so a velocity is a displacement per sixtieth of a second whatever the room's step
|
|
235
|
+
* is (verified against matter-js 0.20.0's `Body.js`). One step moves a body by
|
|
236
|
+
* `v * dt / baseDelta`, so the tolerance divides by exactly that ratio — which is 1, and the
|
|
237
|
+
* answer plain `epsilon`, in the 60 Hz room this was designed for.
|
|
238
|
+
*/
|
|
239
|
+
velocityTolerance(epsilon, timestepSeconds) {
|
|
240
|
+
const perStep = timestepSeconds / MATTER_BASE_DELTA;
|
|
241
|
+
return perStep > 0 ? epsilon / perStep : epsilon;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
var Physics2dPredictor = class extends Predictor {
|
|
245
|
+
constructor(ext, store, options, meOf, rttOf, tickIntervalOf, log = (m) => console.warn(m)) {
|
|
246
|
+
super(ext, store, new MatterAdapter(options), options, meOf, rttOf, tickIntervalOf, log);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
function createMatterAdapter(options) {
|
|
250
|
+
return new MatterAdapter(options);
|
|
251
|
+
}
|
|
252
|
+
export {
|
|
253
|
+
Physics2dPredictor,
|
|
254
|
+
createMatterAdapter
|
|
255
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "irtio client SDK: joinRoom, owned-write batching, corrections, typed RPCs, presence, reconnection",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -19,22 +19,31 @@
|
|
|
19
19
|
"dist"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"
|
|
23
|
-
"@irtio/protocol": "0.
|
|
22
|
+
"mediasoup-client": "^3.23.1",
|
|
23
|
+
"@irtio/protocol": "0.7.0",
|
|
24
|
+
"@irtio/schema": "0.7.0"
|
|
24
25
|
},
|
|
25
26
|
"peerDependencies": {
|
|
26
|
-
"@dimforge/rapier3d-compat": ">=0.20.0"
|
|
27
|
+
"@dimforge/rapier3d-compat": ">=0.20.0",
|
|
28
|
+
"matter-js": ">=0.20.0"
|
|
27
29
|
},
|
|
28
30
|
"peerDependenciesMeta": {
|
|
29
31
|
"@dimforge/rapier3d-compat": {
|
|
30
32
|
"optional": true
|
|
33
|
+
},
|
|
34
|
+
"matter-js": {
|
|
35
|
+
"optional": true
|
|
31
36
|
}
|
|
32
37
|
},
|
|
33
38
|
"devDependencies": {
|
|
34
|
-
"@dimforge/rapier3d-compat": "0.20.0"
|
|
39
|
+
"@dimforge/rapier3d-compat": "0.20.0",
|
|
40
|
+
"@types/matter-js": "0.20.2",
|
|
41
|
+
"matter-js": "0.20.0",
|
|
42
|
+
"@irtio/sfu": "0.0.0"
|
|
35
43
|
},
|
|
36
44
|
"scripts": {
|
|
37
45
|
"build": "tsup",
|
|
38
|
-
"test": "vitest run"
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:browser": "playwright test"
|
|
39
48
|
}
|
|
40
49
|
}
|