@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.
Files changed (143) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +15 -1
  3. package/dist/{index-8cf8t6by.js → index-re3rrfwd.js} +2935 -104
  4. package/dist/index-re3rrfwd.js.map +56 -0
  5. package/dist/index-ttwy0znv.js +145 -0
  6. package/dist/index-ttwy0znv.js.map +12 -0
  7. package/dist/index.js +557 -147
  8. package/dist/index.js.map +8 -6
  9. package/dist/kernel/bodies.d.ts +20 -1
  10. package/dist/kernel/bodies.d.ts.map +1 -1
  11. package/dist/kernel/boxSelect.d.ts +22 -0
  12. package/dist/kernel/boxSelect.d.ts.map +1 -0
  13. package/dist/kernel/build.d.ts +165 -7
  14. package/dist/kernel/build.d.ts.map +1 -1
  15. package/dist/kernel/cameraShake.d.ts.map +1 -1
  16. package/dist/kernel/describeScene.d.ts +50 -0
  17. package/dist/kernel/describeScene.d.ts.map +1 -0
  18. package/dist/kernel/description.d.ts +223 -1
  19. package/dist/kernel/description.d.ts.map +1 -1
  20. package/dist/kernel/diff.d.ts +15 -0
  21. package/dist/kernel/diff.d.ts.map +1 -1
  22. package/dist/kernel/equality.d.ts +11 -1
  23. package/dist/kernel/equality.d.ts.map +1 -1
  24. package/dist/kernel/gizmo.d.ts +69 -0
  25. package/dist/kernel/gizmo.d.ts.map +1 -0
  26. package/dist/kernel/history.d.ts +61 -0
  27. package/dist/kernel/history.d.ts.map +1 -0
  28. package/dist/kernel/index.d.ts +19 -6
  29. package/dist/kernel/index.d.ts.map +1 -1
  30. package/dist/kernel/index.js +124 -3
  31. package/dist/kernel/index.js.map +1 -1
  32. package/dist/kernel/inspect.d.ts +85 -0
  33. package/dist/kernel/inspect.d.ts.map +1 -0
  34. package/dist/kernel/lineGeometry.d.ts +25 -0
  35. package/dist/kernel/lineGeometry.d.ts.map +1 -0
  36. package/dist/kernel/lockstep.d.ts +114 -0
  37. package/dist/kernel/lockstep.d.ts.map +1 -0
  38. package/dist/kernel/loop.d.ts.map +1 -1
  39. package/dist/kernel/materials.d.ts.map +1 -1
  40. package/dist/kernel/paintOrder.d.ts +53 -0
  41. package/dist/kernel/paintOrder.d.ts.map +1 -0
  42. package/dist/kernel/physics.d.ts +39 -0
  43. package/dist/kernel/physics.d.ts.map +1 -1
  44. package/dist/kernel/pivot.d.ts +54 -0
  45. package/dist/kernel/pivot.d.ts.map +1 -0
  46. package/dist/kernel/pointBuffers.d.ts +17 -0
  47. package/dist/kernel/pointBuffers.d.ts.map +1 -0
  48. package/dist/kernel/pointer.d.ts +31 -1
  49. package/dist/kernel/pointer.d.ts.map +1 -1
  50. package/dist/kernel/preview.d.ts +32 -0
  51. package/dist/kernel/preview.d.ts.map +1 -0
  52. package/dist/kernel/renderKit.d.ts +21 -1
  53. package/dist/kernel/renderKit.d.ts.map +1 -1
  54. package/dist/kernel/renderer.d.ts +6 -0
  55. package/dist/kernel/renderer.d.ts.map +1 -1
  56. package/dist/kernel/sceneChanges.d.ts +87 -0
  57. package/dist/kernel/sceneChanges.d.ts.map +1 -0
  58. package/dist/kernel/sceneGraph.d.ts +9 -1
  59. package/dist/kernel/sceneGraph.d.ts.map +1 -1
  60. package/dist/kernel/sceneIndex.d.ts +5 -0
  61. package/dist/kernel/sceneIndex.d.ts.map +1 -1
  62. package/dist/kernel/seeded.d.ts +9 -0
  63. package/dist/kernel/seeded.d.ts.map +1 -0
  64. package/dist/kernel/shots.d.ts +26 -0
  65. package/dist/kernel/shots.d.ts.map +1 -0
  66. package/dist/kernel/tape.d.ts +38 -0
  67. package/dist/kernel/tape.d.ts.map +1 -0
  68. package/dist/kernel/timeline.d.ts +60 -0
  69. package/dist/kernel/timeline.d.ts.map +1 -0
  70. package/dist/kernel/trail.d.ts +31 -0
  71. package/dist/kernel/trail.d.ts.map +1 -0
  72. package/dist/kernel/viewportGizmo.d.ts +25 -0
  73. package/dist/kernel/viewportGizmo.d.ts.map +1 -0
  74. package/dist/runtime/Inspector.d.ts +20 -0
  75. package/dist/runtime/Inspector.d.ts.map +1 -0
  76. package/dist/runtime/SceneFile.d.ts +462 -0
  77. package/dist/runtime/SceneFile.d.ts.map +1 -0
  78. package/dist/runtime/SceneSchema.d.ts +404 -0
  79. package/dist/runtime/SceneSchema.d.ts.map +1 -1
  80. package/dist/runtime/errors.d.ts +9 -0
  81. package/dist/runtime/errors.d.ts.map +1 -1
  82. package/dist/runtime/index.d.ts +3 -1
  83. package/dist/runtime/index.d.ts.map +1 -1
  84. package/dist/webgl/index.d.ts.map +1 -1
  85. package/dist/webgl/index.js +228 -3
  86. package/dist/webgl/index.js.map +6 -4
  87. package/dist/webgl/line.d.ts +4 -0
  88. package/dist/webgl/line.d.ts.map +1 -0
  89. package/dist/webgl/points.d.ts +4 -0
  90. package/dist/webgl/points.d.ts.map +1 -0
  91. package/dist/webgpu/index.d.ts.map +1 -1
  92. package/dist/webgpu/index.js +206 -26
  93. package/dist/webgpu/index.js.map +6 -4
  94. package/dist/webgpu/line.d.ts +4 -0
  95. package/dist/webgpu/line.d.ts.map +1 -0
  96. package/dist/webgpu/points.d.ts +4 -0
  97. package/dist/webgpu/points.d.ts.map +1 -0
  98. package/package.json +4 -2
  99. package/src/kernel/bodies.ts +299 -17
  100. package/src/kernel/boxSelect.ts +73 -0
  101. package/src/kernel/build.ts +374 -1
  102. package/src/kernel/cameraShake.ts +3 -13
  103. package/src/kernel/describeScene.ts +224 -0
  104. package/src/kernel/description.ts +243 -0
  105. package/src/kernel/diff.ts +39 -0
  106. package/src/kernel/equality.ts +123 -0
  107. package/src/kernel/gizmo.ts +478 -0
  108. package/src/kernel/history.ts +118 -0
  109. package/src/kernel/index.ts +110 -1
  110. package/src/kernel/inspect.ts +214 -0
  111. package/src/kernel/lineGeometry.ts +75 -0
  112. package/src/kernel/lockstep.ts +237 -0
  113. package/src/kernel/loop.ts +7 -0
  114. package/src/kernel/materials.ts +3 -0
  115. package/src/kernel/paintOrder.ts +274 -0
  116. package/src/kernel/physics.ts +86 -1
  117. package/src/kernel/pivot.ts +418 -0
  118. package/src/kernel/pointBuffers.ts +115 -0
  119. package/src/kernel/pointer.ts +98 -17
  120. package/src/kernel/preview.ts +467 -0
  121. package/src/kernel/renderKit.ts +23 -1
  122. package/src/kernel/renderer.ts +6 -0
  123. package/src/kernel/sceneChanges.ts +265 -0
  124. package/src/kernel/sceneGraph.ts +354 -11
  125. package/src/kernel/sceneIndex.ts +8 -0
  126. package/src/kernel/seeded.ts +18 -0
  127. package/src/kernel/shots.ts +167 -0
  128. package/src/kernel/tape.ts +124 -0
  129. package/src/kernel/timeline.ts +93 -0
  130. package/src/kernel/trail.ts +170 -0
  131. package/src/kernel/viewportGizmo.ts +194 -0
  132. package/src/runtime/Inspector.ts +43 -0
  133. package/src/runtime/SceneFile.ts +46 -0
  134. package/src/runtime/SceneSchema.ts +319 -0
  135. package/src/runtime/errors.ts +6 -0
  136. package/src/runtime/index.ts +3 -1
  137. package/src/webgl/index.ts +4 -0
  138. package/src/webgl/line.ts +66 -0
  139. package/src/webgl/points.ts +205 -0
  140. package/src/webgpu/index.ts +4 -0
  141. package/src/webgpu/line.ts +67 -0
  142. package/src/webgpu/points.ts +193 -0
  143. package/dist/index-8cf8t6by.js.map +0 -41
@@ -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 type { BodyPose, PhysicsEngine, PhysicsJoint, PhysicsWorld, Quat } from './physics.ts';
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
- if (world === undefined) world = engine.createWorld(physics);
253
- // Joints are made below, not configured.
254
- else if (!physicsEquals(settings && { ...settings, joints: physics.joints }, physics)) {
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 step = (active: PhysicsWorld, seconds: number): void => {
281
- for (const body of hosted.values()) body.previous = body.current;
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
- const tick: BodyHost['tick'] = (elapsed, animating) => {
300
- const delta = last === undefined ? 0 : Math.min(0.5, Math.max(0, elapsed - last));
301
- last = elapsed;
302
- if (world === undefined || settings === null || settings.paused || !animating) return;
303
- const timeStep = settings.timeStep;
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(world, timeStep);
484
+ step(active, timeStep);
308
485
  accumulator -= timeStep;
309
486
  }
310
- const alpha = settings.interpolate ? accumulator / timeStep : 1;
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
+ };