@opsydyn/effect-three 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/README.md +14 -1
  3. package/dist/{index-01kwfekm.js → index-h3vf50fn.js} +2418 -85
  4. package/dist/index-h3vf50fn.js.map +53 -0
  5. package/dist/index-ttwy0znv.js +145 -0
  6. package/dist/index-ttwy0znv.js.map +12 -0
  7. package/dist/index.js +543 -149
  8. package/dist/index.js.map +8 -6
  9. package/dist/kernel/behaviours.d.ts.map +1 -1
  10. package/dist/kernel/boxSelect.d.ts +22 -0
  11. package/dist/kernel/boxSelect.d.ts.map +1 -0
  12. package/dist/kernel/build.d.ts +197 -6
  13. package/dist/kernel/build.d.ts.map +1 -1
  14. package/dist/kernel/cameraMotion.d.ts +3 -1
  15. package/dist/kernel/cameraMotion.d.ts.map +1 -1
  16. package/dist/kernel/cameraShake.d.ts +33 -0
  17. package/dist/kernel/cameraShake.d.ts.map +1 -0
  18. package/dist/kernel/describeScene.d.ts +50 -0
  19. package/dist/kernel/describeScene.d.ts.map +1 -0
  20. package/dist/kernel/description.d.ts +240 -2
  21. package/dist/kernel/description.d.ts.map +1 -1
  22. package/dist/kernel/diff.d.ts +18 -1
  23. package/dist/kernel/diff.d.ts.map +1 -1
  24. package/dist/kernel/equality.d.ts +13 -1
  25. package/dist/kernel/equality.d.ts.map +1 -1
  26. package/dist/kernel/gizmo.d.ts +69 -0
  27. package/dist/kernel/gizmo.d.ts.map +1 -0
  28. package/dist/kernel/history.d.ts +61 -0
  29. package/dist/kernel/history.d.ts.map +1 -0
  30. package/dist/kernel/index.d.ts +13 -2
  31. package/dist/kernel/index.d.ts.map +1 -1
  32. package/dist/kernel/index.js +116 -3
  33. package/dist/kernel/index.js.map +1 -1
  34. package/dist/kernel/inspect.d.ts +85 -0
  35. package/dist/kernel/inspect.d.ts.map +1 -0
  36. package/dist/kernel/lineGeometry.d.ts +25 -0
  37. package/dist/kernel/lineGeometry.d.ts.map +1 -0
  38. package/dist/kernel/lockstep.d.ts +114 -0
  39. package/dist/kernel/lockstep.d.ts.map +1 -0
  40. package/dist/kernel/loop.d.ts.map +1 -1
  41. package/dist/kernel/materials.d.ts.map +1 -1
  42. package/dist/kernel/pivot.d.ts +54 -0
  43. package/dist/kernel/pivot.d.ts.map +1 -0
  44. package/dist/kernel/pointBuffers.d.ts +17 -0
  45. package/dist/kernel/pointBuffers.d.ts.map +1 -0
  46. package/dist/kernel/pointer.d.ts +12 -0
  47. package/dist/kernel/pointer.d.ts.map +1 -1
  48. package/dist/kernel/renderKit.d.ts +21 -1
  49. package/dist/kernel/renderKit.d.ts.map +1 -1
  50. package/dist/kernel/renderer.d.ts +6 -0
  51. package/dist/kernel/renderer.d.ts.map +1 -1
  52. package/dist/kernel/resources.d.ts.map +1 -1
  53. package/dist/kernel/sceneChanges.d.ts +87 -0
  54. package/dist/kernel/sceneChanges.d.ts.map +1 -0
  55. package/dist/kernel/sceneGraph.d.ts +6 -1
  56. package/dist/kernel/sceneGraph.d.ts.map +1 -1
  57. package/dist/kernel/seeded.d.ts +9 -0
  58. package/dist/kernel/seeded.d.ts.map +1 -0
  59. package/dist/kernel/shots.d.ts +26 -0
  60. package/dist/kernel/shots.d.ts.map +1 -0
  61. package/dist/kernel/tape.d.ts +38 -0
  62. package/dist/kernel/tape.d.ts.map +1 -0
  63. package/dist/kernel/trail.d.ts +31 -0
  64. package/dist/kernel/trail.d.ts.map +1 -0
  65. package/dist/kernel/viewportGizmo.d.ts +25 -0
  66. package/dist/kernel/viewportGizmo.d.ts.map +1 -0
  67. package/dist/runtime/Inspector.d.ts +20 -0
  68. package/dist/runtime/Inspector.d.ts.map +1 -0
  69. package/dist/runtime/SceneFile.d.ts +448 -0
  70. package/dist/runtime/SceneFile.d.ts.map +1 -0
  71. package/dist/runtime/SceneSchema.d.ts +614 -5
  72. package/dist/runtime/SceneSchema.d.ts.map +1 -1
  73. package/dist/runtime/errors.d.ts +9 -0
  74. package/dist/runtime/errors.d.ts.map +1 -1
  75. package/dist/runtime/index.d.ts +3 -1
  76. package/dist/runtime/index.d.ts.map +1 -1
  77. package/dist/webgl/index.d.ts.map +1 -1
  78. package/dist/webgl/index.js +228 -3
  79. package/dist/webgl/index.js.map +6 -4
  80. package/dist/webgl/line.d.ts +4 -0
  81. package/dist/webgl/line.d.ts.map +1 -0
  82. package/dist/webgl/points.d.ts +4 -0
  83. package/dist/webgl/points.d.ts.map +1 -0
  84. package/dist/webgpu/index.d.ts.map +1 -1
  85. package/dist/webgpu/index.js +206 -26
  86. package/dist/webgpu/index.js.map +6 -4
  87. package/dist/webgpu/line.d.ts +4 -0
  88. package/dist/webgpu/line.d.ts.map +1 -0
  89. package/dist/webgpu/points.d.ts +4 -0
  90. package/dist/webgpu/points.d.ts.map +1 -0
  91. package/package.json +2 -1
  92. package/src/kernel/behaviours.ts +5 -0
  93. package/src/kernel/bodySources.ts +1 -1
  94. package/src/kernel/boxSelect.ts +73 -0
  95. package/src/kernel/build.ts +430 -0
  96. package/src/kernel/cameraMotion.ts +3 -0
  97. package/src/kernel/cameraShake.ts +87 -0
  98. package/src/kernel/describeScene.ts +224 -0
  99. package/src/kernel/description.ts +266 -1
  100. package/src/kernel/diff.ts +46 -1
  101. package/src/kernel/equality.ts +148 -0
  102. package/src/kernel/gizmo.ts +478 -0
  103. package/src/kernel/history.ts +118 -0
  104. package/src/kernel/index.ts +102 -0
  105. package/src/kernel/inspect.ts +214 -0
  106. package/src/kernel/lineGeometry.ts +75 -0
  107. package/src/kernel/lockstep.ts +237 -0
  108. package/src/kernel/loop.ts +5 -0
  109. package/src/kernel/materials.ts +3 -0
  110. package/src/kernel/pivot.ts +418 -0
  111. package/src/kernel/pointBuffers.ts +115 -0
  112. package/src/kernel/pointer.ts +63 -12
  113. package/src/kernel/renderKit.ts +23 -1
  114. package/src/kernel/renderer.ts +6 -0
  115. package/src/kernel/resources.ts +69 -0
  116. package/src/kernel/sceneChanges.ts +265 -0
  117. package/src/kernel/sceneGraph.ts +461 -9
  118. package/src/kernel/seeded.ts +18 -0
  119. package/src/kernel/shots.ts +167 -0
  120. package/src/kernel/tape.ts +124 -0
  121. package/src/kernel/trail.ts +170 -0
  122. package/src/kernel/viewportGizmo.ts +194 -0
  123. package/src/runtime/Inspector.ts +43 -0
  124. package/src/runtime/SceneFile.ts +46 -0
  125. package/src/runtime/SceneSchema.ts +305 -4
  126. package/src/runtime/errors.ts +6 -0
  127. package/src/runtime/index.ts +3 -1
  128. package/src/webgl/index.ts +4 -0
  129. package/src/webgl/line.ts +66 -0
  130. package/src/webgl/points.ts +205 -0
  131. package/src/webgpu/index.ts +4 -0
  132. package/src/webgpu/line.ts +67 -0
  133. package/src/webgpu/points.ts +193 -0
  134. package/dist/index-01kwfekm.js.map +0 -40
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The buffers a render kit draws points from, made from their look: given points as
3
+ * they are, Drei's sparkles and stars scattered as Drei scatters them (`Sparkles.tsx`,
4
+ * `Stars.tsx` at `bf6f4ad`) but from a seed, so the same look gives the same buffers.
5
+ *
6
+ * @module
7
+ */
8
+ import { Color, Spherical, Vector3 } from 'three';
9
+
10
+ import type { PointLook } from './description.ts';
11
+ import { seededRandom } from './seeded.ts';
12
+
13
+ /** Per-point attributes: three numbers a point for positions, colours and noise. */
14
+ export type PointBuffers = {
15
+ readonly count: number;
16
+ readonly positions: Float32Array;
17
+ readonly colors: Float32Array;
18
+ readonly sizes: Float32Array;
19
+ /** Sparkles only: each mote's drift speed, opacity and noise. */
20
+ readonly speeds: Float32Array;
21
+ readonly opacities: Float32Array;
22
+ readonly noise: Float32Array;
23
+ };
24
+
25
+ const linear = new Color();
26
+
27
+ const dots = (look: Extract<PointLook, { readonly _tag: 'Dots' }>): PointBuffers => {
28
+ const count = look.positions.length;
29
+ const colors = new Float32Array(count * 3);
30
+ for (let point = 0; point < count; point++) {
31
+ linear.setHex(look.colors?.[point] ?? look.color).toArray(colors, point * 3);
32
+ }
33
+ return {
34
+ count,
35
+ positions: Float32Array.from(look.positions.flat()),
36
+ colors,
37
+ sizes: Float32Array.from({ length: count }, (_, point) => look.sizes?.[point] ?? 1),
38
+ speeds: new Float32Array(0),
39
+ opacities: new Float32Array(0),
40
+ noise: new Float32Array(0),
41
+ };
42
+ };
43
+
44
+ /** Drei's `Sparkles`: positions spread through the box, random sizes unless given. */
45
+ const sparkles = (look: Extract<PointLook, { readonly _tag: 'Sparkles' }>): PointBuffers => {
46
+ const random = seededRandom(look.seed);
47
+ // three's `MathUtils.randFloatSpread`, as Drei spreads them, from the seed.
48
+ const spread = (range: number) => range * (0.5 - random());
49
+ const { count } = look;
50
+ const positions = new Float32Array(count * 3);
51
+ for (let point = 0; point < count; point++) {
52
+ positions[point * 3] = spread(look.scale[0]);
53
+ positions[point * 3 + 1] = spread(look.scale[1]);
54
+ positions[point * 3 + 2] = spread(look.scale[2]);
55
+ }
56
+ const colors = new Float32Array(count * 3);
57
+ linear.setHex(look.color);
58
+ for (let point = 0; point < count; point++) linear.toArray(colors, point * 3);
59
+ const noise = new Float32Array(count * 3);
60
+ for (let point = 0; point < count; point++) noise.set(look.noise, point * 3);
61
+ return {
62
+ count,
63
+ positions,
64
+ colors,
65
+ sizes: Float32Array.from({ length: count }, () => look.size ?? random()),
66
+ speeds: new Float32Array(count).fill(look.speed),
67
+ opacities: new Float32Array(count).fill(look.opacity),
68
+ noise,
69
+ };
70
+ };
71
+
72
+ /** Drei's `Stars`: a shell of stars, nearer ones later, hues round the colour wheel. */
73
+ const stars = (look: Extract<PointLook, { readonly _tag: 'Stars' }>): PointBuffers => {
74
+ const random = seededRandom(look.seed);
75
+ const { count } = look;
76
+ const positions = new Float32Array(count * 3);
77
+ const colors = new Float32Array(count * 3);
78
+ const sizes = Float32Array.from({ length: count }, () => (0.5 + 0.5 * random()) * look.factor);
79
+ const place = new Vector3();
80
+ const spherical = new Spherical();
81
+ const increment = look.depth / count;
82
+ let radius = look.radius + look.depth;
83
+ for (let star = 0; star < count; star++) {
84
+ radius -= increment * random();
85
+ place.setFromSpherical(
86
+ spherical.set(radius, Math.acos(1 - random() * 2), random() * 2 * Math.PI),
87
+ );
88
+ place.toArray(positions, star * 3);
89
+ linear.setHSL(star / count, look.saturation, 0.9).toArray(colors, star * 3);
90
+ }
91
+ return {
92
+ count,
93
+ positions,
94
+ colors,
95
+ sizes,
96
+ speeds: new Float32Array(0),
97
+ opacities: new Float32Array(0),
98
+ noise: new Float32Array(0),
99
+ };
100
+ };
101
+
102
+ /** The buffers for a look. */
103
+ export const pointBuffers = (look: PointLook): PointBuffers => {
104
+ switch (look._tag) {
105
+ case 'Dots':
106
+ return dots(look);
107
+ case 'Sparkles':
108
+ return sparkles(look);
109
+ case 'Stars':
110
+ return stars(look);
111
+ }
112
+ };
113
+
114
+ /** Whether a look moves on its own: sparkles drift and stars twinkle in scene time. */
115
+ export const animatedLook = (look: PointLook): boolean => look._tag !== 'Dots';
@@ -209,6 +209,18 @@ export type SceneEvent =
209
209
  }
210
210
  /** A drag, wheel or pinch on the canvas, when the scene claims gestures. */
211
211
  | { readonly _tag: 'Gesture'; readonly gesture: Gesture }
212
+ /**
213
+ * A gesture that moved has ended: released, or the pointer left. Only after a `Drag`
214
+ * or `Pinch`, with the press's button, modifiers, mesh and view: the moment to finish
215
+ * what the gesture drew, such as a selection box.
216
+ */
217
+ | {
218
+ readonly _tag: 'GestureEnd';
219
+ readonly button: number;
220
+ readonly modifiers: Modifiers;
221
+ readonly key?: string;
222
+ readonly view?: string;
223
+ }
212
224
  /**
213
225
  * A gesture is about to begin on a camera that has turned by itself (`autoRotate`):
214
226
  * where it got to, so the Model can keep it (`autoRotated(camera, detail)`) and the
@@ -359,6 +371,12 @@ const pathOf = (graph: SceneGraph, object: Object3D): ReadonlyArray<string> => {
359
371
  return path;
360
372
  };
361
373
 
374
+ /** Whether an object and every ancestor are visible. */
375
+ const shown = (object: Object3D): boolean => {
376
+ for (let at: Object3D | null = object; at !== null; at = at.parent) if (!at.visible) return false;
377
+ return true;
378
+ };
379
+
362
380
  /** Creates a raycasting picker over a graph's interactive meshes and groups. */
363
381
  export const createPicker = (graph: SceneGraph): Picker => {
364
382
  const raycaster = new Raycaster();
@@ -399,12 +417,17 @@ export const createPicker = (graph: SceneGraph): Picker => {
399
417
  const rayOrigin = new Vector3();
400
418
  const rayDirection = new Vector3();
401
419
 
402
- /** The nearest keyed hit along the raycaster's ray, reporting `view` when there is one. */
403
- const nearestHit = (view: string | undefined): Hit | undefined => {
420
+ /**
421
+ * The nearest keyed hit in `on` (the scene's graph or its HUD's) along the raycaster's
422
+ * ray, reporting `view` when there is one.
423
+ */
424
+ const nearestHit = (on: SceneGraph, view: string | undefined): Hit | undefined => {
404
425
  // Recursive, so an interactive group makes every mesh inside it pickable.
405
- for (const intersection of raycaster.intersectObjects(graph.interactiveObjects(), true)) {
406
- const owner = keyedOwner(graph, intersection.object);
407
- const key = owner === undefined ? undefined : graph.keyOf(owner);
426
+ for (const intersection of raycaster.intersectObjects(on.interactiveObjects(), true)) {
427
+ // Hidden things are not hit: a level of detail not showing, as three's `LOD.raycast`.
428
+ if (!shown(intersection.object)) continue;
429
+ const owner = keyedOwner(on, intersection.object);
430
+ const key = owner === undefined ? undefined : on.keyOf(owner);
408
431
  if (owner !== undefined && key !== undefined) {
409
432
  normal.set(0, 0, 0);
410
433
  if (intersection.face) {
@@ -413,13 +436,13 @@ export const createPicker = (graph: SceneGraph): Picker => {
413
436
  const instance =
414
437
  intersection.instanceId === undefined
415
438
  ? undefined
416
- : graph.instanceKeyOf(intersection.object, intersection.instanceId);
439
+ : on.instanceKeyOf(intersection.object, intersection.instanceId);
417
440
  return {
418
441
  key,
419
442
  ...(instance === undefined ? {} : { instance }),
420
443
  ...partOf(owner, intersection.object),
421
444
  ...(view === undefined ? {} : { view }),
422
- path: pathOf(graph, owner),
445
+ path: pathOf(on, owner),
423
446
  point: toVec3(intersection.point),
424
447
  distance: intersection.distance,
425
448
  normal: toVec3(normal),
@@ -431,6 +454,17 @@ export const createPicker = (graph: SceneGraph): Picker => {
431
454
  return undefined;
432
455
  };
433
456
 
457
+ /** A hit on the HUD's interactive meshes, which are over everything else. */
458
+ const hudHit = (x: number, y: number): Hit | undefined => {
459
+ const hud = graph.hud();
460
+ if (hud === undefined || hud.graph.interactiveObjects().length === 0) return undefined;
461
+ hud.graph.scene.updateMatrixWorld();
462
+ hud.graph.camera.updateMatrixWorld();
463
+ ndc.set(x, y);
464
+ raycaster.setFromCamera(ndc, hud.graph.camera);
465
+ return nearestHit(hud.graph, undefined);
466
+ };
467
+
434
468
  const currentRay = (): Ray => ({
435
469
  origin: toVec3(raycaster.ray.origin),
436
470
  direction: toVec3(raycaster.ray.direction),
@@ -464,10 +498,12 @@ export const createPicker = (graph: SceneGraph): Picker => {
464
498
  holdCameras: graph.holdCameras,
465
499
  turned: graph.turned,
466
500
  pick: (x, y) => {
501
+ const hud = hudHit(x, y);
502
+ if (hud !== undefined) return hud;
467
503
  if (graph.interactiveObjects().length === 0) return undefined;
468
504
  const view = aim(x, y);
469
505
  if (view === false) return undefined;
470
- return nearestHit(view);
506
+ return nearestHit(graph, view);
471
507
  },
472
508
  pickRay: (ray) => {
473
509
  if (graph.interactiveObjects().length === 0) return undefined;
@@ -476,7 +512,7 @@ export const createPicker = (graph: SceneGraph): Picker => {
476
512
  rayOrigin.fromArray(ray.origin),
477
513
  rayDirection.fromArray(ray.direction).normalize(),
478
514
  );
479
- return nearestHit(undefined);
515
+ return nearestHit(graph, undefined);
480
516
  },
481
517
  viewport: graph.viewport,
482
518
  };
@@ -524,6 +560,8 @@ export const createPointerTracker = (picker: Picker): PointerTracker => {
524
560
  const pressedPointers = new Map<number, Point>();
525
561
  let primary: number | undefined;
526
562
  let gesturePress: GesturePress | undefined;
563
+ // Whether the gesture press has moved: only then does its release end a gesture.
564
+ let gestureMoved = false;
527
565
  let hovered: string | undefined;
528
566
  // The latest press, kept after release: double-click and context-menu events follow it.
529
567
  let press: Press | undefined;
@@ -616,6 +654,7 @@ export const createPointerTracker = (picker: Picker): PointerTracker => {
616
654
  pressedPointers.clear();
617
655
  primary = id;
618
656
  pressedPointers.set(id, { x: event.x, y: event.y });
657
+ gestureMoved = false;
619
658
  const region = picker.region(event.x, event.y);
620
659
  reportTurned(region, events);
621
660
  // Cameras turning by themselves hold still while it is pressed, as `OrbitControls`.
@@ -633,6 +672,7 @@ export const createPointerTracker = (picker: Picker): PointerTracker => {
633
672
  const from = pressedPointers.get(id);
634
673
  if (from === undefined || gesturePress === undefined) return;
635
674
  pressedPointers.set(id, to);
675
+ gestureMoved = true;
636
676
  const { view, width, height, x, y } = gesturePress;
637
677
  const region = { ...(view === undefined ? {} : { view }), width, height, x, y };
638
678
  const moved = pixels(from, to);
@@ -672,11 +712,22 @@ export const createPointerTracker = (picker: Picker): PointerTracker => {
672
712
  });
673
713
  };
674
714
 
675
- const endGestures = (): void => {
715
+ const endGestures = (events: Array<SceneEvent>): void => {
676
716
  if (gesturePress !== undefined) picker.holdCameras(false);
717
+ if (gesturePress !== undefined && gestureMoved) {
718
+ const { button, modifiers, key, view } = gesturePress;
719
+ events.push({
720
+ _tag: 'GestureEnd',
721
+ button,
722
+ modifiers,
723
+ ...(key === undefined ? {} : { key }),
724
+ ...(view === undefined ? {} : { view }),
725
+ });
726
+ }
677
727
  pressedPointers.clear();
678
728
  primary = undefined;
679
729
  gesturePress = undefined;
730
+ gestureMoved = false;
680
731
  };
681
732
 
682
733
  /**
@@ -706,7 +757,7 @@ export const createPointerTracker = (picker: Picker): PointerTracker => {
706
757
  if (isSecondary(event, id)) return secondary(event, id);
707
758
  const events: Array<SceneEvent> = [];
708
759
  if (event._tag === 'PointerLeave') {
709
- endGestures();
760
+ endGestures(events);
710
761
  // A cancelled or abandoned press still ends its drag, at the last known position.
711
762
  if (position !== undefined) endDrag(captured(), position.x, position.y, events);
712
763
  dragged = false;
@@ -740,7 +791,7 @@ export const createPointerTracker = (picker: Picker): PointerTracker => {
740
791
  release(hit, event, events);
741
792
  // Any release ends capture: a mouse's buttons share one pointer.
742
793
  endDrag(drag, event.x, event.y, events);
743
- endGestures();
794
+ endGestures(events);
744
795
  break;
745
796
  case 'DoubleClick':
746
797
  if (hit !== undefined && pressed(hit)) {
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import type { Object3D, Scene } from 'three';
11
11
 
12
- import type { ContactShadows, ShaderGridNode, Sky } from './description.ts';
12
+ import type { ContactShadows, LineNode, PointsNode, ShaderGridNode, Sky } from './description.ts';
13
13
  import type { SceneRenderer } from './renderer.ts';
14
14
 
15
15
  /** A sky as a kit draws it: the object the graph adds to its scene, never picked. */
@@ -26,6 +26,24 @@ export type HostedShaderGrid = {
26
26
  readonly dispose: () => void;
27
27
  };
28
28
 
29
+ /** A line as a kit draws it (three's fat lines), inside the node's wrapper. */
30
+ export type HostedLine = {
31
+ readonly object: Object3D;
32
+ /** Applies new points or a new look; the wrapper carries the transform. */
33
+ readonly update: (line: LineNode) => void;
34
+ readonly dispose: () => void;
35
+ };
36
+
37
+ /** Points as a kit draws them, inside the node's wrapper. */
38
+ export type HostedPoints = {
39
+ readonly object: Object3D;
40
+ /** Applies a new look; the wrapper carries the transform. */
41
+ readonly update: (points: PointsNode) => void;
42
+ /** Moves sparkles and twinkles stars to scene time `seconds`. */
43
+ readonly tick: (seconds: number) => void;
44
+ readonly dispose: () => void;
45
+ };
46
+
29
47
  /** The contact shadow passes for one scene, drawn by the frame loop before each frame. */
30
48
  export type ContactShadowPasses = {
31
49
  /** Draws the scene's contact shadows, if it has any, before the frame is drawn. */
@@ -50,5 +68,9 @@ export type RenderKit = {
50
68
  readonly shading: 'webgl' | 'tsl';
51
69
  readonly sky: () => HostedSky;
52
70
  readonly shaderGrid: (node: ShaderGridNode) => HostedShaderGrid;
71
+ /** Draws a line; a kit without lines leaves them empty. */
72
+ readonly line?: (node: LineNode) => HostedLine;
73
+ /** Draws points; a kit without points leaves them empty. */
74
+ readonly points?: (node: PointsNode) => HostedPoints;
53
75
  readonly contactShadows: (graph: ShadowedScene, renderer: SceneRenderer) => ContactShadowPasses;
54
76
  };
@@ -43,6 +43,12 @@ export type SceneRenderer = {
43
43
  * otherwise across the whole surface.
44
44
  */
45
45
  readonly render: (scene: Scene, camera: Camera, area?: RenderArea, target?: RenderTarget) => void;
46
+ /**
47
+ * Draws `scene` over what the surface already shows, across the whole of it: keeping
48
+ * the colour, clearing only depth (a HUD, as Drei's `Hud` draws). Without it, a HUD is
49
+ * not drawn. Not while presenting immersively.
50
+ */
51
+ readonly renderOver?: (scene: Scene, camera: Camera) => void;
46
52
  /** Creates an offscreen target of `width` × `height` pixels to render into. */
47
53
  readonly createTarget: (width: number, height: number) => RenderTarget;
48
54
  /**
@@ -22,17 +22,21 @@ import {
22
22
  CircleGeometry,
23
23
  ConeGeometry,
24
24
  CylinderGeometry,
25
+ Euler,
25
26
  ExtrudeGeometry,
26
27
  Material,
27
28
  type Object3D,
28
29
  type Texture,
29
30
  PlaneGeometry,
31
+ Quaternion,
30
32
  RingGeometry,
33
+ Shape,
31
34
  ShapeGeometry as ShapeGeometry3,
32
35
  SphereGeometry,
33
36
  TorusGeometry,
34
37
  } from 'three';
35
38
  import type { Font } from 'three/addons/loaders/FontLoader.js';
39
+ import { toCreasedNormals } from 'three/addons/utils/BufferGeometryUtils.js';
36
40
 
37
41
  import type {
38
42
  GeometryRef,
@@ -191,6 +195,67 @@ const buildText = (shape: TextGeometry, font: Font | undefined): BufferGeometry
191
195
  return geometry;
192
196
  };
193
197
 
198
+ /** Drei's `RoundedBox` nudge, keeping its corner arcs from being zero length. */
199
+ const roundedEpsilon = 0.00001;
200
+
201
+ /**
202
+ * Drei's `RoundedBox` (`bf6f4ad`): the corners' arcs drawn tiny and the bevel making the
203
+ * rounding, extruded through the depth less the bevels, centred, and its normals creased
204
+ * (three's `toCreasedNormals`, which Drei takes from three-stdlib).
205
+ */
206
+ const buildRoundedBox = (shape: Extract<ShapeGeometry, { readonly _tag: 'RoundedBox' }>) => {
207
+ const { width, height, depth, radius } = shape;
208
+ const outline = new Shape();
209
+ const inset = radius - roundedEpsilon;
210
+ const e = roundedEpsilon;
211
+ outline.absarc(e, e, e, -Math.PI / 2, -Math.PI, true);
212
+ outline.absarc(e, height - inset * 2, e, Math.PI, Math.PI / 2, true);
213
+ outline.absarc(width - inset * 2, height - inset * 2, e, Math.PI / 2, 0, true);
214
+ outline.absarc(width - inset * 2, e, e, 0, -Math.PI / 2, true);
215
+ const geometry = new ExtrudeGeometry(outline, {
216
+ depth: depth - radius * 2,
217
+ bevelEnabled: true,
218
+ bevelSegments: shape.bevelSegments * 2,
219
+ steps: shape.steps,
220
+ bevelSize: radius - roundedEpsilon,
221
+ bevelThickness: radius,
222
+ curveSegments: shape.smoothness,
223
+ });
224
+ geometry.center();
225
+ return toCreasedNormals(geometry, shape.creaseAngle);
226
+ };
227
+
228
+ /** Drei's `Backdrop` mesh turn, built into the geometry: the floor flat, the wall behind. */
229
+ const backdropTurn = new Quaternion().setFromEuler(new Euler(-Math.PI / 2, 0, Math.PI / 2));
230
+
231
+ /** Drei's `Backdrop` easing: the floor stays flat, then sweeps up. */
232
+ const easeInExpo = (x: number): number => (x === 0 ? 0 : 2 ** (10 * x - 10));
233
+
234
+ /**
235
+ * Drei's `Backdrop` (`bf6f4ad`): a unit plane whose rows rise by an exponential ease, its
236
+ * first row pulled forward by `floor`, then turned as Drei turns its mesh.
237
+ */
238
+ const buildBackdrop = (shape: Extract<ShapeGeometry, { readonly _tag: 'Backdrop' }>) => {
239
+ const { segments, floor } = shape;
240
+ const geometry = new PlaneGeometry(1, 1, segments, segments);
241
+ const position = geometry.getAttribute('position');
242
+ const offset = 0.5;
243
+ let vertex = 0;
244
+ for (let x = 0; x < segments + 1; x++) {
245
+ for (let y = 0; y < segments + 1; y++) {
246
+ position.setXYZ(
247
+ vertex++,
248
+ x / segments - offset + (x === 0 ? -floor : 0),
249
+ y / segments - offset,
250
+ easeInExpo(x / segments),
251
+ );
252
+ }
253
+ }
254
+ position.needsUpdate = true;
255
+ geometry.computeVertexNormals();
256
+ return geometry.applyQuaternion(backdropTurn);
257
+ };
258
+
194
259
  /** Builds the three.js geometry for a shape description. */
195
260
  const buildShape = (
196
261
  shape: ShapeGeometry,
@@ -232,6 +297,10 @@ const buildShape = (
232
297
  return new RingGeometry(shape.innerRadius, shape.outerRadius, shape.thetaSegments);
233
298
  case 'Circle':
234
299
  return new CircleGeometry(shape.radius, shape.segments);
300
+ case 'RoundedBox':
301
+ return buildRoundedBox(shape);
302
+ case 'Backdrop':
303
+ return buildBackdrop(shape);
235
304
  }
236
305
  };
237
306