@opsydyn/effect-three 0.3.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 (125) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +12 -0
  3. package/dist/{index-8cf8t6by.js → index-h3vf50fn.js} +2113 -83
  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 +488 -147
  8. package/dist/index.js.map +8 -6
  9. package/dist/kernel/boxSelect.d.ts +22 -0
  10. package/dist/kernel/boxSelect.d.ts.map +1 -0
  11. package/dist/kernel/build.d.ts +142 -6
  12. package/dist/kernel/build.d.ts.map +1 -1
  13. package/dist/kernel/cameraShake.d.ts.map +1 -1
  14. package/dist/kernel/describeScene.d.ts +50 -0
  15. package/dist/kernel/describeScene.d.ts.map +1 -0
  16. package/dist/kernel/description.d.ts +155 -1
  17. package/dist/kernel/description.d.ts.map +1 -1
  18. package/dist/kernel/diff.d.ts +15 -0
  19. package/dist/kernel/diff.d.ts.map +1 -1
  20. package/dist/kernel/equality.d.ts +9 -1
  21. package/dist/kernel/equality.d.ts.map +1 -1
  22. package/dist/kernel/gizmo.d.ts +69 -0
  23. package/dist/kernel/gizmo.d.ts.map +1 -0
  24. package/dist/kernel/history.d.ts +61 -0
  25. package/dist/kernel/history.d.ts.map +1 -0
  26. package/dist/kernel/index.d.ts +13 -2
  27. package/dist/kernel/index.d.ts.map +1 -1
  28. package/dist/kernel/index.js +106 -3
  29. package/dist/kernel/index.js.map +1 -1
  30. package/dist/kernel/inspect.d.ts +85 -0
  31. package/dist/kernel/inspect.d.ts.map +1 -0
  32. package/dist/kernel/lineGeometry.d.ts +25 -0
  33. package/dist/kernel/lineGeometry.d.ts.map +1 -0
  34. package/dist/kernel/lockstep.d.ts +114 -0
  35. package/dist/kernel/lockstep.d.ts.map +1 -0
  36. package/dist/kernel/loop.d.ts.map +1 -1
  37. package/dist/kernel/materials.d.ts.map +1 -1
  38. package/dist/kernel/pivot.d.ts +54 -0
  39. package/dist/kernel/pivot.d.ts.map +1 -0
  40. package/dist/kernel/pointBuffers.d.ts +17 -0
  41. package/dist/kernel/pointBuffers.d.ts.map +1 -0
  42. package/dist/kernel/pointer.d.ts +12 -0
  43. package/dist/kernel/pointer.d.ts.map +1 -1
  44. package/dist/kernel/renderKit.d.ts +21 -1
  45. package/dist/kernel/renderKit.d.ts.map +1 -1
  46. package/dist/kernel/renderer.d.ts +6 -0
  47. package/dist/kernel/renderer.d.ts.map +1 -1
  48. package/dist/kernel/sceneChanges.d.ts +87 -0
  49. package/dist/kernel/sceneChanges.d.ts.map +1 -0
  50. package/dist/kernel/sceneGraph.d.ts +6 -0
  51. package/dist/kernel/sceneGraph.d.ts.map +1 -1
  52. package/dist/kernel/seeded.d.ts +9 -0
  53. package/dist/kernel/seeded.d.ts.map +1 -0
  54. package/dist/kernel/shots.d.ts +26 -0
  55. package/dist/kernel/shots.d.ts.map +1 -0
  56. package/dist/kernel/tape.d.ts +38 -0
  57. package/dist/kernel/tape.d.ts.map +1 -0
  58. package/dist/kernel/trail.d.ts +31 -0
  59. package/dist/kernel/trail.d.ts.map +1 -0
  60. package/dist/kernel/viewportGizmo.d.ts +25 -0
  61. package/dist/kernel/viewportGizmo.d.ts.map +1 -0
  62. package/dist/runtime/Inspector.d.ts +20 -0
  63. package/dist/runtime/Inspector.d.ts.map +1 -0
  64. package/dist/runtime/SceneFile.d.ts +448 -0
  65. package/dist/runtime/SceneFile.d.ts.map +1 -0
  66. package/dist/runtime/SceneSchema.d.ts +318 -0
  67. package/dist/runtime/SceneSchema.d.ts.map +1 -1
  68. package/dist/runtime/errors.d.ts +9 -0
  69. package/dist/runtime/errors.d.ts.map +1 -1
  70. package/dist/runtime/index.d.ts +3 -1
  71. package/dist/runtime/index.d.ts.map +1 -1
  72. package/dist/webgl/index.d.ts.map +1 -1
  73. package/dist/webgl/index.js +228 -3
  74. package/dist/webgl/index.js.map +6 -4
  75. package/dist/webgl/line.d.ts +4 -0
  76. package/dist/webgl/line.d.ts.map +1 -0
  77. package/dist/webgl/points.d.ts +4 -0
  78. package/dist/webgl/points.d.ts.map +1 -0
  79. package/dist/webgpu/index.d.ts.map +1 -1
  80. package/dist/webgpu/index.js +206 -26
  81. package/dist/webgpu/index.js.map +6 -4
  82. package/dist/webgpu/line.d.ts +4 -0
  83. package/dist/webgpu/line.d.ts.map +1 -0
  84. package/dist/webgpu/points.d.ts +4 -0
  85. package/dist/webgpu/points.d.ts.map +1 -0
  86. package/package.json +2 -1
  87. package/src/kernel/boxSelect.ts +73 -0
  88. package/src/kernel/build.ts +332 -0
  89. package/src/kernel/cameraShake.ts +3 -13
  90. package/src/kernel/describeScene.ts +224 -0
  91. package/src/kernel/description.ts +175 -0
  92. package/src/kernel/diff.ts +39 -0
  93. package/src/kernel/equality.ts +101 -0
  94. package/src/kernel/gizmo.ts +478 -0
  95. package/src/kernel/history.ts +118 -0
  96. package/src/kernel/index.ts +97 -0
  97. package/src/kernel/inspect.ts +214 -0
  98. package/src/kernel/lineGeometry.ts +75 -0
  99. package/src/kernel/lockstep.ts +237 -0
  100. package/src/kernel/loop.ts +5 -0
  101. package/src/kernel/materials.ts +3 -0
  102. package/src/kernel/pivot.ts +418 -0
  103. package/src/kernel/pointBuffers.ts +115 -0
  104. package/src/kernel/pointer.ts +55 -12
  105. package/src/kernel/renderKit.ts +23 -1
  106. package/src/kernel/renderer.ts +6 -0
  107. package/src/kernel/sceneChanges.ts +265 -0
  108. package/src/kernel/sceneGraph.ts +319 -8
  109. package/src/kernel/seeded.ts +18 -0
  110. package/src/kernel/shots.ts +167 -0
  111. package/src/kernel/tape.ts +124 -0
  112. package/src/kernel/trail.ts +170 -0
  113. package/src/kernel/viewportGizmo.ts +194 -0
  114. package/src/runtime/Inspector.ts +43 -0
  115. package/src/runtime/SceneFile.ts +46 -0
  116. package/src/runtime/SceneSchema.ts +247 -0
  117. package/src/runtime/errors.ts +6 -0
  118. package/src/runtime/index.ts +3 -1
  119. package/src/webgl/index.ts +4 -0
  120. package/src/webgl/line.ts +66 -0
  121. package/src/webgl/points.ts +205 -0
  122. package/src/webgpu/index.ts +4 -0
  123. package/src/webgpu/line.ts +67 -0
  124. package/src/webgpu/points.ts +193 -0
  125. package/dist/index-8cf8t6by.js.map +0 -41
@@ -104,12 +104,24 @@ export {
104
104
  sharedMaterial,
105
105
  sphere,
106
106
  spin,
107
+ points,
108
+ sparkles,
109
+ stars,
110
+ trail,
111
+ edges,
112
+ line,
113
+ catmullRomPoints,
114
+ catmullRomPointAt,
115
+ catmullRomColors,
116
+ cubicBezierPoints,
117
+ quadraticBezierPoints,
107
118
  cameraShake,
108
119
  detailed,
109
120
  billboard,
110
121
  spotLight,
111
122
  standard,
112
123
  overlay,
124
+ hud,
113
125
  renderTexture,
114
126
  torus,
115
127
  type TransformOptions,
@@ -199,6 +211,8 @@ export {
199
211
  export type { BodyCollision, BodyContactForce } from './bodies.ts';
200
212
  export type {
201
213
  ContactShadowPasses,
214
+ HostedLine,
215
+ HostedPoints,
202
216
  HostedShaderGrid,
203
217
  HostedSky,
204
218
  RenderKit,
@@ -277,3 +291,86 @@ export {
277
291
  type ParentKey,
278
292
  type SceneIndex,
279
293
  } from './sceneIndex.ts';
294
+ export {
295
+ dragGizmo,
296
+ gizmo,
297
+ type Gizmo,
298
+ type GizmoGrab,
299
+ type GizmoHandle,
300
+ gizmoHandleOf,
301
+ type GizmoMode,
302
+ gizmoNodes,
303
+ type GizmoSpace,
304
+ hoverGizmo,
305
+ movedAlike,
306
+ type Placement,
307
+ pressGizmo,
308
+ releaseGizmo,
309
+ } from './gizmo.ts';
310
+ export { type BoxCorner, boxSelect, type CanvasSize, type Selectable } from './boxSelect.ts';
311
+ export * as History from './history.ts';
312
+ export * as Lockstep from './lockstep.ts';
313
+ export {
314
+ describeScene,
315
+ formatSummary,
316
+ type LightSummary,
317
+ type SceneSummary,
318
+ type ThingSummary,
319
+ thingsBounds,
320
+ } from './describeScene.ts';
321
+ export {
322
+ type Mood,
323
+ moodStaging,
324
+ moods,
325
+ shotAngles,
326
+ type ShotAngle,
327
+ shotCamera,
328
+ type Staging,
329
+ } from './shots.ts';
330
+ export {
331
+ type FieldPath,
332
+ type FieldValue,
333
+ type InspectableAst,
334
+ type InspectedField,
335
+ type InspectHints,
336
+ inspectFields,
337
+ setAt,
338
+ } from './inspect.ts';
339
+ export {
340
+ type ChangePlace,
341
+ formatChanges,
342
+ type SceneChange,
343
+ type SceneChanges,
344
+ sceneChanges,
345
+ type SceneSetting,
346
+ } from './sceneChanges.ts';
347
+ export {
348
+ clearTape,
349
+ placeTapeEnd,
350
+ pointTape,
351
+ type Tape,
352
+ tape,
353
+ tapeLength,
354
+ type TapeLook,
355
+ tapeMiddle,
356
+ tapeNodes,
357
+ } from './tape.ts';
358
+ export {
359
+ dragPivot,
360
+ hoverPivot,
361
+ type Pivot,
362
+ pivot,
363
+ type PivotGrab,
364
+ type PivotHandle,
365
+ pivotHandleOf,
366
+ pivotNodes,
367
+ pressPivot,
368
+ releasePivot,
369
+ } from './pivot.ts';
370
+ export {
371
+ cameraAlong,
372
+ type ViewAxis,
373
+ viewportAxisOf,
374
+ viewportGizmoNodes,
375
+ type ViewportGizmoOptions,
376
+ } from './viewportGizmo.ts';
@@ -0,0 +1,214 @@
1
+ /**
2
+ * The fields of a value, read from its schema (roadmap, "Beyond parity" 4: an inspector
3
+ * generated from the scene schemas). The runtime's `Inspector` passes a schema's AST in;
4
+ * this walks it beside the value and lists what an inspector shows: numbers, text,
5
+ * switches, choices of literals, colours, vectors, and the rest read-only (a node's
6
+ * kind, an absent part, a list of children). A tagged union shows the member the value
7
+ * is; nested structs show their fields under their name.
8
+ *
9
+ * The AST is read through the few shapes used here, so the kernel needs no schema
10
+ * library; colours and vectors are recognised by being the very AST nodes the caller
11
+ * names, not by field names.
12
+ *
13
+ * @module
14
+ */
15
+ import type { Vec3 } from './description.ts';
16
+
17
+ /** Where a field is in the value: property names and list indices from the top. */
18
+ export type FieldPath = ReadonlyArray<string | number>;
19
+
20
+ /** What an inspector's controls produce: a number, text, or a switch's state. */
21
+ export type FieldValue = number | string | boolean;
22
+
23
+ type Base = { readonly path: FieldPath; readonly label: string };
24
+
25
+ /** A field as an inspector shows it. */
26
+ export type InspectedField = Base &
27
+ (
28
+ | { readonly _tag: 'Number'; readonly value: number }
29
+ | { readonly _tag: 'Text'; readonly value: string }
30
+ | { readonly _tag: 'Toggle'; readonly value: boolean }
31
+ | { readonly _tag: 'Choice'; readonly value: string; readonly options: ReadonlyArray<string> }
32
+ | { readonly _tag: 'Colour'; readonly value: number }
33
+ | { readonly _tag: 'Vector'; readonly value: Vec3 }
34
+ /** Shown, not edited: a kind, an absent part, a list, or what this cannot edit. */
35
+ | { readonly _tag: 'Fixed'; readonly text: string }
36
+ );
37
+
38
+ /** The parts of a schema AST read here (Effect's `SchemaAST`, structurally). */
39
+ export type InspectableAst =
40
+ | { readonly _tag: 'Suspend'; readonly thunk: () => InspectableAst }
41
+ | {
42
+ readonly _tag: 'Objects';
43
+ readonly propertySignatures: ReadonlyArray<{
44
+ readonly name: PropertyKey;
45
+ readonly type: InspectableAst;
46
+ }>;
47
+ }
48
+ | {
49
+ readonly _tag: 'Arrays';
50
+ readonly elements: ReadonlyArray<InspectableAst>;
51
+ readonly rest: ReadonlyArray<InspectableAst>;
52
+ }
53
+ | { readonly _tag: 'Union'; readonly types: ReadonlyArray<InspectableAst> }
54
+ | { readonly _tag: 'Literal'; readonly literal: unknown }
55
+ | { readonly _tag: 'Number' | 'String' | 'Boolean' | 'Null' }
56
+ | { readonly _tag: string };
57
+
58
+ /** AST nodes shown as a colour picker or as three numbers, by identity. */
59
+ export type InspectHints = {
60
+ readonly colours: ReadonlyArray<unknown>;
61
+ readonly vectors: ReadonlyArray<unknown>;
62
+ };
63
+
64
+ const resolve = (ast: InspectableAst): InspectableAst =>
65
+ ast._tag === 'Suspend' && 'thunk' in ast ? resolve(ast.thunk()) : ast;
66
+
67
+ const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
68
+ typeof value === 'object' && value !== null && !Array.isArray(value);
69
+
70
+ const labelOf = (path: FieldPath): string => String(path.at(-1) ?? 'value');
71
+
72
+ const literalOf = (ast: InspectableAst): unknown => ('literal' in ast ? ast.literal : undefined);
73
+
74
+ /** The literal of an object member's `_tag`, if it has one. */
75
+ const tagOf = (ast: InspectableAst): unknown => {
76
+ const member = resolve(ast);
77
+ if (!('propertySignatures' in member)) return undefined;
78
+ const tag = member.propertySignatures.find((property) => property.name === '_tag');
79
+ return tag === undefined ? undefined : literalOf(resolve(tag.type));
80
+ };
81
+
82
+ /** The union member `value` is: by its `_tag`, else the non-null one, else the first. */
83
+ const memberFor = (
84
+ types: ReadonlyArray<InspectableAst>,
85
+ value: unknown,
86
+ ): InspectableAst | undefined => {
87
+ if (isRecord(value) && '_tag' in value) {
88
+ const tagged = types.find((member) => tagOf(member) === value['_tag']);
89
+ if (tagged !== undefined) return tagged;
90
+ }
91
+ if (value === null) return types.find((member) => resolve(member)._tag === 'Null');
92
+ return types.find((member) => resolve(member)._tag !== 'Null') ?? types[0];
93
+ };
94
+
95
+ const fixed = (path: FieldPath, text: string): InspectedField => ({
96
+ _tag: 'Fixed',
97
+ path,
98
+ label: labelOf(path),
99
+ text,
100
+ });
101
+
102
+ const isVector = (value: unknown): value is Vec3 =>
103
+ Array.isArray(value) && value.length === 3 && value.every((n) => typeof n === 'number');
104
+
105
+ const walk = (
106
+ given: InspectableAst,
107
+ value: unknown,
108
+ path: FieldPath,
109
+ hints: InspectHints,
110
+ ): ReadonlyArray<InspectedField> => {
111
+ const ast = resolve(given);
112
+ const label = labelOf(path);
113
+ if (hints.colours.includes(ast) && typeof value === 'number') {
114
+ return [{ _tag: 'Colour', path, label, value }];
115
+ }
116
+ if (hints.vectors.includes(ast) && isVector(value)) {
117
+ return [{ _tag: 'Vector', path, label, value }];
118
+ }
119
+ switch (ast._tag) {
120
+ case 'Number':
121
+ return typeof value === 'number' ? [{ _tag: 'Number', path, label, value }] : [];
122
+ case 'String':
123
+ return typeof value === 'string' ? [{ _tag: 'Text', path, label, value }] : [];
124
+ case 'Boolean':
125
+ return typeof value === 'boolean' ? [{ _tag: 'Toggle', path, label, value }] : [];
126
+ case 'Literal':
127
+ case 'Null':
128
+ return [fixed(path, String(value))];
129
+ case 'Union':
130
+ return 'types' in ast ? unionFields(ast.types, value, path, hints) : [];
131
+ case 'Objects':
132
+ return 'propertySignatures' in ast ? objectFields(ast, value, path, hints) : [];
133
+ case 'Arrays':
134
+ return 'elements' in ast ? arrayFields(ast, value, path, hints) : [];
135
+ default:
136
+ return [fixed(path, String(value))];
137
+ }
138
+ };
139
+
140
+ /** A union of string literals is a choice; any other shows the member the value is. */
141
+ const unionFields = (
142
+ types: ReadonlyArray<InspectableAst>,
143
+ value: unknown,
144
+ path: FieldPath,
145
+ hints: InspectHints,
146
+ ): ReadonlyArray<InspectedField> => {
147
+ const literals = types.map((member) => literalOf(resolve(member)));
148
+ if (typeof value === 'string' && literals.every((literal) => typeof literal === 'string')) {
149
+ return [{ _tag: 'Choice', path, label: labelOf(path), value, options: literals.map(String) }];
150
+ }
151
+ const member = memberFor(types, value);
152
+ return member === undefined ? [fixed(path, String(value))] : walk(member, value, path, hints);
153
+ };
154
+
155
+ const objectFields = (
156
+ ast: Extract<InspectableAst, { readonly propertySignatures: unknown }>,
157
+ value: unknown,
158
+ path: FieldPath,
159
+ hints: InspectHints,
160
+ ): ReadonlyArray<InspectedField> => {
161
+ if (!isRecord(value)) return [fixed(path, String(value))];
162
+ return ast.propertySignatures.flatMap((property) =>
163
+ propertyFields(String(property.name), property.type, value, path, hints),
164
+ );
165
+ };
166
+
167
+ /** One property's fields: none when absent; a tag shows what kind it is, never edited. */
168
+ function propertyFields(
169
+ name: string,
170
+ type: InspectableAst,
171
+ value: Readonly<Record<string, unknown>>,
172
+ path: FieldPath,
173
+ hints: InspectHints,
174
+ ): ReadonlyArray<InspectedField> {
175
+ if (!(name in value)) return [];
176
+ if (name === '_tag') return [fixed([...path, name], String(value[name]))];
177
+ return walk(type, value[name], [...path, name], hints);
178
+ }
179
+
180
+ /** A fixed tuple shows each element; a list of any length shows how many it holds. */
181
+ const arrayFields = (
182
+ ast: Extract<InspectableAst, { readonly elements: unknown }>,
183
+ value: unknown,
184
+ path: FieldPath,
185
+ hints: InspectHints,
186
+ ): ReadonlyArray<InspectedField> => {
187
+ if (!Array.isArray(value)) return [fixed(path, String(value))];
188
+ if (ast.rest.length > 0) {
189
+ return [fixed(path, `${value.length} ${value.length === 1 ? 'item' : 'items'}`)];
190
+ }
191
+ return ast.elements.flatMap((element, index) =>
192
+ walk(element, value[index], [...path, index], hints),
193
+ );
194
+ };
195
+
196
+ /** The fields of `value` as its schema's AST describes them, in the schema's order. */
197
+ export const inspectFields = (
198
+ ast: InspectableAst,
199
+ value: unknown,
200
+ hints: InspectHints = { colours: [], vectors: [] },
201
+ ): ReadonlyArray<InspectedField> => walk(ast, value, [], hints);
202
+
203
+ /** `value` with `next` at `path`, every object and list on the way copied, not changed. */
204
+ export const setAt = (value: unknown, path: FieldPath, next: unknown): unknown => {
205
+ const [head, ...rest] = path;
206
+ if (head === undefined) return next;
207
+ if (Array.isArray(value) && typeof head === 'number') {
208
+ return value.map((item, index) => (index === head ? setAt(item, rest, next) : item));
209
+ }
210
+ if (isRecord(value) && typeof head === 'string') {
211
+ return { ...value, [head]: setAt(value[head], rest, next) };
212
+ }
213
+ return value;
214
+ };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * A line's geometry, as Drei's `Line` builds it: three's `LineGeometry` through the points
3
+ * (or `LineSegmentsGeometry`, a segment for each pair), with a colour per point when it
4
+ * has them. Shared by the render kits, whose materials differ; the geometry does not.
5
+ *
6
+ * @module
7
+ */
8
+ import { type BufferGeometry, Color, EdgesGeometry } from 'three';
9
+ import type { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js';
10
+
11
+ import type { Edges, LineNode, Vec3 } from './description.ts';
12
+
13
+ /** Whether there is anything to draw: two points for a line, a pair for segments. */
14
+ export const drawable = (line: LineNode): boolean => line.points.length >= 2;
15
+
16
+ /**
17
+ * Fills a line's geometry: a `LineGeometry` through the points, or a
18
+ * `LineSegmentsGeometry` for segments. Too few points give one empty segment (hidden).
19
+ */
20
+ export const fillLineGeometry = <G extends LineSegmentsGeometry>(
21
+ geometry: G,
22
+ line: LineNode,
23
+ ): G => {
24
+ const points = drawable(line) ? line.points : [[0, 0, 0] as const, [0, 0, 0] as const];
25
+ // Segments come in pairs: an odd last point has nothing to join.
26
+ const used = line.segments ? points.slice(0, points.length - (points.length % 2)) : points;
27
+ geometry.setPositions(used.flat());
28
+ const colors = line.vertexColors;
29
+ if (colors !== null) {
30
+ // A colour for every point: the last given carries on, as a line can't skip one.
31
+ const per = used.map((_, index) => colors[Math.min(index, colors.length - 1)] ?? line.color);
32
+ geometry.setColors(per.flatMap((color) => new Color(color).toArray()));
33
+ }
34
+ return geometry;
35
+ };
36
+
37
+ /**
38
+ * The line segments along a geometry's edges where faces meet at more than `threshold`
39
+ * degrees, as Drei's `Edges` takes them from three's `EdgesGeometry`.
40
+ */
41
+ export const edgePoints = (geometry: BufferGeometry, threshold: number): ReadonlyArray<Vec3> => {
42
+ const found = new EdgesGeometry(geometry, threshold);
43
+ const position = found.getAttribute('position');
44
+ const points = Array.from(
45
+ { length: position.count },
46
+ (_, index): Vec3 => [position.getX(index), position.getY(index), position.getZ(index)],
47
+ );
48
+ found.dispose();
49
+ return points;
50
+ };
51
+
52
+ /** The line a mesh's edges are drawn as: segments in the mesh's own space. */
53
+ export const edgeLine = (key: string, look: Edges, points: ReadonlyArray<Vec3>): LineNode => ({
54
+ _tag: 'Line',
55
+ key: `${key}:edges`,
56
+ position: [0, 0, 0],
57
+ rotation: { _tag: 'Fixed', euler: [0, 0, 0] },
58
+ scale: [1, 1, 1],
59
+ transition: { _tag: 'Instant' },
60
+ behaviour: null,
61
+ interactive: false,
62
+ points,
63
+ segments: true,
64
+ color: look.color,
65
+ vertexColors: null,
66
+ width: look.width,
67
+ worldUnits: look.worldUnits,
68
+ dashed: look.dashed,
69
+ dashSize: look.dashSize,
70
+ gapSize: look.gapSize,
71
+ dashScale: 1,
72
+ dashOffset: 0,
73
+ opacity: look.opacity,
74
+ transparent: look.transparent,
75
+ });
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Lockstep, as data (roadmap, "Beyond parity" 2): peers that run the same `update` stay
3
+ * in the same state by applying the same inputs in the same order. Time is cut into
4
+ * turns; every member sends one packet for each turn (empty when it did nothing), and a
5
+ * turn applies only once every member's packet for it has arrived, its inputs in peer
6
+ * order. Any transport that delivers each packet to every peer will do: a
7
+ * `BroadcastChannel` between tabs, a socket relay between machines.
8
+ *
9
+ * Membership changes in step too. The leader (the member whose id sorts first) admits
10
+ * a newcomer with an entry in its own packet; when that turn applies, everywhere at
11
+ * once, the newcomer becomes a member `lead + 1` turns later, and the leader sends it
12
+ * the state as it is then (`welcomeDue`), from which it carries on. A member that leaves
13
+ * says which turn it sent last (`leave`); one that goes quiet is dropped by the leader
14
+ * from the turn everyone is waiting on, which no one can have applied.
15
+ *
16
+ * Pure functions over a value, like the rest of the kernel: an app keeps the `Lockstep`
17
+ * in its Model, sends what `send` gives it, passes what arrives to `receive`, and
18
+ * applies what `advance` returns. Use it as a namespace: `Lockstep.make(self)`.
19
+ *
20
+ * @module
21
+ */
22
+
23
+ /** What a turn carries: an app's input, or the leader admitting a newcomer. */
24
+ export type Entry<I> =
25
+ | { readonly _tag: 'Input'; readonly input: I }
26
+ | { readonly _tag: 'Admit'; readonly peer: string };
27
+
28
+ /** One member's packet for one turn. */
29
+ export type Packet<I> = {
30
+ readonly peer: string;
31
+ readonly turn: number;
32
+ readonly entries: ReadonlyArray<Entry<I>>;
33
+ };
34
+
35
+ /** A member: the first turn it sends for, and the turn it stopped before, once it has. */
36
+ export type Member = {
37
+ readonly peer: string;
38
+ readonly from: number;
39
+ readonly until: number | null;
40
+ };
41
+
42
+ export type Lockstep<I> = {
43
+ readonly self: string;
44
+ readonly members: ReadonlyArray<Member>;
45
+ /** The next turn to apply. */
46
+ readonly turn: number;
47
+ /** The next turn this peer sends for. */
48
+ readonly sent: number;
49
+ /** How many turns ahead of the applied one a peer may send: inputs wait this long. */
50
+ readonly lead: number;
51
+ /** Entries waiting for this peer's next packet. */
52
+ readonly pending: ReadonlyArray<Entry<I>>;
53
+ /** Packets for turns not yet applied, this peer's own among them. */
54
+ readonly received: ReadonlyArray<Packet<I>>;
55
+ };
56
+
57
+ /** An input applied: the turn, and the peer it came from. */
58
+ export type Applied<I> = { readonly turn: number; readonly peer: string; readonly input: I };
59
+
60
+ const sortedPeers = (members: ReadonlyArray<Member>): ReadonlyArray<Member> =>
61
+ members.toSorted((a, b) => (a.peer < b.peer ? -1 : a.peer > b.peer ? 1 : 0));
62
+
63
+ const memberAt = (member: Member, turn: number): boolean =>
64
+ member.from <= turn && (member.until === null || turn < member.until);
65
+
66
+ /** The members a turn waits for, in the order their inputs apply. */
67
+ export const membersAt = <I>(state: Lockstep<I>, turn: number): ReadonlyArray<string> =>
68
+ sortedPeers(state.members.filter((member) => memberAt(member, turn))).map(
69
+ (member) => member.peer,
70
+ );
71
+
72
+ /** The member who admits and drops others at `turn`: the one whose id sorts first. */
73
+ export const leaderAt = <I>(state: Lockstep<I>, turn: number): string | undefined =>
74
+ membersAt(state, turn)[0];
75
+
76
+ /** A session of one: this peer alone, from turn 0. */
77
+ export const make = <I>(self: string, lead = 2): Lockstep<I> => ({
78
+ self,
79
+ members: [{ peer: self, from: 0, until: null }],
80
+ turn: 0,
81
+ sent: 0,
82
+ lead,
83
+ pending: [],
84
+ received: [],
85
+ });
86
+
87
+ /**
88
+ * A newcomer's session, from the leader's welcome: the members and the turn it starts
89
+ * at, with whatever packets it heard while it waited.
90
+ */
91
+ export const join = <I>(
92
+ self: string,
93
+ welcome: {
94
+ readonly turn: number;
95
+ readonly members: ReadonlyArray<Member>;
96
+ readonly lead: number;
97
+ },
98
+ heard: ReadonlyArray<Packet<I>> = [],
99
+ ): Lockstep<I> => ({
100
+ self,
101
+ members: welcome.members,
102
+ turn: welcome.turn,
103
+ sent: welcome.turn,
104
+ lead: welcome.lead,
105
+ pending: [],
106
+ received: heard.filter((packet) => packet.turn >= welcome.turn),
107
+ });
108
+
109
+ /** `input` waits for this peer's next packet. */
110
+ export const submit = <I>(state: Lockstep<I>, input: I): Lockstep<I> => ({
111
+ ...state,
112
+ pending: [...state.pending, { _tag: 'Input', input }],
113
+ });
114
+
115
+ const known = <I>(state: Lockstep<I>, peer: string): boolean =>
116
+ state.members.some((member) => member.peer === peer && member.until === null) ||
117
+ state.pending.some((entry) => entry._tag === 'Admit' && entry.peer === peer);
118
+
119
+ /** The leader asks for `peer` to be admitted; anyone else, or a known peer, changes nothing. */
120
+ export const admit = <I>(state: Lockstep<I>, peer: string): Lockstep<I> =>
121
+ leaderAt(state, state.sent) !== state.self || known(state, peer)
122
+ ? state
123
+ : { ...state, pending: [...state.pending, { _tag: 'Admit', peer }] };
124
+
125
+ const hasPacket = <I>(state: Lockstep<I>, peer: string, turn: number): boolean =>
126
+ state.received.some((packet) => packet.peer === peer && packet.turn === turn);
127
+
128
+ /**
129
+ * A packet arriving: kept until its turn applies. Ones for turns already applied, from
130
+ * peers not members then, and second copies are dropped.
131
+ */
132
+ export const receive = <I>(state: Lockstep<I>, packet: Packet<I>): Lockstep<I> => {
133
+ if (packet.turn < state.turn || hasPacket(state, packet.peer, packet.turn)) return state;
134
+ const member = state.members.find((each) => each.peer === packet.peer);
135
+ if (member !== undefined && !memberAt(member, packet.turn)) return state;
136
+ return { ...state, received: [...state.received, packet] };
137
+ };
138
+
139
+ /**
140
+ * This peer's packet for its next turn, with what is pending, when it is a member then
141
+ * and not more than `lead` turns ahead; it keeps its own copy.
142
+ */
143
+ export const send = <I>(
144
+ state: Lockstep<I>,
145
+ ): { readonly state: Lockstep<I>; readonly packet: Packet<I> | undefined } => {
146
+ const member = state.members.find((each) => each.peer === state.self);
147
+ const ahead = state.sent >= state.turn + state.lead;
148
+ if (member === undefined || !memberAt(member, state.sent) || ahead) {
149
+ return { state, packet: undefined };
150
+ }
151
+ const packet: Packet<I> = { peer: state.self, turn: state.sent, entries: state.pending };
152
+ return {
153
+ state: { ...state, sent: state.sent + 1, pending: [], received: [...state.received, packet] },
154
+ packet,
155
+ };
156
+ };
157
+
158
+ /** Peers whose packet for the next turn has not arrived: what the session waits on. */
159
+ export const waitingOn = <I>(state: Lockstep<I>): ReadonlyArray<string> =>
160
+ membersAt(state, state.turn).filter((peer) => !hasPacket(state, peer, state.turn));
161
+
162
+ /** `peer`'s packet for `turn`, as a list of none or one. */
163
+ const packetsOf = <I>(state: Lockstep<I>, peer: string, turn: number): ReadonlyArray<Packet<I>> =>
164
+ state.received.filter((packet) => packet.peer === peer && packet.turn === turn);
165
+
166
+ /** One turn applied, its admissions made members `lead + 1` turns on. */
167
+ const applyTurn = <I>(
168
+ state: Lockstep<I>,
169
+ ): { state: Lockstep<I>; applied: ReadonlyArray<Applied<I>> } => {
170
+ const turn = state.turn;
171
+ const packets = membersAt(state, turn).flatMap((peer) => packetsOf(state, peer, turn));
172
+ const entries = packets.flatMap((packet) =>
173
+ packet.entries.map((entry) => ({ peer: packet.peer, entry })),
174
+ );
175
+ const admitted = entries.flatMap(({ entry }) =>
176
+ entry._tag === 'Admit' && !state.members.some((member) => member.peer === entry.peer)
177
+ ? [{ peer: entry.peer, from: turn + state.lead + 1, until: null }]
178
+ : [],
179
+ );
180
+ const applied = entries.flatMap(({ peer, entry }) =>
181
+ entry._tag === 'Input' ? [{ turn, peer, input: entry.input }] : [],
182
+ );
183
+ return {
184
+ state: {
185
+ ...state,
186
+ turn: turn + 1,
187
+ members: [...state.members, ...admitted],
188
+ received: state.received.filter((packet) => packet.turn > turn),
189
+ },
190
+ applied,
191
+ };
192
+ };
193
+
194
+ /**
195
+ * Every turn whose packets have all arrived, applied in order: the new state, and the
196
+ * inputs to apply to the app, oldest first and in peer order within a turn.
197
+ */
198
+ export const advance = <I>(
199
+ state: Lockstep<I>,
200
+ ): { readonly state: Lockstep<I>; readonly applied: ReadonlyArray<Applied<I>> } => {
201
+ let current = state;
202
+ const applied: Array<Applied<I>> = [];
203
+ // A turn with no members waits forever rather than passing unheard.
204
+ while (membersAt(current, current.turn).length > 0 && waitingOn(current).length === 0) {
205
+ const next = applyTurn(current);
206
+ current = next.state;
207
+ applied.push(...next.applied);
208
+ }
209
+ return { state: current, applied };
210
+ };
211
+
212
+ /**
213
+ * Newcomers this peer, as leader, should welcome now: those who become members at the
214
+ * turn about to apply, once every turn before it has (the state is then theirs to start
215
+ * from). Each is due once: at the moment `before` (the previous turn) is passed.
216
+ */
217
+ export const welcomeDue = <I>(before: number, state: Lockstep<I>): ReadonlyArray<string> =>
218
+ state.members
219
+ .filter((member) => member.from > before && member.from <= state.turn)
220
+ // The leader of the turn before the newcomer counts: never the newcomer itself, whose
221
+ // id may sort first.
222
+ .filter((member) => leaderAt(state, member.from - 1) === state.self)
223
+ .map((member) => member.peer);
224
+
225
+ /** This peer leaving: the turn it sent last, for the others to stop waiting after. */
226
+ export const leave = <I>(state: Lockstep<I>): number => state.sent - 1;
227
+
228
+ /** `peer` stops being a member from `until`; its packets from then are dropped. */
229
+ export const remove = <I>(state: Lockstep<I>, peer: string, until: number): Lockstep<I> => ({
230
+ ...state,
231
+ members: state.members.map((member) =>
232
+ member.peer === peer && (member.until === null || until < member.until)
233
+ ? { ...member, until }
234
+ : member,
235
+ ),
236
+ received: state.received.filter((packet) => packet.peer !== peer || packet.turn < until),
237
+ });
@@ -215,6 +215,11 @@ export const createSceneLoop = (options: SceneLoopOptions): SceneLoop => {
215
215
  const views = graph.views();
216
216
  if (views.length === 0) renderer.render(graph.scene, graph.camera);
217
217
  for (const view of views) renderer.render(graph.scene, view.camera, view.area);
218
+ const hud = graph.hud();
219
+ if (hud !== undefined && renderer.renderOver !== undefined) {
220
+ renderer.renderOver(hud.graph.scene, hud.graph.camera);
221
+ hud.graph.drawn();
222
+ }
218
223
  graph.drawn();
219
224
  measure();
220
225
  const overlays = graph.overlays();
@@ -108,6 +108,9 @@ const applyBasic = (
108
108
  textures: Textures,
109
109
  ): void => {
110
110
  material.color.setHex(description.color);
111
+ // Drawn over everything when not depth-tested, and leaving no depth behind.
112
+ material.depthTest = description.depthTest;
113
+ material.depthWrite = description.depthTest;
111
114
  const mapChanged = applyMap(material, description.map, textures);
112
115
  if (applySurface(material, description) || mapChanged) material.needsUpdate = true;
113
116
  };