@weasel-js/labkit 1.0.4 → 1.2.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.
- package/README.md +33 -6
- package/dist/_dts/{DrawCommand-BkZztJsW.d.ts → DrawCommand-C_XboUpz.d.ts} +48 -6
- package/dist/_dts/{index-CFlDPeZh.d.ts → index-JFAYj5Tv.d.ts} +29 -7
- package/dist/_dts/types-DJ79Tg5J.d.ts +56 -0
- package/dist/_dts/useTrialState-BMNIx3Cy.d.ts +171 -0
- package/dist/canvas/index.d.ts +2 -1
- package/dist/{chunk-BAPZPDDA.js → chunk-73KA7WBO.js} +425 -394
- package/dist/chunk-73KA7WBO.js.map +1 -0
- package/dist/{chunk-DWV7SFKR.js → chunk-BOHF3PQO.js} +4397 -3726
- package/dist/chunk-BOHF3PQO.js.map +1 -0
- package/dist/{chunk-DEWXYFEU.js → chunk-G5TJVQQT.js} +3 -3
- package/dist/{chunk-DEWXYFEU.js.map → chunk-G5TJVQQT.js.map} +1 -1
- package/dist/chunk-LN6JDUGB.js +106 -0
- package/dist/chunk-LN6JDUGB.js.map +1 -0
- package/dist/chunk-THBG7FQZ.js +167 -0
- package/dist/chunk-THBG7FQZ.js.map +1 -0
- package/dist/chunk-VUU5UXHE.js +491 -0
- package/dist/chunk-VUU5UXHE.js.map +1 -0
- package/dist/dragdrop/index.d.ts +2 -1
- package/dist/index.d.ts +211 -151
- package/dist/index.js +317 -169
- package/dist/index.js.map +1 -1
- package/dist/job/index.d.ts +13 -0
- package/dist/job/index.js +3 -0
- package/dist/job/index.js.map +1 -0
- package/dist/layers/index.d.ts +3 -2
- package/dist/passthrough/weasel-canvas.d.ts +1 -1
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +29 -6
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/state/index.d.ts +12 -12
- package/dist/state/index.js +5 -5
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +31 -13
- package/dist/surface/index.d.ts +77 -0
- package/dist/surface/index.js +3 -0
- package/dist/surface/index.js.map +1 -0
- package/dist/ui/layers/index.js +3 -3
- package/dist/undo/index.d.ts +2 -1
- package/package.json +9 -1
- package/src/canvas/AGENTS.md +14 -2
- package/src/canvas/useOrbit.test.ts +71 -0
- package/src/canvas/useOrbit.ts +149 -0
- package/src/dragdrop/dragDrop.test.tsx +4 -7
- package/src/index.test.ts +70 -0
- package/src/index.ts +24 -7
- package/src/instrument/SineWave.smoke.test.tsx +2 -1
- package/src/instrument/capabilityDetector.ts +1 -1
- package/src/instrument/types.ts +30 -6
- package/src/job/index.ts +3 -0
- package/src/job/types.ts +47 -0
- package/src/job/useJob.test.tsx +210 -0
- package/src/job/useJob.ts +134 -0
- package/src/lab/Lab.less +16 -0
- package/src/lab/Lab.stories.tsx +6 -7
- package/src/lab/Lab.test.tsx +25 -25
- package/src/lab/Lab.tsx +50 -50
- package/src/lab/LabContext.ts +12 -12
- package/src/lab/LabShell.tsx +1 -1
- package/src/lab/{WorkspaceGrid.less → Workspace.less} +2 -2
- package/src/lab/{WorkspaceGrid.stories.tsx → Workspace.stories.tsx} +13 -13
- package/src/lab/Workspace.surface.test.tsx +49 -0
- package/src/lab/{WorkspaceGrid.test.tsx → Workspace.test.tsx} +24 -24
- package/src/lab/{WorkspaceGrid.tsx → Workspace.tsx} +32 -19
- package/src/lab/index.ts +2 -2
- package/src/layers/AGENTS.md +3 -3
- package/src/primitives/Toolbar.stories.tsx +2 -2
- package/src/state/SingletonExperiment.test.tsx +8 -8
- package/src/state/SingletonExperiment.tsx +9 -9
- package/src/state/context.tsx +12 -12
- package/src/state/document.test.ts +369 -0
- package/src/state/document.ts +194 -0
- package/src/state/helpers.test.ts +74 -46
- package/src/state/helpers.ts +21 -29
- package/src/state/index.ts +16 -8
- package/src/state/store.test.ts +440 -41
- package/src/state/store.ts +165 -102
- package/src/state/types.ts +32 -13
- package/src/state/{useExperimentState.test.tsx → useTrialState.test.tsx} +18 -18
- package/src/state/useTrialState.ts +29 -0
- package/src/state/view.test.ts +127 -0
- package/src/state/view.ts +18 -0
- package/src/styles.less +2 -2
- package/src/surface/AGENTS.md +64 -0
- package/src/surface/SurfaceContext.ts +5 -0
- package/src/surface/composeRects.test.ts +50 -0
- package/src/surface/composeRects.ts +19 -0
- package/src/surface/deviceRect.test.ts +40 -0
- package/src/surface/deviceRect.ts +19 -0
- package/src/surface/index.ts +7 -0
- package/src/surface/rect.ts +16 -0
- package/src/surface/useSurfaceTile.test.tsx +67 -0
- package/src/surface/useSurfaceTile.ts +32 -0
- package/src/surface/useTiledSurface.test.tsx +231 -0
- package/src/surface/useTiledSurface.ts +157 -0
- package/src/theme/base.less +1 -1
- package/src/{workspace → trial}/DefaultSidebar.tsx +3 -3
- package/src/trial/DefaultStatusBar.tsx +19 -0
- package/src/{workspace → trial}/DefaultToolbar.tsx +9 -8
- package/src/trial/Trial.canvas.test.tsx +80 -0
- package/src/trial/Trial.job.test.tsx +75 -0
- package/src/{workspace/Workspace.less → trial/Trial.less} +13 -1
- package/src/{workspace/Workspace.stories.tsx → trial/Trial.stories.tsx} +20 -18
- package/src/{workspace/Workspace.test.tsx → trial/Trial.test.tsx} +26 -26
- package/src/{workspace/Workspace.tsx → trial/Trial.tsx} +77 -46
- package/src/{workspace/WorkspaceChrome.tsx → trial/TrialChrome.tsx} +73 -46
- package/src/{workspace → trial}/index.ts +13 -13
- package/src/{workspace → trial}/slotTypes.ts +18 -17
- package/src/{workspace/workspaceOps.test.ts → trial/trialOps.test.ts} +38 -44
- package/src/trial/trialOps.ts +99 -0
- package/src/ui/format.test.ts +33 -0
- package/src/ui/format.ts +11 -0
- package/src/ui/properties/CurveField.tsx +6 -6
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +3 -3
- package/dist/_dts/useExperimentState-D7EQnnwJ.d.ts +0 -151
- package/dist/chunk-BAPZPDDA.js.map +0 -1
- package/dist/chunk-DWV7SFKR.js.map +0 -1
- package/dist/chunk-KSTEW2AF.js +0 -367
- package/dist/chunk-KSTEW2AF.js.map +0 -1
- package/src/state/useExperimentState.ts +0 -31
- package/src/workspace/DefaultStatusBar.tsx +0 -18
- package/src/workspace/workspaceOps.ts +0 -102
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
React widgets for building self-contained interactive **lab** pages — pages with sliders, controls, and canvas-based experimentation.
|
|
4
4
|
|
|
5
|
-
This is the v0.x of the library. The Lab/
|
|
5
|
+
This is the v0.x of the library. The Lab/Trial/Instrument runtime arrives in later plans; v0.0.1 ships presentational primitives.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -28,16 +28,16 @@ Then point your app at the local clone:
|
|
|
28
28
|
## Usage
|
|
29
29
|
|
|
30
30
|
```tsx
|
|
31
|
-
import { LabShell, Toolbar,
|
|
31
|
+
import { LabShell, Toolbar, Workspace, FpsMeter } from '@weasel-js/labkit';
|
|
32
32
|
import '@weasel-js/labkit/styles.css';
|
|
33
33
|
|
|
34
34
|
function MyLab() {
|
|
35
35
|
return (
|
|
36
36
|
<LabShell title="My Lab" header={<button>+ Add</button>}>
|
|
37
|
-
<
|
|
38
|
-
<div>
|
|
39
|
-
<div>
|
|
40
|
-
</
|
|
37
|
+
<Workspace>
|
|
38
|
+
<div>Trial 1</div>
|
|
39
|
+
<div>Trial 2</div>
|
|
40
|
+
</Workspace>
|
|
41
41
|
</LabShell>
|
|
42
42
|
);
|
|
43
43
|
}
|
|
@@ -59,6 +59,33 @@ import { ThemeProvider } from '@weasel-js/theme/react';
|
|
|
59
59
|
`"light"` or `"dark"`. Only `styles.css` needs importing — the token values
|
|
60
60
|
arrive through the provider.
|
|
61
61
|
|
|
62
|
+
## Driving your own renderer
|
|
63
|
+
|
|
64
|
+
`CanvasStack` is 2D. For three.js or raw WebGL, take rects and dirtiness from
|
|
65
|
+
labkit and keep the GL yourself:
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { toDeviceRect, useSurfaceTile, useTiledSurface } from '@weasel-js/labkit/surface';
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A trial's `view` is opaque to labkit — it is persisted, restored on Reset and
|
|
72
|
+
handed to the instrument without being read into — so a 3D lab stores an orbit
|
|
73
|
+
there and gets all three. `useOrbit` is the 3D peer of `usePanZoom`.
|
|
74
|
+
|
|
75
|
+
See `src/surface/AGENTS.md` for the contract and the traps.
|
|
76
|
+
|
|
77
|
+
## Long-running work
|
|
78
|
+
|
|
79
|
+
An instrument with work too slow for a render declares a `job`. labkit starts it,
|
|
80
|
+
aborts it on unmount and on a key change, discards results from a superseded run,
|
|
81
|
+
and renders progress and a cancel control into the trial chrome. Per-item failure
|
|
82
|
+
is an event rather than a thrown error, so a run with two failed items is a
|
|
83
|
+
partial success.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import type { JobCapability } from '@weasel-js/labkit/job';
|
|
87
|
+
```
|
|
88
|
+
|
|
62
89
|
## Development
|
|
63
90
|
|
|
64
91
|
```bash
|
|
@@ -88,6 +88,8 @@ interface SerializedHistoryEntry {
|
|
|
88
88
|
label: string;
|
|
89
89
|
forwardOps: SerializedOp[];
|
|
90
90
|
baseOps: SerializedOp[];
|
|
91
|
+
selectionBefore?: readonly string[];
|
|
92
|
+
selectionAfter?: readonly string[];
|
|
91
93
|
}
|
|
92
94
|
/** Snapshot of an entire `History` instance. Designed to live alongside the
|
|
93
95
|
* scene snapshot in IDB so a reload restores the undo / redo stacks to
|
|
@@ -119,6 +121,10 @@ interface HistoryEntry {
|
|
|
119
121
|
* contribute nothing. May be `undefined` for deserialized entries
|
|
120
122
|
* restored from an older snapshot that predates this field. */
|
|
121
123
|
touchedIds?: ReadonlySet<string>;
|
|
124
|
+
/** Selection restored when this entry is undone. */
|
|
125
|
+
selectionBefore?: readonly string[];
|
|
126
|
+
/** Selection restored when this entry is redone. */
|
|
127
|
+
selectionAfter?: readonly string[];
|
|
122
128
|
}
|
|
123
129
|
/** Op-batched undo/redo controller returned by `createHistory`. */
|
|
124
130
|
interface History {
|
|
@@ -170,7 +176,7 @@ interface History {
|
|
|
170
176
|
* Unlike `applyOps`, does NOT call `op.apply()`. Used by Journal.commit
|
|
171
177
|
* to flush a session's net forward ops to the parent as one entry without
|
|
172
178
|
* re-mutating the scene. */
|
|
173
|
-
recordEntry(ops: Op[], label: string): void;
|
|
179
|
+
recordEntry(ops: Op[], label: string, options?: RecordEntryOptions): void;
|
|
174
180
|
/** Concatenated forwardOps of every undo-stack entry, in order. Snapshot
|
|
175
181
|
* of "what changes are currently applied via this history" — useful for
|
|
176
182
|
* Journal.commit to flush to a parent, and for any caller that wants to
|
|
@@ -193,6 +199,14 @@ interface History {
|
|
|
193
199
|
* to resume or discard before calling this. */
|
|
194
200
|
resumeJournal(journal: Journal): void;
|
|
195
201
|
}
|
|
202
|
+
/** Options for `recordEntry`. */
|
|
203
|
+
interface RecordEntryOptions {
|
|
204
|
+
/** Selection as of before the already-applied ops ran. `recordEntry` is
|
|
205
|
+
* called after the fact, so the live selection has moved on by then and
|
|
206
|
+
* the engine cannot sample it — a caller that wants undo to restore the
|
|
207
|
+
* selection captures it when the batch opens and passes it here. */
|
|
208
|
+
selectionBefore?: readonly string[];
|
|
209
|
+
}
|
|
196
210
|
|
|
197
211
|
/** Fill rule used by polygon path hit-testing and `ctx.fill()`. */
|
|
198
212
|
type PathFillRule = 'nonzero' | 'evenodd';
|
|
@@ -500,6 +514,14 @@ interface Scene<TData, TLayer extends string, TPose = RectPose> {
|
|
|
500
514
|
* `SceneCanvasAdapter`). Pass `this` from `sceneToAdapter` or a compatible
|
|
501
515
|
* adapter. */
|
|
502
516
|
applyBatch(ops: Op[], label: string, adapter: unknown): void;
|
|
517
|
+
/** The transient set of active ids — "operate on these N as a unit".
|
|
518
|
+
* Shared by every view over this scene unless a view supplies its own
|
|
519
|
+
* (see `CanvasView.selection`). Not document content: it never appears
|
|
520
|
+
* in `toJSON`. It does ride on history entries, so undo and redo put
|
|
521
|
+
* back the selection an edit was made under; changing it is never an
|
|
522
|
+
* undo step of its own. */
|
|
523
|
+
getSelection(): readonly NodeId[];
|
|
524
|
+
setSelection(ids: readonly NodeId[]): void;
|
|
503
525
|
undo(): boolean;
|
|
504
526
|
redo(): boolean;
|
|
505
527
|
canUndo(): boolean;
|
|
@@ -792,17 +814,22 @@ type StrokeAlign = 'center' | 'inner' | 'outer';
|
|
|
792
814
|
/** Stroke style: a FillStyle plus structural line parameters. */
|
|
793
815
|
interface Stroke {
|
|
794
816
|
paint: FillStyle;
|
|
795
|
-
|
|
817
|
+
/** World units, or `{ px }` for screen pixels — resolved against the
|
|
818
|
+
* accumulated transform scale at draw time, so it holds its on-screen
|
|
819
|
+
* thickness as the view zooms. */
|
|
820
|
+
width?: number | {
|
|
821
|
+
px: number;
|
|
822
|
+
};
|
|
796
823
|
/** Per `CanvasRenderingContext2D.setLineDash` — empty/omitted = solid. */
|
|
797
824
|
dash?: number[];
|
|
798
825
|
cap?: 'butt' | 'round' | 'square';
|
|
799
826
|
join?: 'miter' | 'round' | 'bevel';
|
|
800
827
|
/**
|
|
801
828
|
* Miter join fallback threshold. When the miter length exceeds
|
|
802
|
-
* `miterLimit * width / 2`, the join falls back to a bevel. Default
|
|
803
|
-
*
|
|
804
|
-
*
|
|
805
|
-
*
|
|
829
|
+
* `miterLimit * width / 2`, the join falls back to a bevel. Default 4,
|
|
830
|
+
* matching SVG — which is also what the kit's own serializer implies when
|
|
831
|
+
* it omits the attribute for an unset field. Canvas2D's 10 lets an acute
|
|
832
|
+
* corner throw a spike four times the half-width.
|
|
806
833
|
*/
|
|
807
834
|
miterLimit?: number;
|
|
808
835
|
/** Where the stroke sits relative to the geometric edge. Default `'center'`. */
|
|
@@ -1063,6 +1090,21 @@ interface ImageDrawCommand {
|
|
|
1063
1090
|
* device pixels as hard squares — required by anything magnifying a
|
|
1064
1091
|
* framebuffer readback, where blur destroys the point of the readback. */
|
|
1065
1092
|
sampling?: 'linear' | 'nearest';
|
|
1093
|
+
/** Sub-rectangle of `image` to draw, in bitmap pixels from the top-left.
|
|
1094
|
+
* Omitted draws the whole bitmap. Not range-checked: a rect past the edge
|
|
1095
|
+
* samples outside [0..1], which CLAMP_TO_EDGE smears. With
|
|
1096
|
+
* `sampling: 'linear'` the filter reaches half a texel beyond `source`, so
|
|
1097
|
+
* atlas frames need a gutter (see `SpriteSheet.spacing`) or `'nearest'`. */
|
|
1098
|
+
source?: {
|
|
1099
|
+
x: number;
|
|
1100
|
+
y: number;
|
|
1101
|
+
w: number;
|
|
1102
|
+
h: number;
|
|
1103
|
+
};
|
|
1104
|
+
/** Mirror the sampled region within the destination rect. The quad does not
|
|
1105
|
+
* move — a flipped draw covers exactly the pixels an unflipped one does. */
|
|
1106
|
+
flipX?: boolean;
|
|
1107
|
+
flipY?: boolean;
|
|
1066
1108
|
}
|
|
1067
1109
|
/**
|
|
1068
1110
|
* Custom shader draw command. The renderer generates a quad over `bounds`
|
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
2
|
import { C as ConfigField } from './types-x92Kfeme.js';
|
|
3
|
+
import { J as JobHandle, a as JobCapability } from './types-DJ79Tg5J.js';
|
|
3
4
|
|
|
4
5
|
/** What an instrument's `render` is handed: its state and config, the setters
|
|
5
|
-
* for both, the
|
|
6
|
+
* for both, the trial it is mounted in, and a way to emit named events. */
|
|
6
7
|
interface RenderContext<TS = unknown, TC = unknown> {
|
|
7
8
|
state: TS;
|
|
8
9
|
config: TC;
|
|
9
10
|
setState: (next: TS | ((prev: TS) => TS)) => void;
|
|
10
11
|
setConfig: (key: keyof TC, value: unknown) => void;
|
|
11
|
-
|
|
12
|
+
trial: {
|
|
12
13
|
id: string;
|
|
14
|
+
/** The trial's view, in whatever shape this instrument chose. labkit persists
|
|
15
|
+
* it and restores it on Reset without ever reading into it. */
|
|
16
|
+
view: unknown;
|
|
17
|
+
setView: (next: unknown) => void;
|
|
18
|
+
/** 2D convenience over `view`. Reads 1 and writes nothing when the trial holds
|
|
19
|
+
* a view that is not the 2D one. */
|
|
13
20
|
zoom: number;
|
|
14
21
|
setZoom: (z: number) => void;
|
|
15
22
|
};
|
|
16
23
|
emit: (event: string) => void;
|
|
24
|
+
/** Present only when the instrument declares a `job`. */
|
|
25
|
+
job?: JobHandle;
|
|
17
26
|
}
|
|
18
|
-
/** One 2D canvas layer of an instrument, drawn in declaration order.
|
|
27
|
+
/** One 2D canvas layer of an instrument, drawn in declaration order.
|
|
28
|
+
*
|
|
29
|
+
* `draw` is called with the camera already applied, so it works in world
|
|
30
|
+
* coordinates. `zoom` is passed for the things that must not scale with it —
|
|
31
|
+
* set `ctx.lineWidth = 1 / zoom` to keep a hairline hairline. */
|
|
19
32
|
interface CanvasLayer<TS = unknown, TC = unknown> {
|
|
20
33
|
id: string;
|
|
21
34
|
draw: (ctx: CanvasRenderingContext2D, args: {
|
|
@@ -36,7 +49,7 @@ interface CanvasCapability<TS = unknown, TC = unknown> {
|
|
|
36
49
|
};
|
|
37
50
|
};
|
|
38
51
|
}
|
|
39
|
-
/** Declares which of an instrument's layers the
|
|
52
|
+
/** Declares which of an instrument's layers the trial should offer
|
|
40
53
|
* show/hide controls for. */
|
|
41
54
|
interface LayerCapability {
|
|
42
55
|
ids: string[];
|
|
@@ -72,7 +85,7 @@ type HitResult = {
|
|
|
72
85
|
layerId?: string;
|
|
73
86
|
pointId?: string;
|
|
74
87
|
};
|
|
75
|
-
/** A
|
|
88
|
+
/** A trial's camera. */
|
|
76
89
|
type ViewTransform = {
|
|
77
90
|
zoom: number;
|
|
78
91
|
pan: Point;
|
|
@@ -102,13 +115,15 @@ type DragFeedback = {
|
|
|
102
115
|
* and `state`, what the experiment is currently doing — and renders from both.
|
|
103
116
|
* The optional capability fields declare what else it wants from the runtime:
|
|
104
117
|
* a canvas, a layer list, palette drag-and-drop, undo. Declaring a capability
|
|
105
|
-
* is what makes the
|
|
118
|
+
* is what makes the trial provide the corresponding chrome.
|
|
106
119
|
*/
|
|
107
120
|
interface Instrument<TS = unknown, TC = unknown> {
|
|
108
121
|
name: string;
|
|
109
122
|
defaultConfig: () => TC;
|
|
110
123
|
initialState: (config: TC) => TS;
|
|
111
124
|
configSchema?: () => ConfigField[];
|
|
125
|
+
/** The instrument's DOM. With `canvas`, this renders as an overlay above the
|
|
126
|
+
* layers rather than instead of them; return `null` for canvas only. */
|
|
112
127
|
render: (ctx: RenderContext<TS, TC>) => ReactNode;
|
|
113
128
|
onConfigChange?: (config: TC, prev: TC, state: TS) => TS;
|
|
114
129
|
serialize?: (state: TS) => unknown;
|
|
@@ -117,6 +132,13 @@ interface Instrument<TS = unknown, TC = unknown> {
|
|
|
117
132
|
layers?: LayerCapability;
|
|
118
133
|
dragDrop?: DragDropCapability<TS, TC>;
|
|
119
134
|
undo?: UndoCapability;
|
|
135
|
+
/** Work too slow to do during a render. The runtime starts it, aborts it on
|
|
136
|
+
* unmount and on a `key` change, and renders progress into the trial. */
|
|
137
|
+
job?: JobCapability<TS, TC, never>;
|
|
120
138
|
}
|
|
139
|
+
/** Instruments as a lab receives them. `any` rather than `unknown` because
|
|
140
|
+
* parameter contravariance keeps a `defineInstrument<TS, TC>` result out of
|
|
141
|
+
* an `Instrument<unknown, unknown>[]`; it is contained to this alias. */
|
|
142
|
+
type InstrumentList = readonly Instrument<any, any>[];
|
|
121
143
|
|
|
122
|
-
export type { CanvasCapability as C, DragDropCapability as D, HitResult as H, Instrument as I, LayerCapability as L, PaletteItem as P, RenderContext as R, SystemEvent as S, UndoCapability as U, ViewTransform as V,
|
|
144
|
+
export type { CanvasCapability as C, DragDropCapability as D, HitResult as H, Instrument as I, LayerCapability as L, PaletteItem as P, RenderContext as R, SystemEvent as S, UndoCapability as U, ViewTransform as V, InstrumentList as a, CanvasLayer as b, DragFeedback as c, LayerDescriptor as d, Point as e };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** What a running job reports as it goes. `total` may arrive at any point and may
|
|
2
|
+
* arrive more than once; a job that cannot count up front simply never sends it.
|
|
3
|
+
*
|
|
4
|
+
* `failed` is a first-class event rather than a thrown error because these
|
|
5
|
+
* failures are per item: a run with two failed items is a partial success, and
|
|
6
|
+
* its other items are worth showing. */
|
|
7
|
+
type JobEvent<T> = {
|
|
8
|
+
kind: 'total';
|
|
9
|
+
total: number;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'item';
|
|
12
|
+
item: T;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'failed';
|
|
15
|
+
index: number;
|
|
16
|
+
error: string;
|
|
17
|
+
};
|
|
18
|
+
/** Where a job is. `idle` before its first run and after a cancel; `done` when the
|
|
19
|
+
* iterable finished, whether or not items failed along the way. */
|
|
20
|
+
type JobStatus = 'idle' | 'running' | 'done' | 'error';
|
|
21
|
+
/** Declares that an instrument has work too slow to do during a render: what to
|
|
22
|
+
* run, when to re-run it, and how each result folds into state. */
|
|
23
|
+
interface JobCapability<TS = unknown, TC = unknown, TItem = unknown> {
|
|
24
|
+
/** Re-run whenever this value changes, compared element-wise. A job with no
|
|
25
|
+
* `key` runs only when something calls `start()`. */
|
|
26
|
+
key?: (config: TC, state: TS) => readonly unknown[];
|
|
27
|
+
/** Start on mount and on every `key` change. Default false. */
|
|
28
|
+
auto?: boolean;
|
|
29
|
+
run: (args: {
|
|
30
|
+
config: TC;
|
|
31
|
+
state: TS;
|
|
32
|
+
signal: AbortSignal;
|
|
33
|
+
}) => AsyncIterable<JobEvent<TItem>>;
|
|
34
|
+
/** Fold one result into state. Called once per `item` event, in arrival order. */
|
|
35
|
+
onItem: (item: TItem, state: TS) => TS;
|
|
36
|
+
}
|
|
37
|
+
/** One item that failed, and why. */
|
|
38
|
+
interface JobFailure {
|
|
39
|
+
index: number;
|
|
40
|
+
error: string;
|
|
41
|
+
}
|
|
42
|
+
/** What `RenderContext.job` exposes. Present only when the instrument declares the
|
|
43
|
+
* capability; `undefined` otherwise. */
|
|
44
|
+
interface JobHandle {
|
|
45
|
+
status: JobStatus;
|
|
46
|
+
done: number;
|
|
47
|
+
/** Null until the job reports a total, and forever if it never does. */
|
|
48
|
+
total: number | null;
|
|
49
|
+
failures: readonly JobFailure[];
|
|
50
|
+
/** The error that ended the run, when `status` is `'error'`. */
|
|
51
|
+
error: string | null;
|
|
52
|
+
start: () => void;
|
|
53
|
+
cancel: () => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type { JobHandle as J, JobCapability as a, JobEvent as b, JobFailure as c, JobStatus as d };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, ReactElement } from 'react';
|
|
3
|
+
import { StoreApi } from 'zustand/vanilla';
|
|
4
|
+
|
|
5
|
+
/** A trial's undo history, as snapshots of its state either side of the
|
|
6
|
+
* present. */
|
|
7
|
+
interface UndoStack {
|
|
8
|
+
past: unknown[];
|
|
9
|
+
future: unknown[];
|
|
10
|
+
}
|
|
11
|
+
/** One trial as the store holds it: which instrument it runs, that
|
|
12
|
+
* instrument's config and state, the camera, and the undo history. */
|
|
13
|
+
interface TrialRecord<TS = unknown, TC = unknown, TV = unknown> {
|
|
14
|
+
id: string;
|
|
15
|
+
instrumentName: string;
|
|
16
|
+
config: TC;
|
|
17
|
+
state: TS;
|
|
18
|
+
/** Opaque to labkit: persisted, restored on Reset and handed to the instrument,
|
|
19
|
+
* but never read into. A 3D lab puts an orbit here and keeps all three. */
|
|
20
|
+
view: TV;
|
|
21
|
+
undoStack: UndoStack;
|
|
22
|
+
}
|
|
23
|
+
/** A named, saved copy of a trial's config and state, restorable into any
|
|
24
|
+
* trial running the same instrument. */
|
|
25
|
+
interface SavedSnapshot {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
trialId: string;
|
|
29
|
+
instrumentName: string;
|
|
30
|
+
config: unknown;
|
|
31
|
+
state: unknown;
|
|
32
|
+
savedAt: number;
|
|
33
|
+
}
|
|
34
|
+
/** `auto` follows the OS; the other two are an explicit choice. */
|
|
35
|
+
type LabMode = 'auto' | 'light' | 'dark';
|
|
36
|
+
/** Everything a lab persists: its trials, its saved snapshots, and the
|
|
37
|
+
* chosen color mode. */
|
|
38
|
+
interface LabStoreState {
|
|
39
|
+
trials: TrialRecord[];
|
|
40
|
+
savedSnapshots: SavedSnapshot[];
|
|
41
|
+
mode: LabMode;
|
|
42
|
+
/** Per-trial tile extents, keyed by trial id. Opaque here — the
|
|
43
|
+
* shape belongs to whatever lays the trials out. */
|
|
44
|
+
layout: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
/** Where a lab persists itself. Implementations are keyed string storage and
|
|
47
|
+
* nothing more, so the same store works against localStorage, the URL hash,
|
|
48
|
+
* or memory. */
|
|
49
|
+
interface StorageAdapter {
|
|
50
|
+
read(key: string): string | null;
|
|
51
|
+
write(key: string, value: string): void;
|
|
52
|
+
delete?(key: string): void;
|
|
53
|
+
}
|
|
54
|
+
/** What `useTrialState` hands an instrument: its state and config, with
|
|
55
|
+
* a setter for each. */
|
|
56
|
+
interface TrialStateHandle<TS, TC> {
|
|
57
|
+
state: TS;
|
|
58
|
+
setState: (next: TS | ((prev: TS) => TS)) => void;
|
|
59
|
+
config: TC;
|
|
60
|
+
setConfig: (key: keyof TC, value: TC[keyof TC]) => void;
|
|
61
|
+
}
|
|
62
|
+
/** Options for `createLabStore`. `storageKey` namespaces the keys written, so
|
|
63
|
+
* two labs on one origin do not collide. */
|
|
64
|
+
interface CreateLabStoreOptions {
|
|
65
|
+
storageKey: string;
|
|
66
|
+
storage: StorageAdapter;
|
|
67
|
+
initialMode?: LabMode;
|
|
68
|
+
}
|
|
69
|
+
/** Per-instrument serialize/deserialize hooks, keyed by instrument name. An
|
|
70
|
+
* instrument whose state is already JSON-safe needs no entry. */
|
|
71
|
+
type InstrumentSerializers = Record<string, {
|
|
72
|
+
serialize?: (state: unknown) => unknown;
|
|
73
|
+
deserialize?: (data: unknown) => unknown;
|
|
74
|
+
} | undefined>;
|
|
75
|
+
/** A trial as it is persisted: everything but the undo history, which is
|
|
76
|
+
* session-only. */
|
|
77
|
+
type SerializedTrial = Omit<TrialRecord, 'undoStack'>;
|
|
78
|
+
/** Everything a lab persists, under one key, at a known version. */
|
|
79
|
+
interface LabDocument {
|
|
80
|
+
version: number;
|
|
81
|
+
trials: SerializedTrial[];
|
|
82
|
+
saves: SavedSnapshot[];
|
|
83
|
+
layout: Record<string, unknown>;
|
|
84
|
+
mode: LabMode;
|
|
85
|
+
}
|
|
86
|
+
/** Migrates a document one version forward. Index `i` in the chain takes a
|
|
87
|
+
* version-`i` document to version `i + 1`. */
|
|
88
|
+
type Migration = (doc: Record<string, unknown>) => Record<string, unknown>;
|
|
89
|
+
|
|
90
|
+
/** Persist to `localStorage` — state survives a reload and a new tab. Falls
|
|
91
|
+
* back to no-ops when storage is unavailable. */
|
|
92
|
+
declare const localStorageAdapter: StorageAdapter;
|
|
93
|
+
/** Persist to `sessionStorage` — state survives a reload but not a new tab. */
|
|
94
|
+
declare const sessionStorageAdapter: StorageAdapter;
|
|
95
|
+
/** Persist into the URL fragment, so the page's link carries its state and
|
|
96
|
+
* can be shared or bookmarked. */
|
|
97
|
+
declare const urlHashAdapter: StorageAdapter;
|
|
98
|
+
/** An in-memory store, discarded on reload. For tests, and for labs that
|
|
99
|
+
* should start fresh every time. */
|
|
100
|
+
declare function createMemoryAdapter(): StorageAdapter;
|
|
101
|
+
/** Persists nothing and reads back nothing. */
|
|
102
|
+
declare const noneAdapter: StorageAdapter;
|
|
103
|
+
|
|
104
|
+
/** Every mutation a lab store supports: managing trials, saving and
|
|
105
|
+
* restoring snapshots, and setting the color mode. */
|
|
106
|
+
interface LabStoreActions {
|
|
107
|
+
addTrial: (record: Omit<TrialRecord, 'undoStack'>) => void;
|
|
108
|
+
removeTrial: (id: string) => void;
|
|
109
|
+
updateTrialState: <TS>(id: string, next: TS | ((prev: TS) => TS)) => void;
|
|
110
|
+
updateTrialConfig: <TC>(id: string, key: keyof TC, value: TC[keyof TC]) => void;
|
|
111
|
+
updateTrialView: (id: string, view: unknown) => void;
|
|
112
|
+
updateTrialUndoStack: (id: string, next: TrialRecord['undoStack'] | ((prev: TrialRecord['undoStack']) => TrialRecord['undoStack'])) => void;
|
|
113
|
+
setTrialInstrument: (id: string, instrumentName: string) => void;
|
|
114
|
+
saveSnapshot: (trialId: string, name: string) => void;
|
|
115
|
+
loadSnapshot: (snapshotId: string, trialId: string) => void;
|
|
116
|
+
deleteSnapshot: (snapshotId: string) => void;
|
|
117
|
+
listSnapshots: (trialId?: string) => SavedSnapshot[];
|
|
118
|
+
setMode: (mode: LabMode) => void;
|
|
119
|
+
setLayout: (layout: Record<string, unknown>) => void;
|
|
120
|
+
}
|
|
121
|
+
/** A lab's store: its state and actions, plus the hook instruments use to
|
|
122
|
+
* register how their state is serialized. */
|
|
123
|
+
type LabStore = StoreApi<LabStoreState & LabStoreActions> & {
|
|
124
|
+
registerSerializers: (s: InstrumentSerializers) => void;
|
|
125
|
+
};
|
|
126
|
+
/** Build a lab store, hydrating from storage if anything was saved under the
|
|
127
|
+
* same key. Writes back are debounced. */
|
|
128
|
+
declare function createLabStore(options: CreateLabStoreOptions): LabStore;
|
|
129
|
+
|
|
130
|
+
type LabStoreCtx = {
|
|
131
|
+
store: LabStore;
|
|
132
|
+
} | null;
|
|
133
|
+
/** Context carrying the lab store. Prefer `useLabStore`; this is exported for
|
|
134
|
+
* code that needs to read the context without subscribing. */
|
|
135
|
+
declare const LabStoreContext: react.Context<LabStoreCtx>;
|
|
136
|
+
/** Provides a lab store to its subtree. */
|
|
137
|
+
declare function LabStoreProvider({ store, children, }: {
|
|
138
|
+
store: LabStore;
|
|
139
|
+
children: ReactNode;
|
|
140
|
+
}): ReactElement;
|
|
141
|
+
/** Subscribe to the whole lab store. Throws outside a `<LabStoreProvider>`. */
|
|
142
|
+
declare function useLabStore(): LabStoreState & ReturnType<LabStore['getState']>;
|
|
143
|
+
/** Context carrying which trial the subtree belongs to. */
|
|
144
|
+
declare const TrialIdContext: react.Context<string | null>;
|
|
145
|
+
/** Names the trial its subtree belongs to, so an instrument's hooks can
|
|
146
|
+
* find their own record in the store without being passed an id. */
|
|
147
|
+
declare function TrialIdProvider({ trialId, children, }: {
|
|
148
|
+
trialId: string;
|
|
149
|
+
children: ReactNode;
|
|
150
|
+
}): ReactElement;
|
|
151
|
+
/** The id of the trial this component is inside. Throws outside a
|
|
152
|
+
* `<TrialIdProvider>`. */
|
|
153
|
+
declare function useTrialId(): string;
|
|
154
|
+
|
|
155
|
+
/** Bumped whenever the persisted shape changes; every bump needs a migration. */
|
|
156
|
+
declare const CURRENT_DOCUMENT_VERSION = 2;
|
|
157
|
+
/** The one key a lab persists under. The `:doc` suffix keeps it out of the
|
|
158
|
+
* legacy bucket namespace, where a lab named `a:saves` would otherwise write
|
|
159
|
+
* its document over lab `a`'s saves bucket. */
|
|
160
|
+
declare function labDocumentKey(storageKey: string): string;
|
|
161
|
+
/** Where a document that failed to migrate is set aside, so a bad migration
|
|
162
|
+
* loses state loudly rather than silently. */
|
|
163
|
+
declare function quarantineKey(storageKey: string): string;
|
|
164
|
+
|
|
165
|
+
/** An instrument's own state and config, plus setters. Reads the surrounding
|
|
166
|
+
* trial id, so an instrument never has to know which trial it is
|
|
167
|
+
* running in. Throws outside a lab store and trial. */
|
|
168
|
+
declare function useTrialState<TS = unknown, TC = unknown>(): TrialStateHandle<TS, TC>;
|
|
169
|
+
|
|
170
|
+
export { CURRENT_DOCUMENT_VERSION as C, LabStoreContext as e, LabStoreProvider as f, TrialIdContext as i, TrialIdProvider as j, createLabStore as l, createMemoryAdapter as m, labDocumentKey as n, localStorageAdapter as o, noneAdapter as p, quarantineKey as q, useLabStore as r, sessionStorageAdapter as s, useTrialId as t, urlHashAdapter as u, useTrialState as v };
|
|
171
|
+
export type { InstrumentSerializers as I, LabDocument as L, Migration as M, SerializedTrial as S, TrialRecord as T, UndoStack as U, StorageAdapter as a, CreateLabStoreOptions as b, LabStore as c, LabStoreActions as d, LabStoreState as g, SavedSnapshot as h, TrialStateHandle as k, LabMode as w };
|
package/dist/canvas/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as react from 'react';
|
|
3
3
|
import { ReactNode } from 'react';
|
|
4
|
-
import { V as ViewTransform,
|
|
4
|
+
import { V as ViewTransform, e as Point } from '../_dts/index-JFAYj5Tv.js';
|
|
5
5
|
import '../_dts/types-x92Kfeme.js';
|
|
6
|
+
import '../_dts/types-DJ79Tg5J.js';
|
|
6
7
|
|
|
7
8
|
/** One layer of a canvas stack: its id, whether it is currently shown, and how
|
|
8
9
|
* it paints itself. */
|