@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
@@ -30,14 +30,21 @@ import { clone as cloneSkinned } from 'three/addons/utils/SkeletonUtils.js';
30
30
 
31
31
  import { clipRunning, clipTime } from './animation.ts';
32
32
  import { writeRotation } from './behaviours.ts';
33
- import { type BodyCollision, type BodyContactForce, createBodyHost } from './bodies.ts';
33
+ import {
34
+ type BodyCollision,
35
+ type BodyContactForce,
36
+ createBodyHost,
37
+ type PhysicsReached,
38
+ } from './bodies.ts';
34
39
  import { type BodySource, instanceSources, nodeSource, placeObject } from './bodySources.ts';
35
- import { renderSettings, scene as describeScene } from './build.ts';
40
+ import { orthographicCamera, renderSettings, scene as describeScene } from './build.ts';
36
41
  import { type CameraMotion, createCameraMotion } from './cameraMotion.ts';
37
42
  import { type CameraShaker, createCameraShaker } from './cameraShake.ts';
38
43
  import type {
44
+ Hud,
39
45
  AmbientLightNode,
40
46
  BorrowedNode,
47
+ Edges,
41
48
  DirectionalLightNode,
42
49
  GridNode,
43
50
  HemisphereLightNode,
@@ -45,6 +52,8 @@ import type {
45
52
  Instance,
46
53
  InstancesNode,
47
54
  LabelNode,
55
+ LineNode,
56
+ PointsNode,
48
57
  MaterialRef,
49
58
  ShaderGridNode,
50
59
  MeshNode,
@@ -68,9 +77,16 @@ import type {
68
77
  ViewRect,
69
78
  } from './description.ts';
70
79
  import { diff, type Field, type Op } from './diff.ts';
71
- import { contactShadowsEquals, environmentEquals, overlaysEqual, skyEquals } from './equality.ts';
80
+ import {
81
+ contactShadowsEquals,
82
+ edgesEquals,
83
+ environmentEquals,
84
+ overlaysEqual,
85
+ skyEquals,
86
+ } from './equality.ts';
72
87
  import { createInstances, fits, type HostInstances, writeInstances } from './instances.ts';
73
88
  import type { HostedLabel, LabelEngine, LabelLayout } from './labels.ts';
89
+ import { edgeLine, edgePoints } from './lineGeometry.ts';
74
90
  import type { HostMaterial } from './materials.ts';
75
91
  import {
76
92
  type Behaviour,
@@ -80,7 +96,14 @@ import {
80
96
  resetPose,
81
97
  } from './motion.ts';
82
98
  import type { BodyPose, PhysicsEngine } from './physics.ts';
83
- import type { HostedShaderGrid, HostedSky, RenderKit } from './renderKit.ts';
99
+ import { animatedLook } from './pointBuffers.ts';
100
+ import type {
101
+ HostedLine,
102
+ HostedPoints,
103
+ HostedShaderGrid,
104
+ HostedSky,
105
+ RenderKit,
106
+ } from './renderKit.ts';
84
107
  import { createResourceStore, type ResourceStore } from './resources.ts';
85
108
  import {
86
109
  type DescriptionError,
@@ -89,6 +112,7 @@ import {
89
112
  type ParentKey,
90
113
  type SceneIndex,
91
114
  } from './sceneIndex.ts';
115
+ import { createTrailRibbon, type TrailRibbon } from './trail.ts';
92
116
  import { applyTween, startTween, type Tween } from './tween.ts';
93
117
 
94
118
  /** The mesh type the kernel hosts. */
@@ -142,6 +166,20 @@ type Entry =
142
166
  readonly _tag: 'HemisphereLight';
143
167
  node: HemisphereLightNode;
144
168
  readonly object: HemisphereLight;
169
+ }
170
+ // `hosted` is the points the render kit drew, inside the wrapper; none without a kit.
171
+ | {
172
+ readonly _tag: 'Points';
173
+ node: PointsNode;
174
+ readonly object: Group;
175
+ readonly hosted: HostedPoints | undefined;
176
+ }
177
+ // `hosted` is the line the render kit drew, inside the wrapper; none without a kit.
178
+ | {
179
+ readonly _tag: 'Line';
180
+ node: LineNode;
181
+ readonly object: Group;
182
+ readonly hosted: HostedLine | undefined;
145
183
  };
146
184
 
147
185
  /** Entries with a transform: position, rotation, scale and transitions. */
@@ -154,12 +192,26 @@ type Player = {
154
192
  readonly startedAt: number;
155
193
  };
156
194
 
195
+ /** A mesh's edge lines: the kit's line, and the geometry and look it was built from. */
196
+ type EdgeLines = {
197
+ readonly hosted: HostedLine;
198
+ geometry: BufferGeometry | undefined;
199
+ edges: Edges | undefined;
200
+ };
201
+
157
202
  /** Descriptions with a transform. */
158
- type PlacedNode = GroupNode | MeshNode | InstancesNode | BorrowedNode | LabelNode;
203
+ type PlacedNode =
204
+ | GroupNode
205
+ | MeshNode
206
+ | InstancesNode
207
+ | BorrowedNode
208
+ | LabelNode
209
+ | LineNode
210
+ | PointsNode;
159
211
 
160
212
  type Placed = Extract<
161
213
  Entry,
162
- { readonly _tag: 'Group' | 'Mesh' | 'Instances' | 'Borrowed' | 'Label' }
214
+ { readonly _tag: 'Group' | 'Mesh' | 'Instances' | 'Borrowed' | 'Label' | 'Line' | 'Points' }
163
215
  >;
164
216
 
165
217
  /** Entries drawing a geometry with a material. */
@@ -170,12 +222,17 @@ const isPlaced = (entry: Entry): entry is Placed =>
170
222
  entry._tag === 'Mesh' ||
171
223
  entry._tag === 'Instances' ||
172
224
  entry._tag === 'Borrowed' ||
173
- entry._tag === 'Label';
225
+ entry._tag === 'Label' ||
226
+ entry._tag === 'Line' ||
227
+ entry._tag === 'Points';
174
228
 
175
229
  const isSurfaced = (entry: Entry): entry is Surfaced =>
176
230
  entry._tag === 'Mesh' || entry._tag === 'Instances';
177
231
 
178
232
  /** A render texture as hosted: its id, size and the nested graph that draws it. */
233
+ /** The HUD as hosted: its nested graph, drawn over the scene by the frame loop. */
234
+ export type HostedHud = { readonly graph: SceneGraph };
235
+
179
236
  export type HostedTexture = {
180
237
  readonly id: string;
181
238
  readonly graph: SceneGraph;
@@ -232,6 +289,8 @@ export type SceneGraph = {
232
289
  readonly turned: (view: string | undefined) => Vec3 | undefined;
233
290
  /** Render textures, each a nested graph to draw into its texture (by the frame loop). */
234
291
  readonly renderTextures: () => ReadonlyArray<HostedTexture>;
292
+ /** The HUD, a nested graph drawn over the scene (by the frame loop), if there is one. */
293
+ readonly hud: () => HostedHud | undefined;
235
294
  /** Whether the scene claims the pointer's gestures from the page. */
236
295
  readonly gestures: () => Gestures;
237
296
  /** Points to show HTML at; the frame loop places them after drawing. */
@@ -244,6 +303,8 @@ export type SceneGraph = {
244
303
  readonly collisions: () => ReadonlyArray<BodyCollision>;
245
304
  /** Contact forces over their bodies' thresholds since the last call. */
246
305
  readonly contactForces: () => ReadonlyArray<BodyContactForce>;
306
+ /** Steps a world on `physics({ until })` has reached since the last call. */
307
+ readonly physicsReached: () => ReadonlyArray<PhysicsReached>;
247
308
  /** How the scene is presented immersively, for the host; `null` if it never is. */
248
309
  readonly xr: () => Xr | null;
249
310
  /** Keys of the nodes whose bounds the frame loop reports. */
@@ -448,6 +509,9 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
448
509
  { readonly graph: SceneGraph; description: RenderTexture }
449
510
  >();
450
511
  let textureList: ReadonlyArray<HostedTexture> = [];
512
+ let hudHosted:
513
+ | { readonly graph: SceneGraph; description: Hud; parent: SceneDescription }
514
+ | undefined;
451
515
  let viewDescriptions: ReadonlyArray<View> = [];
452
516
  let gestures: Gestures = emptyScene.gestures;
453
517
  let overlayList: ReadonlyArray<Overlay> = emptyScene.overlays;
@@ -557,8 +621,30 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
557
621
  const spinning = new Set<Entry>();
558
622
  // Billboards: turned towards the camera each frame, after everything else has moved.
559
623
  const facing = new Set<Entry>();
624
+ // Parents first, so a billboard inside a billboard reads this frame's parent turn.
625
+ const facingOrder: Array<Entry> = [];
560
626
  const cameraTurn = new Quaternion();
561
627
  const parentTurn = new Quaternion();
628
+ // Points that move by themselves (sparkles, stars), ticked in scene time while animating.
629
+ const sparkling = new Set<Entry>();
630
+ const trackSparkle = (entry: Entry, node: PointsNode): void => {
631
+ if (animatedLook(node.look)) sparkling.add(entry);
632
+ else sparkling.delete(entry);
633
+ if (entry._tag === 'Points') entry.hosted?.tick(animating ? elapsed : 0);
634
+ };
635
+ const sparkle = (entry: Entry): void => {
636
+ if (entry._tag === 'Points') entry.hosted?.tick(elapsed);
637
+ };
638
+ // Groups and meshes with trails: each ribbon, in the scene's space.
639
+ const trailing = new Map<Entry, TrailRibbon>();
640
+ const trailPlace = new Vector3();
641
+ const eye = new Vector3();
642
+ const trailsMoving = (): boolean => {
643
+ for (const ribbon of trailing.values()) if (!ribbon.settled()) return true;
644
+ return false;
645
+ };
646
+ // Meshes with edge lines: rebuilt when the mesh's geometry or the edges' look changes.
647
+ const edged = new Map<Entry, EdgeLines>();
562
648
  // Groups with levels of detail: one child shown at a time, by the camera's distance.
563
649
  const detailing = new Set<Entry>();
564
650
  const cameraPlace = new Vector3();
@@ -639,6 +725,99 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
639
725
  if (lockZ) object.rotation.z = 0;
640
726
  };
641
727
 
728
+ const depthOf = (object: Object3D): number => {
729
+ let depth = 0;
730
+ for (let parent = object.parent; parent !== null; parent = parent.parent) depth += 1;
731
+ return depth;
732
+ };
733
+
734
+ /**
735
+ * Faces every billboard, shallowest first: a nested one is turned against its parent's
736
+ * world rotation, so the parent must have turned this frame. Drei gets this from React
737
+ * running a parent's frame callback before its child's.
738
+ */
739
+ const faceCameras = (): void => {
740
+ facingOrder.length = 0;
741
+ facing.forEach((entry) => facingOrder.push(entry));
742
+ facingOrder.sort((a, b) => depthOf(a.object) - depthOf(b.object));
743
+ facingOrder.forEach(faceCamera);
744
+ };
745
+
746
+ /** Where a trail follows its node: the world position, or with `local` its own. */
747
+ const trailTarget = (object: Object3D, local: boolean): Vector3 =>
748
+ local ? trailPlace.copy(object.position) : object.getWorldPosition(trailPlace);
749
+
750
+ /** Adds, restyles or removes a node's trail as its description says. */
751
+ const trackTrail = (entry: Entry, node: GroupNode | MeshNode): void => {
752
+ const current = trailing.get(entry);
753
+ if (node.trail === null) {
754
+ if (current === undefined) return;
755
+ current.object.removeFromParent();
756
+ current.dispose();
757
+ trailing.delete(entry);
758
+ return;
759
+ }
760
+ const start = trailTarget(entry.object, node.trail.local);
761
+ if (current !== undefined) {
762
+ current.restyle(node.trail, start, elapsed);
763
+ return;
764
+ }
765
+ const ribbon = createTrailRibbon(node.trail, start, elapsed);
766
+ scene.add(ribbon.object);
767
+ trailing.set(entry, ribbon);
768
+ };
769
+
770
+ /** Records where a trailing node is now, and turns its ribbon to the camera. */
771
+ const followTrail = (ribbon: TrailRibbon, entry: Entry): void => {
772
+ if (entry._tag !== 'Group' && entry._tag !== 'Mesh') return;
773
+ if (entry.node.trail === null) return;
774
+ ribbon.record(trailTarget(entry.object, entry.node.trail.local), elapsed);
775
+ ribbon.face(eye);
776
+ };
777
+
778
+ /** Drei's `Edges` turns off picking on its line: hits belong to the mesh. */
779
+ const unpickable = (part: Object3D): void => {
780
+ part.raycast = () => undefined;
781
+ };
782
+
783
+ /** Adds, keeps or removes a mesh's edge lines as its description says. */
784
+ const trackEdges = (entry: Entry, node: MeshNode): void => {
785
+ const current = edged.get(entry);
786
+ const draw = options.kit?.line;
787
+ if (node.edges === null || draw === undefined) {
788
+ if (current === undefined) return;
789
+ current.hosted.object.removeFromParent();
790
+ current.hosted.dispose();
791
+ edged.delete(entry);
792
+ return;
793
+ }
794
+ if (current !== undefined) return;
795
+ const hosted = draw(edgeLine(node.key, node.edges, []));
796
+ entry.object.add(hosted.object);
797
+ edged.set(entry, { hosted, geometry: undefined, edges: undefined });
798
+ };
799
+
800
+ /**
801
+ * Builds the edge lines again when the mesh shows another geometry (its own changed, a
802
+ * shared one was replaced, a lent one arrived) or the edges' look changed.
803
+ */
804
+ const refreshEdges = (lines: EdgeLines, entry: Entry): void => {
805
+ if (entry._tag !== 'Mesh' || entry.node.edges === null) return;
806
+ const { geometry } = entry.object;
807
+ const look = entry.node.edges;
808
+ if (
809
+ geometry === lines.geometry &&
810
+ lines.edges !== undefined &&
811
+ edgesEquals(look, lines.edges)
812
+ ) {
813
+ return;
814
+ }
815
+ lines.hosted.update(edgeLine(entry.node.key, look, edgePoints(geometry, look.threshold)));
816
+ lines.hosted.object.traverse(unpickable);
817
+ lines.geometry = geometry;
818
+ lines.edges = look;
819
+ };
820
+
642
821
  /** The object of a group's `index`th child, as described. */
643
822
  const childObject = (node: GroupNode, index: number): Object3D | undefined => {
644
823
  const child = node.children[index];
@@ -704,6 +883,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
704
883
  // A group following a tracked space shows only once the space is found.
705
884
  if (node.follow !== null) entry.object.visible = false;
706
885
  trackDetail(entry, node);
886
+ trackTrail(entry, node);
707
887
  return entry;
708
888
  }
709
889
  case 'Mesh': {
@@ -719,6 +899,8 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
719
899
  trackInteractive(object, node.interactive);
720
900
  object.castShadow = node.castShadow;
721
901
  object.receiveShadow = node.receiveShadow;
902
+ trackEdges(entry, node);
903
+ trackTrail(entry, node);
722
904
  return entry;
723
905
  }
724
906
  case 'Instances': {
@@ -799,6 +981,28 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
799
981
  const hosted = options.kit?.shaderGrid(node);
800
982
  return { _tag: 'ShaderGrid', node, object: hosted?.object ?? new Group(), hosted };
801
983
  }
984
+ case 'Points': {
985
+ const hosted = options.kit?.points?.(node);
986
+ const entry: Entry = { _tag: 'Points', node, object: new Group(), hosted };
987
+ if (hosted !== undefined) entry.object.add(hosted.object);
988
+ writeVec3(entry.object.position, node.position);
989
+ writeVec3(entry.object.scale, node.scale);
990
+ trackRotation(entry, node);
991
+ trackBehaviour(entry, node);
992
+ trackSparkle(entry, node);
993
+ return entry;
994
+ }
995
+ case 'Line': {
996
+ const hosted = options.kit?.line?.(node);
997
+ const entry: Entry = { _tag: 'Line', node, object: new Group(), hosted };
998
+ if (hosted !== undefined) entry.object.add(hosted.object);
999
+ writeVec3(entry.object.position, node.position);
1000
+ writeVec3(entry.object.scale, node.scale);
1001
+ trackRotation(entry, node);
1002
+ trackBehaviour(entry, node);
1003
+ trackInteractive(entry.object, node.interactive);
1004
+ return entry;
1005
+ }
802
1006
  case 'HemisphereLight': {
803
1007
  const object = new HemisphereLight();
804
1008
  writeHemisphereLight(object, node);
@@ -973,6 +1177,17 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
973
1177
  spinning.delete(entry);
974
1178
  facing.delete(entry);
975
1179
  detailing.delete(entry);
1180
+ const lines = edged.get(entry);
1181
+ if (lines !== undefined) {
1182
+ lines.hosted.dispose();
1183
+ edged.delete(entry);
1184
+ }
1185
+ const ribbon = trailing.get(entry);
1186
+ if (ribbon !== undefined) {
1187
+ ribbon.object.removeFromParent();
1188
+ ribbon.dispose();
1189
+ trailing.delete(entry);
1190
+ }
976
1191
  moving.delete(entry);
977
1192
  reparented.delete(entry);
978
1193
  trackInteractive(entry.object, false);
@@ -984,7 +1199,10 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
984
1199
  if (entry._tag === 'Instances') entry.object.dispose();
985
1200
  // A grid's lines are its own.
986
1201
  if (entry._tag === 'Grid') disposeGridLines(entry.helper);
987
- if (entry._tag === 'ShaderGrid') entry.hosted?.dispose();
1202
+ if (entry._tag === 'ShaderGrid' || entry._tag === 'Line' || entry._tag === 'Points') {
1203
+ entry.hosted?.dispose();
1204
+ }
1205
+ sparkling.delete(entry);
988
1206
  if (entry._tag === 'Label') {
989
1207
  entry.hosted?.dispose();
990
1208
  if (entry.node.material !== null && entry.material !== undefined) {
@@ -1114,6 +1332,12 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1114
1332
  entry.node = next;
1115
1333
  if (movesTransform(entry, fields)) retarget(entry, next);
1116
1334
  if (fields.includes('rotation')) trackRotation(entry, next);
1335
+ if (entry._tag === 'Mesh' && next._tag === 'Mesh' && fields.includes('edges')) {
1336
+ trackEdges(entry, next);
1337
+ }
1338
+ if (entry._tag === 'Mesh' && next._tag === 'Mesh' && fields.includes('trail')) {
1339
+ trackTrail(entry, next);
1340
+ }
1117
1341
  if (fields.includes('behaviour')) trackBehaviour(entry, next);
1118
1342
  if (fields.includes('interactive')) trackInteractive(mesh, next.interactive);
1119
1343
  mesh.castShadow = next.castShadow;
@@ -1150,6 +1374,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1150
1374
  if (fields.includes('behaviour')) trackBehaviour(entry, next);
1151
1375
  if (fields.includes('interactive')) trackInteractive(entry.object, next.interactive);
1152
1376
  if (fields.includes('detail')) trackDetail(entry, next);
1377
+ if (fields.includes('trail')) trackTrail(entry, next);
1153
1378
  if (fields.includes('follow')) {
1154
1379
  // Back to its own transform, or hidden until the new space is found.
1155
1380
  entry.object.visible = next.follow === null;
@@ -1186,6 +1411,26 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1186
1411
  entry.node = next;
1187
1412
  return;
1188
1413
  }
1414
+ if (entry._tag === 'Points' && next._tag === 'Points') {
1415
+ entry.node = next;
1416
+ if (movesTransform(entry, fields)) retarget(entry, next);
1417
+ if (fields.includes('rotation')) trackRotation(entry, next);
1418
+ if (fields.includes('behaviour')) trackBehaviour(entry, next);
1419
+ if (fields.includes('look')) {
1420
+ entry.hosted?.update(next);
1421
+ trackSparkle(entry, next);
1422
+ }
1423
+ return;
1424
+ }
1425
+ if (entry._tag === 'Line' && next._tag === 'Line') {
1426
+ entry.node = next;
1427
+ if (movesTransform(entry, fields)) retarget(entry, next);
1428
+ if (fields.includes('rotation')) trackRotation(entry, next);
1429
+ if (fields.includes('behaviour')) trackBehaviour(entry, next);
1430
+ if (fields.includes('interactive')) trackInteractive(entry.object, next.interactive);
1431
+ if (fields.includes('line')) entry.hosted?.update(next);
1432
+ return;
1433
+ }
1189
1434
  if (entry._tag === 'ShaderGrid' && next._tag === 'ShaderGrid') {
1190
1435
  entry.hosted?.update(entry.node, next);
1191
1436
  entry.node = next;
@@ -1317,6 +1562,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1317
1562
  gestures: 'page',
1318
1563
  views: [],
1319
1564
  renderTextures: [],
1565
+ hud: null,
1320
1566
  overlays: [],
1321
1567
  environment: null,
1322
1568
  sky: null,
@@ -1324,11 +1570,84 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1324
1570
  physics: null,
1325
1571
  xr: null,
1326
1572
  measure: [],
1573
+ assets: [],
1327
1574
  render: parent.render,
1328
1575
  resources: parent.resources,
1329
1576
  children: texture.children,
1330
1577
  });
1331
1578
 
1579
+ /**
1580
+ * The HUD's camera: orthographic in CSS pixels, from in front, aimed so that the HUD's
1581
+ * origin falls `margin` in from the aligned edges (as Drei's `GizmoHelper` places its
1582
+ * group). The camera moves rather than the nodes, so their positions stay the Model's.
1583
+ */
1584
+ const hudCamera = (hud: Hud): CameraDescription => {
1585
+ const [marginX, marginY] = hud.margin;
1586
+ const [vertical, horizontal] = hud.alignment.split('-');
1587
+ const across = { left: -viewport.width / 2 + marginX, right: viewport.width / 2 - marginX };
1588
+ const up = { top: viewport.height / 2 - marginY, bottom: -viewport.height / 2 + marginY };
1589
+ const x = across[horizontal as keyof typeof across] ?? 0;
1590
+ const y = up[vertical as keyof typeof up] ?? 0;
1591
+ return orthographicCamera({
1592
+ zoom: 1,
1593
+ near: 0.1,
1594
+ far: 2000,
1595
+ position: [-x, -y, 200],
1596
+ lookAt: [-x, -y, 0],
1597
+ });
1598
+ };
1599
+
1600
+ /** The HUD as a scene of its own, sharing the parent's resources. */
1601
+ const hudScene = (hud: Hud, parent: SceneDescription): SceneDescription =>
1602
+ textureScene(
1603
+ {
1604
+ id: 'hud',
1605
+ width: viewport.width,
1606
+ height: viewport.height,
1607
+ camera: hudCamera(hud),
1608
+ background: null,
1609
+ children: hud.children,
1610
+ },
1611
+ parent,
1612
+ );
1613
+
1614
+ /**
1615
+ * Brings the HUD in line with the description, `parent` the scene it is over: a nested
1616
+ * graph on the same store, created, reconciled, re-aimed or disposed.
1617
+ */
1618
+ const syncHud = (next: SceneDescription): ReadonlyArray<Op> => {
1619
+ if (next.hud === null) {
1620
+ if (hudHosted === undefined) return [];
1621
+ hudHosted.graph.dispose();
1622
+ hudHosted = undefined;
1623
+ return [{ _tag: 'RemoveHud' }];
1624
+ }
1625
+ const created = hudHosted === undefined;
1626
+ const hosted = hudHosted ?? {
1627
+ graph: createSceneGraph({
1628
+ store,
1629
+ ...(options.behaviours === undefined ? {} : { behaviours: options.behaviours }),
1630
+ ...(options.kit === undefined ? {} : { kit: options.kit }),
1631
+ }),
1632
+ description: next.hud,
1633
+ parent: next,
1634
+ };
1635
+ if (created) hosted.graph.resize(viewport.width, viewport.height);
1636
+ const result = hosted.graph.reconcile(hudScene(next.hud, next));
1637
+ hosted.description = next.hud;
1638
+ hosted.parent = next;
1639
+ hudHosted = hosted;
1640
+ const ops = result._tag === 'Applied' ? result.ops : [];
1641
+ return created || ops.length > 0 ? [{ _tag: 'UpdateHud', ops }] : [];
1642
+ };
1643
+
1644
+ /** The HUD fitted to a resized canvas: its camera re-aimed at the aligned point. */
1645
+ const refitHud = (): void => {
1646
+ if (hudHosted === undefined) return;
1647
+ hudHosted.graph.resize(viewport.width, viewport.height);
1648
+ hudHosted.graph.reconcile(hudScene(hudHosted.description, hudHosted.parent));
1649
+ };
1650
+
1332
1651
  /** A hosted render texture as the loop sees it; none if it is not hosted. */
1333
1652
  function hostedTexture(texture: RenderTexture): ReadonlyArray<HostedTexture> {
1334
1653
  const hosted = textureGraphs.get(texture.id);
@@ -1389,6 +1708,10 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1389
1708
  const checked = indexScene(textureScene(texture, next), knownBehaviours);
1390
1709
  if (checked._tag === 'Invalid') return checked;
1391
1710
  }
1711
+ if (next.hud !== null) {
1712
+ const checked = indexScene(hudScene(next.hud, next), knownBehaviours);
1713
+ if (checked._tag === 'Invalid') return checked;
1714
+ }
1392
1715
  const result = diff(index, next, knownBehaviours);
1393
1716
  if (result._tag === 'Invalid') return result;
1394
1717
  for (const op of result.ops) apply(op);
@@ -1417,12 +1740,14 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1417
1740
  if ((physicsDescription?.debug ?? false) !== (next.physics?.debug ?? false)) invalidate();
1418
1741
  physicsDescription = next.physics;
1419
1742
  bodyHost.sync(next.physics, bodySources());
1743
+ // A world on `until` that owes steps needs frames, though nothing else may be moving.
1744
+ if (bodyHost.owesSteps()) invalidate();
1420
1745
  // Read by the host when it presents the scene immersively: no operation, no frame.
1421
1746
  xrDescription = next.xr;
1422
1747
  // Measured after the next drawn frame; a new list needs one.
1423
1748
  if (next.measure.join('\u0000') !== measured.join('\u0000')) invalidate();
1424
1749
  measured = next.measure;
1425
- const ops = [...result.ops, ...syncTextures(next)];
1750
+ const ops = [...result.ops, ...syncTextures(next), ...syncHud(next)];
1426
1751
  if (ops.length > 0) invalidate();
1427
1752
  return { _tag: 'Applied', ops };
1428
1753
  };
@@ -1435,6 +1760,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1435
1760
  const tick = (elapsedSeconds: number): void => {
1436
1761
  elapsed = elapsedSeconds;
1437
1762
  for (const texture of textureList) texture.graph.tick(elapsedSeconds);
1763
+ hudHosted?.graph.tick(elapsedSeconds);
1438
1764
  for (const entry of spinning) {
1439
1765
  if (isPlaced(entry)) {
1440
1766
  writeRotation(entry.object.rotation, entry.node.rotation, elapsed);
@@ -1453,7 +1779,14 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1453
1779
  // After the camera and everything a billboard may sit on have moved.
1454
1780
  if (facing.size > 0) {
1455
1781
  camera.getWorldQuaternion(cameraTurn);
1456
- facing.forEach(faceCamera);
1782
+ faceCameras();
1783
+ }
1784
+ edged.forEach(refreshEdges);
1785
+ // Reduced motion holds sparkles and stars where they are.
1786
+ if (animating) sparkling.forEach(sparkle);
1787
+ if (trailing.size > 0) {
1788
+ camera.getWorldPosition(eye);
1789
+ trailing.forEach(followTrail);
1457
1790
  }
1458
1791
  if (detailing.size > 0) {
1459
1792
  camera.getWorldPosition(cameraPlace);
@@ -1468,6 +1801,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1468
1801
  const setAnimating = (next: boolean): void => {
1469
1802
  if (next !== animating) invalidate();
1470
1803
  for (const texture of textureList) texture.graph.setAnimating(next);
1804
+ hudHosted?.graph.setAnimating(next);
1471
1805
  animating = next;
1472
1806
  if (next) return;
1473
1807
  moving.forEach(finishTween);
@@ -1482,6 +1816,8 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1482
1816
  for (const texture of textureList) texture.graph.dispose();
1483
1817
  textureGraphs.clear();
1484
1818
  textureList = [];
1819
+ hudHosted?.graph.dispose();
1820
+ hudHosted = undefined;
1485
1821
  for (const entry of entries.values()) {
1486
1822
  entry.object.removeFromParent();
1487
1823
  release(entry);
@@ -1512,12 +1848,14 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1512
1848
  return viewMotions.get(view)?.turned();
1513
1849
  },
1514
1850
  renderTextures: () => textureList,
1851
+ hud: () => hudHosted,
1515
1852
  gestures: () => gestures,
1516
1853
  overlays: () => overlayList,
1517
1854
  environment: () => environmentDescription,
1518
1855
  contactShadows: () => contactDescription,
1519
1856
  collisions: bodyHost.drainCollisions,
1520
1857
  contactForces: bodyHost.drainContactForces,
1858
+ physicsReached: bodyHost.drainReached,
1521
1859
  xr: () => xrDescription,
1522
1860
  measured: () => measured,
1523
1861
  labelLayout: (key) => {
@@ -1540,7 +1878,9 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1540
1878
  renderSettings: () => settings,
1541
1879
  needsFrame: () =>
1542
1880
  dirty ||
1881
+ trailsMoving() ||
1543
1882
  textureList.some((texture) => texture.graph.needsFrame()) ||
1883
+ (hudHosted?.graph.needsFrame() ?? false) ||
1544
1884
  (animating &&
1545
1885
  (spinning.size > 0 ||
1546
1886
  moving.size > 0 ||
@@ -1549,7 +1889,9 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1549
1889
  cameraMotion.moving() ||
1550
1890
  viewsMoving() ||
1551
1891
  shaking() ||
1552
- bodyHost.needsFrame())),
1892
+ sparkling.size > 0 ||
1893
+ bodyHost.needsFrame())) ||
1894
+ bodyHost.owesSteps(),
1553
1895
  drawn: () => {
1554
1896
  dirty = false;
1555
1897
  },
@@ -1576,6 +1918,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1576
1918
  viewport = { width, height };
1577
1919
  fitCamera(camera, viewport);
1578
1920
  hostViews(viewDescriptions);
1921
+ refitHud();
1579
1922
  invalidate();
1580
1923
  },
1581
1924
  dispose,
@@ -32,6 +32,8 @@ export type DescriptionError =
32
32
  | { readonly _tag: 'UnknownOverlayNode'; readonly key: string; readonly node: string }
33
33
  | { readonly _tag: 'UnknownOverlayView'; readonly key: string; readonly view: string }
34
34
  | { readonly _tag: 'UnknownMeasuredNode'; readonly key: string }
35
+ /** Two assets the scene asks for share an id. */
36
+ | { readonly _tag: 'DuplicateAssetId'; readonly id: string }
35
37
  /** Two joints share a key. */
36
38
  | { readonly _tag: 'DuplicateJointKey'; readonly key: string }
37
39
  /** A joint names a node that is not in the scene. */
@@ -104,6 +106,12 @@ export const indexScene = (
104
106
  if (duplicateView !== undefined) {
105
107
  return { _tag: 'Invalid', error: { _tag: 'DuplicateViewKey', key: duplicateView } };
106
108
  }
109
+ const duplicateAsset = firstDuplicateInstance(
110
+ description.assets.map((asset) => ({ key: asset.id })),
111
+ );
112
+ if (duplicateAsset !== undefined) {
113
+ return { _tag: 'Invalid', error: { _tag: 'DuplicateAssetId', id: duplicateAsset } };
114
+ }
107
115
  const duplicateOverlay = firstDuplicateInstance(description.overlays);
108
116
  if (duplicateOverlay !== undefined) {
109
117
  return { _tag: 'Invalid', error: { _tag: 'DuplicateOverlayKey', key: duplicateOverlay } };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A small seeded random generator (mulberry32): the same numbers for the same seed on
3
+ * every page load, so randomly scattered scene parts are reproducible.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ /** A generator of numbers in [0, 1) from `seed`, as `Math.random` would give them. */
9
+ export const seededRandom = (seed: number): (() => number) => {
10
+ let state = seed >>> 0;
11
+ return () => {
12
+ state = (state + 0x6d2b79f5) >>> 0;
13
+ let t = state;
14
+ t = Math.imul(t ^ (t >>> 15), t | 1);
15
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
16
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
17
+ };
18
+ };