@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,224 @@
1
+ /**
2
+ * What a scene contains, for an assistant (roadmap, "Beyond parity" 3): its camera, its
3
+ * things with their kind, size, colour and place, its lights, the box they fill and its
4
+ * staging, as data (`describeScene`) and as a few lines of text (`formatSummary`) an
5
+ * agent reads before it acts. An agent that reads the Model can describe a scene
6
+ * without three.js, so sizes come from the geometry's parameters and the node's scale:
7
+ * close enough to place things beside one another and to frame a shot.
8
+ *
9
+ * @module
10
+ */
11
+ import type {
12
+ GeometryRef,
13
+ Rgb,
14
+ SceneDescription,
15
+ SceneNode,
16
+ ShapeGeometry,
17
+ Vec3,
18
+ } from './description.ts';
19
+ import type { Box } from './orbit.ts';
20
+
21
+ export type ThingSummary = {
22
+ readonly key: string;
23
+ /** The node's kind, with its geometry's for a mesh: `Mesh (Box)`. */
24
+ readonly kind: string;
25
+ /** In its parent's space; for things at the top, the world's. */
26
+ readonly position: Vec3;
27
+ /** Width, height and depth, from the geometry and scale; `null` when not known. */
28
+ readonly size: Vec3 | null;
29
+ readonly colour: Rgb | null;
30
+ /** The group it is in, `null` at the top. */
31
+ readonly parent: string | null;
32
+ };
33
+
34
+ export type LightSummary = {
35
+ readonly key: string;
36
+ readonly kind: string;
37
+ readonly colour: Rgb;
38
+ readonly intensity: number;
39
+ readonly position: Vec3 | null;
40
+ };
41
+
42
+ export type SceneSummary = {
43
+ readonly camera: {
44
+ readonly kind: 'Perspective' | 'Orthographic';
45
+ readonly position: Vec3;
46
+ readonly lookAt: Vec3;
47
+ };
48
+ readonly things: ReadonlyArray<ThingSummary>;
49
+ readonly lights: ReadonlyArray<LightSummary>;
50
+ /** The box the top-level things of known size fill, `null` with none. */
51
+ readonly bounds: Box | null;
52
+ readonly staging: ReadonlyArray<string>;
53
+ };
54
+
55
+ /** Width, height and depth of a shape, unscaled; `undefined` for text and shapes. */
56
+ const extentOf = (geometry: ShapeGeometry): Vec3 | undefined => {
57
+ switch (geometry._tag) {
58
+ case 'Box':
59
+ case 'RoundedBox':
60
+ return [geometry.width, geometry.height, geometry.depth];
61
+ case 'Sphere':
62
+ return [geometry.radius * 2, geometry.radius * 2, geometry.radius * 2];
63
+ case 'Cone':
64
+ return [geometry.radius * 2, geometry.height, geometry.radius * 2];
65
+ case 'Cylinder': {
66
+ const across = Math.max(geometry.radiusTop, geometry.radiusBottom) * 2;
67
+ return [across, geometry.height, across];
68
+ }
69
+ case 'Capsule':
70
+ return [geometry.radius * 2, geometry.height + geometry.radius * 2, geometry.radius * 2];
71
+ case 'Torus': {
72
+ const across = (geometry.radius + geometry.tube) * 2;
73
+ return [across, across, geometry.tube * 2];
74
+ }
75
+ case 'Plane':
76
+ return [geometry.width, geometry.height, 0];
77
+ case 'Circle':
78
+ return [geometry.radius * 2, geometry.radius * 2, 0];
79
+ case 'Ring':
80
+ return [geometry.outerRadius * 2, geometry.outerRadius * 2, 0];
81
+ default:
82
+ return undefined;
83
+ }
84
+ };
85
+
86
+ const scaled = (extent: Vec3, scale: Vec3): Vec3 => [
87
+ extent[0] * Math.abs(scale[0]),
88
+ extent[1] * Math.abs(scale[1]),
89
+ extent[2] * Math.abs(scale[2]),
90
+ ];
91
+
92
+ /** The shape a geometry reference draws: its own, a shared one's, or none (lent). */
93
+ const shapeOf = (
94
+ geometry: GeometryRef,
95
+ description: SceneDescription,
96
+ ): ShapeGeometry | undefined => {
97
+ if (geometry._tag === 'LentGeometry') return undefined;
98
+ if (geometry._tag !== 'SharedGeometry') return geometry;
99
+ return description.resources.geometries.find((entry) => entry.id === geometry.id)?.geometry;
100
+ };
101
+
102
+ const sizeOf = (node: SceneNode, description: SceneDescription): Vec3 | null => {
103
+ if (node._tag !== 'Mesh') return null;
104
+ const shape = shapeOf(node.geometry, description);
105
+ const extent = shape === undefined ? undefined : extentOf(shape);
106
+ return extent === undefined ? null : scaled(extent, node.scale);
107
+ };
108
+
109
+ const colourOf = (node: SceneNode): Rgb | null =>
110
+ node._tag === 'Mesh' && 'color' in node.material ? node.material.color : null;
111
+
112
+ const kindOf = (node: SceneNode): string =>
113
+ node._tag === 'Mesh' ? `Mesh (${node.geometry._tag})` : node._tag;
114
+
115
+ const isLight = (node: SceneNode): boolean => node._tag.endsWith('Light');
116
+
117
+ const lightOf = (node: SceneNode): ReadonlyArray<LightSummary> => {
118
+ if (!isLight(node) || !('color' in node) || !('intensity' in node)) return [];
119
+ return [
120
+ {
121
+ key: node.key,
122
+ kind: node._tag,
123
+ colour: node.color,
124
+ intensity: node.intensity,
125
+ position: 'position' in node ? node.position : null,
126
+ },
127
+ ];
128
+ };
129
+
130
+ const thingsIn = (
131
+ nodes: ReadonlyArray<SceneNode>,
132
+ parent: string | null,
133
+ description: SceneDescription,
134
+ ): ReadonlyArray<ThingSummary> =>
135
+ nodes.flatMap((node) => [
136
+ ...(isLight(node)
137
+ ? []
138
+ : [
139
+ {
140
+ key: node.key,
141
+ kind: kindOf(node),
142
+ position: 'position' in node ? node.position : ([0, 0, 0] as const),
143
+ size: sizeOf(node, description),
144
+ colour: colourOf(node),
145
+ parent,
146
+ },
147
+ ]),
148
+ ...(node._tag === 'Group' ? thingsIn(node.children, node.key, description) : []),
149
+ ]);
150
+
151
+ const lightsIn = (nodes: ReadonlyArray<SceneNode>): ReadonlyArray<LightSummary> =>
152
+ nodes.flatMap((node) => [
153
+ ...lightOf(node),
154
+ ...(node._tag === 'Group' ? lightsIn(node.children) : []),
155
+ ]);
156
+
157
+ /** The box top-level things of known size fill, each taken as a box round its place. */
158
+ export const thingsBounds = (things: ReadonlyArray<ThingSummary>): Box | null => {
159
+ const sized = things.filter((thing) => thing.parent === null && thing.size !== null);
160
+ if (sized.length === 0) return null;
161
+ const edge = (axis: 0 | 1 | 2, sign: 1 | -1): ReadonlyArray<number> =>
162
+ sized.map((thing) => thing.position[axis] + sign * ((thing.size?.[axis] ?? 0) / 2));
163
+ const low = (axis: 0 | 1 | 2) => Math.min(...edge(axis, -1));
164
+ const high = (axis: 0 | 1 | 2) => Math.max(...edge(axis, 1));
165
+ return { min: [low(0), low(1), low(2)], max: [high(0), high(1), high(2)] };
166
+ };
167
+
168
+ const stagingOf = (description: SceneDescription): ReadonlyArray<string> => [
169
+ ...(description.environment === null ? [] : ['environment lighting']),
170
+ ...(description.sky === null ? [] : ['sky']),
171
+ ...(description.render.shadows === 'off' ? [] : [`${description.render.shadows} shadows`]),
172
+ ...(description.contactShadows === null ? [] : ['contact shadows']),
173
+ ...description.render.effects.map((effect) => effect._tag.toLowerCase()),
174
+ ...(description.physics === null ? [] : ['physics']),
175
+ ];
176
+
177
+ /** What `description` contains, for an assistant to read. */
178
+ export const describeScene = (description: SceneDescription): SceneSummary => {
179
+ const things = thingsIn(description.children, null, description);
180
+ return {
181
+ camera: {
182
+ kind: description.camera._tag,
183
+ position: description.camera.position,
184
+ lookAt: description.camera.lookAt,
185
+ },
186
+ things,
187
+ lights: lightsIn(description.children),
188
+ bounds: thingsBounds(things),
189
+ staging: stagingOf(description),
190
+ };
191
+ };
192
+
193
+ const number = (value: number): string => String(Math.round(value * 100) / 100);
194
+ const point = (at: Vec3): string => `(${at.map(number).join(', ')})`;
195
+ const hex = (colour: Rgb): string => `#${colour.toString(16).padStart(6, '0')}`;
196
+
197
+ const thingLine = (thing: ThingSummary): string =>
198
+ [
199
+ `- ${thing.key}: ${thing.kind}`,
200
+ thing.size === null ? '' : `, ${thing.size.map(number).join(' × ')}`,
201
+ thing.colour === null ? '' : `, ${hex(thing.colour)}`,
202
+ `, at ${point(thing.position)}`,
203
+ thing.parent === null ? '' : ` in ${thing.parent}`,
204
+ ].join('');
205
+
206
+ const lightLine = (light: LightSummary): string =>
207
+ `- ${light.key}: ${light.kind}, ${hex(light.colour)} × ${number(light.intensity)}${
208
+ light.position === null ? '' : ` at ${point(light.position)}`
209
+ }`;
210
+
211
+ /** The summary as a few lines of text, for a prompt or a tool's answer. */
212
+ export const formatSummary = (summary: SceneSummary): string =>
213
+ [
214
+ `Camera: ${summary.camera.kind.toLowerCase()} at ${point(summary.camera.position)}, looking at ${point(summary.camera.lookAt)}`,
215
+ `Things (${summary.things.length})${
216
+ summary.bounds === null
217
+ ? ''
218
+ : `, within ${point(summary.bounds.min)} to ${point(summary.bounds.max)}`
219
+ }:`,
220
+ ...summary.things.map(thingLine),
221
+ `Lights (${summary.lights.length}):`,
222
+ ...summary.lights.map(lightLine),
223
+ `Staging: ${summary.staging.length === 0 ? 'none' : summary.staging.join(', ')}`,
224
+ ].join('\n');
@@ -30,6 +30,17 @@ export type Rotation =
30
30
  readonly axis: Axis;
31
31
  readonly radiansPerSecond: number;
32
32
  readonly phase: number;
33
+ }
34
+ /**
35
+ * Faces the scene's camera every frame, as Drei's `Billboard`: the node turns so its
36
+ * world rotation is the camera's. A locked axis stays at 0 in the node's own Euler
37
+ * angles (Drei keeps its previous value, which starts at 0).
38
+ */
39
+ | {
40
+ readonly _tag: 'Billboard';
41
+ readonly lockX: boolean;
42
+ readonly lockY: boolean;
43
+ readonly lockZ: boolean;
33
44
  };
34
45
 
35
46
  /** Box geometry parameters. */
@@ -90,6 +101,36 @@ export type CapsuleGeometry = {
90
101
  readonly radialSegments: number;
91
102
  };
92
103
 
104
+ /**
105
+ * A box with rounded edges and corners, centred, as Drei's `RoundedBox`: a rounded
106
+ * rectangle extruded and bevelled, with normals creased at `creaseAngle`.
107
+ */
108
+ export type RoundedBoxGeometry = {
109
+ readonly _tag: 'RoundedBox';
110
+ readonly width: number;
111
+ readonly height: number;
112
+ readonly depth: number;
113
+ readonly radius: number;
114
+ /** Segments in each rounded corner of the outline. */
115
+ readonly smoothness: number;
116
+ /** Segments across the bevel, which Drei doubles. */
117
+ readonly bevelSegments: number;
118
+ readonly steps: number;
119
+ /** Faces meeting at more than this angle (radians) keep a hard edge. */
120
+ readonly creaseAngle: number;
121
+ };
122
+
123
+ /**
124
+ * A photo backdrop, as Drei's `Backdrop`: a floor that sweeps up into a wall behind it,
125
+ * one unit each way (scale it to size). The floor lies along +Z towards the viewer, the
126
+ * wall rises at its back, and `floor` extends the flat front.
127
+ */
128
+ export type BackdropGeometry = {
129
+ readonly _tag: 'Backdrop';
130
+ readonly floor: number;
131
+ readonly segments: number;
132
+ };
133
+
93
134
  /** A flat ring (annulus) in the XY plane, facing +Z. */
94
135
  export type RingGeometry = {
95
136
  readonly _tag: 'Ring';
@@ -150,7 +191,9 @@ export type ShapeGeometry =
150
191
  | ConeGeometry
151
192
  | CapsuleGeometry
152
193
  | RingGeometry
153
- | CircleGeometry;
194
+ | CircleGeometry
195
+ | RoundedBoxGeometry
196
+ | BackdropGeometry;
154
197
 
155
198
  /** Which faces a material draws. */
156
199
  export type MaterialSide = 'front' | 'back' | 'double';
@@ -165,6 +208,11 @@ export type BasicMaterial = {
165
208
  readonly transparent: boolean;
166
209
  readonly side: MaterialSide;
167
210
  readonly wireframe: boolean;
211
+ /**
212
+ * Whether it is hidden behind what is nearer, as usual; `false` draws it over
213
+ * everything (and writes no depth), as three's gizmos and helpers are drawn.
214
+ */
215
+ readonly depthTest: boolean;
168
216
  };
169
217
 
170
218
  /** A matte (Lambertian) material: cheap per-vertex lighting, no highlights. */
@@ -342,6 +390,24 @@ export type GroupNode = {
342
390
  * not tracked (or no session presents). `null` for the group's own transform.
343
391
  */
344
392
  readonly follow: XrSpace | null;
393
+ /**
394
+ * Levels of detail: the group shows one child at a time, chosen by the camera's
395
+ * distance (Drei's `Detailed`, three's `LOD`), or `null` to show them all.
396
+ */
397
+ readonly detail: Detail | null;
398
+ /** A ribbon behind the group as it moves (Drei's `Trail`), or `null` for none. */
399
+ readonly trail: Trail | null;
400
+ };
401
+
402
+ /**
403
+ * Levels of detail for a group's children, in order: child `i` shows from
404
+ * `distances[i]` (scene units from the camera, divided by its zoom) until the next level's
405
+ * distance. `hysteresis` (a fraction) keeps a shown level until the camera is that much
406
+ * nearer than its distance, so the switch does not flicker at the boundary.
407
+ */
408
+ export type Detail = {
409
+ readonly distances: ReadonlyArray<number>;
410
+ readonly hysteresis: number;
345
411
  };
346
412
 
347
413
  /** The joints of a tracked hand, as WebXR names them (`XRHandJoint`). */
@@ -411,6 +477,47 @@ export type MeshNode = {
411
477
  readonly receiveShadow: boolean;
412
478
  /** A rigid body simulating the mesh, when the scene has `physics`; `null` for none. */
413
479
  readonly body: Body | null;
480
+ /** Lines along the mesh's hard edges (Drei's `Edges`), or `null` for none. */
481
+ readonly edges: Edges | null;
482
+ /** A ribbon behind the mesh as it moves (Drei's `Trail`), or `null` for none. */
483
+ readonly trail: Trail | null;
484
+ };
485
+
486
+ /**
487
+ * A trail behind a moving node, as Drei's `Trail`: `length × 10` points of where it has
488
+ * been, a new one every `interval` steps (60 a second of scene time) when it has moved at
489
+ * least `stride`, `decay` of them at a time; drawn as a ribbon facing the camera, a tenth
490
+ * of `width` wide in scene units, narrowing (`taper`) and fading (`fade`) to the tail.
491
+ * `local` follows the node's own position rather than its world position, as Drei's
492
+ * does; the ribbon is drawn in the scene's space either way.
493
+ */
494
+ export type Trail = {
495
+ readonly width: number;
496
+ readonly length: number;
497
+ readonly decay: number;
498
+ readonly local: boolean;
499
+ readonly stride: number;
500
+ readonly interval: number;
501
+ readonly color: Rgb;
502
+ readonly taper: 'none' | 'linear' | 'quadratic';
503
+ readonly fade: boolean;
504
+ };
505
+
506
+ /**
507
+ * Lines along a mesh's edges where its faces meet at more than `threshold` degrees
508
+ * (three's `EdgesGeometry`, as Drei's `Edges`), drawn as a line (`width` pixels, or scene
509
+ * units with `worldUnits`) by the render kit. Never picked: hits go to the mesh.
510
+ */
511
+ export type Edges = {
512
+ readonly threshold: number;
513
+ readonly color: Rgb;
514
+ readonly width: number;
515
+ readonly worldUnits: boolean;
516
+ readonly dashed: boolean;
517
+ readonly dashSize: number;
518
+ readonly gapSize: number;
519
+ readonly opacity: number;
520
+ readonly transparent: boolean;
414
521
  };
415
522
 
416
523
  /**
@@ -656,6 +763,13 @@ export type BorrowedNode = {
656
763
  readonly animation: Animation | null;
657
764
  /** A rigid body whose colliders are the model's meshes, once it is lent; or `null`. */
658
765
  readonly body: Body | null;
766
+ /**
767
+ * Every mesh in what it shows casts (receives) shadows, as Drei's `Clone` shorthands;
768
+ * `false` leaves each mesh's own setting. Set on the lent object itself where it is
769
+ * shown as lent rather than cloned.
770
+ */
771
+ readonly castShadow: boolean;
772
+ readonly receiveShadow: boolean;
659
773
  };
660
774
 
661
775
  /** Where a label's origin is along its width: an edge, the middle, or a fraction of it. */
@@ -862,8 +976,106 @@ export type ShaderGridNode = {
862
976
  readonly side: MaterialSide;
863
977
  };
864
978
 
979
+ /**
980
+ * A line through points, as Drei's `Line` (three's fat lines, `Line2`): `width` pixels
981
+ * wide on screen, or scene units with `worldUnits`; one colour, or one per point; solid
982
+ * or dashed. With `segments` each pair of points is its own segment (Drei's `Segments`).
983
+ * A render kit draws it; without one it is an empty group.
984
+ */
985
+ export type LineNode = {
986
+ readonly _tag: 'Line';
987
+ readonly key: string;
988
+ readonly position: Vec3;
989
+ readonly rotation: Rotation;
990
+ readonly scale: Vec3;
991
+ readonly transition: Transition;
992
+ readonly behaviour: Behave | null;
993
+ /** Makes the line pickable, within its width. */
994
+ readonly interactive: boolean;
995
+ readonly points: ReadonlyArray<Vec3>;
996
+ readonly segments: boolean;
997
+ readonly color: Rgb;
998
+ /** A colour for each point, blended along the line, or `null` for `color` throughout. */
999
+ readonly vertexColors: ReadonlyArray<Rgb> | null;
1000
+ readonly width: number;
1001
+ readonly worldUnits: boolean;
1002
+ readonly dashed: boolean;
1003
+ readonly dashSize: number;
1004
+ readonly gapSize: number;
1005
+ readonly dashScale: number;
1006
+ readonly dashOffset: number;
1007
+ readonly opacity: number;
1008
+ readonly transparent: boolean;
1009
+ };
1010
+
1011
+ /**
1012
+ * Points, drawn by the render kit as round or square dots, Drei's sparkles or its
1013
+ * stars. The look says which, and carries everything needed to draw them: given
1014
+ * points, or a seed that scatters them the same way every time.
1015
+ */
1016
+ export type PointsNode = {
1017
+ readonly _tag: 'Points';
1018
+ readonly key: string;
1019
+ readonly position: Vec3;
1020
+ readonly rotation: Rotation;
1021
+ readonly scale: Vec3;
1022
+ readonly transition: Transition;
1023
+ readonly behaviour: Behave | null;
1024
+ readonly look: PointLook;
1025
+ };
1026
+
1027
+ /**
1028
+ * What points look like:
1029
+ *
1030
+ * - `Dots`: Drei's `Points` with its `PointMaterial` (three's `PointsMaterial`, `round`
1031
+ * to circles): `size` pixels across, smaller with distance under `sizeAttenuation`;
1032
+ * one colour or one a point, one size or a factor a point.
1033
+ * - `Sparkles`: Drei's `Sparkles`: `count` glowing motes scattered through a box of
1034
+ * `scale`, drifting by `noise` at `speed`.
1035
+ * - `Stars`: Drei's `Stars`: `count` stars in a shell from `radius` to `radius + depth`,
1036
+ * `factor` sizing them, twinkling at `speed`, `fade` softening their edges.
1037
+ */
1038
+ export type PointLook =
1039
+ | {
1040
+ readonly _tag: 'Dots';
1041
+ readonly positions: ReadonlyArray<Vec3>;
1042
+ readonly colors: ReadonlyArray<Rgb> | null;
1043
+ readonly sizes: ReadonlyArray<number> | null;
1044
+ readonly color: Rgb;
1045
+ readonly size: number;
1046
+ readonly sizeAttenuation: boolean;
1047
+ readonly round: boolean;
1048
+ readonly opacity: number;
1049
+ readonly transparent: boolean;
1050
+ }
1051
+ | {
1052
+ readonly _tag: 'Sparkles';
1053
+ readonly count: number;
1054
+ readonly speed: number;
1055
+ readonly opacity: number;
1056
+ readonly scale: Vec3;
1057
+ /** Each mote's size, or `null` for Drei's: random, up to 1. */
1058
+ readonly size: number | null;
1059
+ readonly color: Rgb;
1060
+ readonly noise: Vec3;
1061
+ readonly seed: number;
1062
+ }
1063
+ | {
1064
+ readonly _tag: 'Stars';
1065
+ readonly radius: number;
1066
+ readonly depth: number;
1067
+ readonly count: number;
1068
+ readonly factor: number;
1069
+ readonly saturation: number;
1070
+ readonly fade: boolean;
1071
+ readonly speed: number;
1072
+ readonly seed: number;
1073
+ };
1074
+
865
1075
  /** Any node in a scene. Keys are unique across the whole scene. */
866
1076
  export type SceneNode =
1077
+ | PointsNode
1078
+ | LineNode
867
1079
  | GridNode
868
1080
  | ShaderGridNode
869
1081
  | GroupNode
@@ -902,6 +1114,8 @@ export type PerspectiveCamera = {
902
1114
  * (`autoRotated(camera, detail)`). A new position or `lookAt` turns on from there.
903
1115
  */
904
1116
  readonly autoRotate: number;
1117
+ /** Shakes the rendered camera (Drei's `CameraShake`), or `null` for none. */
1118
+ readonly shake: CameraShake | null;
905
1119
  };
906
1120
 
907
1121
  /**
@@ -919,6 +1133,28 @@ export type OrthographicCamera = {
919
1133
  readonly damping: number;
920
1134
  /** As on `PerspectiveCamera`. */
921
1135
  readonly autoRotate: number;
1136
+ /** As on `PerspectiveCamera`. */
1137
+ readonly shake: CameraShake | null;
1138
+ };
1139
+
1140
+ /**
1141
+ * Camera shake, as Drei's `CameraShake`: yaw, pitch and roll of up to `max*` radians,
1142
+ * from noise at `*Frequency`, scaled by `intensity` squared (0 to 1). With `decay` the
1143
+ * intensity falls by `decayRate` a second from when the shake began, so a decaying shake
1144
+ * is a one-off: it begins when it first appears or changes, and `trigger` (a count the
1145
+ * Model bumps) begins the same shake again. Reduced motion holds the camera still.
1146
+ */
1147
+ export type CameraShake = {
1148
+ readonly intensity: number;
1149
+ readonly decay: boolean;
1150
+ readonly decayRate: number;
1151
+ readonly maxYaw: number;
1152
+ readonly maxPitch: number;
1153
+ readonly maxRoll: number;
1154
+ readonly yawFrequency: number;
1155
+ readonly pitchFrequency: number;
1156
+ readonly rollFrequency: number;
1157
+ readonly trigger: number;
922
1158
  };
923
1159
 
924
1160
  /** The single active camera. */
@@ -990,6 +1226,33 @@ export type ViewRect = {
990
1226
  */
991
1227
  export type View = { readonly key: string; readonly camera: Camera; readonly rect: ViewRect };
992
1228
 
1229
+ /** Where a HUD's origin sits on the canvas: a corner, an edge's middle, or the centre. */
1230
+ export type HudAlignment =
1231
+ | 'top-left'
1232
+ | 'top-center'
1233
+ | 'top-right'
1234
+ | 'center-left'
1235
+ | 'center-center'
1236
+ | 'center-right'
1237
+ | 'bottom-left'
1238
+ | 'bottom-center'
1239
+ | 'bottom-right';
1240
+
1241
+ /**
1242
+ * Nodes drawn over the scene, Drei's `Hud`, as data: a scene of their own (keys apart
1243
+ * from the scene's), drawn after it with only its depth cleared, through an orthographic
1244
+ * camera measured in CSS pixels, y up, whose origin sits `margin` pixels in from the
1245
+ * `alignment`'s edges (Drei's `GizmoHelper` places its gizmo so). Pointer events reach
1246
+ * its interactive meshes before anything behind them; their points and rays are in the
1247
+ * HUD's own space. Scene lighting and fog do not reach it.
1248
+ */
1249
+ export type Hud = {
1250
+ readonly alignment: HudAlignment;
1251
+ /** CSS pixels in from the aligned edges, `[x, y]`: unused for a centred direction. */
1252
+ readonly margin: readonly [number, number];
1253
+ readonly children: ReadonlyArray<SceneNode>;
1254
+ };
1255
+
993
1256
  /**
994
1257
  * A small scene rendered into a texture that materials use as `map: id`: Drei's
995
1258
  * `RenderTexture`, as data. Its nodes have their own key namespace and may use the
@@ -1174,6 +1437,8 @@ export type SceneDescription = {
1174
1437
  readonly views: ReadonlyArray<View>;
1175
1438
  /** Scenes rendered into textures for materials to use. */
1176
1439
  readonly renderTextures: ReadonlyArray<RenderTexture>;
1440
+ /** Nodes drawn over the scene, or `null` for none. */
1441
+ readonly hud: Hud | null;
1177
1442
  /** Points to show HTML at, placed every frame the scene draws. */
1178
1443
  readonly overlays: ReadonlyArray<Overlay>;
1179
1444
  /** Image-based lighting and background, or `null` for none. */
@@ -31,6 +31,11 @@ import {
31
31
  labelMaterialEquals,
32
32
  labelTypeEquals,
33
33
  xrSpaceEquals,
34
+ lineLookEquals,
35
+ edgesEquals,
36
+ trailEquals,
37
+ pointLookEquals,
38
+ detailEquals,
34
39
  shaderGridLookEquals,
35
40
  instancesEqual,
36
41
  materialRefEquals,
@@ -81,8 +86,18 @@ export type Field =
81
86
  | 'text'
82
87
  /** A shader grid's look: anything but where it is. */
83
88
  | 'grid'
89
+ /** A line's points or look: anything but where it is. */
90
+ | 'line'
91
+ /** A mesh's edge lines. */
92
+ | 'edges'
93
+ /** A group's or mesh's trail. */
94
+ | 'trail'
95
+ /** Points' look: what they are and where, within the node. */
96
+ | 'look'
84
97
  /** The tracked space a group follows. */
85
- | 'follow';
98
+ | 'follow'
99
+ /** A group's levels of detail. */
100
+ | 'detail';
86
101
 
87
102
  /** A single change to apply to the hosted scene graph. */
88
103
  export type Op =
@@ -91,6 +106,9 @@ export type Op =
91
106
  /** A render texture was created, resized or its scene changed (its own operations). */
92
107
  | { readonly _tag: 'UpdateRenderTexture'; readonly id: string; readonly ops: ReadonlyArray<Op> }
93
108
  | { readonly _tag: 'RemoveRenderTexture'; readonly id: string }
109
+ /** The HUD was created or its scene changed (its own operations). */
110
+ | { readonly _tag: 'UpdateHud'; readonly ops: ReadonlyArray<Op> }
111
+ | { readonly _tag: 'RemoveHud' }
94
112
  | { readonly _tag: 'SetRenderSettings'; readonly settings: RenderSettings }
95
113
  | { readonly _tag: 'DefineResources'; readonly resources: ResourceDefinitions }
96
114
  | { readonly _tag: 'Create'; readonly node: SceneNode; readonly parent: ParentKey }
@@ -131,6 +149,8 @@ export const changedFields = (
131
149
  if (prev.interactive !== next.interactive) fields.push('interactive');
132
150
  if (!bodyEquals(prev.body, next.body)) fields.push('body');
133
151
  if (!xrSpaceEquals(prev.follow, next.follow)) fields.push('follow');
152
+ if (!detailEquals(prev.detail, next.detail)) fields.push('detail');
153
+ if (!trailEquals(prev.trail, next.trail)) fields.push('trail');
134
154
  return fields;
135
155
  }
136
156
  case 'Mesh': {
@@ -146,6 +166,8 @@ export const changedFields = (
146
166
  if (prev.castShadow !== next.castShadow) fields.push('castShadow');
147
167
  if (prev.receiveShadow !== next.receiveShadow) fields.push('receiveShadow');
148
168
  if (!bodyEquals(prev.body, next.body)) fields.push('body');
169
+ if (!edgesEquals(prev.edges, next.edges)) fields.push('edges');
170
+ if (!trailEquals(prev.trail, next.trail)) fields.push('trail');
149
171
  return fields;
150
172
  }
151
173
  case 'Instances': {
@@ -175,6 +197,8 @@ export const changedFields = (
175
197
  if (!animationEquals(prev.animation, next.animation)) fields.push('animation');
176
198
  if (prev.interactive !== next.interactive) fields.push('interactive');
177
199
  if (!bodyEquals(prev.body, next.body)) fields.push('body');
200
+ if (prev.castShadow !== next.castShadow) fields.push('castShadow');
201
+ if (prev.receiveShadow !== next.receiveShadow) fields.push('receiveShadow');
178
202
  return fields;
179
203
  }
180
204
  case 'Label': {
@@ -233,6 +257,27 @@ export const changedFields = (
233
257
  if (!Object.is(prev.shadowBias, next.shadowBias)) fields.push('shadowBias');
234
258
  return fields;
235
259
  }
260
+ case 'Points': {
261
+ if (prev._tag !== 'Points') return undefined;
262
+ if (!behaviourEquals(prev.behaviour, next.behaviour)) fields.push('behaviour');
263
+ if (!vec3Equals(prev.position, next.position)) fields.push('position');
264
+ if (!rotationEquals(prev.rotation, next.rotation)) fields.push('rotation');
265
+ if (!vec3Equals(prev.scale, next.scale)) fields.push('scale');
266
+ if (!transitionEquals(prev.transition, next.transition)) fields.push('transition');
267
+ if (!pointLookEquals(prev.look, next.look)) fields.push('look');
268
+ return fields;
269
+ }
270
+ case 'Line': {
271
+ if (prev._tag !== 'Line') return undefined;
272
+ if (!behaviourEquals(prev.behaviour, next.behaviour)) fields.push('behaviour');
273
+ if (!vec3Equals(prev.position, next.position)) fields.push('position');
274
+ if (!rotationEquals(prev.rotation, next.rotation)) fields.push('rotation');
275
+ if (!vec3Equals(prev.scale, next.scale)) fields.push('scale');
276
+ if (!transitionEquals(prev.transition, next.transition)) fields.push('transition');
277
+ if (prev.interactive !== next.interactive) fields.push('interactive');
278
+ if (!lineLookEquals(prev, next)) fields.push('line');
279
+ return fields;
280
+ }
236
281
  case 'ShaderGrid': {
237
282
  if (prev._tag !== 'ShaderGrid') return undefined;
238
283
  if (!vec3Equals(prev.position, next.position)) fields.push('position');