@opsydyn/effect-three 0.3.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 +56 -0
- package/README.md +15 -1
- package/dist/{index-8cf8t6by.js → index-re3rrfwd.js} +2935 -104
- package/dist/index-re3rrfwd.js.map +56 -0
- package/dist/index-ttwy0znv.js +145 -0
- package/dist/index-ttwy0znv.js.map +12 -0
- package/dist/index.js +557 -147
- package/dist/index.js.map +8 -6
- package/dist/kernel/bodies.d.ts +20 -1
- package/dist/kernel/bodies.d.ts.map +1 -1
- package/dist/kernel/boxSelect.d.ts +22 -0
- package/dist/kernel/boxSelect.d.ts.map +1 -0
- package/dist/kernel/build.d.ts +165 -7
- package/dist/kernel/build.d.ts.map +1 -1
- package/dist/kernel/cameraShake.d.ts.map +1 -1
- package/dist/kernel/describeScene.d.ts +50 -0
- package/dist/kernel/describeScene.d.ts.map +1 -0
- package/dist/kernel/description.d.ts +223 -1
- package/dist/kernel/description.d.ts.map +1 -1
- package/dist/kernel/diff.d.ts +15 -0
- package/dist/kernel/diff.d.ts.map +1 -1
- package/dist/kernel/equality.d.ts +11 -1
- package/dist/kernel/equality.d.ts.map +1 -1
- package/dist/kernel/gizmo.d.ts +69 -0
- package/dist/kernel/gizmo.d.ts.map +1 -0
- package/dist/kernel/history.d.ts +61 -0
- package/dist/kernel/history.d.ts.map +1 -0
- package/dist/kernel/index.d.ts +19 -6
- package/dist/kernel/index.d.ts.map +1 -1
- package/dist/kernel/index.js +124 -3
- package/dist/kernel/index.js.map +1 -1
- package/dist/kernel/inspect.d.ts +85 -0
- package/dist/kernel/inspect.d.ts.map +1 -0
- package/dist/kernel/lineGeometry.d.ts +25 -0
- package/dist/kernel/lineGeometry.d.ts.map +1 -0
- package/dist/kernel/lockstep.d.ts +114 -0
- package/dist/kernel/lockstep.d.ts.map +1 -0
- package/dist/kernel/loop.d.ts.map +1 -1
- package/dist/kernel/materials.d.ts.map +1 -1
- package/dist/kernel/paintOrder.d.ts +53 -0
- package/dist/kernel/paintOrder.d.ts.map +1 -0
- package/dist/kernel/physics.d.ts +39 -0
- package/dist/kernel/physics.d.ts.map +1 -1
- package/dist/kernel/pivot.d.ts +54 -0
- package/dist/kernel/pivot.d.ts.map +1 -0
- package/dist/kernel/pointBuffers.d.ts +17 -0
- package/dist/kernel/pointBuffers.d.ts.map +1 -0
- package/dist/kernel/pointer.d.ts +31 -1
- package/dist/kernel/pointer.d.ts.map +1 -1
- package/dist/kernel/preview.d.ts +32 -0
- package/dist/kernel/preview.d.ts.map +1 -0
- package/dist/kernel/renderKit.d.ts +21 -1
- package/dist/kernel/renderKit.d.ts.map +1 -1
- package/dist/kernel/renderer.d.ts +6 -0
- package/dist/kernel/renderer.d.ts.map +1 -1
- package/dist/kernel/sceneChanges.d.ts +87 -0
- package/dist/kernel/sceneChanges.d.ts.map +1 -0
- package/dist/kernel/sceneGraph.d.ts +9 -1
- package/dist/kernel/sceneGraph.d.ts.map +1 -1
- package/dist/kernel/sceneIndex.d.ts +5 -0
- package/dist/kernel/sceneIndex.d.ts.map +1 -1
- package/dist/kernel/seeded.d.ts +9 -0
- package/dist/kernel/seeded.d.ts.map +1 -0
- package/dist/kernel/shots.d.ts +26 -0
- package/dist/kernel/shots.d.ts.map +1 -0
- package/dist/kernel/tape.d.ts +38 -0
- package/dist/kernel/tape.d.ts.map +1 -0
- package/dist/kernel/timeline.d.ts +60 -0
- package/dist/kernel/timeline.d.ts.map +1 -0
- package/dist/kernel/trail.d.ts +31 -0
- package/dist/kernel/trail.d.ts.map +1 -0
- package/dist/kernel/viewportGizmo.d.ts +25 -0
- package/dist/kernel/viewportGizmo.d.ts.map +1 -0
- package/dist/runtime/Inspector.d.ts +20 -0
- package/dist/runtime/Inspector.d.ts.map +1 -0
- package/dist/runtime/SceneFile.d.ts +462 -0
- package/dist/runtime/SceneFile.d.ts.map +1 -0
- package/dist/runtime/SceneSchema.d.ts +404 -0
- package/dist/runtime/SceneSchema.d.ts.map +1 -1
- package/dist/runtime/errors.d.ts +9 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +3 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/webgl/index.d.ts.map +1 -1
- package/dist/webgl/index.js +228 -3
- package/dist/webgl/index.js.map +6 -4
- package/dist/webgl/line.d.ts +4 -0
- package/dist/webgl/line.d.ts.map +1 -0
- package/dist/webgl/points.d.ts +4 -0
- package/dist/webgl/points.d.ts.map +1 -0
- package/dist/webgpu/index.d.ts.map +1 -1
- package/dist/webgpu/index.js +206 -26
- package/dist/webgpu/index.js.map +6 -4
- package/dist/webgpu/line.d.ts +4 -0
- package/dist/webgpu/line.d.ts.map +1 -0
- package/dist/webgpu/points.d.ts +4 -0
- package/dist/webgpu/points.d.ts.map +1 -0
- package/package.json +4 -2
- package/src/kernel/bodies.ts +299 -17
- package/src/kernel/boxSelect.ts +73 -0
- package/src/kernel/build.ts +374 -1
- package/src/kernel/cameraShake.ts +3 -13
- package/src/kernel/describeScene.ts +224 -0
- package/src/kernel/description.ts +243 -0
- package/src/kernel/diff.ts +39 -0
- package/src/kernel/equality.ts +123 -0
- package/src/kernel/gizmo.ts +478 -0
- package/src/kernel/history.ts +118 -0
- package/src/kernel/index.ts +110 -1
- package/src/kernel/inspect.ts +214 -0
- package/src/kernel/lineGeometry.ts +75 -0
- package/src/kernel/lockstep.ts +237 -0
- package/src/kernel/loop.ts +7 -0
- package/src/kernel/materials.ts +3 -0
- package/src/kernel/paintOrder.ts +274 -0
- package/src/kernel/physics.ts +86 -1
- package/src/kernel/pivot.ts +418 -0
- package/src/kernel/pointBuffers.ts +115 -0
- package/src/kernel/pointer.ts +98 -17
- package/src/kernel/preview.ts +467 -0
- package/src/kernel/renderKit.ts +23 -1
- package/src/kernel/renderer.ts +6 -0
- package/src/kernel/sceneChanges.ts +265 -0
- package/src/kernel/sceneGraph.ts +354 -11
- package/src/kernel/sceneIndex.ts +8 -0
- package/src/kernel/seeded.ts +18 -0
- package/src/kernel/shots.ts +167 -0
- package/src/kernel/tape.ts +124 -0
- package/src/kernel/timeline.ts +93 -0
- package/src/kernel/trail.ts +170 -0
- package/src/kernel/viewportGizmo.ts +194 -0
- package/src/runtime/Inspector.ts +43 -0
- package/src/runtime/SceneFile.ts +46 -0
- package/src/runtime/SceneSchema.ts +319 -0
- package/src/runtime/errors.ts +6 -0
- package/src/runtime/index.ts +3 -1
- package/src/webgl/index.ts +4 -0
- package/src/webgl/line.ts +66 -0
- package/src/webgl/points.ts +205 -0
- package/src/webgpu/index.ts +4 -0
- package/src/webgpu/line.ts +67 -0
- package/src/webgpu/points.ts +193 -0
- package/dist/index-8cf8t6by.js.map +0 -41
package/src/kernel/bodies.ts
CHANGED
|
@@ -15,7 +15,16 @@ import { Euler, Quaternion, type Scene } from 'three';
|
|
|
15
15
|
import { bodyId, type BodySource } from './bodySources.ts';
|
|
16
16
|
import type { Body, Joint, Physics, Vec3 } from './description.ts';
|
|
17
17
|
import { bodyEquals, collidersEqual, jointEquals, physicsEquals } from './equality.ts';
|
|
18
|
-
import
|
|
18
|
+
import {
|
|
19
|
+
type BodyPose,
|
|
20
|
+
type Collision,
|
|
21
|
+
type PhysicsEngine,
|
|
22
|
+
type PhysicsJoint,
|
|
23
|
+
type PhysicsWorld,
|
|
24
|
+
type Quat,
|
|
25
|
+
snapshotBytes,
|
|
26
|
+
snapshotText,
|
|
27
|
+
} from './physics.ts';
|
|
19
28
|
import { createPhysicsDebug } from './physicsDebug.ts';
|
|
20
29
|
|
|
21
30
|
/** Two bodies began (`started`) or stopped touching: their nodes' keys, and instances'. */
|
|
@@ -48,6 +57,19 @@ export type BodyContactForce = {
|
|
|
48
57
|
readonly maxForceMagnitude: number;
|
|
49
58
|
};
|
|
50
59
|
|
|
60
|
+
/**
|
|
61
|
+
* A world driven by `physics({ until })` has reached that step and waits: its run, what
|
|
62
|
+
* it has done (its step count since it was made), and a checksum of every body's pose
|
|
63
|
+
* there, so peers that should agree can check that they do.
|
|
64
|
+
*/
|
|
65
|
+
export type PhysicsReached = {
|
|
66
|
+
readonly run: number;
|
|
67
|
+
readonly step: number;
|
|
68
|
+
readonly checksum: string;
|
|
69
|
+
/** At `physics({ snapshotAt })`: the world there, for another to start `from`. */
|
|
70
|
+
readonly snapshot?: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
51
73
|
type Hosted = {
|
|
52
74
|
source: BodySource;
|
|
53
75
|
/** What its colliders were made from; a change makes them again. */
|
|
@@ -69,6 +91,13 @@ export type BodyHost = {
|
|
|
69
91
|
readonly drainCollisions: () => ReadonlyArray<BodyCollision>;
|
|
70
92
|
/** Contact forces over their thresholds since the last call. */
|
|
71
93
|
readonly drainContactForces: () => ReadonlyArray<BodyContactForce>;
|
|
94
|
+
/** Steps the Model has asked for and the world has reached since the last call. */
|
|
95
|
+
readonly drainReached: () => ReadonlyArray<PhysicsReached>;
|
|
96
|
+
/**
|
|
97
|
+
* Whether a world on `until` still owes steps, or a report: it needs frames even when
|
|
98
|
+
* nothing else moves, and even under reduced motion, as other peers wait on it.
|
|
99
|
+
*/
|
|
100
|
+
readonly owesSteps: () => boolean;
|
|
72
101
|
readonly dispose: () => void;
|
|
73
102
|
};
|
|
74
103
|
|
|
@@ -108,6 +137,53 @@ const between = (a: BodyPose, b: BodyPose, alpha: number): BodyPose => {
|
|
|
108
137
|
};
|
|
109
138
|
};
|
|
110
139
|
|
|
140
|
+
/**
|
|
141
|
+
* How far behind `until` (in seconds of simulation) a world may run at scene time's pace,
|
|
142
|
+
* following an `until` that moves on as time passes; further back (a newcomer replaying,
|
|
143
|
+
* a page that was busy), it goes straight there.
|
|
144
|
+
*/
|
|
145
|
+
const catchUpAfter = 0.25;
|
|
146
|
+
|
|
147
|
+
/** The most simulation a world on `until` runs in one frame, in seconds. */
|
|
148
|
+
const mostPerFrame = 1;
|
|
149
|
+
|
|
150
|
+
const checksumBytes = new DataView(new ArrayBuffer(8));
|
|
151
|
+
|
|
152
|
+
/** FNV-1a, a byte at a time. */
|
|
153
|
+
const mixByte = (hash: number, byte: number): number => Math.imul(hash ^ byte, 0x0100_0193) >>> 0;
|
|
154
|
+
|
|
155
|
+
const mixNumber = (hash: number, value: number): number => {
|
|
156
|
+
checksumBytes.setFloat64(0, value);
|
|
157
|
+
let mixed = hash;
|
|
158
|
+
for (let i = 0; i < 8; i++) mixed = mixByte(mixed, checksumBytes.getUint8(i));
|
|
159
|
+
return mixed;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const mixText = (hash: number, text: string): number => {
|
|
163
|
+
let mixed = hash;
|
|
164
|
+
for (let i = 0; i < text.length; i++) {
|
|
165
|
+
const code = text.charCodeAt(i);
|
|
166
|
+
mixed = mixByte(mixByte(mixed, code & 0xff), code >>> 8);
|
|
167
|
+
}
|
|
168
|
+
return mixed;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A checksum of where every body is, by id: the bits of each pose's numbers, so peers
|
|
173
|
+
* agree only if their worlds are the same to the last bit.
|
|
174
|
+
*/
|
|
175
|
+
const checksumOf = (poses: ReadonlyArray<readonly [string, BodyPose]>): string => {
|
|
176
|
+
let hash = 0x811c_9dc5;
|
|
177
|
+
for (const [id, { position, quaternion }] of poses.toSorted(([a], [b]) =>
|
|
178
|
+
a < b ? -1 : a > b ? 1 : 0,
|
|
179
|
+
)) {
|
|
180
|
+
hash = mixText(hash, id);
|
|
181
|
+
for (const value of position) hash = mixNumber(hash, value);
|
|
182
|
+
for (const value of quaternion) hash = mixNumber(hash, value);
|
|
183
|
+
}
|
|
184
|
+
return hash.toString(16).padStart(8, '0');
|
|
185
|
+
};
|
|
186
|
+
|
|
111
187
|
/** A change that needs a new body: another holder or kind, or colliders made otherwise. */
|
|
112
188
|
const remade = (hosted: Hosted, source: BodySource, signature: string): boolean =>
|
|
113
189
|
hosted.source.holder !== source.holder ||
|
|
@@ -156,6 +232,19 @@ export const createBodyHost = (engine: PhysicsEngine | undefined, scene: Scene):
|
|
|
156
232
|
const debug = createPhysicsDebug(scene);
|
|
157
233
|
let last: number | undefined;
|
|
158
234
|
let accumulator = 0;
|
|
235
|
+
/** Steps taken since the world was made. */
|
|
236
|
+
let steps = 0;
|
|
237
|
+
/** The last `until` reported as reached. */
|
|
238
|
+
let reported: number | undefined;
|
|
239
|
+
/** The last `snapshotAt` reported, snapshot and all. */
|
|
240
|
+
let snapped: number | undefined;
|
|
241
|
+
let reached: Array<PhysicsReached> = [];
|
|
242
|
+
/** Snapshots of the latest stops, by step, oldest first, while `keep` asks for them. */
|
|
243
|
+
const kept = new Map<number, Uint8Array>();
|
|
244
|
+
/** A run just started from a snapshot: it goes straight to `until`, as one far behind. */
|
|
245
|
+
let rushing = false;
|
|
246
|
+
/** What a world made from a snapshot holds already, until this sync takes it on. */
|
|
247
|
+
let restored: { readonly bodies: Set<string>; readonly joints: Set<string> } | undefined;
|
|
159
248
|
|
|
160
249
|
/** Draws the world's lines while the description asks for them. */
|
|
161
250
|
const drawDebug = (): void => {
|
|
@@ -172,10 +261,51 @@ export const createBodyHost = (engine: PhysicsEngine | undefined, scene: Scene):
|
|
|
172
261
|
joined.clear();
|
|
173
262
|
accumulator = 0;
|
|
174
263
|
last = undefined;
|
|
264
|
+
steps = 0;
|
|
265
|
+
reported = undefined;
|
|
266
|
+
snapped = undefined;
|
|
267
|
+
reached = [];
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The world for a run: made from the snapshot it starts `from`, counting on from its
|
|
272
|
+
* step, when the engine can; otherwise from the description, at step 0.
|
|
273
|
+
*/
|
|
274
|
+
const make = (active: PhysicsEngine, physics: Physics): PhysicsWorld => {
|
|
275
|
+
const { from } = physics;
|
|
276
|
+
const bytes =
|
|
277
|
+
from === null
|
|
278
|
+
? undefined
|
|
279
|
+
: from.snapshot === null
|
|
280
|
+
? kept.get(from.step)
|
|
281
|
+
: snapshotBytes(from.snapshot);
|
|
282
|
+
if (from === null || bytes === undefined || active.restoreWorld === undefined) {
|
|
283
|
+
kept.clear();
|
|
284
|
+
return active.createWorld(physics);
|
|
285
|
+
}
|
|
286
|
+
// What was kept after the step it goes back to was of a world that went another way.
|
|
287
|
+
for (const step of kept.keys())
|
|
288
|
+
if (step > from.step || from.snapshot !== null) kept.delete(step);
|
|
289
|
+
const made = active.restoreWorld(physics, bytes);
|
|
290
|
+
steps = from.step;
|
|
291
|
+
rushing = true;
|
|
292
|
+
restored = { bodies: new Set(made.bodies), joints: new Set(made.joints) };
|
|
293
|
+
return made.world;
|
|
175
294
|
};
|
|
176
295
|
|
|
296
|
+
/** Takes on a body the restored world holds: shown where the world has it. */
|
|
297
|
+
const adopt = (active: PhysicsWorld, source: BodySource, signature: string): void => {
|
|
298
|
+
const pose = active.pose(source.id) ?? source.described();
|
|
299
|
+
source.place(pose);
|
|
300
|
+
hosted.set(source.id, { source, signature, previous: pose, current: pose, shown: pose });
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/** Whether this sync begins a new run: bodies start where the description puts them. */
|
|
304
|
+
let restarting = false;
|
|
305
|
+
|
|
177
306
|
const add = (active: PhysicsWorld, source: BodySource, signature: string): void => {
|
|
178
|
-
const pose = source.pose();
|
|
307
|
+
const pose = restarting ? source.described() : source.pose();
|
|
308
|
+
if (restarting) source.place(pose);
|
|
179
309
|
active.add(source.id, source.body, source.colliders(), pose);
|
|
180
310
|
if (source.body.impulse !== null) active.applyImpulse(source.id, source.body.impulse.impulse);
|
|
181
311
|
hosted.set(source.id, { source, signature, previous: pose, current: pose, shown: pose });
|
|
@@ -185,6 +315,10 @@ export const createBodyHost = (engine: PhysicsEngine | undefined, scene: Scene):
|
|
|
185
315
|
const syncBody = (active: PhysicsWorld, source: BodySource): void => {
|
|
186
316
|
const existing = hosted.get(source.id);
|
|
187
317
|
const signature = source.signature();
|
|
318
|
+
if (existing === undefined && restored?.bodies.has(source.id) === true) {
|
|
319
|
+
adopt(active, source, signature);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
188
322
|
if (existing === undefined || remade(existing, source, signature)) {
|
|
189
323
|
if (existing !== undefined) active.remove(source.id);
|
|
190
324
|
add(active, source, signature);
|
|
@@ -247,11 +381,27 @@ export const createBodyHost = (engine: PhysicsEngine | undefined, scene: Scene):
|
|
|
247
381
|
const sync: BodyHost['sync'] = (physics, sources) => {
|
|
248
382
|
if (physics === null || engine === undefined) {
|
|
249
383
|
dispose();
|
|
384
|
+
kept.clear();
|
|
250
385
|
return;
|
|
251
386
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
387
|
+
// Another run: the world goes, and a new one counts from step 0.
|
|
388
|
+
restarting = settings !== null && settings.run !== physics.run;
|
|
389
|
+
if (restarting) dispose();
|
|
390
|
+
if (world === undefined) world = make(engine, physics);
|
|
391
|
+
// Joints are made below, and the clock and snapshots are the host's: none is configured.
|
|
392
|
+
else if (
|
|
393
|
+
!physicsEquals(
|
|
394
|
+
settings && {
|
|
395
|
+
...settings,
|
|
396
|
+
joints: physics.joints,
|
|
397
|
+
until: physics.until,
|
|
398
|
+
snapshotAt: physics.snapshotAt,
|
|
399
|
+
from: physics.from,
|
|
400
|
+
keep: physics.keep,
|
|
401
|
+
},
|
|
402
|
+
physics,
|
|
403
|
+
)
|
|
404
|
+
) {
|
|
255
405
|
world.configure(physics);
|
|
256
406
|
}
|
|
257
407
|
settings = physics;
|
|
@@ -267,7 +417,20 @@ export const createBodyHost = (engine: PhysicsEngine | undefined, scene: Scene):
|
|
|
267
417
|
world.remove(id);
|
|
268
418
|
hosted.delete(id);
|
|
269
419
|
}
|
|
420
|
+
if (restored !== undefined) {
|
|
421
|
+
// What the snapshot held and the description does not, goes; its joints that the
|
|
422
|
+
// description still has are kept as they were.
|
|
423
|
+
for (const id of restored.bodies) if (!seen.has(id)) world.remove(id);
|
|
424
|
+
for (const joint of physics.joints) {
|
|
425
|
+
if (restored.joints.has(joint.key)) joined.set(joint.key, joint);
|
|
426
|
+
}
|
|
427
|
+
for (const id of restored.joints) {
|
|
428
|
+
if (!joined.has(id)) world.removeJoint(id);
|
|
429
|
+
}
|
|
430
|
+
restored = undefined;
|
|
431
|
+
}
|
|
270
432
|
syncJoints(world, physics.joints);
|
|
433
|
+
restarting = false;
|
|
271
434
|
drawDebug();
|
|
272
435
|
};
|
|
273
436
|
|
|
@@ -277,10 +440,8 @@ export const createBodyHost = (engine: PhysicsEngine | undefined, scene: Scene):
|
|
|
277
440
|
}
|
|
278
441
|
};
|
|
279
442
|
|
|
280
|
-
const
|
|
281
|
-
for (const
|
|
282
|
-
kinematicTargets(active);
|
|
283
|
-
for (const collision of active.step(seconds)) {
|
|
443
|
+
const heard = (arrived: ReadonlyArray<Collision>, active: PhysicsWorld): void => {
|
|
444
|
+
for (const collision of arrived) {
|
|
284
445
|
collisions.push({
|
|
285
446
|
...named(hosted.get(collision.a)?.source, hosted.get(collision.b)?.source, collision),
|
|
286
447
|
started: collision.started,
|
|
@@ -293,21 +454,128 @@ export const createBodyHost = (engine: PhysicsEngine | undefined, scene: Scene):
|
|
|
293
454
|
...payload,
|
|
294
455
|
});
|
|
295
456
|
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const step = (active: PhysicsWorld, seconds: number): void => {
|
|
460
|
+
for (const body of hosted.values()) body.previous = body.current;
|
|
461
|
+
kinematicTargets(active);
|
|
462
|
+
heard(active.step(seconds), active);
|
|
296
463
|
for (const [id, body] of hosted) body.current = active.pose(id) ?? body.current;
|
|
464
|
+
steps += 1;
|
|
297
465
|
};
|
|
298
466
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
467
|
+
/**
|
|
468
|
+
* What a world working elsewhere has sent back since the last frame: its collisions and
|
|
469
|
+
* forces, and where its bodies are now. Whether anything is still to come back; never,
|
|
470
|
+
* for a world that does what it is asked at once.
|
|
471
|
+
*/
|
|
472
|
+
const collect = (active: PhysicsWorld): boolean => {
|
|
473
|
+
if (active.received === undefined) return false;
|
|
474
|
+
const { collisions: arrived, pending } = active.received();
|
|
475
|
+
heard(arrived, active);
|
|
476
|
+
for (const [id, body] of hosted) body.current = active.pose(id) ?? body.current;
|
|
477
|
+
return pending;
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
/** On scene time: every whole step the time since the last frame holds. */
|
|
481
|
+
const free = (active: PhysicsWorld, timeStep: number, delta: number): number => {
|
|
304
482
|
accumulator += delta;
|
|
305
|
-
scene.updateMatrixWorld();
|
|
306
483
|
while (accumulator >= timeStep) {
|
|
307
|
-
step(
|
|
484
|
+
step(active, timeStep);
|
|
308
485
|
accumulator -= timeStep;
|
|
309
486
|
}
|
|
310
|
-
|
|
487
|
+
collect(active);
|
|
488
|
+
return accumulator / timeStep;
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
/** Whether the world is to stop at `snapshotAt` on its way to `until` and report a snapshot there. */
|
|
492
|
+
const snapshotOwed = (physics: Physics, until: number): boolean =>
|
|
493
|
+
physics.snapshotAt !== null &&
|
|
494
|
+
snapped !== physics.snapshotAt &&
|
|
495
|
+
steps <= physics.snapshotAt &&
|
|
496
|
+
physics.snapshotAt <= until;
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Reports where the world is: its checksum, and a snapshot of it when `snapshot` asks;
|
|
500
|
+
* one is kept too, while the description asks to `keep` some. False, reporting nothing,
|
|
501
|
+
* while a world working elsewhere has yet to send the snapshot (asking it for one).
|
|
502
|
+
*/
|
|
503
|
+
const report = (active: PhysicsWorld, physics: Physics, snapshot: boolean): boolean => {
|
|
504
|
+
const wanted = snapshot || physics.keep > 0;
|
|
505
|
+
const bytes = wanted ? active.snapshot?.() : undefined;
|
|
506
|
+
if (wanted && active.snapshot !== undefined && bytes === undefined) return false;
|
|
507
|
+
if (physics.keep > 0 && bytes !== undefined) {
|
|
508
|
+
kept.delete(steps);
|
|
509
|
+
kept.set(steps, bytes);
|
|
510
|
+
for (const step of kept.keys()) if (kept.size > physics.keep) kept.delete(step);
|
|
511
|
+
}
|
|
512
|
+
const { run } = physics;
|
|
513
|
+
reached.push({
|
|
514
|
+
run,
|
|
515
|
+
step: steps,
|
|
516
|
+
checksum: checksumOf([...hosted.keys()].flatMap((id) => posed(active, id))),
|
|
517
|
+
...(snapshot && bytes !== undefined ? { snapshot: snapshotText(bytes) } : {}),
|
|
518
|
+
});
|
|
519
|
+
return true;
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Toward `until`: at scene time's pace while close behind it, straight there (a step
|
|
524
|
+
* budget a frame) when further back, never past it. A world that has reached it waits
|
|
525
|
+
* there and reports it once, with the bodies' checksum. A `snapshotAt` on the way is a
|
|
526
|
+
* stop too: the world reports there with a snapshot, and goes on.
|
|
527
|
+
*/
|
|
528
|
+
const driven = (active: PhysicsWorld, physics: Physics, until: number, delta: number) => {
|
|
529
|
+
const { timeStep } = physics;
|
|
530
|
+
const snapping = snapshotOwed(physics, until);
|
|
531
|
+
const stop = snapping && physics.snapshotAt !== null ? physics.snapshotAt : until;
|
|
532
|
+
accumulator += delta;
|
|
533
|
+
const owed = Math.max(0, stop - steps);
|
|
534
|
+
const paced = Math.floor(accumulator / timeStep);
|
|
535
|
+
const behind = rushing || owed > Math.ceil(catchUpAfter / timeStep);
|
|
536
|
+
rushing = false;
|
|
537
|
+
const count = Math.min(owed, behind ? owed : paced, Math.ceil(mostPerFrame / timeStep));
|
|
538
|
+
for (let i = 0; i < count; i++) step(active, timeStep);
|
|
539
|
+
accumulator = Math.max(0, accumulator - count * timeStep);
|
|
540
|
+
const pending = collect(active);
|
|
541
|
+
if (steps < stop) return Math.min(1, accumulator / timeStep);
|
|
542
|
+
// A world working elsewhere is there once all it was asked has come back, poses and all.
|
|
543
|
+
if (stop < until) {
|
|
544
|
+
if (!pending && report(active, physics, true)) snapped = stop;
|
|
545
|
+
return Math.min(1, accumulator / timeStep);
|
|
546
|
+
}
|
|
547
|
+
// Waiting: time spent here is not owed once `until` moves on.
|
|
548
|
+
accumulator = 0;
|
|
549
|
+
// Reported there already, it reports again for a snapshot asked for afterwards.
|
|
550
|
+
if (
|
|
551
|
+
steps === until &&
|
|
552
|
+
(reported !== until || snapping) &&
|
|
553
|
+
!pending &&
|
|
554
|
+
report(active, physics, snapping)
|
|
555
|
+
) {
|
|
556
|
+
reported = until;
|
|
557
|
+
if (snapping) snapped = until;
|
|
558
|
+
}
|
|
559
|
+
return 1;
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const posed = (active: PhysicsWorld, id: string): ReadonlyArray<[string, BodyPose]> => {
|
|
563
|
+
const pose = active.pose(id);
|
|
564
|
+
return pose === undefined ? [] : [[id, pose]];
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const tick: BodyHost['tick'] = (elapsed, animating) => {
|
|
568
|
+
const delta = last === undefined ? 0 : Math.min(0.5, Math.max(0, elapsed - last));
|
|
569
|
+
last = elapsed;
|
|
570
|
+
if (world === undefined || settings === null || settings.paused) return;
|
|
571
|
+
// A world on `until` steps under reduced motion too: other peers wait on it.
|
|
572
|
+
if (settings.until === null && !animating) return;
|
|
573
|
+
scene.updateMatrixWorld();
|
|
574
|
+
const progress =
|
|
575
|
+
settings.until === null
|
|
576
|
+
? free(world, settings.timeStep, delta)
|
|
577
|
+
: driven(world, settings, settings.until, delta);
|
|
578
|
+
const alpha = settings.interpolate ? progress : 1;
|
|
311
579
|
for (const body of hosted.values()) {
|
|
312
580
|
if (body.source.body.type === 'dynamic') {
|
|
313
581
|
body.shown = between(body.previous, body.current, alpha);
|
|
@@ -333,6 +601,20 @@ export const createBodyHost = (engine: PhysicsEngine | undefined, scene: Scene):
|
|
|
333
601
|
contactForces = [];
|
|
334
602
|
return drained;
|
|
335
603
|
},
|
|
604
|
+
drainReached: () => {
|
|
605
|
+
if (reached.length === 0) return reached;
|
|
606
|
+
const drained = reached;
|
|
607
|
+
reached = [];
|
|
608
|
+
return drained;
|
|
609
|
+
},
|
|
610
|
+
owesSteps: () =>
|
|
611
|
+
world !== undefined &&
|
|
612
|
+
settings !== null &&
|
|
613
|
+
!settings.paused &&
|
|
614
|
+
settings.until !== null &&
|
|
615
|
+
(steps < settings.until ||
|
|
616
|
+
reported !== settings.until ||
|
|
617
|
+
snapshotOwed(settings, settings.until)),
|
|
336
618
|
dispose,
|
|
337
619
|
};
|
|
338
620
|
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Box selection as data, after three.js's `SelectionBox` (0.186.1,
|
|
3
|
+
* `examples/jsm/interactive/SelectionBox.js`), which Drei's `Select` wraps. three's box
|
|
4
|
+
* walks the scene for meshes whose centres fall inside the frustum a rectangle on screen
|
|
5
|
+
* cuts from the camera; here the Model names the candidates and their centres, and
|
|
6
|
+
* `boxSelect` returns the keys inside, so a selection is a value the Model keeps.
|
|
7
|
+
*
|
|
8
|
+
* A point is inside when it projects into the rectangle, as three's side planes have it,
|
|
9
|
+
* and lies between the camera's near and far planes: what the camera can show. (three's
|
|
10
|
+
* perspective box reaches past far, and its orthographic box neither stops at far nor
|
|
11
|
+
* starts at near, `0.186.1`; a box here never selects what cannot be seen.)
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { OrthographicCamera, PerspectiveCamera, Vector3 } from 'three';
|
|
16
|
+
|
|
17
|
+
import type { Camera, Vec3 } from './description.ts';
|
|
18
|
+
|
|
19
|
+
/** A corner of the box, in the canvas's normalised coordinates: −1 to 1, y up. */
|
|
20
|
+
export type BoxCorner = { readonly x: number; readonly y: number };
|
|
21
|
+
|
|
22
|
+
/** Something that can be selected: its key, and the point that must fall in the box. */
|
|
23
|
+
export type Selectable = { readonly key: string; readonly at: Vec3 };
|
|
24
|
+
|
|
25
|
+
/** The canvas's size in CSS pixels, which sets the camera's aspect (or ortho frustum). */
|
|
26
|
+
export type CanvasSize = { readonly width: number; readonly height: number };
|
|
27
|
+
|
|
28
|
+
/** The three.js camera the description renders through, fitted to `size`. */
|
|
29
|
+
const hostCamera = (camera: Camera, size: CanvasSize): PerspectiveCamera | OrthographicCamera => {
|
|
30
|
+
const host =
|
|
31
|
+
camera._tag === 'Perspective'
|
|
32
|
+
? new PerspectiveCamera(camera.fov, size.width / size.height, camera.near, camera.far)
|
|
33
|
+
: new OrthographicCamera(
|
|
34
|
+
size.width / -2,
|
|
35
|
+
size.width / 2,
|
|
36
|
+
size.height / 2,
|
|
37
|
+
size.height / -2,
|
|
38
|
+
camera.near,
|
|
39
|
+
camera.far,
|
|
40
|
+
);
|
|
41
|
+
if (host instanceof OrthographicCamera && camera._tag === 'Orthographic') {
|
|
42
|
+
host.zoom = camera.zoom;
|
|
43
|
+
}
|
|
44
|
+
host.position.set(...camera.position);
|
|
45
|
+
host.lookAt(...camera.lookAt);
|
|
46
|
+
host.updateMatrixWorld();
|
|
47
|
+
host.updateProjectionMatrix();
|
|
48
|
+
return host;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const between = (value: number, a: number, b: number): boolean =>
|
|
52
|
+
value >= Math.min(a, b) && value <= Math.max(a, b);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The keys of `candidates` whose points fall inside the box from corner `from` to corner
|
|
56
|
+
* `to`, seen through `camera` on a canvas of `size`, in the order given.
|
|
57
|
+
*/
|
|
58
|
+
export const boxSelect = (
|
|
59
|
+
camera: Camera,
|
|
60
|
+
size: CanvasSize,
|
|
61
|
+
from: BoxCorner,
|
|
62
|
+
to: BoxCorner,
|
|
63
|
+
candidates: ReadonlyArray<Selectable>,
|
|
64
|
+
): ReadonlyArray<string> => {
|
|
65
|
+
const host = hostCamera(camera, size);
|
|
66
|
+
// Projected, a point between near and far has a depth from −1 to 1; one behind the
|
|
67
|
+
// camera, above 1.
|
|
68
|
+
const inside = (candidate: Selectable): boolean => {
|
|
69
|
+
const seen = new Vector3(...candidate.at).project(host);
|
|
70
|
+
return between(seen.x, from.x, to.x) && between(seen.y, from.y, to.y) && between(seen.z, -1, 1);
|
|
71
|
+
};
|
|
72
|
+
return candidates.filter(inside).map((candidate) => candidate.key);
|
|
73
|
+
};
|