@irtio/client 0.5.2 → 0.6.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-7UTJ7RSF.js → chunk-6J6PKFUS.js} +69 -8
- package/dist/{physics-H2VDQLAU.js → chunk-TE66EDBB.js} +174 -239
- package/dist/index.d.ts +1170 -158
- package/dist/index.js +1240 -42
- package/dist/physics-S5LHL3HK.js +196 -0
- package/dist/physics2d-ZV2IRRRZ.js +206 -0
- package/package.json +15 -6
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Predictor
|
|
3
|
+
} from "./chunk-TE66EDBB.js";
|
|
4
|
+
import "./chunk-6J6PKFUS.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
|
+
step() {
|
|
104
|
+
this.world?.step();
|
|
105
|
+
}
|
|
106
|
+
/** Server record → body channels (the same mapping the runtime's sync uses, inverted). */
|
|
107
|
+
applyRecord(handle, channels, record) {
|
|
108
|
+
const body = handle;
|
|
109
|
+
const t = { ...body.translation() };
|
|
110
|
+
const r = { ...body.rotation() };
|
|
111
|
+
const v = { ...body.linvel() };
|
|
112
|
+
const w = { ...body.angvel() };
|
|
113
|
+
let setT = false;
|
|
114
|
+
let setR = false;
|
|
115
|
+
let setV = false;
|
|
116
|
+
let setW = false;
|
|
117
|
+
for (const [channel, field] of channels) {
|
|
118
|
+
const raw = record[field];
|
|
119
|
+
if (typeof raw !== "number") continue;
|
|
120
|
+
switch (channel) {
|
|
121
|
+
case "x":
|
|
122
|
+
case "y":
|
|
123
|
+
case "z":
|
|
124
|
+
t[channel] = raw;
|
|
125
|
+
setT = true;
|
|
126
|
+
break;
|
|
127
|
+
case "qx":
|
|
128
|
+
r.x = raw;
|
|
129
|
+
setR = true;
|
|
130
|
+
break;
|
|
131
|
+
case "qy":
|
|
132
|
+
r.y = raw;
|
|
133
|
+
setR = true;
|
|
134
|
+
break;
|
|
135
|
+
case "qz":
|
|
136
|
+
r.z = raw;
|
|
137
|
+
setR = true;
|
|
138
|
+
break;
|
|
139
|
+
case "qw":
|
|
140
|
+
r.w = raw;
|
|
141
|
+
setR = true;
|
|
142
|
+
break;
|
|
143
|
+
case "vx":
|
|
144
|
+
v.x = raw;
|
|
145
|
+
setV = true;
|
|
146
|
+
break;
|
|
147
|
+
case "vy":
|
|
148
|
+
v.y = raw;
|
|
149
|
+
setV = true;
|
|
150
|
+
break;
|
|
151
|
+
case "vz":
|
|
152
|
+
v.z = raw;
|
|
153
|
+
setV = true;
|
|
154
|
+
break;
|
|
155
|
+
case "wx":
|
|
156
|
+
w.x = raw;
|
|
157
|
+
setW = true;
|
|
158
|
+
break;
|
|
159
|
+
case "wy":
|
|
160
|
+
w.y = raw;
|
|
161
|
+
setW = true;
|
|
162
|
+
break;
|
|
163
|
+
case "wz":
|
|
164
|
+
w.z = raw;
|
|
165
|
+
setW = true;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (setT) body.setTranslation(t, true);
|
|
170
|
+
if (setR) body.setRotation(r, true);
|
|
171
|
+
if (setV) body.setLinvel(v, true);
|
|
172
|
+
if (setW) body.setAngvel(w, true);
|
|
173
|
+
}
|
|
174
|
+
readPose(body, into) {
|
|
175
|
+
readPose(body, into);
|
|
176
|
+
}
|
|
177
|
+
applyIntent(collection, body, instance) {
|
|
178
|
+
const hook = this.options.intents?.[collection];
|
|
179
|
+
const world = this.world;
|
|
180
|
+
const rapier = this.rapier;
|
|
181
|
+
if (!hook || !world || !rapier) return;
|
|
182
|
+
hook(body, instance, rapier, world);
|
|
183
|
+
}
|
|
184
|
+
/** Rapier velocities are per second, so one tick of error is `epsilon` when `dv = eps / dt`. */
|
|
185
|
+
velocityTolerance(epsilon, timestepSeconds) {
|
|
186
|
+
return epsilon / timestepSeconds;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var PhysicsPredictor = class extends Predictor {
|
|
190
|
+
constructor(ext, store, options, meOf, rttOf, tickIntervalOf, log = (m) => console.warn(m)) {
|
|
191
|
+
super(ext, store, new RapierAdapter(options), options, meOf, rttOf, tickIntervalOf, log);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
export {
|
|
195
|
+
PhysicsPredictor
|
|
196
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Predictor
|
|
3
|
+
} from "./chunk-TE66EDBB.js";
|
|
4
|
+
import "./chunk-6J6PKFUS.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
|
+
get timestep() {
|
|
51
|
+
return this.options.timestep;
|
|
52
|
+
}
|
|
53
|
+
async start(timestepSeconds) {
|
|
54
|
+
const matter = await loadEngine();
|
|
55
|
+
this.matter = matter;
|
|
56
|
+
this.timestepSeconds = timestepSeconds;
|
|
57
|
+
this.stepMs = timestepSeconds * 1e3;
|
|
58
|
+
const engine2 = matter.Engine.create();
|
|
59
|
+
engine2.gravity.x = this.options.gravity.x;
|
|
60
|
+
engine2.gravity.y = this.options.gravity.y;
|
|
61
|
+
if (this.options.setup) this.options.setup(engine2, matter);
|
|
62
|
+
this.engine = engine2;
|
|
63
|
+
}
|
|
64
|
+
free() {
|
|
65
|
+
if (this.matter && this.engine) this.matter.Engine.clear(this.engine);
|
|
66
|
+
this.engine = void 0;
|
|
67
|
+
}
|
|
68
|
+
hasFactory(collection) {
|
|
69
|
+
return this.options.bodies?.[collection] !== void 0;
|
|
70
|
+
}
|
|
71
|
+
createBody(desc, id, record, warn) {
|
|
72
|
+
const matter = this.matter;
|
|
73
|
+
const engine2 = this.engine;
|
|
74
|
+
const factory = this.options.bodies?.[desc.name];
|
|
75
|
+
if (!matter || !engine2 || !factory) return void 0;
|
|
76
|
+
const spec = factory(matter, record, id);
|
|
77
|
+
if (!spec || !spec.body) return void 0;
|
|
78
|
+
if (spec.body.isStatic && this.options.settle?.[desc.name] !== void 0) {
|
|
79
|
+
warn(
|
|
80
|
+
`settle-static:${desc.name}`,
|
|
81
|
+
`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.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const constraints = spec.constraints ?? [];
|
|
85
|
+
matter.Composite.add(engine2.world, [spec.body, ...constraints]);
|
|
86
|
+
if (constraints.length > 0) this.constraints.set(spec.body, constraints);
|
|
87
|
+
return spec.body;
|
|
88
|
+
}
|
|
89
|
+
removeBody(handle) {
|
|
90
|
+
const matter = this.matter;
|
|
91
|
+
const engine2 = this.engine;
|
|
92
|
+
if (!matter || !engine2) return;
|
|
93
|
+
const body = handle;
|
|
94
|
+
const pinned = this.constraints.get(body);
|
|
95
|
+
matter.Composite.remove(engine2.world, pinned ? [body, ...pinned] : [body]);
|
|
96
|
+
this.constraints.delete(body);
|
|
97
|
+
}
|
|
98
|
+
step() {
|
|
99
|
+
if (!this.matter || !this.engine) return;
|
|
100
|
+
this.matter.Engine.update(this.engine, this.stepMs);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Server record → body state, in `applyRecordToBody`'s order.
|
|
104
|
+
*
|
|
105
|
+
* Position before velocity and angle before spin, matching the server. In matter-js 0.20 the
|
|
106
|
+
* two orders happen to agree — `setPosition` shifts `positionPrev` by the same delta, so it
|
|
107
|
+
* preserves the velocity either way — but the server's order is the one to keep: it is the
|
|
108
|
+
* order the runtime's `applyRecordToBody` uses, and a version that stopped agreeing would break
|
|
109
|
+
* both sides the same way rather than only this one.
|
|
110
|
+
*
|
|
111
|
+
* Then the body is woken. A rebase writes state a sleeping body would ignore, the same reason
|
|
112
|
+
* Rapier's setters are called with `wakeUp = true`. matter's `enableSleeping` is off by default
|
|
113
|
+
* on both sides, so this is usually a no-op, and it is here for the room that turns it on.
|
|
114
|
+
*/
|
|
115
|
+
applyRecord(handle, channels, record) {
|
|
116
|
+
const matter = this.matter;
|
|
117
|
+
if (!matter) return;
|
|
118
|
+
const body = handle;
|
|
119
|
+
const t = this.channels;
|
|
120
|
+
t.x = body.position.x;
|
|
121
|
+
t.y = body.position.y;
|
|
122
|
+
t.qz = Math.sin(body.angle / 2);
|
|
123
|
+
t.qw = Math.cos(body.angle / 2);
|
|
124
|
+
t.vx = body.velocity.x;
|
|
125
|
+
t.vy = body.velocity.y;
|
|
126
|
+
t.wz = body.angularVelocity;
|
|
127
|
+
for (const [channel, field] of channels) {
|
|
128
|
+
const raw = record[field];
|
|
129
|
+
if (typeof raw !== "number") continue;
|
|
130
|
+
applyChannel2d(channel, raw, t);
|
|
131
|
+
}
|
|
132
|
+
matter.Body.setPosition(body, { x: t.x, y: t.y });
|
|
133
|
+
matter.Body.setAngle(body, angleFrom2d(t.qz, t.qw));
|
|
134
|
+
matter.Body.setVelocity(body, { x: t.vx, y: t.vy });
|
|
135
|
+
matter.Body.setAngularVelocity(body, t.wz);
|
|
136
|
+
matter.Sleeping.set(body, false);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Body state → a 3D pose, through `channelOf2d` so the plane-to-channel mapping stays written
|
|
140
|
+
* once, in `@irtio/schema`: `t = (x, y, 0)`, `r` a quaternion about Z, `v = (vx, vy, 0)`,
|
|
141
|
+
* `w = (0, 0, spin)`.
|
|
142
|
+
*/
|
|
143
|
+
readPose(handle, into) {
|
|
144
|
+
const body = handle;
|
|
145
|
+
const s = this.state;
|
|
146
|
+
s.x = body.position.x;
|
|
147
|
+
s.y = body.position.y;
|
|
148
|
+
s.angle = body.angle;
|
|
149
|
+
s.vx = body.velocity.x;
|
|
150
|
+
s.vy = body.velocity.y;
|
|
151
|
+
s.angularVelocity = body.angularVelocity;
|
|
152
|
+
const b = s;
|
|
153
|
+
into.t.x = channelOf2d("x", b);
|
|
154
|
+
into.t.y = channelOf2d("y", b);
|
|
155
|
+
into.t.z = channelOf2d("z", b);
|
|
156
|
+
into.r.x = channelOf2d("qx", b);
|
|
157
|
+
into.r.y = channelOf2d("qy", b);
|
|
158
|
+
into.r.z = channelOf2d("qz", b);
|
|
159
|
+
into.r.w = channelOf2d("qw", b);
|
|
160
|
+
into.v.x = channelOf2d("vx", b);
|
|
161
|
+
into.v.y = channelOf2d("vy", b);
|
|
162
|
+
into.v.z = channelOf2d("vz", b);
|
|
163
|
+
into.w.x = channelOf2d("wx", b);
|
|
164
|
+
into.w.y = channelOf2d("wy", b);
|
|
165
|
+
into.w.z = channelOf2d("wz", b);
|
|
166
|
+
}
|
|
167
|
+
applyIntent(collection, body, instance) {
|
|
168
|
+
this.run(this.options.intents?.[collection], body, instance);
|
|
169
|
+
}
|
|
170
|
+
settle(collection, body, instance) {
|
|
171
|
+
this.run(this.options.settle?.[collection], body, instance);
|
|
172
|
+
}
|
|
173
|
+
run(hook, body, instance) {
|
|
174
|
+
const matter = this.matter;
|
|
175
|
+
const engine2 = this.engine;
|
|
176
|
+
if (!hook || !matter || !engine2) return;
|
|
177
|
+
hook(body, instance, matter, engine2, this.timestepSeconds);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* What one step of a velocity disagreement moves the body by, inverted.
|
|
181
|
+
*
|
|
182
|
+
* Rapier's velocities are per second, so its answer is `epsilon / dt`. matter's are neither per
|
|
183
|
+
* second nor, strictly, per step: `Body.updateVelocities` normalises `body.velocity` against
|
|
184
|
+
* `Body._baseDelta` at the end of every `Engine.update`, and `Body.setVelocity` reads the same
|
|
185
|
+
* units back, so a velocity is a displacement per sixtieth of a second whatever the room's step
|
|
186
|
+
* is (verified against matter-js 0.20.0's `Body.js`). One step moves a body by
|
|
187
|
+
* `v * dt / baseDelta`, so the tolerance divides by exactly that ratio — which is 1, and the
|
|
188
|
+
* answer plain `epsilon`, in the 60 Hz room this was designed for.
|
|
189
|
+
*/
|
|
190
|
+
velocityTolerance(epsilon, timestepSeconds) {
|
|
191
|
+
const perStep = timestepSeconds / MATTER_BASE_DELTA;
|
|
192
|
+
return perStep > 0 ? epsilon / perStep : epsilon;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
var Physics2dPredictor = class extends Predictor {
|
|
196
|
+
constructor(ext, store, options, meOf, rttOf, tickIntervalOf, log = (m) => console.warn(m)) {
|
|
197
|
+
super(ext, store, new MatterAdapter(options), options, meOf, rttOf, tickIntervalOf, log);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
function createMatterAdapter(options) {
|
|
201
|
+
return new MatterAdapter(options);
|
|
202
|
+
}
|
|
203
|
+
export {
|
|
204
|
+
Physics2dPredictor,
|
|
205
|
+
createMatterAdapter
|
|
206
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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.6.0",
|
|
24
|
+
"@irtio/schema": "0.6.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
|
}
|