@irtio/client 0.6.0 → 0.8.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/{chunk-6J6PKFUS.js → chunk-5Z4DHUA3.js} +5 -0
- package/dist/{chunk-TE66EDBB.js → chunk-XFD6WYQW.js} +251 -39
- package/dist/index.d.ts +705 -37
- package/dist/index.js +436 -11
- package/dist/{physics-S5LHL3HK.js → physics-4SGKNBAC.js} +26 -3
- package/dist/physics-rapier2d-TBOAJMVM.js +186 -0
- package/dist/{physics2d-ZV2IRRRZ.js → physics2d-CNSEWKP4.js} +51 -2
- package/package.json +8 -3
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Predictor
|
|
3
|
+
} from "./chunk-XFD6WYQW.js";
|
|
4
|
+
import "./chunk-5Z4DHUA3.js";
|
|
5
|
+
|
|
6
|
+
// src/physics-rapier2d.ts
|
|
7
|
+
import {
|
|
8
|
+
angleFrom2d,
|
|
9
|
+
applyChannel2d,
|
|
10
|
+
channelOf2d
|
|
11
|
+
} from "@irtio/schema";
|
|
12
|
+
var engine;
|
|
13
|
+
var loading;
|
|
14
|
+
async function loadEngine() {
|
|
15
|
+
if (engine) return engine;
|
|
16
|
+
loading ??= (async () => {
|
|
17
|
+
const mod = await import("@dimforge/rapier2d-compat");
|
|
18
|
+
const ns = mod.default ?? mod;
|
|
19
|
+
await ns.init();
|
|
20
|
+
engine = ns;
|
|
21
|
+
return ns;
|
|
22
|
+
})();
|
|
23
|
+
return loading;
|
|
24
|
+
}
|
|
25
|
+
var Rapier2dAdapter = class {
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this.options = options;
|
|
28
|
+
}
|
|
29
|
+
options;
|
|
30
|
+
engineName = "@dimforge/rapier2d-compat";
|
|
31
|
+
optionName = "physics2d";
|
|
32
|
+
rapier;
|
|
33
|
+
world;
|
|
34
|
+
timestepSeconds = 1 / 30;
|
|
35
|
+
/** Reused by `readPose`, which runs per body per step and must not allocate. */
|
|
36
|
+
state = {
|
|
37
|
+
x: 0,
|
|
38
|
+
y: 0,
|
|
39
|
+
angle: 0,
|
|
40
|
+
vx: 0,
|
|
41
|
+
vy: 0,
|
|
42
|
+
angularVelocity: 0
|
|
43
|
+
};
|
|
44
|
+
/** `applyChannel2d`'s target shape, likewise reused. */
|
|
45
|
+
channels = { x: 0, y: 0, qz: 0, qw: 1, vx: 0, vy: 0, wz: 0 };
|
|
46
|
+
/** `moveKinematic`'s translation argument, reused: it runs per proxy per step (D71). */
|
|
47
|
+
moveTarget = { x: 0, y: 0 };
|
|
48
|
+
get timestep() {
|
|
49
|
+
return this.options.timestep;
|
|
50
|
+
}
|
|
51
|
+
async start(timestepSeconds) {
|
|
52
|
+
const rapier = await loadEngine();
|
|
53
|
+
this.rapier = rapier;
|
|
54
|
+
this.timestepSeconds = timestepSeconds;
|
|
55
|
+
const world = new rapier.World({ x: this.options.gravity.x, y: this.options.gravity.y });
|
|
56
|
+
world.timestep = timestepSeconds;
|
|
57
|
+
if (this.options.setup) this.options.setup(world, rapier);
|
|
58
|
+
this.world = world;
|
|
59
|
+
}
|
|
60
|
+
free() {
|
|
61
|
+
this.world?.free();
|
|
62
|
+
this.world = void 0;
|
|
63
|
+
}
|
|
64
|
+
hasFactory(collection) {
|
|
65
|
+
return this.options.bodies?.[collection] !== void 0;
|
|
66
|
+
}
|
|
67
|
+
createBody(desc, id, record, _warn) {
|
|
68
|
+
const world = this.world;
|
|
69
|
+
const rapier = this.rapier;
|
|
70
|
+
const factory = this.options.bodies?.[desc.name];
|
|
71
|
+
if (!world || !rapier || !factory) return void 0;
|
|
72
|
+
const spec = factory(rapier, record, id);
|
|
73
|
+
if (!spec || !spec.body) return void 0;
|
|
74
|
+
const body = world.createRigidBody(spec.body);
|
|
75
|
+
for (const collider of spec.colliders ?? []) world.createCollider(collider, body);
|
|
76
|
+
return body;
|
|
77
|
+
}
|
|
78
|
+
removeBody(body) {
|
|
79
|
+
this.world?.removeRigidBody(body);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* D71: a proxy is a `KinematicPositionBased` body, exactly as in 3D — driven by the position it
|
|
83
|
+
* is told to be at next, never by a force, an impulse or a contact, with Rapier deriving the
|
|
84
|
+
* velocity its contacts see from the move itself. The body is *switched* rather than built
|
|
85
|
+
* kinematic, so its collider and mass properties are the ones the shared factory produced.
|
|
86
|
+
*/
|
|
87
|
+
makeKinematic(handle) {
|
|
88
|
+
const rapier = this.rapier;
|
|
89
|
+
if (!rapier) return;
|
|
90
|
+
handle.setBodyType(rapier.RigidBodyType.KinematicPositionBased, true);
|
|
91
|
+
}
|
|
92
|
+
moveKinematic(handle, pose) {
|
|
93
|
+
const body = handle;
|
|
94
|
+
const to = this.moveTarget;
|
|
95
|
+
to.x = pose.t.x;
|
|
96
|
+
to.y = pose.t.y;
|
|
97
|
+
body.setNextKinematicTranslation(to);
|
|
98
|
+
body.setNextKinematicRotation(angleFrom2d(pose.r.z, pose.r.w));
|
|
99
|
+
}
|
|
100
|
+
step() {
|
|
101
|
+
this.world?.step();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Server record → body state, through `applyChannel2d` so the plane-to-channel mapping stays
|
|
105
|
+
* written once, and in `Rapier2dRuntime.applyRecordToBody`'s order — translation, rotation,
|
|
106
|
+
* linear velocity, spin — so the two sides write the same body the same way. Every setter takes
|
|
107
|
+
* `wakeUp = true`: a rebase writes state a sleeping body would otherwise ignore.
|
|
108
|
+
*/
|
|
109
|
+
applyRecord(handle, channels, record) {
|
|
110
|
+
const body = handle;
|
|
111
|
+
const t = this.channels;
|
|
112
|
+
const pos = body.translation();
|
|
113
|
+
const vel = body.linvel();
|
|
114
|
+
const angle = body.rotation();
|
|
115
|
+
t.x = pos.x;
|
|
116
|
+
t.y = pos.y;
|
|
117
|
+
t.qz = Math.sin(angle / 2);
|
|
118
|
+
t.qw = Math.cos(angle / 2);
|
|
119
|
+
t.vx = vel.x;
|
|
120
|
+
t.vy = vel.y;
|
|
121
|
+
t.wz = body.angvel();
|
|
122
|
+
for (const [channel, field] of channels) {
|
|
123
|
+
const raw = record[field];
|
|
124
|
+
if (typeof raw !== "number") continue;
|
|
125
|
+
applyChannel2d(channel, raw, t);
|
|
126
|
+
}
|
|
127
|
+
body.setTranslation({ x: t.x, y: t.y }, true);
|
|
128
|
+
body.setRotation(angleFrom2d(t.qz, t.qw), true);
|
|
129
|
+
body.setLinvel({ x: t.vx, y: t.vy }, true);
|
|
130
|
+
body.setAngvel(t.wz, true);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Body state → a 3D pose, through `channelOf2d`: `t = (x, y, 0)`, `r` a quaternion about Z,
|
|
134
|
+
* `v = (vx, vy, 0)`, `w = (0, 0, spin)`.
|
|
135
|
+
*/
|
|
136
|
+
readPose(handle, into) {
|
|
137
|
+
const body = handle;
|
|
138
|
+
const s = this.state;
|
|
139
|
+
const pos = body.translation();
|
|
140
|
+
const vel = body.linvel();
|
|
141
|
+
s.x = pos.x;
|
|
142
|
+
s.y = pos.y;
|
|
143
|
+
s.angle = body.rotation();
|
|
144
|
+
s.vx = vel.x;
|
|
145
|
+
s.vy = vel.y;
|
|
146
|
+
s.angularVelocity = body.angvel();
|
|
147
|
+
const b = s;
|
|
148
|
+
into.t.x = channelOf2d("x", b);
|
|
149
|
+
into.t.y = channelOf2d("y", b);
|
|
150
|
+
into.t.z = channelOf2d("z", b);
|
|
151
|
+
into.r.x = channelOf2d("qx", b);
|
|
152
|
+
into.r.y = channelOf2d("qy", b);
|
|
153
|
+
into.r.z = channelOf2d("qz", b);
|
|
154
|
+
into.r.w = channelOf2d("qw", b);
|
|
155
|
+
into.v.x = channelOf2d("vx", b);
|
|
156
|
+
into.v.y = channelOf2d("vy", b);
|
|
157
|
+
into.v.z = channelOf2d("vz", b);
|
|
158
|
+
into.w.x = channelOf2d("wx", b);
|
|
159
|
+
into.w.y = channelOf2d("wy", b);
|
|
160
|
+
into.w.z = channelOf2d("wz", b);
|
|
161
|
+
}
|
|
162
|
+
applyIntent(collection, body, instance) {
|
|
163
|
+
const hook = this.options.intents?.[collection];
|
|
164
|
+
const world = this.world;
|
|
165
|
+
const rapier = this.rapier;
|
|
166
|
+
if (!hook || !world || !rapier) return;
|
|
167
|
+
hook(body, instance, rapier, world, this.timestepSeconds);
|
|
168
|
+
}
|
|
169
|
+
// No `settle`. The world applies gravity; see the module docblock.
|
|
170
|
+
/** Rapier velocities are per second, so one tick of error is `epsilon` when `dv = eps / dt`. */
|
|
171
|
+
velocityTolerance(epsilon, timestepSeconds) {
|
|
172
|
+
return epsilon / timestepSeconds;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var Rapier2dPredictor = class extends Predictor {
|
|
176
|
+
constructor(ext, store, options, meOf, rttOf, tickIntervalOf, log = (m) => console.warn(m)) {
|
|
177
|
+
super(ext, store, new Rapier2dAdapter(options), options, meOf, rttOf, tickIntervalOf, log);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
function createRapier2dAdapter(options) {
|
|
181
|
+
return new Rapier2dAdapter(options);
|
|
182
|
+
}
|
|
183
|
+
export {
|
|
184
|
+
Rapier2dPredictor,
|
|
185
|
+
createRapier2dAdapter
|
|
186
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Predictor
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-XFD6WYQW.js";
|
|
4
|
+
import "./chunk-5Z4DHUA3.js";
|
|
5
5
|
|
|
6
6
|
// src/physics2d.ts
|
|
7
7
|
import {
|
|
@@ -47,6 +47,8 @@ var MatterAdapter = class {
|
|
|
47
47
|
};
|
|
48
48
|
/** `applyChannel2d`'s target shape, likewise reused. */
|
|
49
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 };
|
|
50
52
|
get timestep() {
|
|
51
53
|
return this.options.timestep;
|
|
52
54
|
}
|
|
@@ -95,6 +97,53 @@ var MatterAdapter = class {
|
|
|
95
97
|
matter.Composite.remove(engine2.world, pinned ? [body, ...pinned] : [body]);
|
|
96
98
|
this.constraints.delete(body);
|
|
97
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
|
+
}
|
|
98
147
|
step() {
|
|
99
148
|
if (!this.matter || !this.engine) return;
|
|
100
149
|
this.matter.Engine.update(this.engine, this.stepMs);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "irtio client SDK: joinRoom, owned-write batching, corrections, typed RPCs, presence, reconnection",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -20,14 +20,18 @@
|
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"mediasoup-client": "^3.23.1",
|
|
23
|
-
"@irtio/protocol": "0.
|
|
24
|
-
"@irtio/schema": "0.
|
|
23
|
+
"@irtio/protocol": "0.8.0",
|
|
24
|
+
"@irtio/schema": "0.8.0"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
+
"@dimforge/rapier2d-compat": ">=0.20.0",
|
|
27
28
|
"@dimforge/rapier3d-compat": ">=0.20.0",
|
|
28
29
|
"matter-js": ">=0.20.0"
|
|
29
30
|
},
|
|
30
31
|
"peerDependenciesMeta": {
|
|
32
|
+
"@dimforge/rapier2d-compat": {
|
|
33
|
+
"optional": true
|
|
34
|
+
},
|
|
31
35
|
"@dimforge/rapier3d-compat": {
|
|
32
36
|
"optional": true
|
|
33
37
|
},
|
|
@@ -39,6 +43,7 @@
|
|
|
39
43
|
"@dimforge/rapier3d-compat": "0.20.0",
|
|
40
44
|
"@types/matter-js": "0.20.2",
|
|
41
45
|
"matter-js": "0.20.0",
|
|
46
|
+
"@dimforge/rapier2d-compat": "0.20.0",
|
|
42
47
|
"@irtio/sfu": "0.0.0"
|
|
43
48
|
},
|
|
44
49
|
"scripts": {
|