@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,118 @@
1
+ /**
2
+ * Undo and branching history from an immutable Model (roadmap, "Beyond parity" 4). A
3
+ * Model's values are never changed in place, so keeping every one costs only the
4
+ * references: a history is a tree of them. Recording after an undo starts a new branch
5
+ * and keeps the old future as another, so nothing done is ever lost; redo follows the
6
+ * branch last taken, and `chooseBranch` or `jumpTo` take another.
7
+ *
8
+ * Entries are numbered in the order they were recorded, from 0 (the first value), and
9
+ * each names the entry it followed. Pure data with a schema (`SceneSchema.History`),
10
+ * so a history can be saved, sent or inspected like the rest of the Model. Use it as a
11
+ * namespace: `History.make(value)`, `History.record(history, next, 'Moved crate')`.
12
+ *
13
+ * @module
14
+ */
15
+
16
+ /** A value in the history: the entry it followed, and the one redo goes to from it. */
17
+ export type HistoryEntry<A> = {
18
+ readonly id: number;
19
+ /** The entry this one followed, `null` for the first. */
20
+ readonly parent: number | null;
21
+ /** A few words for the change that made it, for a list of steps. */
22
+ readonly label: string;
23
+ readonly value: A;
24
+ /** The entry redo goes to from here: the branch last taken, `null` with none. */
25
+ readonly next: number | null;
26
+ };
27
+
28
+ export type History<A> = {
29
+ /** Every value recorded, by id. */
30
+ readonly entries: ReadonlyArray<HistoryEntry<A>>;
31
+ /** The present value's id. */
32
+ readonly current: number;
33
+ };
34
+
35
+ /** A history holding `value` alone. */
36
+ export const make = <A>(value: A, label = 'Start'): History<A> => ({
37
+ entries: [{ id: 0, parent: null, label, value, next: null }],
38
+ current: 0,
39
+ });
40
+
41
+ const entryOf = <A>(state: History<A>, id: number): HistoryEntry<A> => {
42
+ const entry = state.entries[id];
43
+ // Ids are indices, made only here: one out of range is a broken history.
44
+ if (entry === undefined) throw new RangeError(`No history entry ${id}`);
45
+ return entry;
46
+ };
47
+
48
+ /** The present entry. */
49
+ export const presentEntry = <A>(state: History<A>): HistoryEntry<A> =>
50
+ entryOf(state, state.current);
51
+
52
+ /** The present value. */
53
+ export const present = <A>(state: History<A>): A => presentEntry(state).value;
54
+
55
+ const withNext = <A>(state: History<A>, id: number, next: number): History<A> => ({
56
+ ...state,
57
+ entries: state.entries.map((entry) => (entry.id === id ? { ...entry, next } : entry)),
58
+ });
59
+
60
+ /** `value` after the present, labelled: the new present, its own branch after an undo. */
61
+ export const record = <A>(state: History<A>, value: A, label: string): History<A> => {
62
+ const id = state.entries.length;
63
+ const grown: History<A> = {
64
+ entries: [...state.entries, { id, parent: state.current, label, value, next: null }],
65
+ current: id,
66
+ };
67
+ return withNext(grown, state.current, id);
68
+ };
69
+
70
+ /** Whether there is a value before the present. */
71
+ export const canUndo = <A>(state: History<A>): boolean => presentEntry(state).parent !== null;
72
+
73
+ /** Whether there is a value after the present on the branch last taken. */
74
+ export const canRedo = <A>(state: History<A>): boolean => presentEntry(state).next !== null;
75
+
76
+ /** Back one value; at the first, as it was. */
77
+ export const undo = <A>(state: History<A>): History<A> => {
78
+ const parent = presentEntry(state).parent;
79
+ return parent === null ? state : { ...state, current: parent };
80
+ };
81
+
82
+ /** Forward one value along the branch last taken; at its end, as it was. */
83
+ export const redo = <A>(state: History<A>): History<A> => {
84
+ const next = presentEntry(state).next;
85
+ return next === null ? state : { ...state, current: next };
86
+ };
87
+
88
+ /** The entries that followed the present: one per branch, in the order recorded. */
89
+ export const branches = <A>(state: History<A>): ReadonlyArray<HistoryEntry<A>> =>
90
+ state.entries.filter((entry) => entry.parent === state.current);
91
+
92
+ /** Redo from the present goes to `id`, when it is one of the present's branches. */
93
+ export const chooseBranch = <A>(state: History<A>, id: number): History<A> =>
94
+ branches(state).some((entry) => entry.id === id) ? withNext(state, state.current, id) : state;
95
+
96
+ /** The entries from the first to `id`, in order. */
97
+ export const pathTo = <A>(state: History<A>, id: number): ReadonlyArray<HistoryEntry<A>> => {
98
+ const path: Array<HistoryEntry<A>> = [];
99
+ for (let at: number | null = id; at !== null; at = entryOf(state, at).parent) {
100
+ path.unshift(entryOf(state, at));
101
+ }
102
+ return path;
103
+ };
104
+
105
+ /**
106
+ * Straight to entry `id`, wherever it is in the tree, with each step to it the branch
107
+ * redo takes, so undoing and redoing comes back to it.
108
+ */
109
+ export const jumpTo = <A>(state: History<A>, id: number): History<A> => {
110
+ const path = pathTo(state, id);
111
+ const chosen = path
112
+ .slice(1)
113
+ .reduce((at, entry) => withNext(at, entry.parent ?? 0, entry.id), state);
114
+ return { ...chosen, current: id };
115
+ };
116
+
117
+ /** How many steps the present is from the first value. */
118
+ export const depth = <A>(state: History<A>): number => pathTo(state, state.current).length - 1;
@@ -96,15 +96,32 @@ export {
96
96
  pointLight,
97
97
  renderSettings,
98
98
  ring,
99
+ roundedBox,
100
+ backdrop,
99
101
  scene,
100
102
  sharedGeometry,
101
103
  lentGeometry,
102
104
  sharedMaterial,
103
105
  sphere,
104
106
  spin,
107
+ points,
108
+ sparkles,
109
+ stars,
110
+ trail,
111
+ edges,
112
+ line,
113
+ catmullRomPoints,
114
+ catmullRomPointAt,
115
+ catmullRomColors,
116
+ cubicBezierPoints,
117
+ quadraticBezierPoints,
118
+ cameraShake,
119
+ detailed,
120
+ billboard,
105
121
  spotLight,
106
122
  standard,
107
123
  overlay,
124
+ hud,
108
125
  renderTexture,
109
126
  torus,
110
127
  type TransformOptions,
@@ -194,6 +211,8 @@ export {
194
211
  export type { BodyCollision, BodyContactForce } from './bodies.ts';
195
212
  export type {
196
213
  ContactShadowPasses,
214
+ HostedLine,
215
+ HostedPoints,
197
216
  HostedShaderGrid,
198
217
  HostedSky,
199
218
  RenderKit,
@@ -272,3 +291,86 @@ export {
272
291
  type ParentKey,
273
292
  type SceneIndex,
274
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
+ });