@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
@@ -3,8 +3,16 @@
3
3
  *
4
4
  * @module
5
5
  */
6
+ import { CatmullRomCurve3, Color, CubicBezierCurve3, QuadraticBezierCurve3, Vector3 } from 'three';
7
+
6
8
  import type {
9
+ Asset,
7
10
  AmbientLightNode,
11
+ LineNode,
12
+ PointLook,
13
+ PointsNode,
14
+ Edges,
15
+ Trail,
8
16
  Animation,
9
17
  CameraShake,
10
18
  Detail,
@@ -18,6 +26,8 @@ import type {
18
26
  ContactShadows,
19
27
  Environment,
20
28
  Gestures,
29
+ Hud,
30
+ HudAlignment,
21
31
  GridNode,
22
32
  Sky,
23
33
  Xr,
@@ -344,6 +354,7 @@ export const basic = (options: Partial<Omit<BasicMaterial, '_tag'>> = {}): Basic
344
354
  transparent: options.transparent ?? false,
345
355
  side: options.side ?? 'front',
346
356
  wireframe: options.wireframe ?? false,
357
+ depthTest: options.depthTest ?? true,
347
358
  });
348
359
 
349
360
  /**
@@ -539,6 +550,7 @@ export const group = (
539
550
  readonly body?: Body | null;
540
551
  readonly follow?: XrSpace | null;
541
552
  readonly detail?: Detail | null;
553
+ readonly trail?: Trail | null;
542
554
  },
543
555
  ): GroupNode => ({
544
556
  _tag: 'Group',
@@ -553,6 +565,7 @@ export const group = (
553
565
  body: options.body ?? null,
554
566
  follow: options.follow ?? null,
555
567
  detail: options.detail ?? null,
568
+ trail: options.trail ?? null,
556
569
  });
557
570
 
558
571
  /**
@@ -566,6 +579,272 @@ export const detailed = (options: {
566
579
  readonly hysteresis?: number;
567
580
  }): Detail => ({ distances: options.distances, hysteresis: options.hysteresis ?? 0 });
568
581
 
582
+ /**
583
+ * A line through `points`, as Drei's `Line` (`bf6f4ad`, `src/core/Line.tsx`), with its
584
+ * defaults: white, 1 pixel wide, solid, opaque. three's `LineMaterial` defaults the dashes:
585
+ * size 1, gap 1, scale 1, offset 0. `segments: true` is Drei's `Segments`: pairs of points.
586
+ */
587
+ export const line = (
588
+ options: TransformOptions & {
589
+ readonly points: ReadonlyArray<Vec3>;
590
+ readonly segments?: boolean;
591
+ readonly color?: Rgb;
592
+ readonly vertexColors?: ReadonlyArray<Rgb> | null;
593
+ readonly width?: number;
594
+ readonly worldUnits?: boolean;
595
+ readonly dashed?: boolean;
596
+ readonly dashSize?: number;
597
+ readonly gapSize?: number;
598
+ readonly dashScale?: number;
599
+ readonly dashOffset?: number;
600
+ readonly opacity?: number;
601
+ readonly transparent?: boolean;
602
+ readonly interactive?: boolean;
603
+ },
604
+ ): LineNode => ({
605
+ _tag: 'Line',
606
+ key: options.key,
607
+ position: options.position ?? origin,
608
+ rotation: options.rotation ?? noRotation,
609
+ scale: options.scale ?? unitScale,
610
+ transition: options.transition ?? instantTransition,
611
+ behaviour: options.behaviour ?? null,
612
+ interactive: options.interactive ?? false,
613
+ points: options.points,
614
+ segments: options.segments ?? false,
615
+ color: options.color ?? 0xffffff,
616
+ vertexColors: options.vertexColors ?? null,
617
+ width: options.width ?? 1,
618
+ worldUnits: options.worldUnits ?? false,
619
+ dashed: options.dashed ?? false,
620
+ dashSize: options.dashSize ?? 1,
621
+ gapSize: options.gapSize ?? 1,
622
+ dashScale: options.dashScale ?? 1,
623
+ dashOffset: options.dashOffset ?? 0,
624
+ opacity: options.opacity ?? 1,
625
+ transparent: options.transparent ?? false,
626
+ });
627
+
628
+ const asVector = (point: Vec3): Vector3 => new Vector3(point[0], point[1], point[2]);
629
+ const asVec3 = (point: Vector3): Vec3 => [point.x, point.y, point.z];
630
+
631
+ /** Where a points node sits, as the other placed nodes. */
632
+ type Placing = Pick<
633
+ TransformOptions,
634
+ 'key' | 'position' | 'rotation' | 'scale' | 'transition' | 'behaviour'
635
+ >;
636
+
637
+ const placed = (options: Placing, look: PointLook): PointsNode => ({
638
+ _tag: 'Points',
639
+ key: options.key,
640
+ position: options.position ?? origin,
641
+ rotation: options.rotation ?? noRotation,
642
+ scale: options.scale ?? unitScale,
643
+ transition: options.transition ?? instantTransition,
644
+ behaviour: options.behaviour ?? null,
645
+ look,
646
+ });
647
+
648
+ /**
649
+ * Dots at `positions`, as Drei's `Points` (`bf6f4ad`, `src/core/Points.tsx`) with its
650
+ * `PointMaterial`: round, white, 1 pixel (three's `PointsMaterial` size), smaller with
651
+ * distance. `colors` gives each its own colour, `sizes` a factor on `size`.
652
+ */
653
+ export const points = (
654
+ options: Placing & {
655
+ readonly positions: ReadonlyArray<Vec3>;
656
+ readonly colors?: ReadonlyArray<Rgb> | null;
657
+ readonly sizes?: ReadonlyArray<number> | null;
658
+ readonly color?: Rgb;
659
+ readonly size?: number;
660
+ readonly sizeAttenuation?: boolean;
661
+ readonly round?: boolean;
662
+ readonly opacity?: number;
663
+ readonly transparent?: boolean;
664
+ },
665
+ ): PointsNode =>
666
+ placed(options, {
667
+ _tag: 'Dots',
668
+ positions: options.positions,
669
+ colors: options.colors ?? null,
670
+ sizes: options.sizes ?? null,
671
+ color: options.color ?? 0xffffff,
672
+ size: options.size ?? 1,
673
+ sizeAttenuation: options.sizeAttenuation ?? true,
674
+ round: options.round ?? true,
675
+ opacity: options.opacity ?? 1,
676
+ transparent: options.transparent ?? false,
677
+ });
678
+
679
+ /**
680
+ * Glowing motes drifting in a box, as Drei's `Sparkles` (`src/core/Sparkles.tsx`), with
681
+ * its defaults: 100 of them, speed 1, opaque, a unit box (`spread`, Drei's `scale`),
682
+ * random sizes up to 1, white, noise 1. Drei scatters them with `Math.random`; `seed`
683
+ * scatters them the same way every time.
684
+ */
685
+ export const sparkles = (
686
+ options: Placing & {
687
+ readonly count?: number;
688
+ readonly speed?: number;
689
+ readonly opacity?: number;
690
+ readonly size?: number | null;
691
+ readonly color?: Rgb;
692
+ readonly spread?: number | Vec3;
693
+ readonly noise?: number | Vec3;
694
+ readonly seed?: number;
695
+ },
696
+ ): PointsNode => {
697
+ const spread = options.spread ?? 1;
698
+ const noise = options.noise ?? 1;
699
+ return placed(options, {
700
+ _tag: 'Sparkles',
701
+ count: options.count ?? 100,
702
+ speed: options.speed ?? 1,
703
+ opacity: options.opacity ?? 1,
704
+ scale: typeof spread === 'number' ? [spread, spread, spread] : spread,
705
+ size: options.size ?? null,
706
+ color: options.color ?? 0xffffff,
707
+ noise: typeof noise === 'number' ? [noise, noise, noise] : noise,
708
+ seed: options.seed ?? 1,
709
+ });
710
+ };
711
+
712
+ /**
713
+ * A starfield, as Drei's `Stars` (`src/core/Stars.tsx`), with its defaults: 5000 stars
714
+ * from radius 100 to 150, size factor 4, white (saturation 0), hard-edged, twinkling at
715
+ * speed 1. `seed` places them the same way every time.
716
+ */
717
+ export const stars = (
718
+ options: Placing & {
719
+ readonly radius?: number;
720
+ readonly depth?: number;
721
+ readonly count?: number;
722
+ readonly factor?: number;
723
+ readonly saturation?: number;
724
+ readonly fade?: boolean;
725
+ readonly speed?: number;
726
+ readonly seed?: number;
727
+ },
728
+ ): PointsNode =>
729
+ placed(options, {
730
+ _tag: 'Stars',
731
+ radius: options.radius ?? 100,
732
+ depth: options.depth ?? 50,
733
+ count: options.count ?? 5000,
734
+ factor: options.factor ?? 4,
735
+ saturation: options.saturation ?? 0,
736
+ fade: options.fade ?? false,
737
+ speed: options.speed ?? 1,
738
+ seed: options.seed ?? 1,
739
+ });
740
+
741
+ /**
742
+ * Points along a Catmull-Rom curve through `points`, as Drei's `CatmullRomLine`, with its
743
+ * defaults: open, centripetal, tension 0.5, 20 segments (so 21 points).
744
+ */
745
+ export const catmullRomPoints = (
746
+ points: ReadonlyArray<Vec3>,
747
+ options: {
748
+ readonly closed?: boolean;
749
+ readonly curveType?: 'centripetal' | 'chordal' | 'catmullrom';
750
+ readonly tension?: number;
751
+ readonly segments?: number;
752
+ } = {},
753
+ ): ReadonlyArray<Vec3> =>
754
+ new CatmullRomCurve3(
755
+ points.map(asVector),
756
+ options.closed ?? false,
757
+ options.curveType ?? 'centripetal',
758
+ options.tension ?? 0.5,
759
+ )
760
+ .getPoints(options.segments ?? 20)
761
+ .map(asVec3);
762
+
763
+ /**
764
+ * The point `t` of the way along a Catmull-Rom curve through `points` (0 its start, 1 its
765
+ * end; three's `getPoint`, by parameter rather than arc length), with
766
+ * `catmullRomPoints`'s defaults: for something that rides the curve, a behaviour say.
767
+ */
768
+ export const catmullRomPointAt = (
769
+ points: ReadonlyArray<Vec3>,
770
+ t: number,
771
+ options: {
772
+ readonly closed?: boolean;
773
+ readonly curveType?: 'centripetal' | 'chordal' | 'catmullrom';
774
+ readonly tension?: number;
775
+ } = {},
776
+ ): Vec3 =>
777
+ asVec3(
778
+ new CatmullRomCurve3(
779
+ points.map(asVector),
780
+ options.closed ?? false,
781
+ options.curveType ?? 'centripetal',
782
+ options.tension ?? 0.5,
783
+ ).getPoint(Math.min(1, Math.max(0, t))),
784
+ );
785
+
786
+ /**
787
+ * Colours for a Catmull-Rom line's points from a few along it, as Drei's
788
+ * `CatmullRomLine` spreads `vertexColors` over its segments (blended in linear colour).
789
+ */
790
+ export const catmullRomColors = (
791
+ colors: ReadonlyArray<Rgb>,
792
+ options: { readonly segments?: number; readonly closed?: boolean } = {},
793
+ ): ReadonlyArray<Rgb> => {
794
+ const segments = options.segments ?? 20;
795
+ if (colors.length < 2 || colors.length === segments + 1) return colors;
796
+ const stops = colors.map((color) => new Color(color));
797
+ const first = stops[0];
798
+ if (options.closed === true && first !== undefined) stops.push(first.clone());
799
+ const divisions = segments / (stops.length - 1);
800
+ // The colour at point `index`: between the two stops either side of it.
801
+ const at = (index: number): Rgb =>
802
+ (stops[Math.floor(index / divisions)] ?? new Color())
803
+ .clone()
804
+ .lerp(
805
+ stops[Math.floor(index / divisions) + 1] ?? new Color(),
806
+ (index % divisions) / divisions,
807
+ )
808
+ .getHex();
809
+ const blended = Array.from({ length: segments - 1 }, (_, step) => at(step + 1));
810
+ return [colors[0] ?? 0, ...blended, stops.at(-1)?.getHex() ?? 0];
811
+ };
812
+
813
+ /** Points along a cubic Bézier curve, as Drei's `CubicBezierLine` (20 segments). */
814
+ export const cubicBezierPoints = (options: {
815
+ readonly start: Vec3;
816
+ readonly midA: Vec3;
817
+ readonly midB: Vec3;
818
+ readonly end: Vec3;
819
+ readonly segments?: number;
820
+ }): ReadonlyArray<Vec3> =>
821
+ new CubicBezierCurve3(
822
+ asVector(options.start),
823
+ asVector(options.midA),
824
+ asVector(options.midB),
825
+ asVector(options.end),
826
+ )
827
+ .getPoints(options.segments ?? 20)
828
+ .map(asVec3);
829
+
830
+ /**
831
+ * Points along a quadratic Bézier curve, as Drei's `QuadraticBezierLine` (20 segments).
832
+ * Without `mid`, Drei's default: level with `start`, above or below `end`.
833
+ */
834
+ export const quadraticBezierPoints = (options: {
835
+ readonly start: Vec3;
836
+ readonly end: Vec3;
837
+ readonly mid?: Vec3;
838
+ readonly segments?: number;
839
+ }): ReadonlyArray<Vec3> => {
840
+ const [, startY] = options.start;
841
+ const [endX, , endZ] = options.end;
842
+ const mid = options.mid ?? [endX, startY, endZ];
843
+ return new QuadraticBezierCurve3(asVector(options.start), asVector(mid), asVector(options.end))
844
+ .getPoints(options.segments ?? 20)
845
+ .map(asVec3);
846
+ };
847
+
569
848
  /** The head (the viewer), for content that stays in view. */
570
849
  export const headSpace = (): XrSpace => ({ _tag: 'Head' });
571
850
 
@@ -620,6 +899,8 @@ export const mesh = (
620
899
  readonly castShadow?: boolean;
621
900
  readonly receiveShadow?: boolean;
622
901
  readonly body?: Body | null;
902
+ readonly edges?: Edges | null;
903
+ readonly trail?: Trail | null;
623
904
  },
624
905
  ): MeshNode => ({
625
906
  _tag: 'Mesh',
@@ -635,6 +916,42 @@ export const mesh = (
635
916
  castShadow: options.castShadow ?? false,
636
917
  receiveShadow: options.receiveShadow ?? false,
637
918
  body: options.body ?? null,
919
+ edges: options.edges ?? null,
920
+ trail: options.trail ?? null,
921
+ });
922
+
923
+ /**
924
+ * A trail behind a moving node, as Drei's `Trail` (`bf6f4ad`, `src/core/Trail.tsx`), with
925
+ * its defaults: width 0.2, length 1, decay 1, world positions, no stride, every step, hot
926
+ * pink; no taper or fade (Drei's `attenuation` and `meshline` options).
927
+ */
928
+ export const trail = (options: Partial<Trail> = {}): Trail => ({
929
+ width: options.width ?? 0.2,
930
+ length: options.length ?? 1,
931
+ decay: options.decay ?? 1,
932
+ local: options.local ?? false,
933
+ stride: options.stride ?? 0,
934
+ interval: options.interval ?? 1,
935
+ color: options.color ?? 0xff69b4,
936
+ taper: options.taper ?? 'none',
937
+ fade: options.fade ?? false,
938
+ });
939
+
940
+ /**
941
+ * Lines along a mesh's hard edges, as Drei's `Edges` (`bf6f4ad`, `src/core/Edges.tsx`),
942
+ * with its defaults: faces meeting at more than 15 degrees, a white line 1 pixel wide
943
+ * (Drei's `Line` defaults). `mesh({ edges: edges({ color: 0x000000 }) })`.
944
+ */
945
+ export const edges = (options: Partial<Edges> = {}): Edges => ({
946
+ threshold: options.threshold ?? 15,
947
+ color: options.color ?? 0xffffff,
948
+ width: options.width ?? 1,
949
+ worldUnits: options.worldUnits ?? false,
950
+ dashed: options.dashed ?? false,
951
+ dashSize: options.dashSize ?? 1,
952
+ gapSize: options.gapSize ?? 1,
953
+ opacity: options.opacity ?? 1,
954
+ transparent: options.transparent ?? false,
638
955
  });
639
956
 
640
957
  /**
@@ -751,13 +1068,19 @@ const noJoints: ReadonlyArray<Joint> = [];
751
1068
 
752
1069
  /**
753
1070
  * A physics world. Defaults as react-three-rapier's `Physics`: Earth gravity
754
- * `[0, -9.81, 0]`, running, fixed steps of 1/60 s, interpolated; no joints.
1071
+ * `[0, -9.81, 0]`, running, fixed steps of 1/60 s, interpolated; no joints; on scene
1072
+ * time rather than `until` a step the Model sets.
755
1073
  */
756
1074
  export const physics = (options: Partial<Physics> = {}): Physics => ({
757
1075
  gravity: options.gravity ?? [0, -9.81, 0],
758
1076
  paused: options.paused ?? false,
759
1077
  timeStep: options.timeStep ?? 1 / 60,
760
1078
  interpolate: options.interpolate ?? true,
1079
+ until: options.until ?? null,
1080
+ run: options.run ?? 0,
1081
+ snapshotAt: options.snapshotAt ?? null,
1082
+ from: options.from ?? null,
1083
+ keep: options.keep ?? 0,
761
1084
  debug: options.debug ?? false,
762
1085
  joints: options.joints ?? noJoints,
763
1086
  });
@@ -1358,6 +1681,38 @@ export const contactShadows = (
1358
1681
 
1359
1682
  const noOverlays: ReadonlyArray<Overlay> = [];
1360
1683
  const noMeasures: ReadonlyArray<string> = [];
1684
+ const noAssets: ReadonlyArray<Asset> = [];
1685
+
1686
+ /** What an asset builder takes: an id, the finest URL, and coarser ones to show first. */
1687
+ type AssetOptions = {
1688
+ readonly id: string;
1689
+ readonly url: string;
1690
+ readonly levels?: ReadonlyArray<string>;
1691
+ };
1692
+
1693
+ const noLevels: ReadonlyArray<string> = [];
1694
+
1695
+ const assetOf =
1696
+ (kind: Asset['kind']) =>
1697
+ ({ id, url, levels }: AssetOptions): Asset => ({ kind, id, url, levels: levels ?? noLevels });
1698
+
1699
+ /** A glTF or GLB model for `scene({ assets })`, lent under `id` for `borrowed({ id })`. */
1700
+ export const modelAsset = assetOf('model');
1701
+
1702
+ /** A colour texture (sRGB) for `scene({ assets })`, lent under `id` for a material's `map`. */
1703
+ export const textureAsset = assetOf('texture');
1704
+
1705
+ /**
1706
+ * An equirectangular HDR panorama (`.hdr`) for `scene({ assets })`, lent under `id` for
1707
+ * `environment({ map: id })`.
1708
+ */
1709
+ export const environmentAsset = assetOf('environment');
1710
+
1711
+ /**
1712
+ * A typeface JSON font (three.js's `FontLoader` format) for `scene({ assets })`, lent under
1713
+ * `id` for `text({ font: id })`.
1714
+ */
1715
+ export const fontAsset = assetOf('font');
1361
1716
 
1362
1717
  /** Drei's `zIndexRange` default: near points on top, far ones at 0. */
1363
1718
  const defaultZIndexRange: readonly [number, number] = [16777271, 0];
@@ -1386,11 +1741,26 @@ export const overlay = (options: {
1386
1741
  view: options.view ?? null,
1387
1742
  });
1388
1743
 
1744
+ /**
1745
+ * Nodes drawn over the scene (Drei's `Hud`), with Drei's `GizmoHelper` defaults: the
1746
+ * origin 80 CSS pixels in from the bottom right.
1747
+ */
1748
+ export const hud = (options: {
1749
+ readonly alignment?: HudAlignment;
1750
+ readonly margin?: readonly [number, number];
1751
+ readonly children?: ReadonlyArray<SceneNode>;
1752
+ }): Hud => ({
1753
+ alignment: options.alignment ?? 'bottom-right',
1754
+ margin: options.margin ?? [80, 80],
1755
+ children: options.children ?? [],
1756
+ });
1757
+
1389
1758
  export const scene = (options: {
1390
1759
  readonly camera?: Camera;
1391
1760
  readonly gestures?: Gestures;
1392
1761
  readonly views?: ReadonlyArray<View>;
1393
1762
  readonly renderTextures?: ReadonlyArray<RenderTexture>;
1763
+ readonly hud?: Hud | null;
1394
1764
  readonly overlays?: ReadonlyArray<Overlay>;
1395
1765
  /** Optional staging: leave out or pass `null` for none, so a Model can switch it off. */
1396
1766
  readonly environment?: Environment | null;
@@ -1399,6 +1769,7 @@ export const scene = (options: {
1399
1769
  readonly physics?: Physics | null;
1400
1770
  readonly xr?: Xr | null;
1401
1771
  readonly measure?: ReadonlyArray<string>;
1772
+ readonly assets?: ReadonlyArray<Asset>;
1402
1773
  readonly render?: RenderSettings;
1403
1774
  readonly resources?: Partial<ResourceDefinitions>;
1404
1775
  readonly children?: ReadonlyArray<SceneNode>;
@@ -1407,6 +1778,7 @@ export const scene = (options: {
1407
1778
  gestures: options.gestures ?? 'page',
1408
1779
  views: options.views ?? noViews,
1409
1780
  renderTextures: options.renderTextures ?? noRenderTextures,
1781
+ hud: options.hud ?? null,
1410
1782
  overlays: options.overlays ?? noOverlays,
1411
1783
  environment: options.environment ?? null,
1412
1784
  sky: options.sky ?? null,
@@ -1414,6 +1786,7 @@ export const scene = (options: {
1414
1786
  physics: options.physics ?? null,
1415
1787
  xr: options.xr ?? null,
1416
1788
  measure: options.measure ?? noMeasures,
1789
+ assets: options.assets ?? noAssets,
1417
1790
  render: options.render ?? defaultRenderSettings,
1418
1791
  resources: {
1419
1792
  geometries: options.resources?.geometries ?? noResources.geometries,
@@ -15,20 +15,10 @@ import { SimplexNoise } from 'three/addons/math/SimplexNoise.js';
15
15
 
16
16
  import type { CameraShake } from './description.ts';
17
17
  import { cameraShakeEquals } from './equality.ts';
18
+ import { seededRandom } from './seeded.ts';
18
19
 
19
- /** A small seeded generator (mulberry32), so the noise is the same on every page load. */
20
- const seeded = (seed: number): { readonly random: () => number } => {
21
- let state = seed >>> 0;
22
- return {
23
- random: () => {
24
- state = (state + 0x6d2b79f5) >>> 0;
25
- let t = state;
26
- t = Math.imul(t ^ (t >>> 15), t | 1);
27
- t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
28
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
29
- },
30
- };
31
- };
20
+ /** Each axis's noise, seeded so the shake is the same on every page load. */
21
+ const seeded = (seed: number) => ({ random: seededRandom(seed) });
32
22
 
33
23
  const yawNoise = new SimplexNoise(seeded(1));
34
24
  const pitchNoise = new SimplexNoise(seeded(2));