@opsydyn/effect-three-rapier 0.4.0 → 0.6.4
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 +37 -0
- package/README.md +61 -0
- package/dist/deterministic.d.ts +7 -0
- package/dist/deterministic.d.ts.map +1 -0
- package/dist/deterministic.js +387 -0
- package/dist/deterministic.js.map +12 -0
- package/dist/engine.d.ts +14 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +160 -27
- package/dist/index.js.map +6 -4
- package/dist/snapshot.d.ts +41 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/worker.d.ts +131 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +361 -0
- package/dist/worker.js.map +11 -0
- package/package.json +21 -4
- package/src/deterministic.ts +33 -0
- package/src/engine.ts +400 -0
- package/src/index.ts +6 -329
- package/src/snapshot.ts +130 -0
- package/src/worker.ts +432 -0
package/dist/worker.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// src/snapshot.ts
|
|
2
|
+
var format = 1;
|
|
3
|
+
var encoder = new TextEncoder;
|
|
4
|
+
var decoder = new TextDecoder;
|
|
5
|
+
var writeSnapshot = (index, world) => {
|
|
6
|
+
const parts = [Uint8Array.of(format)];
|
|
7
|
+
const count = (value) => {
|
|
8
|
+
const bytes = new Uint8Array(4);
|
|
9
|
+
new DataView(bytes.buffer).setUint32(0, value, true);
|
|
10
|
+
parts.push(bytes);
|
|
11
|
+
};
|
|
12
|
+
const handle = (value) => {
|
|
13
|
+
const bytes = new Uint8Array(8);
|
|
14
|
+
new DataView(bytes.buffer).setFloat64(0, value, true);
|
|
15
|
+
parts.push(bytes);
|
|
16
|
+
};
|
|
17
|
+
const text = (value) => {
|
|
18
|
+
const bytes = encoder.encode(value);
|
|
19
|
+
count(bytes.length);
|
|
20
|
+
parts.push(bytes);
|
|
21
|
+
};
|
|
22
|
+
count(index.bodies.length);
|
|
23
|
+
for (const body of index.bodies) {
|
|
24
|
+
text(body.key);
|
|
25
|
+
handle(body.handle);
|
|
26
|
+
count(body.colliders.length);
|
|
27
|
+
for (const collider of body.colliders) {
|
|
28
|
+
handle(collider.handle);
|
|
29
|
+
text(collider.part === undefined ? "" : `:${collider.part}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
count(index.joints.length);
|
|
33
|
+
for (const joint of index.joints) {
|
|
34
|
+
text(joint.id);
|
|
35
|
+
handle(joint.handle);
|
|
36
|
+
text(joint.a);
|
|
37
|
+
text(joint.b);
|
|
38
|
+
}
|
|
39
|
+
parts.push(world);
|
|
40
|
+
const out = new Uint8Array(parts.reduce((length, part) => length + part.length, 0));
|
|
41
|
+
let at = 0;
|
|
42
|
+
for (const part of parts) {
|
|
43
|
+
out.set(part, at);
|
|
44
|
+
at += part.length;
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
};
|
|
48
|
+
var readSnapshot = (snapshot) => {
|
|
49
|
+
if (snapshot[0] !== format)
|
|
50
|
+
throw new Error("Not a snapshot of an effect-three Rapier world");
|
|
51
|
+
const view = new DataView(snapshot.buffer, snapshot.byteOffset, snapshot.byteLength);
|
|
52
|
+
let at = 1;
|
|
53
|
+
const count = () => {
|
|
54
|
+
const value = view.getUint32(at, true);
|
|
55
|
+
at += 4;
|
|
56
|
+
return value;
|
|
57
|
+
};
|
|
58
|
+
const handle = () => {
|
|
59
|
+
const value = view.getFloat64(at, true);
|
|
60
|
+
at += 8;
|
|
61
|
+
return value;
|
|
62
|
+
};
|
|
63
|
+
const text = () => {
|
|
64
|
+
const length = count();
|
|
65
|
+
const value = decoder.decode(snapshot.subarray(at, at + length));
|
|
66
|
+
at += length;
|
|
67
|
+
return value;
|
|
68
|
+
};
|
|
69
|
+
function collider() {
|
|
70
|
+
const colliderHandle = handle();
|
|
71
|
+
const part = text();
|
|
72
|
+
return part === "" ? { handle: colliderHandle } : { handle: colliderHandle, part: part.slice(1) };
|
|
73
|
+
}
|
|
74
|
+
function body() {
|
|
75
|
+
const key = text();
|
|
76
|
+
const bodyHandle = handle();
|
|
77
|
+
return { key, handle: bodyHandle, colliders: Array.from({ length: count() }, collider) };
|
|
78
|
+
}
|
|
79
|
+
const bodies = Array.from({ length: count() }, body);
|
|
80
|
+
const joints = Array.from({ length: count() }, () => ({
|
|
81
|
+
id: text(),
|
|
82
|
+
handle: handle(),
|
|
83
|
+
a: text(),
|
|
84
|
+
b: text()
|
|
85
|
+
}));
|
|
86
|
+
return { index: { bodies, joints }, world: snapshot.subarray(at) };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// src/worker.ts
|
|
90
|
+
var noLines = { vertices: new Float32Array, colors: new Float32Array };
|
|
91
|
+
var remoteWorld = (id, port, forget) => {
|
|
92
|
+
let calls = [];
|
|
93
|
+
let flushing = false;
|
|
94
|
+
let disposed = false;
|
|
95
|
+
let sent = 1;
|
|
96
|
+
let answered = 0;
|
|
97
|
+
let collisions = [];
|
|
98
|
+
let forces = [];
|
|
99
|
+
let awake = false;
|
|
100
|
+
let lines = noLines;
|
|
101
|
+
let snapshot;
|
|
102
|
+
let asking = false;
|
|
103
|
+
const poses = new Map;
|
|
104
|
+
const placedIn = new Map;
|
|
105
|
+
const flush = () => {
|
|
106
|
+
flushing = false;
|
|
107
|
+
if (disposed || calls.length === 0)
|
|
108
|
+
return;
|
|
109
|
+
sent += 1;
|
|
110
|
+
const message = { _tag: "Batch", world: id, batch: sent, calls };
|
|
111
|
+
calls = [];
|
|
112
|
+
port.postMessage(message);
|
|
113
|
+
};
|
|
114
|
+
const call = (next) => {
|
|
115
|
+
calls.push(next);
|
|
116
|
+
if (flushing)
|
|
117
|
+
return;
|
|
118
|
+
flushing = true;
|
|
119
|
+
queueMicrotask(flush);
|
|
120
|
+
};
|
|
121
|
+
const place = (key, pose) => {
|
|
122
|
+
if (pose === undefined)
|
|
123
|
+
poses.delete(key);
|
|
124
|
+
else
|
|
125
|
+
poses.set(key, pose);
|
|
126
|
+
placedIn.set(key, sent + 1);
|
|
127
|
+
};
|
|
128
|
+
const heard = (message) => {
|
|
129
|
+
if (disposed)
|
|
130
|
+
return;
|
|
131
|
+
if (message._tag === "Failed") {
|
|
132
|
+
queueMicrotask(() => {
|
|
133
|
+
throw new Error(`Physics in the worker failed: ${message.reason}`);
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
answered = message.batch;
|
|
138
|
+
collisions.push(...message.collisions);
|
|
139
|
+
forces.push(...message.contactForces);
|
|
140
|
+
awake = message.awake;
|
|
141
|
+
lines = message.debugLines ?? noLines;
|
|
142
|
+
if (message.snapshot !== undefined) {
|
|
143
|
+
snapshot = message.snapshot;
|
|
144
|
+
asking = false;
|
|
145
|
+
}
|
|
146
|
+
for (const [key, pose] of message.poses) {
|
|
147
|
+
if ((placedIn.get(key) ?? 0) <= message.batch)
|
|
148
|
+
poses.set(key, pose);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const world = {
|
|
152
|
+
configure: (physics) => call({ _tag: "Configure", physics }),
|
|
153
|
+
add: (key, body, colliders, pose) => {
|
|
154
|
+
call({ _tag: "Add", key, body, colliders, pose });
|
|
155
|
+
place(key, pose);
|
|
156
|
+
},
|
|
157
|
+
update: (key, body) => call({ _tag: "Update", key, body }),
|
|
158
|
+
teleport: (key, pose) => {
|
|
159
|
+
call({ _tag: "Teleport", key, pose });
|
|
160
|
+
place(key, pose);
|
|
161
|
+
},
|
|
162
|
+
moveKinematic: (key, pose) => call({ _tag: "MoveKinematic", key, pose }),
|
|
163
|
+
applyImpulse: (key, impulse) => call({ _tag: "ApplyImpulse", key, impulse }),
|
|
164
|
+
remove: (key) => {
|
|
165
|
+
call({ _tag: "Remove", key });
|
|
166
|
+
place(key, undefined);
|
|
167
|
+
},
|
|
168
|
+
addJoint: (jointId, joint, a, b) => call({ _tag: "AddJoint", id: jointId, joint, a, b }),
|
|
169
|
+
removeJoint: (jointId) => call({ _tag: "RemoveJoint", id: jointId }),
|
|
170
|
+
step: (seconds) => {
|
|
171
|
+
call({ _tag: "Step", seconds });
|
|
172
|
+
return [];
|
|
173
|
+
},
|
|
174
|
+
contactForces: () => {
|
|
175
|
+
const drained = forces;
|
|
176
|
+
forces = [];
|
|
177
|
+
return drained;
|
|
178
|
+
},
|
|
179
|
+
debugLines: () => lines,
|
|
180
|
+
pose: (key) => poses.get(key),
|
|
181
|
+
awake: () => awake || calls.length > 0 || answered < sent,
|
|
182
|
+
dispose: () => {
|
|
183
|
+
disposed = true;
|
|
184
|
+
calls = [];
|
|
185
|
+
forget();
|
|
186
|
+
const message = { _tag: "Dispose", world: id };
|
|
187
|
+
port.postMessage(message);
|
|
188
|
+
},
|
|
189
|
+
received: () => {
|
|
190
|
+
const drained = collisions;
|
|
191
|
+
collisions = [];
|
|
192
|
+
return { collisions: drained, pending: calls.length > 0 || answered < sent };
|
|
193
|
+
},
|
|
194
|
+
snapshot: () => {
|
|
195
|
+
const taken = snapshot;
|
|
196
|
+
snapshot = undefined;
|
|
197
|
+
if (taken === undefined && !asking) {
|
|
198
|
+
asking = true;
|
|
199
|
+
call({ _tag: "Snapshot" });
|
|
200
|
+
}
|
|
201
|
+
return taken;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
return { world, heard };
|
|
205
|
+
};
|
|
206
|
+
var remotePhysics = (port) => {
|
|
207
|
+
const worlds = new Map;
|
|
208
|
+
let made = 0;
|
|
209
|
+
port.addEventListener("message", (event) => {
|
|
210
|
+
const message = event.data;
|
|
211
|
+
worlds.get(message.world)?.(message);
|
|
212
|
+
});
|
|
213
|
+
port.start?.();
|
|
214
|
+
const make = (physics, snapshot) => {
|
|
215
|
+
made += 1;
|
|
216
|
+
const id = made;
|
|
217
|
+
const create = snapshot === undefined ? { _tag: "Create", world: id, physics } : { _tag: "Create", world: id, physics, snapshot };
|
|
218
|
+
port.postMessage(create);
|
|
219
|
+
const { world, heard } = remoteWorld(id, port, () => worlds.delete(id));
|
|
220
|
+
worlds.set(id, heard);
|
|
221
|
+
return world;
|
|
222
|
+
};
|
|
223
|
+
return {
|
|
224
|
+
createWorld: (physics) => make(physics),
|
|
225
|
+
restoreWorld: (physics, snapshot) => {
|
|
226
|
+
const { index } = readSnapshot(snapshot);
|
|
227
|
+
return {
|
|
228
|
+
world: make(physics, snapshot),
|
|
229
|
+
bodies: index.bodies.map((body) => body.key),
|
|
230
|
+
joints: index.joints.map((joint) => joint.id)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
var perform = (served, next, collisions) => {
|
|
236
|
+
const { world } = served;
|
|
237
|
+
switch (next._tag) {
|
|
238
|
+
case "Configure":
|
|
239
|
+
world.configure(next.physics);
|
|
240
|
+
served.debug = next.physics.debug;
|
|
241
|
+
return;
|
|
242
|
+
case "Add":
|
|
243
|
+
world.add(next.key, next.body, next.colliders, next.pose);
|
|
244
|
+
served.keys.add(next.key);
|
|
245
|
+
return;
|
|
246
|
+
case "Update":
|
|
247
|
+
world.update(next.key, next.body);
|
|
248
|
+
return;
|
|
249
|
+
case "Teleport":
|
|
250
|
+
world.teleport(next.key, next.pose);
|
|
251
|
+
return;
|
|
252
|
+
case "MoveKinematic":
|
|
253
|
+
world.moveKinematic(next.key, next.pose);
|
|
254
|
+
return;
|
|
255
|
+
case "ApplyImpulse":
|
|
256
|
+
world.applyImpulse(next.key, next.impulse);
|
|
257
|
+
return;
|
|
258
|
+
case "Remove":
|
|
259
|
+
world.remove(next.key);
|
|
260
|
+
served.keys.delete(next.key);
|
|
261
|
+
return;
|
|
262
|
+
case "AddJoint":
|
|
263
|
+
world.addJoint(next.id, next.joint, next.a, next.b);
|
|
264
|
+
return;
|
|
265
|
+
case "RemoveJoint":
|
|
266
|
+
world.removeJoint(next.id);
|
|
267
|
+
return;
|
|
268
|
+
case "Step":
|
|
269
|
+
collisions.push(...world.step(next.seconds));
|
|
270
|
+
return;
|
|
271
|
+
case "Snapshot":
|
|
272
|
+
if (world.snapshot === undefined)
|
|
273
|
+
throw new Error("This engine cannot take snapshots");
|
|
274
|
+
served.snapshot = world.snapshot();
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
var answer = (id, batch, served, calls) => {
|
|
278
|
+
const collisions = [];
|
|
279
|
+
served.snapshot = undefined;
|
|
280
|
+
for (const next of calls)
|
|
281
|
+
perform(served, next, collisions);
|
|
282
|
+
const poses = [];
|
|
283
|
+
for (const key of served.keys) {
|
|
284
|
+
const pose = served.world.pose(key);
|
|
285
|
+
if (pose !== undefined)
|
|
286
|
+
poses.push([key, pose]);
|
|
287
|
+
}
|
|
288
|
+
const done = {
|
|
289
|
+
_tag: "Done",
|
|
290
|
+
world: id,
|
|
291
|
+
batch,
|
|
292
|
+
collisions,
|
|
293
|
+
contactForces: served.world.contactForces(),
|
|
294
|
+
poses,
|
|
295
|
+
awake: served.world.awake(),
|
|
296
|
+
debugLines: served.debug ? served.world.debugLines() : null,
|
|
297
|
+
...served.snapshot === undefined ? {} : { snapshot: served.snapshot }
|
|
298
|
+
};
|
|
299
|
+
return done;
|
|
300
|
+
};
|
|
301
|
+
var create = (engine, message) => {
|
|
302
|
+
const { physics, snapshot } = message;
|
|
303
|
+
if (snapshot === undefined) {
|
|
304
|
+
return {
|
|
305
|
+
world: engine.createWorld(physics),
|
|
306
|
+
keys: new Set,
|
|
307
|
+
debug: physics.debug,
|
|
308
|
+
snapshot: undefined
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (engine.restoreWorld === undefined)
|
|
312
|
+
throw new Error("This engine cannot restore snapshots");
|
|
313
|
+
const restored = engine.restoreWorld(physics, snapshot);
|
|
314
|
+
return {
|
|
315
|
+
world: restored.world,
|
|
316
|
+
keys: new Set(restored.bodies),
|
|
317
|
+
debug: physics.debug,
|
|
318
|
+
snapshot: undefined
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
var servePhysics = (port, engine) => {
|
|
322
|
+
const loaded = Promise.resolve(engine);
|
|
323
|
+
const worlds = new Map;
|
|
324
|
+
const handle = (ready, message) => {
|
|
325
|
+
if (message._tag === "Create") {
|
|
326
|
+
const made = create(ready, message);
|
|
327
|
+
worlds.set(message.world, made);
|
|
328
|
+
port.postMessage(answer(message.world, 1, made, []));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const served = worlds.get(message.world);
|
|
332
|
+
if (served === undefined)
|
|
333
|
+
return;
|
|
334
|
+
if (message._tag === "Dispose") {
|
|
335
|
+
served.world.dispose();
|
|
336
|
+
worlds.delete(message.world);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
port.postMessage(answer(message.world, message.batch, served, message.calls));
|
|
340
|
+
};
|
|
341
|
+
let queue = Promise.resolve();
|
|
342
|
+
port.addEventListener("message", (event) => {
|
|
343
|
+
const message = event.data;
|
|
344
|
+
queue = queue.then(() => loaded).then((ready) => handle(ready, message)).catch((cause) => {
|
|
345
|
+
const failed = {
|
|
346
|
+
_tag: "Failed",
|
|
347
|
+
world: message.world,
|
|
348
|
+
reason: cause instanceof Error ? cause.message : String(cause)
|
|
349
|
+
};
|
|
350
|
+
port.postMessage(failed);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
port.start?.();
|
|
354
|
+
};
|
|
355
|
+
export {
|
|
356
|
+
remotePhysics,
|
|
357
|
+
servePhysics
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
//# debugId=F54470BCE189C23464756E2164756E21
|
|
361
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/snapshot.ts", "../src/worker.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * A snapshot of one of this package's worlds: Rapier's own (`World.takeSnapshot`), and an\n * index of which of its bodies, colliders and joints are which, by the keys the scene\n * graph knows them by. Nothing here needs Rapier, so the page side of a worker\n * (`remotePhysics`) can read the index of a snapshot the worker took.\n *\n * The layout: a format byte; the bodies (each its key, handle, and colliders with their\n * handles and parts); the joints (each its id, handle and bodies); then Rapier's bytes.\n * Counts and string lengths are 32-bit, handles 64-bit floats (Rapier's own numbers),\n * all little-endian.\n *\n * @module\n */\n\n/** A body in a snapshot: its key, Rapier handle, and its colliders'. */\nexport type IndexedBody = {\n readonly key: string;\n readonly handle: number;\n readonly colliders: ReadonlyArray<{ readonly handle: number; readonly part?: string }>;\n};\n\n/** A joint in a snapshot: its id, Rapier handle, and the keys of its bodies. */\nexport type IndexedJoint = {\n readonly id: string;\n readonly handle: number;\n readonly a: string;\n readonly b: string;\n};\n\nexport type SnapshotIndex = {\n readonly bodies: ReadonlyArray<IndexedBody>;\n readonly joints: ReadonlyArray<IndexedJoint>;\n};\n\n/** The format byte this version writes and reads. */\nconst format = 1;\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\n/** Writes `index` and Rapier's `world` bytes as one snapshot. */\nexport const writeSnapshot = (index: SnapshotIndex, world: Uint8Array): Uint8Array => {\n const parts: Array<Uint8Array> = [Uint8Array.of(format)];\n const count = (value: number): void => {\n const bytes = new Uint8Array(4);\n new DataView(bytes.buffer).setUint32(0, value, true);\n parts.push(bytes);\n };\n const handle = (value: number): void => {\n const bytes = new Uint8Array(8);\n new DataView(bytes.buffer).setFloat64(0, value, true);\n parts.push(bytes);\n };\n const text = (value: string): void => {\n const bytes = encoder.encode(value);\n count(bytes.length);\n parts.push(bytes);\n };\n count(index.bodies.length);\n for (const body of index.bodies) {\n text(body.key);\n handle(body.handle);\n count(body.colliders.length);\n for (const collider of body.colliders) {\n handle(collider.handle);\n // A part is written with a leading mark, so that no part and an empty one differ.\n text(collider.part === undefined ? '' : `:${collider.part}`);\n }\n }\n count(index.joints.length);\n for (const joint of index.joints) {\n text(joint.id);\n handle(joint.handle);\n text(joint.a);\n text(joint.b);\n }\n parts.push(world);\n const out = new Uint8Array(parts.reduce((length, part) => length + part.length, 0));\n let at = 0;\n for (const part of parts) {\n out.set(part, at);\n at += part.length;\n }\n return out;\n};\n\n/** Reads a snapshot written by `writeSnapshot`: its index, and Rapier's bytes. */\nexport const readSnapshot = (\n snapshot: Uint8Array,\n): { readonly index: SnapshotIndex; readonly world: Uint8Array } => {\n if (snapshot[0] !== format) throw new Error('Not a snapshot of an effect-three Rapier world');\n const view = new DataView(snapshot.buffer, snapshot.byteOffset, snapshot.byteLength);\n let at = 1;\n const count = (): number => {\n const value = view.getUint32(at, true);\n at += 4;\n return value;\n };\n const handle = (): number => {\n const value = view.getFloat64(at, true);\n at += 8;\n return value;\n };\n const text = (): string => {\n const length = count();\n const value = decoder.decode(snapshot.subarray(at, at + length));\n at += length;\n return value;\n };\n function collider(): IndexedBody['colliders'][number] {\n const colliderHandle = handle();\n const part = text();\n return part === ''\n ? { handle: colliderHandle }\n : { handle: colliderHandle, part: part.slice(1) };\n }\n function body(): IndexedBody {\n const key = text();\n const bodyHandle = handle();\n return { key, handle: bodyHandle, colliders: Array.from({ length: count() }, collider) };\n }\n const bodies: Array<IndexedBody> = Array.from({ length: count() }, body);\n const joints: Array<IndexedJoint> = Array.from({ length: count() }, () => ({\n id: text(),\n handle: handle(),\n a: text(),\n b: text(),\n }));\n return { index: { bodies, joints }, world: snapshot.subarray(at) };\n};\n",
|
|
6
|
+
"/**\n * Physics in a worker, behind the same engine contract (ADR-017). In the page,\n * `remotePhysics(worker)` is a `PhysicsEngine` whose worlds send what the scene graph asks\n * of them to the worker, a frame's worth in one message, and answer from what the worker\n * last sent back. In the worker, `servePhysics(self, loadRapier())` does what each world\n * is asked with a real engine and sends back the steps' collisions and forces and every\n * body's pose. Nothing here imports Rapier, so the page does not load its WebAssembly.\n *\n * ```ts\n * // physics.worker.ts\n * import { loadRapier } from '@opsydyn/effect-three-rapier/deterministic';\n * import { servePhysics } from '@opsydyn/effect-three-rapier/worker';\n * servePhysics(self, loadRapier());\n *\n * // the page\n * import { remotePhysics } from '@opsydyn/effect-three-rapier/worker';\n * const worker = new Worker(new URL('./physics.worker.ts', import.meta.url), { type: 'module' });\n * const environment = EffectThreeFoldkit.makeEnvironment(window, { physics: remotePhysics(worker) });\n * ```\n *\n * The stepping is pipelined: what a world shows and reports lags what it was asked by\n * the time the worker takes to answer, about a frame. A world on `physics({ until })` is\n * reported as there only once all it was asked has come back, so its checksum is the one\n * an engine in the page would give, a frame later.\n *\n * @module\n */\nimport type {\n Body,\n BodyPose,\n Collider,\n Collision,\n ContactForce,\n DebugLines,\n Physics,\n PhysicsEngine,\n PhysicsJoint,\n PhysicsWorld,\n Vec3,\n} from '@opsydyn/effect-three';\n\nimport { readSnapshot } from './snapshot.ts';\n\n/** One thing a world is asked to do, as `PhysicsWorld`'s method of that name. */\nexport type Call =\n | { readonly _tag: 'Configure'; readonly physics: Physics }\n | {\n readonly _tag: 'Add';\n readonly key: string;\n readonly body: Body;\n readonly colliders: ReadonlyArray<Collider>;\n readonly pose: BodyPose;\n }\n | { readonly _tag: 'Update'; readonly key: string; readonly body: Body }\n | { readonly _tag: 'Teleport'; readonly key: string; readonly pose: BodyPose }\n | { readonly _tag: 'MoveKinematic'; readonly key: string; readonly pose: BodyPose }\n | { readonly _tag: 'ApplyImpulse'; readonly key: string; readonly impulse: Vec3 }\n | { readonly _tag: 'Remove'; readonly key: string }\n | {\n readonly _tag: 'AddJoint';\n readonly id: string;\n readonly joint: PhysicsJoint;\n readonly a: string;\n readonly b: string;\n }\n | { readonly _tag: 'RemoveJoint'; readonly id: string }\n | { readonly _tag: 'Step'; readonly seconds: number }\n | { readonly _tag: 'Snapshot' };\n\n/**\n * What the page sends the worker. A world's creation is answered as its batch 1; the\n * batches after it are numbered on from 2.\n */\nexport type ToWorker =\n | {\n readonly _tag: 'Create';\n readonly world: number;\n readonly physics: Physics;\n /** A snapshot to make the world from (`PhysicsEngine.restoreWorld`). */\n readonly snapshot?: Uint8Array;\n }\n | {\n readonly _tag: 'Batch';\n readonly world: number;\n readonly batch: number;\n readonly calls: ReadonlyArray<Call>;\n }\n | { readonly _tag: 'Dispose'; readonly world: number };\n\n/** What the worker sends back after each batch, or why one could not be done. */\nexport type FromWorker =\n | {\n readonly _tag: 'Done';\n readonly world: number;\n readonly batch: number;\n readonly collisions: ReadonlyArray<Collision>;\n readonly contactForces: ReadonlyArray<ContactForce>;\n readonly poses: ReadonlyArray<readonly [string, BodyPose]>;\n readonly awake: boolean;\n /** The world as lines, while it is configured to draw them. */\n readonly debugLines: DebugLines | null;\n /** The world's snapshot, when the batch asked for one. */\n readonly snapshot?: Uint8Array;\n }\n | { readonly _tag: 'Failed'; readonly world: number; readonly reason: string };\n\n/**\n * Either end of the channel: a `Worker` in the page, the worker's own global scope (`self`)\n * inside it, or a `MessagePort`.\n */\nexport type PhysicsPort = {\n readonly postMessage: (message: unknown) => void;\n readonly addEventListener: (type: 'message', listener: (event: MessageEvent) => void) => void;\n /** A `MessagePort`'s: it delivers nothing to its listeners until started. */\n readonly start?: () => void;\n};\n\nconst noLines: DebugLines = { vertices: new Float32Array(), colors: new Float32Array() };\n\n/**\n * A world in the worker, as the page sees it: `heard` takes what the worker sends back,\n * and `forget` is called when it is disposed.\n */\nconst remoteWorld = (\n id: number,\n port: PhysicsPort,\n forget: () => void,\n): { readonly world: PhysicsWorld; readonly heard: (message: FromWorker) => void } => {\n let calls: Array<Call> = [];\n let flushing = false;\n let disposed = false;\n /** Batches sent (its creation the first), and the last one the worker has answered. */\n let sent = 1;\n let answered = 0;\n let collisions: Array<Collision> = [];\n let forces: Array<ContactForce> = [];\n let awake = false;\n let lines = noLines;\n /** A snapshot come back and not yet taken, and whether one has been asked for. */\n let snapshot: Uint8Array | undefined;\n let asking = false;\n const poses = new Map<string, BodyPose>();\n /** Bodies placed here: an answer to a batch sent before then would put them back. */\n const placedIn = new Map<string, number>();\n\n const flush = (): void => {\n flushing = false;\n if (disposed || calls.length === 0) return;\n sent += 1;\n const message: ToWorker = { _tag: 'Batch', world: id, batch: sent, calls };\n calls = [];\n port.postMessage(message);\n };\n /** Queues a call; the frame's calls go together, once what asked for them is done. */\n const call = (next: Call): void => {\n calls.push(next);\n if (flushing) return;\n flushing = true;\n queueMicrotask(flush);\n };\n const place = (key: string, pose: BodyPose | undefined): void => {\n if (pose === undefined) poses.delete(key);\n else poses.set(key, pose);\n placedIn.set(key, sent + 1);\n };\n\n const heard = (message: FromWorker): void => {\n if (disposed) return;\n if (message._tag === 'Failed') {\n // Nothing in the page can do it instead: say so where the page's errors go.\n queueMicrotask(() => {\n throw new Error(`Physics in the worker failed: ${message.reason}`);\n });\n return;\n }\n answered = message.batch;\n collisions.push(...message.collisions);\n forces.push(...message.contactForces);\n awake = message.awake;\n lines = message.debugLines ?? noLines;\n if (message.snapshot !== undefined) {\n snapshot = message.snapshot;\n asking = false;\n }\n for (const [key, pose] of message.poses) {\n if ((placedIn.get(key) ?? 0) <= message.batch) poses.set(key, pose);\n }\n };\n\n const world: PhysicsWorld = {\n configure: (physics) => call({ _tag: 'Configure', physics }),\n add: (key, body, colliders, pose) => {\n call({ _tag: 'Add', key, body, colliders, pose });\n place(key, pose);\n },\n update: (key, body) => call({ _tag: 'Update', key, body }),\n teleport: (key, pose) => {\n call({ _tag: 'Teleport', key, pose });\n place(key, pose);\n },\n moveKinematic: (key, pose) => call({ _tag: 'MoveKinematic', key, pose }),\n applyImpulse: (key, impulse) => call({ _tag: 'ApplyImpulse', key, impulse }),\n remove: (key) => {\n call({ _tag: 'Remove', key });\n place(key, undefined);\n },\n addJoint: (jointId, joint, a, b) => call({ _tag: 'AddJoint', id: jointId, joint, a, b }),\n removeJoint: (jointId) => call({ _tag: 'RemoveJoint', id: jointId }),\n // Its collisions come back later, through `received`.\n step: (seconds) => {\n call({ _tag: 'Step', seconds });\n return [];\n },\n contactForces: () => {\n const drained = forces;\n forces = [];\n return drained;\n },\n debugLines: () => lines,\n pose: (key) => poses.get(key),\n // Moving, as far as the page knows, until the worker has answered everything.\n awake: () => awake || calls.length > 0 || answered < sent,\n dispose: () => {\n disposed = true;\n calls = [];\n forget();\n const message: ToWorker = { _tag: 'Dispose', world: id };\n port.postMessage(message);\n },\n received: () => {\n const drained = collisions;\n collisions = [];\n return { collisions: drained, pending: calls.length > 0 || answered < sent };\n },\n // Asked for, it comes back with the batch that asked; until then there is none.\n snapshot: () => {\n const taken = snapshot;\n snapshot = undefined;\n if (taken === undefined && !asking) {\n asking = true;\n call({ _tag: 'Snapshot' });\n }\n return taken;\n },\n };\n return { world, heard };\n};\n\n/**\n * A physics engine in a worker, for the page: each world it makes is made in the worker\n * that `servePhysics` runs in, on the far end of `port`.\n */\nexport const remotePhysics = (port: PhysicsPort): PhysicsEngine => {\n const worlds = new Map<number, (message: FromWorker) => void>();\n let made = 0;\n port.addEventListener('message', (event) => {\n // SAFETY: only `servePhysics` posts on this channel, and only `FromWorker` messages.\n const message = event.data as FromWorker;\n worlds.get(message.world)?.(message);\n });\n port.start?.();\n const make = (physics: Physics, snapshot?: Uint8Array): PhysicsWorld => {\n made += 1;\n const id = made;\n const create: ToWorker =\n snapshot === undefined\n ? { _tag: 'Create', world: id, physics }\n : { _tag: 'Create', world: id, physics, snapshot };\n port.postMessage(create);\n const { world, heard } = remoteWorld(id, port, () => worlds.delete(id));\n worlds.set(id, heard);\n return world;\n };\n return {\n createWorld: (physics) => make(physics),\n // What the world holds is in the snapshot's index, which the page can read itself.\n restoreWorld: (physics, snapshot) => {\n const { index } = readSnapshot(snapshot);\n return {\n world: make(physics, snapshot),\n bodies: index.bodies.map((body) => body.key),\n joints: index.joints.map((joint) => joint.id),\n };\n },\n };\n};\n\ntype Served = {\n readonly world: PhysicsWorld;\n readonly keys: Set<string>;\n debug: boolean;\n /** The snapshot the batch being done asked for. */\n snapshot: Uint8Array | undefined;\n};\n\n/** Does one call to a world; a step's collisions are added to `collisions`. */\nconst perform = (served: Served, next: Call, collisions: Array<Collision>): void => {\n const { world } = served;\n switch (next._tag) {\n case 'Configure':\n world.configure(next.physics);\n served.debug = next.physics.debug;\n return;\n case 'Add':\n world.add(next.key, next.body, next.colliders, next.pose);\n served.keys.add(next.key);\n return;\n case 'Update':\n world.update(next.key, next.body);\n return;\n case 'Teleport':\n world.teleport(next.key, next.pose);\n return;\n case 'MoveKinematic':\n world.moveKinematic(next.key, next.pose);\n return;\n case 'ApplyImpulse':\n world.applyImpulse(next.key, next.impulse);\n return;\n case 'Remove':\n world.remove(next.key);\n served.keys.delete(next.key);\n return;\n case 'AddJoint':\n world.addJoint(next.id, next.joint, next.a, next.b);\n return;\n case 'RemoveJoint':\n world.removeJoint(next.id);\n return;\n case 'Step':\n collisions.push(...world.step(next.seconds));\n return;\n case 'Snapshot':\n if (world.snapshot === undefined) throw new Error('This engine cannot take snapshots');\n served.snapshot = world.snapshot();\n }\n};\n\n/** Does a batch to a world and says what came of it. */\nconst answer = (id: number, batch: number, served: Served, calls: ReadonlyArray<Call>) => {\n const collisions: Array<Collision> = [];\n served.snapshot = undefined;\n for (const next of calls) perform(served, next, collisions);\n const poses: Array<readonly [string, BodyPose]> = [];\n for (const key of served.keys) {\n const pose = served.world.pose(key);\n if (pose !== undefined) poses.push([key, pose]);\n }\n const done: FromWorker = {\n _tag: 'Done',\n world: id,\n batch,\n collisions,\n contactForces: served.world.contactForces(),\n poses,\n awake: served.world.awake(),\n debugLines: served.debug ? served.world.debugLines() : null,\n ...(served.snapshot === undefined ? {} : { snapshot: served.snapshot }),\n };\n return done;\n};\n\n/** A world as the page asked for it made: new, or from a snapshot. */\nconst create = (\n engine: PhysicsEngine,\n message: Extract<ToWorker, { readonly _tag: 'Create' }>,\n): Served => {\n const { physics, snapshot } = message;\n if (snapshot === undefined) {\n return {\n world: engine.createWorld(physics),\n keys: new Set(),\n debug: physics.debug,\n snapshot: undefined,\n };\n }\n if (engine.restoreWorld === undefined) throw new Error('This engine cannot restore snapshots');\n const restored = engine.restoreWorld(physics, snapshot);\n return {\n world: restored.world,\n keys: new Set(restored.bodies),\n debug: physics.debug,\n snapshot: undefined,\n };\n};\n\n/**\n * Serves the worlds a page's `remotePhysics` makes, with `engine` (or the engine it is\n * loading): in the worker, `servePhysics(self, loadRapier())`. Messages are handled in\n * the order they came, once the engine has loaded.\n */\nexport const servePhysics = (\n port: PhysicsPort,\n engine: PhysicsEngine | Promise<PhysicsEngine>,\n): void => {\n const loaded = Promise.resolve(engine);\n const worlds = new Map<number, Served>();\n const handle = (ready: PhysicsEngine, message: ToWorker): void => {\n if (message._tag === 'Create') {\n const made = create(ready, message);\n worlds.set(message.world, made);\n // Answered as batch 1: the page then knows where a restored world's bodies are.\n port.postMessage(answer(message.world, 1, made, []));\n return;\n }\n const served = worlds.get(message.world);\n if (served === undefined) return;\n if (message._tag === 'Dispose') {\n served.world.dispose();\n worlds.delete(message.world);\n return;\n }\n port.postMessage(answer(message.world, message.batch, served, message.calls));\n };\n let queue: Promise<void> = Promise.resolve();\n port.addEventListener('message', (event) => {\n // SAFETY: only `remotePhysics` posts on this channel, and only `ToWorker` messages.\n const message = event.data as ToWorker;\n queue = queue\n .then(() => loaded)\n .then((ready) => handle(ready, message))\n .catch((cause: unknown) => {\n const failed: FromWorker = {\n _tag: 'Failed',\n world: message.world,\n reason: cause instanceof Error ? cause.message : String(cause),\n };\n port.postMessage(failed);\n });\n });\n port.start?.();\n};\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "F54470BCE189C23464756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opsydyn/effect-three-rapier",
|
|
3
|
-
"version": "0.4
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "Rapier physics for @opsydyn/effect-three: rigid bodies described as data.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -26,6 +26,16 @@
|
|
|
26
26
|
"bun": "./src/index.ts",
|
|
27
27
|
"default": "./dist/index.js"
|
|
28
28
|
},
|
|
29
|
+
"./deterministic": {
|
|
30
|
+
"types": "./dist/deterministic.d.ts",
|
|
31
|
+
"bun": "./src/deterministic.ts",
|
|
32
|
+
"default": "./dist/deterministic.js"
|
|
33
|
+
},
|
|
34
|
+
"./worker": {
|
|
35
|
+
"types": "./dist/worker.d.ts",
|
|
36
|
+
"bun": "./src/worker.ts",
|
|
37
|
+
"default": "./dist/worker.js"
|
|
38
|
+
},
|
|
29
39
|
"./package.json": "./package.json"
|
|
30
40
|
},
|
|
31
41
|
"publishConfig": {
|
|
@@ -36,7 +46,7 @@
|
|
|
36
46
|
"lint": "bunx --bun oxlint --config oxlint.strict.ts",
|
|
37
47
|
"check": "bun run lint",
|
|
38
48
|
"test": "bun test --pass-with-no-tests ./src ./test",
|
|
39
|
-
"build": "tsc -b && bun build ./src/index.ts --root ./src --outdir ./dist --target browser --packages external --sourcemap=linked",
|
|
49
|
+
"build": "tsc -b && bun build ./src/index.ts ./src/deterministic.ts ./src/worker.ts --root ./src --outdir ./dist --target browser --packages external --sourcemap=linked",
|
|
40
50
|
"api:report": "api-extractor run --local",
|
|
41
51
|
"api:check": "api-extractor run",
|
|
42
52
|
"pack:check": "bun run build && publint && rm -rf dist/pack && bun pm pack --destination dist/pack && attw dist/pack/*.tgz --profile esm-only"
|
|
@@ -46,8 +56,9 @@
|
|
|
46
56
|
},
|
|
47
57
|
"devDependencies": {
|
|
48
58
|
"@arethetypeswrong/cli": "0.18.5",
|
|
59
|
+
"@dimforge/rapier3d-deterministic-compat": "0.21.0",
|
|
49
60
|
"@microsoft/api-extractor": "7.59.4",
|
|
50
|
-
"@opsydyn/effect-three": "0.
|
|
61
|
+
"@opsydyn/effect-three": "0.6.3",
|
|
51
62
|
"@opsydyn/oxlint-effect": "2.0.1",
|
|
52
63
|
"@types/bun": "1.4.3",
|
|
53
64
|
"@types/three": "0.186.0",
|
|
@@ -58,7 +69,13 @@
|
|
|
58
69
|
"typescript": "7.0.2"
|
|
59
70
|
},
|
|
60
71
|
"peerDependencies": {
|
|
61
|
-
"@
|
|
72
|
+
"@dimforge/rapier3d-deterministic-compat": "0.21.0",
|
|
73
|
+
"@opsydyn/effect-three": "0.6.4",
|
|
62
74
|
"three": ">=0.186.0 <0.187.0"
|
|
75
|
+
},
|
|
76
|
+
"peerDependenciesMeta": {
|
|
77
|
+
"@dimforge/rapier3d-deterministic-compat": {
|
|
78
|
+
"optional": true
|
|
79
|
+
}
|
|
63
80
|
}
|
|
64
81
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rapier's deterministic build for `@opsydyn/effect-three`: the same engine as the main
|
|
3
|
+
* entry, over `@dimforge/rapier3d-deterministic-compat` (install it alongside), which
|
|
4
|
+
* gives bit-identical results on every platform for the same inputs at the same steps.
|
|
5
|
+
* Use it where peers must agree (ADR-014): a lockstep session between machines, or a
|
|
6
|
+
* replay shared as a link.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { loadRapier } from '@opsydyn/effect-three-rapier/deterministic';
|
|
10
|
+
* const physics = await loadRapier();
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Determinism covers the engine. What a scene feeds it must agree too: the same bodies,
|
|
14
|
+
* added in the same order, with the same colliders, at the same steps.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
import RAPIER from '@dimforge/rapier3d-deterministic-compat';
|
|
19
|
+
import type { PhysicsEngine } from '@opsydyn/effect-three';
|
|
20
|
+
|
|
21
|
+
import { engineOf, type Rapier } from './engine.ts';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Loads the deterministic Rapier build's WebAssembly and returns the engine. Call once
|
|
25
|
+
* per page.
|
|
26
|
+
*/
|
|
27
|
+
export const loadRapier = async (): Promise<PhysicsEngine> => {
|
|
28
|
+
await RAPIER.init();
|
|
29
|
+
// SAFETY: the deterministic build is the same source as `rapier3d-compat` at the same
|
|
30
|
+
// version, built with another feature: the same API, typed by separate declarations
|
|
31
|
+
// that TypeScript keeps apart only by their private fields. The tests run it.
|
|
32
|
+
return engineOf(RAPIER as unknown as Rapier);
|
|
33
|
+
};
|