@opsydyn/effect-three 0.3.0 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/README.md +15 -1
- package/dist/{index-8cf8t6by.js → index-re3rrfwd.js} +2935 -104
- package/dist/index-re3rrfwd.js.map +56 -0
- package/dist/index-ttwy0znv.js +145 -0
- package/dist/index-ttwy0znv.js.map +12 -0
- package/dist/index.js +557 -147
- package/dist/index.js.map +8 -6
- package/dist/kernel/bodies.d.ts +20 -1
- package/dist/kernel/bodies.d.ts.map +1 -1
- package/dist/kernel/boxSelect.d.ts +22 -0
- package/dist/kernel/boxSelect.d.ts.map +1 -0
- package/dist/kernel/build.d.ts +165 -7
- package/dist/kernel/build.d.ts.map +1 -1
- package/dist/kernel/cameraShake.d.ts.map +1 -1
- package/dist/kernel/describeScene.d.ts +50 -0
- package/dist/kernel/describeScene.d.ts.map +1 -0
- package/dist/kernel/description.d.ts +223 -1
- package/dist/kernel/description.d.ts.map +1 -1
- package/dist/kernel/diff.d.ts +15 -0
- package/dist/kernel/diff.d.ts.map +1 -1
- package/dist/kernel/equality.d.ts +11 -1
- package/dist/kernel/equality.d.ts.map +1 -1
- package/dist/kernel/gizmo.d.ts +69 -0
- package/dist/kernel/gizmo.d.ts.map +1 -0
- package/dist/kernel/history.d.ts +61 -0
- package/dist/kernel/history.d.ts.map +1 -0
- package/dist/kernel/index.d.ts +19 -6
- package/dist/kernel/index.d.ts.map +1 -1
- package/dist/kernel/index.js +124 -3
- package/dist/kernel/index.js.map +1 -1
- package/dist/kernel/inspect.d.ts +85 -0
- package/dist/kernel/inspect.d.ts.map +1 -0
- package/dist/kernel/lineGeometry.d.ts +25 -0
- package/dist/kernel/lineGeometry.d.ts.map +1 -0
- package/dist/kernel/lockstep.d.ts +114 -0
- package/dist/kernel/lockstep.d.ts.map +1 -0
- package/dist/kernel/loop.d.ts.map +1 -1
- package/dist/kernel/materials.d.ts.map +1 -1
- package/dist/kernel/paintOrder.d.ts +53 -0
- package/dist/kernel/paintOrder.d.ts.map +1 -0
- package/dist/kernel/physics.d.ts +39 -0
- package/dist/kernel/physics.d.ts.map +1 -1
- package/dist/kernel/pivot.d.ts +54 -0
- package/dist/kernel/pivot.d.ts.map +1 -0
- package/dist/kernel/pointBuffers.d.ts +17 -0
- package/dist/kernel/pointBuffers.d.ts.map +1 -0
- package/dist/kernel/pointer.d.ts +31 -1
- package/dist/kernel/pointer.d.ts.map +1 -1
- package/dist/kernel/preview.d.ts +32 -0
- package/dist/kernel/preview.d.ts.map +1 -0
- package/dist/kernel/renderKit.d.ts +21 -1
- package/dist/kernel/renderKit.d.ts.map +1 -1
- package/dist/kernel/renderer.d.ts +6 -0
- package/dist/kernel/renderer.d.ts.map +1 -1
- package/dist/kernel/sceneChanges.d.ts +87 -0
- package/dist/kernel/sceneChanges.d.ts.map +1 -0
- package/dist/kernel/sceneGraph.d.ts +9 -1
- package/dist/kernel/sceneGraph.d.ts.map +1 -1
- package/dist/kernel/sceneIndex.d.ts +5 -0
- package/dist/kernel/sceneIndex.d.ts.map +1 -1
- package/dist/kernel/seeded.d.ts +9 -0
- package/dist/kernel/seeded.d.ts.map +1 -0
- package/dist/kernel/shots.d.ts +26 -0
- package/dist/kernel/shots.d.ts.map +1 -0
- package/dist/kernel/tape.d.ts +38 -0
- package/dist/kernel/tape.d.ts.map +1 -0
- package/dist/kernel/timeline.d.ts +60 -0
- package/dist/kernel/timeline.d.ts.map +1 -0
- package/dist/kernel/trail.d.ts +31 -0
- package/dist/kernel/trail.d.ts.map +1 -0
- package/dist/kernel/viewportGizmo.d.ts +25 -0
- package/dist/kernel/viewportGizmo.d.ts.map +1 -0
- package/dist/runtime/Inspector.d.ts +20 -0
- package/dist/runtime/Inspector.d.ts.map +1 -0
- package/dist/runtime/SceneFile.d.ts +462 -0
- package/dist/runtime/SceneFile.d.ts.map +1 -0
- package/dist/runtime/SceneSchema.d.ts +404 -0
- package/dist/runtime/SceneSchema.d.ts.map +1 -1
- package/dist/runtime/errors.d.ts +9 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +3 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/webgl/index.d.ts.map +1 -1
- package/dist/webgl/index.js +228 -3
- package/dist/webgl/index.js.map +6 -4
- package/dist/webgl/line.d.ts +4 -0
- package/dist/webgl/line.d.ts.map +1 -0
- package/dist/webgl/points.d.ts +4 -0
- package/dist/webgl/points.d.ts.map +1 -0
- package/dist/webgpu/index.d.ts.map +1 -1
- package/dist/webgpu/index.js +206 -26
- package/dist/webgpu/index.js.map +6 -4
- package/dist/webgpu/line.d.ts +4 -0
- package/dist/webgpu/line.d.ts.map +1 -0
- package/dist/webgpu/points.d.ts +4 -0
- package/dist/webgpu/points.d.ts.map +1 -0
- package/package.json +4 -2
- package/src/kernel/bodies.ts +299 -17
- package/src/kernel/boxSelect.ts +73 -0
- package/src/kernel/build.ts +374 -1
- package/src/kernel/cameraShake.ts +3 -13
- package/src/kernel/describeScene.ts +224 -0
- package/src/kernel/description.ts +243 -0
- package/src/kernel/diff.ts +39 -0
- package/src/kernel/equality.ts +123 -0
- package/src/kernel/gizmo.ts +478 -0
- package/src/kernel/history.ts +118 -0
- package/src/kernel/index.ts +110 -1
- package/src/kernel/inspect.ts +214 -0
- package/src/kernel/lineGeometry.ts +75 -0
- package/src/kernel/lockstep.ts +237 -0
- package/src/kernel/loop.ts +7 -0
- package/src/kernel/materials.ts +3 -0
- package/src/kernel/paintOrder.ts +274 -0
- package/src/kernel/physics.ts +86 -1
- package/src/kernel/pivot.ts +418 -0
- package/src/kernel/pointBuffers.ts +115 -0
- package/src/kernel/pointer.ts +98 -17
- package/src/kernel/preview.ts +467 -0
- package/src/kernel/renderKit.ts +23 -1
- package/src/kernel/renderer.ts +6 -0
- package/src/kernel/sceneChanges.ts +265 -0
- package/src/kernel/sceneGraph.ts +354 -11
- package/src/kernel/sceneIndex.ts +8 -0
- package/src/kernel/seeded.ts +18 -0
- package/src/kernel/shots.ts +167 -0
- package/src/kernel/tape.ts +124 -0
- package/src/kernel/timeline.ts +93 -0
- package/src/kernel/trail.ts +170 -0
- package/src/kernel/viewportGizmo.ts +194 -0
- package/src/runtime/Inspector.ts +43 -0
- package/src/runtime/SceneFile.ts +46 -0
- package/src/runtime/SceneSchema.ts +319 -0
- package/src/runtime/errors.ts +6 -0
- package/src/runtime/index.ts +3 -1
- package/src/webgl/index.ts +4 -0
- package/src/webgl/line.ts +66 -0
- package/src/webgl/points.ts +205 -0
- package/src/webgpu/index.ts +4 -0
- package/src/webgpu/line.ts +67 -0
- package/src/webgpu/points.ts +193 -0
- package/dist/index-8cf8t6by.js.map +0 -41
|
@@ -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;
|
package/src/kernel/index.ts
CHANGED
|
@@ -48,6 +48,10 @@ export {
|
|
|
48
48
|
xrMesh,
|
|
49
49
|
xrMeshGeometryId,
|
|
50
50
|
physics,
|
|
51
|
+
modelAsset,
|
|
52
|
+
textureAsset,
|
|
53
|
+
environmentAsset,
|
|
54
|
+
fontAsset,
|
|
51
55
|
cuboidCollider,
|
|
52
56
|
ballCollider,
|
|
53
57
|
capsuleCollider,
|
|
@@ -104,12 +108,24 @@ export {
|
|
|
104
108
|
sharedMaterial,
|
|
105
109
|
sphere,
|
|
106
110
|
spin,
|
|
111
|
+
points,
|
|
112
|
+
sparkles,
|
|
113
|
+
stars,
|
|
114
|
+
trail,
|
|
115
|
+
edges,
|
|
116
|
+
line,
|
|
117
|
+
catmullRomPoints,
|
|
118
|
+
catmullRomPointAt,
|
|
119
|
+
catmullRomColors,
|
|
120
|
+
cubicBezierPoints,
|
|
121
|
+
quadraticBezierPoints,
|
|
107
122
|
cameraShake,
|
|
108
123
|
detailed,
|
|
109
124
|
billboard,
|
|
110
125
|
spotLight,
|
|
111
126
|
standard,
|
|
112
127
|
overlay,
|
|
128
|
+
hud,
|
|
113
129
|
renderTexture,
|
|
114
130
|
torus,
|
|
115
131
|
type TransformOptions,
|
|
@@ -137,6 +153,7 @@ export {
|
|
|
137
153
|
overlaysEqual,
|
|
138
154
|
xrEquals,
|
|
139
155
|
physicsEquals,
|
|
156
|
+
assetsEqual,
|
|
140
157
|
skyEquals,
|
|
141
158
|
viewsEqual,
|
|
142
159
|
vec3Equals,
|
|
@@ -195,10 +212,16 @@ export {
|
|
|
195
212
|
type PhysicsJoint,
|
|
196
213
|
type PhysicsWorld,
|
|
197
214
|
type Quat,
|
|
215
|
+
type Received,
|
|
216
|
+
type RestoredWorld,
|
|
217
|
+
snapshotBytes,
|
|
218
|
+
snapshotText,
|
|
198
219
|
} from './physics.ts';
|
|
199
|
-
export type { BodyCollision, BodyContactForce } from './bodies.ts';
|
|
220
|
+
export type { BodyCollision, BodyContactForce, PhysicsReached } from './bodies.ts';
|
|
200
221
|
export type {
|
|
201
222
|
ContactShadowPasses,
|
|
223
|
+
HostedLine,
|
|
224
|
+
HostedPoints,
|
|
202
225
|
HostedShaderGrid,
|
|
203
226
|
HostedSky,
|
|
204
227
|
RenderKit,
|
|
@@ -241,6 +264,7 @@ export {
|
|
|
241
264
|
type Picker,
|
|
242
265
|
type PlaneEquation,
|
|
243
266
|
pointOnPlane,
|
|
267
|
+
type PointerDevice,
|
|
244
268
|
type PointerInput,
|
|
245
269
|
type PointerTracker,
|
|
246
270
|
type Ray,
|
|
@@ -268,6 +292,7 @@ export {
|
|
|
268
292
|
type SceneGraph,
|
|
269
293
|
type SceneGraphOptions,
|
|
270
294
|
} from './sceneGraph.ts';
|
|
295
|
+
export { type PreviewOptions, type PreviewResult, previewSvg } from './preview.ts';
|
|
271
296
|
export {
|
|
272
297
|
type DescriptionError,
|
|
273
298
|
emptyIndex,
|
|
@@ -277,3 +302,87 @@ export {
|
|
|
277
302
|
type ParentKey,
|
|
278
303
|
type SceneIndex,
|
|
279
304
|
} from './sceneIndex.ts';
|
|
305
|
+
export {
|
|
306
|
+
dragGizmo,
|
|
307
|
+
gizmo,
|
|
308
|
+
type Gizmo,
|
|
309
|
+
type GizmoGrab,
|
|
310
|
+
type GizmoHandle,
|
|
311
|
+
gizmoHandleOf,
|
|
312
|
+
type GizmoMode,
|
|
313
|
+
gizmoNodes,
|
|
314
|
+
type GizmoSpace,
|
|
315
|
+
hoverGizmo,
|
|
316
|
+
movedAlike,
|
|
317
|
+
type Placement,
|
|
318
|
+
pressGizmo,
|
|
319
|
+
releaseGizmo,
|
|
320
|
+
} from './gizmo.ts';
|
|
321
|
+
export { type BoxCorner, boxSelect, type CanvasSize, type Selectable } from './boxSelect.ts';
|
|
322
|
+
export * as History from './history.ts';
|
|
323
|
+
export * as Lockstep from './lockstep.ts';
|
|
324
|
+
export * as Timeline from './timeline.ts';
|
|
325
|
+
export {
|
|
326
|
+
describeScene,
|
|
327
|
+
formatSummary,
|
|
328
|
+
type LightSummary,
|
|
329
|
+
type SceneSummary,
|
|
330
|
+
type ThingSummary,
|
|
331
|
+
thingsBounds,
|
|
332
|
+
} from './describeScene.ts';
|
|
333
|
+
export {
|
|
334
|
+
type Mood,
|
|
335
|
+
moodStaging,
|
|
336
|
+
moods,
|
|
337
|
+
shotAngles,
|
|
338
|
+
type ShotAngle,
|
|
339
|
+
shotCamera,
|
|
340
|
+
type Staging,
|
|
341
|
+
} from './shots.ts';
|
|
342
|
+
export {
|
|
343
|
+
type FieldPath,
|
|
344
|
+
type FieldValue,
|
|
345
|
+
type InspectableAst,
|
|
346
|
+
type InspectedField,
|
|
347
|
+
type InspectHints,
|
|
348
|
+
inspectFields,
|
|
349
|
+
setAt,
|
|
350
|
+
} from './inspect.ts';
|
|
351
|
+
export {
|
|
352
|
+
type ChangePlace,
|
|
353
|
+
formatChanges,
|
|
354
|
+
type SceneChange,
|
|
355
|
+
type SceneChanges,
|
|
356
|
+
sceneChanges,
|
|
357
|
+
type SceneSetting,
|
|
358
|
+
} from './sceneChanges.ts';
|
|
359
|
+
export {
|
|
360
|
+
clearTape,
|
|
361
|
+
placeTapeEnd,
|
|
362
|
+
pointTape,
|
|
363
|
+
type Tape,
|
|
364
|
+
tape,
|
|
365
|
+
tapeLength,
|
|
366
|
+
type TapeLook,
|
|
367
|
+
tapeMiddle,
|
|
368
|
+
tapeNodes,
|
|
369
|
+
} from './tape.ts';
|
|
370
|
+
export {
|
|
371
|
+
dragPivot,
|
|
372
|
+
hoverPivot,
|
|
373
|
+
type Pivot,
|
|
374
|
+
pivot,
|
|
375
|
+
type PivotGrab,
|
|
376
|
+
type PivotHandle,
|
|
377
|
+
pivotHandleOf,
|
|
378
|
+
pivotNodes,
|
|
379
|
+
pressPivot,
|
|
380
|
+
releasePivot,
|
|
381
|
+
} from './pivot.ts';
|
|
382
|
+
export {
|
|
383
|
+
cameraAlong,
|
|
384
|
+
type ViewAxis,
|
|
385
|
+
viewportAxisOf,
|
|
386
|
+
viewportGizmoNodes,
|
|
387
|
+
type ViewportGizmoOptions,
|
|
388
|
+
} 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
|
+
});
|