@weasel-js/labkit 1.6.0 → 1.7.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 (222) hide show
  1. package/dist/_dts/{CanvasStackContext-DQgcH1ny.d.ts → CanvasStackContext-D2jnIwfk.d.ts} +1 -1
  2. package/dist/_dts/FloatingPanel-7j0IcXwz.d.ts +30 -0
  3. package/dist/_dts/{LayerList.d-2c9yup5Y.d.ts → LayerList.d-DiuEArKq.d.ts} +4 -4
  4. package/dist/_dts/PrefsRow.d-TOZ7BgD9.d.ts +79 -0
  5. package/dist/_dts/PropertyPanel.d-D-1paCbE.d.ts +141 -0
  6. package/dist/_dts/{Select.d-BchGMC2Y.d.ts → Select.d-Bensr_MT.d.ts} +15 -6
  7. package/dist/_dts/Stage-CCWfQQWM.d.ts +202 -0
  8. package/dist/_dts/index-B2Yj5aA0.d.ts +274 -0
  9. package/dist/_dts/{index-B0--8PEM.d.ts → index-CsYqUHD3.d.ts} +12 -13
  10. package/dist/_dts/{types-B7y8hX1D.d.ts → types-CRPSGFAp.d.ts} +33 -5
  11. package/dist/_dts/{frac-DTRq7Xpb.d.ts → types-CSm9jgol.d.ts} +700 -650
  12. package/dist/_dts/{useTrialState-CP4jfuKl.d.ts → useTrialState-CTZPgeZ7.d.ts} +2 -3
  13. package/dist/canvas/index.d.ts +40 -8
  14. package/dist/canvas/index.js +5 -3
  15. package/dist/chrome/index.d.ts +5 -6
  16. package/dist/chrome/index.js +9 -7
  17. package/dist/chunk-24W5VEJW.js +18 -0
  18. package/dist/chunk-24W5VEJW.js.map +1 -0
  19. package/dist/chunk-275JRRVD.js +728 -0
  20. package/dist/chunk-275JRRVD.js.map +1 -0
  21. package/dist/chunk-3RY6WL5T.js +38 -0
  22. package/dist/chunk-3RY6WL5T.js.map +1 -0
  23. package/dist/chunk-4LJSC7MB.js +143 -0
  24. package/dist/chunk-4LJSC7MB.js.map +1 -0
  25. package/dist/{chunk-3FI7Y4R4.js → chunk-7BO7HNGF.js} +5 -5
  26. package/dist/{chunk-3FI7Y4R4.js.map → chunk-7BO7HNGF.js.map} +1 -1
  27. package/dist/chunk-AWQG7FCC.js +19 -0
  28. package/dist/chunk-AWQG7FCC.js.map +1 -0
  29. package/dist/chunk-CZZPXUOU.js +3 -0
  30. package/dist/{chunk-SBXNQW4G.js.map → chunk-CZZPXUOU.js.map} +1 -1
  31. package/dist/{chunk-ZORVNOKX.js → chunk-FMK7UQ6V.js} +24 -145
  32. package/dist/chunk-FMK7UQ6V.js.map +1 -0
  33. package/dist/{chunk-BVL7Y574.js → chunk-FZ7YFQN7.js} +26 -10
  34. package/dist/chunk-FZ7YFQN7.js.map +1 -0
  35. package/dist/{chunk-USO6TERP.js → chunk-GOZLNM4L.js} +11 -9
  36. package/dist/chunk-GOZLNM4L.js.map +1 -0
  37. package/dist/{chunk-H5VIV7MQ.js → chunk-I5DUQ5HM.js} +88 -82
  38. package/dist/chunk-I5DUQ5HM.js.map +1 -0
  39. package/dist/chunk-J7OWWTCW.js +143 -0
  40. package/dist/chunk-J7OWWTCW.js.map +1 -0
  41. package/dist/{chunk-H6ZAOWNE.js → chunk-JVBYV6TL.js} +3 -3
  42. package/dist/{chunk-H6ZAOWNE.js.map → chunk-JVBYV6TL.js.map} +1 -1
  43. package/dist/{chunk-VQ47KWD5.js → chunk-KMJ7RWD3.js} +18 -7
  44. package/dist/chunk-KMJ7RWD3.js.map +1 -0
  45. package/dist/{chunk-ERFMC6WU.js → chunk-M7HP3FTK.js} +20 -11
  46. package/dist/chunk-M7HP3FTK.js.map +1 -0
  47. package/dist/chunk-MIM5R4ZT.js +14972 -0
  48. package/dist/chunk-MIM5R4ZT.js.map +1 -0
  49. package/dist/{chunk-HFRE4QQV.js → chunk-T5IBSPAM.js} +96 -32
  50. package/dist/chunk-T5IBSPAM.js.map +1 -0
  51. package/dist/chunk-UNAZSLWE.js +294 -0
  52. package/dist/chunk-UNAZSLWE.js.map +1 -0
  53. package/dist/{chunk-RS3HRQWU.js → chunk-WPLAANH4.js} +4 -3
  54. package/dist/chunk-WPLAANH4.js.map +1 -0
  55. package/dist/{chunk-AWH3SRPW.js → chunk-Y23G2VZL.js} +5 -37
  56. package/dist/chunk-Y23G2VZL.js.map +1 -0
  57. package/dist/{chunk-MOLIUNVH.js → chunk-YT75VYRD.js} +4 -4
  58. package/dist/{chunk-MOLIUNVH.js.map → chunk-YT75VYRD.js.map} +1 -1
  59. package/dist/chunk-ZIFPHIWZ.js +302 -0
  60. package/dist/chunk-ZIFPHIWZ.js.map +1 -0
  61. package/dist/config/index.d.ts +48 -40
  62. package/dist/config/index.js +4 -5
  63. package/dist/controls/index.d.ts +91 -7
  64. package/dist/controls/index.js +5 -4
  65. package/dist/dragdrop/index.d.ts +9 -7
  66. package/dist/dragdrop/index.js +3 -2
  67. package/dist/index.d.ts +294 -82
  68. package/dist/index.js +425 -402
  69. package/dist/index.js.map +1 -1
  70. package/dist/layers/index.d.ts +4 -5
  71. package/dist/layers/index.js +2 -1
  72. package/dist/loupe/index.d.ts +20 -17
  73. package/dist/loupe/index.js +5 -3
  74. package/dist/overview/index.d.ts +56 -0
  75. package/dist/overview/index.js +289 -0
  76. package/dist/overview/index.js.map +1 -0
  77. package/dist/passthrough/weasel-ui.d.ts +239 -69
  78. package/dist/passthrough/weasel-ui.js +3 -2
  79. package/dist/primitives/index.d.ts +5 -204
  80. package/dist/primitives/index.js +6 -4
  81. package/dist/state/index.d.ts +8 -8
  82. package/dist/state/index.js +4 -5
  83. package/dist/state/index.js.map +1 -1
  84. package/dist/styles.css +152 -82
  85. package/dist/undo/index.d.ts +4 -4
  86. package/package.json +17 -12
  87. package/src/annotations/AnnotationOverlay.tsx +2 -2
  88. package/src/annotations/Annotations.less +0 -10
  89. package/src/annotations/MarkList.tsx +4 -8
  90. package/src/annotations/index.ts +1 -0
  91. package/src/annotations/paint.test.ts +3 -14
  92. package/src/annotations/paint.ts +3 -19
  93. package/src/annotations/store.test.ts +26 -0
  94. package/src/annotations/store.ts +25 -6
  95. package/src/annotations/svgNodes.test.ts +6 -9
  96. package/src/annotations/svgNodes.ts +3 -3
  97. package/src/annotations/toolMap.test.ts +37 -2
  98. package/src/annotations/toolMap.ts +13 -0
  99. package/src/annotations/types.ts +27 -0
  100. package/src/canvas/AGENTS.md +9 -10
  101. package/src/canvas/CameraInput.test.tsx +136 -0
  102. package/src/canvas/CameraInput.tsx +322 -0
  103. package/src/canvas/CanvasStack.tsx +47 -58
  104. package/src/canvas/LinkedCursor.less +16 -0
  105. package/src/canvas/LinkedCursor.tsx +54 -0
  106. package/src/canvas/Stage.test.tsx +33 -4
  107. package/src/canvas/Stage.tsx +47 -37
  108. package/src/canvas/cameraRegistry.test.ts +55 -0
  109. package/src/canvas/cameraRegistry.ts +86 -0
  110. package/src/canvas/cameraView.test.ts +60 -0
  111. package/src/canvas/cameraView.ts +61 -0
  112. package/src/canvas/cameraZoom.test.ts +46 -0
  113. package/src/canvas/cameraZoom.ts +24 -0
  114. package/src/canvas/index.ts +23 -2
  115. package/src/canvas/sharedScope.test.tsx +94 -0
  116. package/src/chrome/ChromeRegions.stories.tsx +1 -0
  117. package/src/chrome/regions/PaletteRegion.less +0 -7
  118. package/src/chrome/regions/SidebarRegion.less +12 -2
  119. package/src/chrome/regions/SidebarRegion.test.tsx +9 -2
  120. package/src/config/builder.test.ts +2 -1
  121. package/src/config/builder.ts +15 -8
  122. package/src/config/entry.test.ts +3 -1
  123. package/src/config/fromConfigField.test.ts +14 -0
  124. package/src/config/fromConfigField.ts +19 -4
  125. package/src/config/index.ts +4 -1
  126. package/src/config/path.ts +2 -5
  127. package/src/config/resolve.ts +15 -1
  128. package/src/config/sectionTree.test.ts +12 -6
  129. package/src/config/sectionTree.ts +15 -5
  130. package/src/config/types.ts +34 -9
  131. package/src/controls/ControlPanel.less +1 -1
  132. package/src/controls/ControlPanel.stories.tsx +3 -2
  133. package/src/controls/ControlPanel.test.tsx +76 -13
  134. package/src/controls/ControlPanel.tsx +80 -11
  135. package/src/controls/inDialog.test.tsx +18 -0
  136. package/src/controls/inDialog.tsx +2 -10
  137. package/src/index.test.ts +1 -1
  138. package/src/index.ts +23 -5
  139. package/src/instrument/SineWave.smoke.test.tsx +2 -0
  140. package/src/instrument/types.ts +10 -1
  141. package/src/lab/Lab.nebula.test.tsx +11 -2
  142. package/src/lab/Lab.test.tsx +3 -1
  143. package/src/lab/Lab.tsx +87 -76
  144. package/src/lab/{LabFit.stories.less → LabFit.browser.test.less} +1 -1
  145. package/src/lab/{LabFit.stories.tsx → LabFit.browser.test.tsx} +41 -63
  146. package/src/lab/LabFullChrome.stories.tsx +1 -0
  147. package/src/lab/LabHeader.test.tsx +24 -14
  148. package/src/lab/LabHeader.tsx +11 -12
  149. package/src/lab/LabShell.less +8 -17
  150. package/src/lab/LabSwitcher.less +6 -6
  151. package/src/lab/LabZoom.test.tsx +223 -0
  152. package/src/lab/LabZoom.tsx +170 -0
  153. package/src/lab/index.ts +1 -0
  154. package/src/loupe/AGENTS.md +7 -6
  155. package/src/loupe/LoupeGestures.tsx +15 -10
  156. package/src/loupe/TrialLoupe.tsx +5 -5
  157. package/src/loupe/index.ts +1 -0
  158. package/src/loupe/loupeActions.ts +6 -2
  159. package/src/overview/AGENTS.md +38 -0
  160. package/src/overview/OverviewMarks.test.tsx +100 -0
  161. package/src/overview/OverviewMarks.tsx +93 -0
  162. package/src/overview/TrialOverview.less +53 -0
  163. package/src/overview/TrialOverview.stories.tsx +50 -0
  164. package/src/overview/TrialOverview.test.tsx +116 -0
  165. package/src/overview/TrialOverview.tsx +376 -0
  166. package/src/overview/index.ts +2 -0
  167. package/src/passthrough/weasel-ui.test.ts +2 -0
  168. package/src/passthrough/weasel-ui.ts +23 -4
  169. package/src/primitives/FloatingPanel.test.tsx +11 -0
  170. package/src/primitives/FloatingPanel.tsx +2 -5
  171. package/src/primitives/JobProgress.less +1 -1
  172. package/src/primitives/Legend.stories.tsx +1 -0
  173. package/src/primitives/Readout.browser.test.tsx +137 -0
  174. package/src/primitives/Readout.less +12 -0
  175. package/src/primitives/Readout.stories.tsx +58 -0
  176. package/src/primitives/Readout.test.tsx +93 -0
  177. package/src/primitives/Readout.tsx +56 -0
  178. package/src/primitives/index.ts +2 -0
  179. package/src/specimen/Specimen.stories.tsx +1 -0
  180. package/src/specimen/Specimen.tsx +21 -4
  181. package/src/state/Persistence.browser.test.tsx +47 -0
  182. package/src/state/labRecords.test.ts +4 -1
  183. package/src/state/store.test.ts +9 -5
  184. package/src/state/usePersistedState.test.tsx +1 -1
  185. package/src/state/useTrialState.test.tsx +1 -1
  186. package/src/styles.less +3 -0
  187. package/src/theme/Interstellar.stories.tsx +4 -6
  188. package/src/theme/base.less +21 -47
  189. package/src/theme/interstellar.test.ts +1 -1
  190. package/src/theme/interstellar.theme.json +10 -3
  191. package/src/tools/labTool.ts +6 -2
  192. package/src/trial/Trial.annotations.test.tsx +16 -0
  193. package/src/trial/Trial.less +0 -13
  194. package/src/trial/Trial.loupe.test.tsx +73 -1
  195. package/src/trial/Trial.stories.tsx +2 -0
  196. package/src/trial/Trial.tsx +23 -1
  197. package/src/trial/TrialChrome.tsx +21 -3
  198. package/src/trial/trialOps.test.ts +4 -4
  199. package/dist/_dts/PrefsRow.d-DFpPIeIC.d.ts +0 -39
  200. package/dist/_dts/inDialog-D5ZKGjiD.d.ts +0 -23
  201. package/dist/_dts/index-DINmCuLA.d.ts +0 -453
  202. package/dist/_dts/number.d-OeQYQoUO.d.ts +0 -28
  203. package/dist/_dts/usePanZoom-Bq73pwFe.d.ts +0 -117
  204. package/dist/chunk-2AYGEN57.js +0 -32
  205. package/dist/chunk-2AYGEN57.js.map +0 -1
  206. package/dist/chunk-AWH3SRPW.js.map +0 -1
  207. package/dist/chunk-BVL7Y574.js.map +0 -1
  208. package/dist/chunk-ERFMC6WU.js.map +0 -1
  209. package/dist/chunk-H5VIV7MQ.js.map +0 -1
  210. package/dist/chunk-HFRE4QQV.js.map +0 -1
  211. package/dist/chunk-RS3HRQWU.js.map +0 -1
  212. package/dist/chunk-SBXNQW4G.js +0 -3
  213. package/dist/chunk-TT5C5S6R.js +0 -368
  214. package/dist/chunk-TT5C5S6R.js.map +0 -1
  215. package/dist/chunk-USO6TERP.js.map +0 -1
  216. package/dist/chunk-VQ47KWD5.js.map +0 -1
  217. package/dist/chunk-WUJXJC5O.js +0 -14568
  218. package/dist/chunk-WUJXJC5O.js.map +0 -1
  219. package/dist/chunk-ZORVNOKX.js.map +0 -1
  220. package/src/canvas/usePanZoom.test.ts +0 -244
  221. package/src/canvas/usePanZoom.ts +0 -147
  222. package/src/state/Persistence.stories.tsx +0 -75
@@ -1,37 +1,9 @@
1
- import { ComponentType, ReactNode, RefObject } from 'react';
2
- import { S as StanceProps } from './PrefsRow.d-DFpPIeIC.js';
3
- import { R as ResolvedConfig, a as ConfigPath, V as ValueAtPath, b as ConfigField, c as ConfigSchema } from './types-B7y8hX1D.js';
1
+ import { DrawCommand, Scene, PointerContextValue } from '@weasel-js/core';
2
+ import { RefObject, ComponentType, ReactNode } from 'react';
3
+ import { S as StanceProps } from './PrefsRow.d-TOZ7BgD9.js';
4
+ import { R as ResolvedConfig, C as ConfigPath, V as ValueAtPath, a as ConfigField, b as ConfigSchema } from './types-CRPSGFAp.js';
4
5
  import { ColorModePreference } from '@weasel-js/theme';
5
6
  import { J as JobHandle, a as JobCapability } from './types-DJ79Tg5J.js';
6
- import { Scene } from '@weasel-js/core';
7
-
8
- /** How an instrument's world sits on the canvas, independent of the camera.
9
- * The defaults reproduce the convention labkit shipped before this existed:
10
- * world (0,0) at the element's top-left, y growing downward. */
11
- interface WorldSpec {
12
- /** Where world (0,0) sits at `pan` zero, as a fraction of the viewport.
13
- * `{x:0,y:0}` top-left (default); `{x:0.5,y:0.5}` centre. */
14
- origin?: Point;
15
- /** Which way the world y axis runs on screen. Default `'down'`. */
16
- yAxis?: 'down' | 'up';
17
- }
18
- /** A viewport's CSS-pixel size. */
19
- interface ViewportSize {
20
- width: number;
21
- height: number;
22
- }
23
- /** A `WorldSpec` resolved against a viewport: what the coordinate helpers,
24
- * the camera and the wheel all read. */
25
- interface WorldFrame {
26
- /** Screen position of world (0,0) at `pan` zero. */
27
- originPx: Point;
28
- yDir: 1 | -1;
29
- }
30
- declare const DEFAULT_FRAME: WorldFrame;
31
- declare function resolveFrame(spec: WorldSpec | undefined, size: ViewportSize): WorldFrame;
32
- /** Put a 2D context into the instrument's world coordinates, so a layer's
33
- * `draw` works in them. Must stay the exact inverse of `screenToWorld`. */
34
- declare function applyCamera(ctx: CanvasRenderingContext2D, view: ViewTransform, frame?: WorldFrame): void;
35
7
 
36
8
  /**
37
9
  * Written at a config path to mean "do not pin this — let the instrument
@@ -244,614 +216,178 @@ interface LabDocument {
244
216
  * version-`i` document to version `i + 1`. */
245
217
  type Migration = (doc: Record<string, unknown>) => Record<string, unknown>;
246
218
 
247
- /** A named position in a trial's chrome. Content is not a region — that is
248
- * the instrument. The lab's own boxes are named in `LabRegion`. */
249
- type TrialRegion = 'titlebar' | 'toolbar' | 'palette' | 'sidebar' | 'viewport' | 'status';
250
- /** An icon component taking a pixel size, as `@weasel-js/ui` glyphs do. */
251
- type IconComponent = ComponentType<{
252
- size?: number;
253
- }>;
254
- /** A button in a toolbar. `TCtx` is the chrome context its `onActivate` is
255
- * handed — a trial's, or the lab's for a lab-level bar. */
256
- interface ToolbarItem<TCtx = TrialChromeContext> {
257
- icon: IconComponent;
258
- label: string;
259
- /** Shown in the tooltip. Not bound here — the trial owns its keymap. */
260
- shortcut?: string;
261
- disabled?: boolean;
262
- /** Reddens on hover. For actions that discard work. */
263
- danger?: boolean;
264
- /** A toggle rather than a command: renders `aria-pressed` and reads as held
265
- * down while true. */
266
- pressed?: boolean;
267
- /** Render the label beside the glyph rather than only in the tooltip. */
268
- showLabel?: boolean;
269
- /** Handed the chrome context it was declared against, so a contribution can
270
- * reach `ctx.saveSnapshot()` and the rest without the `render` escape. A
271
- * zero-argument handler stays valid. */
272
- onActivate: (ctx: TCtx) => void;
273
- }
274
- /** A tool in the palette region: a mode by default, a command when it carries
275
- * an `onActivate`. `TCtx` is the chrome context that handler is handed — a
276
- * trial's, or the lab's for a lab-level rail. */
277
- interface ToolItem<TCtx = TrialChromeContext> {
278
- icon: IconComponent;
279
- label: string;
280
- /** Shown in the tooltip. Not bound here — the trial owns its keymap. */
281
- shortcut?: string;
282
- disabled?: boolean;
283
- /** Runs on press instead of selecting. An item with one is a command: it
284
- * never writes the tool slot, and never reads as the current tool. */
285
- onActivate?: (ctx: TCtx) => void;
286
- }
287
- /** A titled block in the sidebar. `stance` and `tone` work as on a
288
- * `<PropertyPanel>`: what kind of content the section holds, and which of its
289
- * peers it is. */
290
- interface SidebarSection extends StanceProps {
291
- title: string;
292
- /** Starts collapsed, until the trial remembers a fold of its own. */
293
- defaultCollapsed?: boolean;
294
- /** Offer the tear-out control. On by default; a section that only makes
295
- * sense beside its trial sets this false. */
296
- undockable?: boolean;
297
- /** Where the tear-out control sends it. Default `'tile'`. */
298
- undockAs?: 'tile' | 'floating';
299
- body: ReactNode;
300
- }
301
- /** A control acting on the view of the trial, not on the trial. */
302
- interface ViewportControl<TCtx = TrialChromeContext> {
303
- icon: IconComponent;
304
- label: string;
305
- disabled?: boolean;
306
- /** Handed the trial's chrome context, as `ToolbarItem.onActivate` is. */
307
- onActivate: (ctx: TCtx) => void;
219
+ /** A mark's box in its target's world: CSS pixels at zoom 1. Matches weasel's
220
+ * default `RectPose`, which is what the scene stores. */
221
+ interface WorldRect {
222
+ x: number;
223
+ y: number;
224
+ width: number;
225
+ height: number;
308
226
  }
309
- /** A readout in the status bar. */
310
- interface StatusReadout {
311
- /** Short enough for a status bar. Rendered as text. */
312
- text: string;
313
- /** Tooltip. */
314
- title?: string;
227
+ /** Where a fraction lands on a content box of this size. */
228
+ declare function fracToWorld(f: FracRect, content: {
229
+ w: number;
230
+ h: number;
231
+ }): WorldRect;
232
+ /** What fraction of the content box a world rect covers.
233
+ *
234
+ * A pane measured before layout is 0×0, and dividing by that would seed every
235
+ * stored position with `NaN` — which compares false against everything and so
236
+ * fails silently rather than loudly. Zero sides give zeros. */
237
+ declare function worldToFrac(r: WorldRect, content: {
238
+ w: number;
239
+ h: number;
240
+ }): FracRect;
241
+ /** Snap to 4dp. Positions are diffed by humans in stored documents, so the
242
+ * last digits of a float are noise that hides the change that matters. */
243
+ declare function roundFrac(f: FracRect): FracRect;
244
+ /** Whether `pt` falls in `box`, widened by `tol` on every side so a hairline
245
+ * mark stays reachable. */
246
+ declare function fracContains(box: FracRect, pt: FracPoint, tol?: number): boolean;
247
+ /** Whether `outer` wholly encloses `inner`. A marquee takes what it encloses,
248
+ * not what it grazes — brushing selection is a different gesture, and this
249
+ * answers false for two rects that merely overlap. */
250
+ declare function fracEncloses(outer: FracRect, inner: FracRect): boolean;
251
+
252
+ /** The subset of a mark's scene node this needs: where it is, and what it is. */
253
+ interface PaintableMark {
254
+ pose: WorldRect;
255
+ data: AnnotationData;
315
256
  }
316
- /** What every contribution shares, whichever chrome it is declared against. */
317
- interface ContributionBase {
318
- id: string;
319
- /** Groups sort by first appearance; items sort within a group by
320
- * declaration order. Contributions with no group sort after grouped ones. */
321
- group?: string;
322
- /** Pushes this contribution, and its group, to the far end of the region. */
323
- end?: boolean;
257
+ /** How a mark is drawn, as opposed to where. Resolved by the overlay from the
258
+ * instrument's vocabulary and the mark's own staleness. */
259
+ interface MarkStyle {
260
+ /** The status's color, or the default. */
261
+ color?: string;
262
+ /** A mark whose stored position no longer describes the picture. Drawn
263
+ * dashed rather than hidden: it still describes *something*, and dropping
264
+ * it would lose it. */
265
+ stale?: boolean;
324
266
  }
325
267
  /**
326
- * A contribution is data the chrome renders, keyed to a region. Supplying
327
- * `render` instead of `item` opts out of the chrome's layout — deliberate,
328
- * and visible in the declaration.
329
- */
330
- type TrialContribution = (ContributionBase & {
331
- region: 'titlebar';
332
- item: ToolbarItem;
333
- render?: never;
334
- }) | (ContributionBase & {
335
- region: 'toolbar';
336
- item: ToolbarItem;
337
- render?: never;
338
- }) | (ContributionBase & {
339
- region: 'palette';
340
- item: ToolItem;
341
- render?: never;
342
- }) | (ContributionBase & {
343
- region: 'sidebar';
344
- item: SidebarSection;
345
- render?: never;
346
- }) | (ContributionBase & {
347
- region: 'viewport';
348
- item: ViewportControl;
349
- render?: never;
350
- }) | (ContributionBase & {
351
- region: 'status';
352
- item: StatusReadout;
353
- render?: never;
354
- }) | (ContributionBase & {
355
- region: TrialRegion;
356
- item?: never;
357
- render: (ctx: TrialChromeContext) => ReactNode;
358
- });
359
- /**
360
- * A tool slot a region can reflect and write. Both chrome contexts carry one:
361
- * a trial's resolves to the lab's when its instrument declares no tools.
268
+ * What one mark draws, in its target's world.
269
+ *
270
+ * Pure: a node and the target's content box in, draw commands out. Geometry
271
+ * that a bounding box cannot describe — a line's ends, a stroke's path — comes
272
+ * from `data.points`, which is in fractions like the bounds.
362
273
  */
363
- interface ToolSlotContext {
364
- activeToolId: string | null;
365
- setActiveTool: (id: string) => void;
274
+ declare function markCommands(m: PaintableMark, content: {
275
+ w: number;
276
+ h: number;
277
+ }, style?: MarkStyle): DrawCommand[];
278
+
279
+ /** A mark's box in its target's world. Matches weasel's default `RectPose`. */
280
+ type MarkPose = WorldRect;
281
+ /** The scene one target's marks live in. */
282
+ type MarkScene = Scene<AnnotationData, 'marks', MarkPose>;
283
+ /** A mark scene: one layer, because marks do not stack in tiers. */
284
+ declare function createAnnotationScene(): MarkScene;
285
+ interface AnnotationStoreOptions {
286
+ /** Re-read on every call, so a target resizing or gaining a dependency takes
287
+ * effect without rebuilding the store. */
288
+ targets: () => readonly AnnotationTargetInfo[];
289
+ /** Serialized scenes from a previous `toJSON`, keyed by target. */
290
+ restore?: Readonly<Record<string, unknown>>;
291
+ /** The instrument's vocabulary, so an export draws a mark in the color its
292
+ * status gives it. A thunk is read at each capture, for a vocabulary that
293
+ * changes under a store built once. */
294
+ meaning?: AnnotationMeaning | (() => AnnotationMeaning | undefined);
295
+ /** The trial's live config. A getter for the same reason `targets` is: the
296
+ * store is built once and the config changes under it. */
297
+ config?: () => unknown;
298
+ /** Notified after every finished export. */
299
+ onCapture?: (result: CaptureResult) => void;
366
300
  }
367
- /** What a sidebar region reflects and writes: which sections are folded, and —
368
- * where the chrome can tear a section out — where it goes. A trial supplies
369
- * all four; a lab supplies the fold state only. */
370
- interface SidebarSlotContext {
371
- collapsedSections: Readonly<Record<string, boolean>>;
372
- setSectionCollapsed: (key: string, collapsed: boolean) => void;
373
- undockedPanels?: readonly string[];
374
- undockPanel?: (sectionId: string, as?: 'tile' | 'floating') => void;
301
+ /** Everything a capture needs that is not the store's own state. Split out so
302
+ * `capture.ts` takes data rather than reaching back into the store. */
303
+ interface CaptureDeps {
304
+ scene: MarkScene;
305
+ target: AnnotationTargetInfo;
306
+ meaning?: AnnotationMeaning;
307
+ config: unknown;
375
308
  }
376
309
  /**
377
- * A contribution as a region renderer sees it. A renderer checks the region
378
- * name against its own and narrows `item` itself, which is what lets one
379
- * renderer serve both chromes — the trial's and the lab's — without knowing
380
- * which context it was handed.
310
+ * A facade over one weasel scene per target: the scenes are the truth, this
311
+ * answers questions about them. Everything crossing this boundary is in
312
+ * fractions of a target's content box; the scenes hold world units.
313
+ *
314
+ * A scene per target rather than one scene with a `target` field, because a
315
+ * pane's hit-test, marquee and paint all walk the whole scene it is given and
316
+ * take no filter — one shared scene puts every other pane's marks under the
317
+ * pointer.
381
318
  */
382
- interface RegionContribution<TCtx> extends ContributionBase {
383
- region: string;
384
- item?: unknown;
385
- render?: (ctx: TCtx) => ReactNode;
319
+ declare function createAnnotationStore(opts: AnnotationStoreOptions): AnnotationsApi;
320
+ /** Rebuild a store from what `toJSON` wrote. Unknown or future versions give
321
+ * an empty store rather than throwing: a lab that cannot read its marks
322
+ * should still open. */
323
+ declare function annotationsFromJSON(raw: unknown, targets: () => readonly AnnotationTargetInfo[], rest?: Omit<AnnotationStoreOptions, 'targets' | 'restore'>): AnnotationsApi;
324
+
325
+ /** A point in fractions of a target's content box. */
326
+ interface FracPoint {
327
+ x: number;
328
+ y: number;
386
329
  }
387
- /**
388
- * Everything a contribution can read about the trial it is being rendered
389
- * into. Replaces the three separate slot contexts, which each carried a
390
- * hand-picked subset.
391
- */
392
- interface TrialChromeContext extends ToolSlotContext, SidebarSlotContext {
393
- trialId: string;
394
- instrumentName: string;
395
- /** What the title bar reads, which is the instrument's title (or, lacking
396
- * one, its name) until something calls `setTitle`. */
397
- title: string;
398
- /** Retitle this trial; `null` restores the instrument's title. Persisted with
399
- * the trial, so a title survives a reload. */
400
- setTitle: (title: string | null) => void;
401
- isLastTrial: boolean;
402
- /** Null when the trial holds a view that is not the 2D one. */
403
- zoom: number | null;
404
- setZoom: (z: number) => void;
405
- canUndo: boolean;
406
- canRedo: boolean;
407
- undo: () => void;
408
- redo: () => void;
409
- /** Whether the trial's loupe is turned on. False for an instrument that
410
- * declares none, whose chrome offers no way to turn one on. */
411
- loupeOn: boolean;
412
- toggleLoupe: () => void;
413
- /** The instrument's controls, resolved against the lab's rules. Always
414
- * populated: a legacy `configSchema()` is adapted into the same shape. */
415
- configSchema: ResolvedConfig;
416
- /** @deprecated Read `configSchema`. Empty for an instrument declaring
417
- * `config`, since a builder schema has no `ConfigField[]` form. */
418
- configFields: ConfigField[];
419
- config: unknown;
420
- /** The trial's unpinned paths. `config` stays raw, so a control drawn from
421
- * it sits at the value un-pinning writes back. */
422
- auto: ReadonlySet<string>;
423
- setConfig: (key: string, value: unknown) => void;
424
- /** Which of this trial's collapsible sections are folded. A sidebar
425
- * section's key is its contribution id; a section *inside* a contribution —
426
- * a control panel's property group — is keyed `<contribution id>/<label>`.
427
- * A section absent here is at its own default. Persisted with the trial. */
428
- collapsedSections: Readonly<Record<string, boolean>>;
429
- /** Fold or unfold one section, keyed as `collapsedSections` is. */
430
- setSectionCollapsed: (key: string, collapsed: boolean) => void;
431
- /** Section ids this trial currently has torn out of its sidebar. */
432
- undockedPanels: readonly string[];
433
- /** Tear a sidebar section out into the workspace. */
434
- undockPanel: (sectionId: string, as?: 'tile' | 'floating') => void;
435
- /** Put a torn-out section back in the sidebar. */
436
- dockPanel: (sectionId: string) => void;
437
- savedSnapshots: SavedSnapshot[];
438
- saveSnapshot: (name?: string) => void;
439
- loadSnapshot: (snapshotId: string) => void;
440
- clone: () => void;
441
- reset: () => void;
442
- close: () => void;
443
- }
444
-
445
- /** A point on the magnified surface, in its own CSS pixels. */
446
- interface LoupePoint {
330
+ /** A rectangle in fractions of a target's content box.
331
+ *
332
+ * Fractions rather than pixels because a mark must survive a change of render
333
+ * resolution: the picture gets bigger, the mark stays on the same feature. */
334
+ interface FracRect {
447
335
  x: number;
448
336
  y: number;
337
+ w: number;
338
+ h: number;
449
339
  }
450
-
451
- /** How a loupe magnifies. `'vector'` re-renders the source through a zoomed-in
452
- * view, so content stays sharp at any factor; `'pixel'` blows up the actual
453
- * pixels the surface presented. */
454
- type LoupeMode = 'vector' | 'pixel';
455
-
456
- /** What a DOM instrument's loupe `render` is handed: the same data its own
457
- * `render` gets, and the camera to draw it through. */
458
- interface LoupeRenderArgs<TS = unknown, TC = unknown> {
459
- state: TS;
460
- config: TC;
461
- /** The trial's own camera composed with the magnification, about the aimed
462
- * point — so drawing the same content through it magnifies in place. */
463
- view: ViewTransform;
464
- /** The magnification on its own, for whatever must not scale with the
465
- * camera. */
466
- factor: number;
467
- mode: LoupeMode;
468
- /** The size of the viewport `view` is written for: the trial's content well,
469
- * not the lens. The lens shows a circle cut out of it. */
470
- size: ViewportSize;
471
- }
472
- /**
473
- * Declares that an instrument can be magnified.
474
- *
475
- * With no `render`, the loupe re-draws the instrument's canvas layers through a
476
- * zoomed camera, so it stays sharp at any factor. An instrument whose content is
477
- * DOM supplies `render` instead: given a camera, draw me again.
478
- */
479
- interface LoupeCapability<TS = unknown, TC = unknown> {
480
- render?: (args: LoupeRenderArgs<TS, TC>) => ReactNode;
481
- /** Opening magnification, clamped to the bounds below. Default 6. */
482
- factor?: number;
483
- /** What the wheel clamps to. Defaults 2 and 32. */
484
- minFactor?: number;
485
- maxFactor?: number;
486
- /** `'vector'` (default) re-renders the content magnified; `'pixel'` blows up
487
- * the pixels the instrument presented. A `render` loupe is always vector —
488
- * DOM has no framebuffer to enlarge. */
489
- mode?: LoupeMode;
490
- /** Lens diameter in CSS px. Default 200. */
491
- diameter?: number;
492
- /** Held for a momentary peek while the loupe is off. Default `'Alt'`; `null`
493
- * turns hold-to-peek off. Matched against `KeyboardEvent.key`. */
494
- peekKey?: string | null;
495
- /** Called with the color under the aim, wherever the surface can say. The
496
- * canvas painter reads it back; a DOM loupe has no pixels to sample. */
497
- onColorChange?: (hex: string) => void;
498
- }
499
- /** An instrument's loupe declaration. `true` takes every default. */
500
- type LoupeDeclaration<TS = unknown, TC = unknown> = true | LoupeCapability<TS, TC>;
501
- /** A {@link LoupeCapability} with every default filled in. */
502
- interface ResolvedLoupe<TS = unknown, TC = unknown> {
503
- render?: (args: LoupeRenderArgs<TS, TC>) => ReactNode;
504
- onColorChange?: (hex: string) => void;
505
- factor: number;
506
- minFactor: number;
507
- maxFactor: number;
508
- mode: LoupeMode;
509
- diameter: number;
510
- peekKey: string | null;
511
- }
512
- declare const LOUPE_DEFAULTS: {
513
- readonly factor: 6;
514
- readonly minFactor: 2;
515
- readonly maxFactor: 32;
516
- readonly mode: "vector";
517
- readonly diameter: 200;
518
- readonly peekKey: "Alt";
519
- };
520
- declare function resolveLoupe<TS, TC>(declared: LoupeDeclaration<TS, TC>): ResolvedLoupe<TS, TC>;
521
-
522
- /**
523
- * A tool a trial can be in. labkit's own — core's `ToolsApi` carries hotkey
524
- * slots, ambient tools, eligibility tiers and canvas overlay layers, all bound
525
- * to the gesture dispatcher, and a labkit instrument is an arbitrary canvas or
526
- * DOM tree rather than a weasel scene.
527
- */
528
- interface TrialTool {
340
+ /** What shape a mark is. The kinds map onto weasel's own tools; an arrow is a
341
+ * line carrying an end marker, not a separate geometry. A point holds one
342
+ * entry in `points` and zero-size bounds at that position. */
343
+ type AnnotationKind = 'stroke' | 'line' | 'arrow' | 'rect' | 'ellipse' | 'text' | 'point';
344
+ /** One selectable status in an instrument's meaning tier. */
345
+ interface AnnotationStatus {
529
346
  id: string;
530
347
  label: string;
531
- icon: IconComponent;
532
- /** Shown in the tooltip. Not bound here — the instrument owns its keymap. */
533
- shortcut?: string;
534
- /** Presentation grouping in the palette. Ungrouped tools sort after grouped. */
535
- group?: string;
348
+ /** What a mark in this status is drawn in. Omitted, it takes the default
349
+ * mark color — a status is allowed to be a label and nothing more. */
350
+ color?: string;
536
351
  }
537
- /** What an instrument declares to get a palette region. */
538
- interface ToolCapability {
539
- tools: TrialTool[];
540
- /** Which tool a fresh trial starts in. Defaults to the first. */
541
- initial?: string;
352
+ /** The optional meaning tier: what a mark means, as opposed to where it is.
353
+ * An instrument declaring it gets labkit's own chrome for the vocabulary;
354
+ * one that omits it keeps `meta` and owns meaning entirely. */
355
+ interface AnnotationMeaning {
356
+ statuses?: readonly AnnotationStatus[];
542
357
  }
543
-
544
- /** What an instrument's `render` is handed: its state and config, the setters
545
- * for both, the trial it is mounted in, and a way to emit named events. */
546
- interface RenderContext<TS = unknown, TC = unknown> {
547
- state: TS;
548
- config: TC;
549
- setState: (next: TS | ((prev: TS) => TS)) => void;
550
- /** Write one config value, by dotted path — `'grid.size'` for a leaf under
551
- * an `f.group`, `'cellSize'` for one at the root. */
552
- setConfig: (path: ConfigPath<TC>, value: unknown) => void;
553
- /** labkit persists `view` and restores it on Reset without ever reading
554
- * into it. */
555
- trial: TrialInfo & {
556
- setView: (next: unknown) => void;
557
- /** 2D convenience over `view`. Reads 1 and writes nothing when the trial holds
558
- * a view that is not the 2D one. */
559
- zoom: number;
560
- setZoom: (z: number) => void;
561
- /** Resolved active tool: this trial's slot, or the lab's. Null when neither
562
- * holds one. */
563
- activeToolId: string | null;
564
- /** The canvas layers currently shown, in declaration order. labkit skips a
565
- * hidden layer's `draw`, so this is the only way to know what was
566
- * painted — a legend listing rows the view did not draw needs it. */
567
- visibleLayers: readonly string[];
568
- };
569
- emit: (event: string) => void;
570
- /** Present only when the instrument declares a `job`. */
571
- job?: JobHandle;
358
+ /** What labkit keeps in a mark's scene-node data. */
359
+ interface AnnotationData {
360
+ target: string;
361
+ kind: AnnotationKind;
362
+ /** Vertices for the kinds a bounding box cannot describe — a line's two
363
+ * ends, a stroke's path. In fractions, like the bounds. */
364
+ points?: readonly FracPoint[];
365
+ title?: string;
366
+ status?: string;
367
+ tags?: readonly string[];
368
+ meta?: unknown;
369
+ /** The target's `positionDependsOn` values when the mark was made. Compared
370
+ * against the live config to answer whether the mark still describes the
371
+ * same picture. */
372
+ seen?: Readonly<Record<string, unknown>>;
572
373
  }
573
- /** One 2D canvas layer of an instrument, drawn in declaration order.
574
- *
575
- * `draw` is called with the camera already applied, so it works in world
576
- * coordinates. `zoom` is passed for the things that must not scale with it —
577
- * set `ctx.lineWidth = 1 / zoom` to keep a hairline hairline. */
578
- interface CanvasLayer<TS = unknown, TC = unknown> {
374
+ /** A mark, as the store reports it. `id` is `<target>/<node>`: one scene per
375
+ * target means a node id is only unique within one. */
376
+ interface Annotation extends AnnotationData {
579
377
  id: string;
580
- draw: (ctx: CanvasRenderingContext2D, args: {
581
- state: TS;
582
- config: TC;
583
- zoom: number;
584
- }) => void;
585
- }
586
- /** Declares that an instrument draws to a canvas: its layers, and where the
587
- * view starts. */
588
- interface CanvasCapability<TS = unknown, TC = unknown> {
589
- layers: CanvasLayer<TS, TC>[];
590
- /** The coordinate system this instrument's world is in. Omitted, world (0,0)
591
- * sits at the canvas top-left with y running down — what labkit assumed
592
- * before an instrument could say otherwise. */
593
- worldSpec?: WorldSpec;
594
- /** Where the view starts. A function is called once the canvas knows its
595
- * size, so an instrument can frame content it can only place in terms of
596
- * the viewport; until then the trial's view is `null`. */
597
- initialView?: ViewTransform | ((size: ViewportSize) => ViewTransform);
598
- /** Widens `usePanZoom`'s default clamp; the opening zoom stays reachable
599
- * regardless of these. */
600
- minZoom?: number;
601
- maxZoom?: number;
378
+ /** Bounds in fractions of the target's content box. */
379
+ frac: FracRect;
602
380
  }
603
- /** Declares that an instrument's DOM is content of a fixed size, which the
604
- * trial pans and zooms the way it does a canvas's layers. Not combined with
605
- * `canvas`: there the DOM is an unzoomed overlay, and `canvas` wins. */
606
- interface StageCapability<TS = unknown, TC = unknown> {
607
- /** The content's own size in CSS pixels, at zoom 1. */
608
- size: ViewportSize;
609
- /** Where the view starts. Omitted, the content opens centered and shrunk to
610
- * fit (`fitStage`). A function is called once the stage knows its size. */
611
- initialView?: ViewTransform | ((viewport: ViewportSize) => ViewTransform);
612
- minZoom?: number;
613
- maxZoom?: number;
614
- /** Drawn over the content in viewport pixels, outside the camera — a legend,
615
- * a floating panel, anything that must not zoom with the picture. */
616
- overlay?: (ctx: RenderContext<TS, TC>) => ReactNode;
617
- }
618
- /** Declares which of an instrument's layers the trial should offer
619
- * show/hide controls for. */
620
- interface LayerCapability {
621
- /** In list order. A bare string is a layer id that doubles as its own label;
622
- * give a descriptor instead to label a layer or mark it `alwaysOn`. */
623
- ids: readonly (string | LayerDescriptor)[];
624
- }
625
- /** Declares that an instrument accepts items dragged from a palette: what the
626
- * palette offers, what a drop does to the state, and — optionally — live
627
- * feedback during the drag and the ability to drag existing items back out. */
628
- interface DragDropCapability<TS = unknown, TC = unknown> {
629
- palette: PaletteItem[] | ((state: TS, config: TC) => PaletteItem[]);
630
- onDrop: (worldPos: Point, item: PaletteItem, state: TS, config: TC) => TS;
631
- onDragOver?: (worldPos: Point, item: PaletteItem, state: TS, config: TC) => DragFeedback | null;
632
- pickUp?: (hit: HitResult, state: TS, config: TC) => {
633
- item: PaletteItem;
634
- state: TS;
635
- } | null;
636
- }
637
- /** Declares that an instrument's state is undoable: which emitted events
638
- * snapshot it, and how many snapshots to keep. */
639
- interface UndoCapability {
640
- snapshotOn?: string[];
641
- maxDepth?: number;
642
- }
643
- /** The name of an event an instrument emits through `RenderContext.emit`. */
644
- type SystemEvent = string;
645
- /** A point in world coordinates. */
646
- type Point = {
647
- x: number;
648
- y: number;
649
- };
650
- /** What a hit-test found, and where. */
651
- type HitResult = {
652
- hit: boolean;
653
- layerId?: string;
654
- pointId?: string;
655
- };
656
- /** A trial's camera. `zoom` is always positive and finite once labkit holds
657
- * it — see `normalize2DView`. */
658
- type ViewTransform = {
659
- zoom: number;
660
- pan: Point;
661
- };
662
- /** A layer as the layer list shows it. `alwaysOn` layers cannot be hidden. */
663
- type LayerDescriptor = {
664
- id: string;
665
- label: string;
666
- alwaysOn?: boolean;
667
- };
668
- /** One draggable entry in an instrument's palette. */
669
- type PaletteItem = {
670
- id: string;
671
- label: string;
672
- data?: unknown;
673
- };
674
- /** Whether a drop would be accepted at the current position, and why not if
675
- * it would not. */
676
- type DragFeedback = {
677
- ok: boolean;
678
- reason?: string;
679
- };
680
- /**
681
- * An instrument: one self-contained interactive experiment a lab can host.
682
- *
683
- * It owns two pieces of data — `config`, the settings the control panel edits,
684
- * and `state`, what the experiment is currently doing — and renders from both.
685
- * The optional capability fields declare what else it wants from the runtime:
686
- * a canvas, a layer list, palette drag-and-drop, undo. Declaring a capability
687
- * is what makes the trial provide the corresponding chrome.
688
- */
689
- interface Instrument<TS = unknown, TC = unknown, TItem = unknown> {
690
- /** The id a trial record names its instrument by. */
691
- name: string;
692
- /** What a trial of this instrument and the add-trial menu read. Default: `name`. */
693
- title?: string;
694
- defaultConfig: () => TC;
695
- initialState: (config: TC) => TS;
696
- /** The instrument's config, declared once: values, types and controls.
697
- * Supplying this makes `defaultConfig` optional — `defineInstrument`
698
- * synthesizes it. Prefer it over `defaultConfig` + `configSchema`. */
699
- config?: ConfigSchema<TC>;
700
- /** @deprecated Declare `config` instead; this repeats what `TC` already
701
- * says and nothing holds the two to one answer. */
702
- configSchema?: () => ConfigField[];
703
- /** The instrument's DOM. With `canvas`, this renders as an overlay above the
704
- * layers rather than instead of them; return `null` for canvas only. */
705
- render: (ctx: RenderContext<TS, TC>) => ReactNode;
706
- onConfigChange?: (config: TC, prev: TC, state: TS) => TS;
707
- /** Moves a stored config's values to where the current schema keeps them —
708
- * `gridSize` to `grid.size` — before the defaults fill its gaps, so it
709
- * returns only what it moved and leaves the rest to them. It runs on every
710
- * read of a stored config, including one already moved, so it must hand
711
- * back a current config unchanged. */
712
- migrateConfig?: (stored: unknown) => unknown;
713
- serialize?: (state: TS) => unknown;
714
- deserialize?: (data: unknown, config: TC) => TS;
715
- canvas?: CanvasCapability<TS, TC>;
716
- stage?: StageCapability<TS, TC>;
717
- layers?: LayerCapability;
718
- dragDrop?: DragDropCapability<TS, TC>;
719
- undo?: UndoCapability;
720
- /** Tools this instrument offers. Declaring them gives the trial a palette
721
- * region and its own tool slot. */
722
- tools?: ToolCapability;
723
- /** Regions of this instrument that accept marks, and optionally what a mark
724
- * is allowed to mean. Declaring it is what makes the trial provide the
725
- * annotation overlay and its chrome. */
726
- annotations?: AnnotationsCapability<TS, TC>;
727
- /** Magnification. `true` takes every default and re-draws the instrument's
728
- * canvas layers through a zoomed camera; an instrument whose content is DOM
729
- * gives a `render` that draws it again at a camera it is handed. A function
730
- * is re-read as the config changes, so a setting can drive the lens. */
731
- loupe?: LoupeDeclaration<TS, TC> | ((config: TC) => LoupeDeclaration<TS, TC>);
732
- /** Chrome this instrument contributes beyond what its capabilities imply. */
733
- chrome?: TrialContribution[];
734
- /** Work too slow to do during a render. The runtime starts it, aborts it on
735
- * unmount and on a `key` change, and renders progress into the trial. */
736
- job?: JobCapability<TS, TC, TItem>;
737
- }
738
- /** Instruments as a lab receives them. `any` rather than `unknown` because
739
- * parameter contravariance keeps a `defineInstrument<TS, TC>` result out of
740
- * an `Instrument<unknown, unknown>[]`; it is contained to this alias. */
741
- type InstrumentList = readonly Instrument<any, any, any>[];
742
-
743
- /** A mark's box in its target's world. Matches weasel's default `RectPose`. */
744
- type MarkPose = WorldRect;
745
- /** The scene one target's marks live in. */
746
- type MarkScene = Scene<AnnotationData, 'marks', MarkPose>;
747
- /** A mark scene: one layer, because marks do not stack in tiers. */
748
- declare function createAnnotationScene(): MarkScene;
749
- interface AnnotationStoreOptions {
750
- /** Re-read on every call, so a target resizing or gaining a dependency takes
751
- * effect without rebuilding the store. */
752
- targets: () => readonly AnnotationTargetInfo[];
753
- /** Serialized scenes from a previous `toJSON`, keyed by target. */
754
- restore?: Readonly<Record<string, unknown>>;
755
- /** The instrument's vocabulary, so an export draws a mark in the color its
756
- * status gives it. A thunk is read at each capture, for a vocabulary that
757
- * changes under a store built once. */
758
- meaning?: AnnotationMeaning | (() => AnnotationMeaning | undefined);
759
- /** The trial's live config. A getter for the same reason `targets` is: the
760
- * store is built once and the config changes under it. */
761
- config?: () => unknown;
762
- /** Notified after every finished export. */
763
- onCapture?: (result: CaptureResult) => void;
764
- }
765
- /** Everything a capture needs that is not the store's own state. Split out so
766
- * `capture.ts` takes data rather than reaching back into the store. */
767
- interface CaptureDeps {
768
- scene: MarkScene;
769
- target: AnnotationTargetInfo;
770
- meaning?: AnnotationMeaning;
771
- config: unknown;
772
- }
773
- /**
774
- * A facade over one weasel scene per target: the scenes are the truth, this
775
- * answers questions about them. Everything crossing this boundary is in
776
- * fractions of a target's content box; the scenes hold world units.
777
- *
778
- * A scene per target rather than one scene with a `target` field, because a
779
- * pane's hit-test, marquee and paint all walk the whole scene it is given and
780
- * take no filter — one shared scene puts every other pane's marks under the
781
- * pointer.
782
- */
783
- declare function createAnnotationStore(opts: AnnotationStoreOptions): AnnotationsApi;
784
- /** Rebuild a store from what `toJSON` wrote. Unknown or future versions give
785
- * an empty store rather than throwing: a lab that cannot read its marks
786
- * should still open. */
787
- declare function annotationsFromJSON(raw: unknown, targets: () => readonly AnnotationTargetInfo[], rest?: Omit<AnnotationStoreOptions, 'targets' | 'restore'>): AnnotationsApi;
788
-
789
- /** A point in fractions of a target's content box. */
790
- interface FracPoint {
791
- x: number;
792
- y: number;
793
- }
794
- /** A rectangle in fractions of a target's content box.
795
- *
796
- * Fractions rather than pixels because a mark must survive a change of render
797
- * resolution: the picture gets bigger, the mark stays on the same feature. */
798
- interface FracRect {
799
- x: number;
800
- y: number;
801
- w: number;
802
- h: number;
803
- }
804
- /** What shape a mark is. The kinds map onto weasel's own tools; an arrow is a
805
- * line carrying an end marker, not a separate geometry. A point holds one
806
- * entry in `points` and zero-size bounds at that position. */
807
- type AnnotationKind = 'stroke' | 'line' | 'arrow' | 'rect' | 'ellipse' | 'text' | 'point';
808
- /** One selectable status in an instrument's meaning tier. */
809
- interface AnnotationStatus {
810
- id: string;
811
- label: string;
812
- /** What a mark in this status is drawn in. Omitted, it takes the default
813
- * mark color — a status is allowed to be a label and nothing more. */
814
- color?: string;
815
- }
816
- /** The optional meaning tier: what a mark means, as opposed to where it is.
817
- * An instrument declaring it gets labkit's own chrome for the vocabulary;
818
- * one that omits it keeps `meta` and owns meaning entirely. */
819
- interface AnnotationMeaning {
820
- statuses?: readonly AnnotationStatus[];
821
- }
822
- /** What labkit keeps in a mark's scene-node data. */
823
- interface AnnotationData {
824
- target: string;
825
- kind: AnnotationKind;
826
- /** Vertices for the kinds a bounding box cannot describe — a line's two
827
- * ends, a stroke's path. In fractions, like the bounds. */
828
- points?: readonly FracPoint[];
829
- title?: string;
830
- status?: string;
831
- tags?: readonly string[];
832
- meta?: unknown;
833
- /** The target's `positionDependsOn` values when the mark was made. Compared
834
- * against the live config to answer whether the mark still describes the
835
- * same picture. */
836
- seen?: Readonly<Record<string, unknown>>;
837
- }
838
- /** A mark, as the store reports it. `id` is `<target>/<node>`: one scene per
839
- * target means a node id is only unique within one. */
840
- interface Annotation extends AnnotationData {
841
- id: string;
842
- /** Bounds in fractions of the target's content box. */
843
- frac: FracRect;
844
- }
845
- /** A new mark, before the store gives it an id and dates it. */
846
- interface AnnotationInit {
847
- target: string;
848
- kind: AnnotationKind;
849
- frac: FracRect;
850
- points?: readonly FracPoint[];
851
- title?: string;
852
- status?: string;
853
- tags?: readonly string[];
854
- meta?: unknown;
381
+ /** A new mark, before the store gives it an id and dates it. */
382
+ interface AnnotationInit {
383
+ target: string;
384
+ kind: AnnotationKind;
385
+ frac: FracRect;
386
+ points?: readonly FracPoint[];
387
+ title?: string;
388
+ status?: string;
389
+ tags?: readonly string[];
390
+ meta?: unknown;
855
391
  }
856
392
  /** Filters, ANDed. A mark matches `tags` when it carries every tag listed. */
857
393
  interface AnnotationQuery {
@@ -934,10 +470,16 @@ interface AnnotationStorage {
934
470
  }
935
471
  /** Declares that an instrument accepts marks: which regions take them,
936
472
  * optionally what a mark is allowed to mean, and optionally where they live. */
473
+ /** The annotation tools a lab's rail can carry. */
474
+ type AnnotationToolId = 'pointer' | 'select' | 'stroke' | 'line' | 'arrow' | 'rect' | 'ellipse' | 'text';
937
475
  interface AnnotationsCapability<TS = unknown, TC = unknown> {
938
476
  /** `trial` is which trial is asking: a declaration made once per instrument
939
477
  * is called once per trial, and its targets are that trial's own. */
940
478
  targets: (state: TS, config: TC, trial: TrialInfo) => readonly AnnotationTarget[];
479
+ /** Which annotation tools the lab's rail offers. The rail is shared, so it
480
+ * carries every tool any annotating instrument asks for, in the kit's own
481
+ * order; an instrument that leaves this unset asks for all of them. */
482
+ tools?: readonly AnnotationToolId[];
941
483
  meaning?: AnnotationMeaning;
942
484
  /** An instrument replacing this one under a live trial must pass the same
943
485
  * object: the trial keeps the marks it loaded from the first. */
@@ -955,6 +497,11 @@ interface SerializedAnnotations {
955
497
  /** One serialized scene per target that has ever held a mark. */
956
498
  scenes: Record<string, unknown>;
957
499
  }
500
+ /** One mark, as `AnnotationsApi.paintedMarks` hands it over. */
501
+ interface PaintedMark {
502
+ mark: PaintableMark;
503
+ style: MarkStyle;
504
+ }
958
505
  /** Everything a host can ask or tell labkit about the marks on its targets. */
959
506
  interface AnnotationsApi {
960
507
  /** The scene a target's marks live in, created on first ask. One per target
@@ -992,6 +539,10 @@ interface AnnotationsApi {
992
539
  /** Take back the most recent mark change, wherever it was made. */
993
540
  undo(): boolean;
994
541
  redo(): boolean;
542
+ /** A target's marks as its pane paints them, in scene order: geometry in
543
+ * the target's content box, each with the style its status and staleness
544
+ * resolve to. For a painter outside the pane — an overview. */
545
+ paintedMarks(target: string): PaintedMark[];
995
546
  /** Export a target's picture with its marks on it. Rejects on an id the
996
547
  * instrument does not declare. */
997
548
  capture(target: string, opts?: CaptureOptions): Promise<CaptureResult>;
@@ -1003,38 +554,537 @@ interface AnnotationsApi {
1003
554
  toJSON(): SerializedAnnotations;
1004
555
  }
1005
556
 
1006
- /** A mark's box in its target's world: CSS pixels at zoom 1. Matches weasel's
1007
- * default `RectPose`, which is what the scene stores. */
1008
- interface WorldRect {
1009
- x: number;
1010
- y: number;
557
+ /** How an instrument's world sits on the canvas, independent of the camera.
558
+ * The defaults reproduce the convention labkit shipped before this existed:
559
+ * world (0,0) at the element's top-left, y growing downward. */
560
+ interface WorldSpec {
561
+ /** Where world (0,0) sits at `pan` zero, as a fraction of the viewport.
562
+ * `{x:0,y:0}` top-left (default); `{x:0.5,y:0.5}` centre. */
563
+ origin?: Point;
564
+ /** Which way the world y axis runs on screen. Default `'down'`. */
565
+ yAxis?: 'down' | 'up';
566
+ }
567
+ /** A viewport's CSS-pixel size. */
568
+ interface ViewportSize {
1011
569
  width: number;
1012
570
  height: number;
1013
571
  }
1014
- /** Where a fraction lands on a content box of this size. */
1015
- declare function fracToWorld(f: FracRect, content: {
1016
- w: number;
1017
- h: number;
1018
- }): WorldRect;
1019
- /** What fraction of the content box a world rect covers.
572
+ /** A `WorldSpec` resolved against a viewport: what the coordinate helpers,
573
+ * the camera and the wheel all read. */
574
+ interface WorldFrame {
575
+ /** Screen position of world (0,0) at `pan` zero. */
576
+ originPx: Point;
577
+ yDir: 1 | -1;
578
+ }
579
+ declare const DEFAULT_FRAME: WorldFrame;
580
+ declare function resolveFrame(spec: WorldSpec | undefined, size: ViewportSize): WorldFrame;
581
+ /** Put a 2D context into the instrument's world coordinates, so a layer's
582
+ * `draw` works in them. Must stay the exact inverse of `screenToWorld`. */
583
+ declare function applyCamera(ctx: CanvasRenderingContext2D, view: ViewTransform, frame?: WorldFrame): void;
584
+
585
+ /** A named position in a trial's chrome. Content is not a region — that is
586
+ * the instrument. The lab's own boxes are named in `LabRegion`. */
587
+ type TrialRegion = 'titlebar' | 'toolbar' | 'palette' | 'sidebar' | 'viewport' | 'status';
588
+ /** An icon component taking a pixel size, as `@weasel-js/ui` glyphs do. */
589
+ type IconComponent = ComponentType<{
590
+ size?: number;
591
+ }>;
592
+ /** A button in a toolbar. `TCtx` is the chrome context its `onActivate` is
593
+ * handed — a trial's, or the lab's for a lab-level bar. */
594
+ interface ToolbarItem<TCtx = TrialChromeContext> {
595
+ icon: IconComponent;
596
+ label: string;
597
+ /** Shown in the tooltip. Not bound here — the trial owns its keymap. */
598
+ shortcut?: string;
599
+ disabled?: boolean;
600
+ /** Reddens on hover. For actions that discard work. */
601
+ danger?: boolean;
602
+ /** A toggle rather than a command: renders `aria-pressed` and reads as held
603
+ * down while true. */
604
+ pressed?: boolean;
605
+ /** Render the label beside the glyph rather than only in the tooltip. */
606
+ showLabel?: boolean;
607
+ /** Handed the chrome context it was declared against, so a contribution can
608
+ * reach `ctx.saveSnapshot()` and the rest without the `render` escape. A
609
+ * zero-argument handler stays valid. */
610
+ onActivate: (ctx: TCtx) => void;
611
+ }
612
+ /** A tool in the palette region: a mode by default, a command when it carries
613
+ * an `onActivate`. `TCtx` is the chrome context that handler is handed — a
614
+ * trial's, or the lab's for a lab-level rail. */
615
+ interface ToolItem<TCtx = TrialChromeContext> {
616
+ icon: IconComponent;
617
+ label: string;
618
+ /** Shown in the tooltip. Not bound here — the trial owns its keymap. */
619
+ shortcut?: string;
620
+ disabled?: boolean;
621
+ /** Runs on press instead of selecting. An item with one is a command: it
622
+ * never writes the tool slot, and never reads as the current tool. */
623
+ onActivate?: (ctx: TCtx) => void;
624
+ }
625
+ /** A titled block in the sidebar. `stance` and `tone` work as on a
626
+ * `<PropertyPanel>`: what kind of content the section holds, and which of its
627
+ * peers it is. */
628
+ interface SidebarSection extends StanceProps {
629
+ title: string;
630
+ /** Starts collapsed, until the trial remembers a fold of its own. */
631
+ defaultCollapsed?: boolean;
632
+ /** Offer the tear-out control. On by default; a section that only makes
633
+ * sense beside its trial sets this false. */
634
+ undockable?: boolean;
635
+ /** Where the tear-out control sends it. Default `'tile'`. */
636
+ undockAs?: 'tile' | 'floating';
637
+ body: ReactNode;
638
+ }
639
+ /** A control acting on the view of the trial, not on the trial. */
640
+ interface ViewportControl<TCtx = TrialChromeContext> {
641
+ icon: IconComponent;
642
+ label: string;
643
+ disabled?: boolean;
644
+ /** Handed the trial's chrome context, as `ToolbarItem.onActivate` is. */
645
+ onActivate: (ctx: TCtx) => void;
646
+ }
647
+ /** A readout in the status bar. */
648
+ interface StatusReadout {
649
+ /** Short enough for a status bar. Rendered as text. */
650
+ text: string;
651
+ /** Tooltip. */
652
+ title?: string;
653
+ }
654
+ /** What every contribution shares, whichever chrome it is declared against. */
655
+ interface ContributionBase {
656
+ id: string;
657
+ /** Groups sort by first appearance; items sort within a group by
658
+ * declaration order. Contributions with no group sort after grouped ones. */
659
+ group?: string;
660
+ /** Pushes this contribution, and its group, to the far end of the region. */
661
+ end?: boolean;
662
+ }
663
+ /**
664
+ * A contribution is data the chrome renders, keyed to a region. Supplying
665
+ * `render` instead of `item` opts out of the chrome's layout — deliberate,
666
+ * and visible in the declaration.
667
+ */
668
+ type TrialContribution = (ContributionBase & {
669
+ region: 'titlebar';
670
+ item: ToolbarItem;
671
+ render?: never;
672
+ }) | (ContributionBase & {
673
+ region: 'toolbar';
674
+ item: ToolbarItem;
675
+ render?: never;
676
+ }) | (ContributionBase & {
677
+ region: 'palette';
678
+ item: ToolItem;
679
+ render?: never;
680
+ }) | (ContributionBase & {
681
+ region: 'sidebar';
682
+ item: SidebarSection;
683
+ render?: never;
684
+ }) | (ContributionBase & {
685
+ region: 'viewport';
686
+ item: ViewportControl;
687
+ render?: never;
688
+ }) | (ContributionBase & {
689
+ region: 'status';
690
+ item: StatusReadout;
691
+ render?: never;
692
+ }) | (ContributionBase & {
693
+ region: TrialRegion;
694
+ item?: never;
695
+ render: (ctx: TrialChromeContext) => ReactNode;
696
+ });
697
+ /**
698
+ * A tool slot a region can reflect and write. Both chrome contexts carry one:
699
+ * a trial's resolves to the lab's when its instrument declares no tools.
700
+ */
701
+ interface ToolSlotContext {
702
+ activeToolId: string | null;
703
+ setActiveTool: (id: string) => void;
704
+ }
705
+ /** What a sidebar region reflects and writes: which sections are folded, and —
706
+ * where the chrome can tear a section out — where it goes. A trial supplies
707
+ * all four; a lab supplies the fold state only. */
708
+ interface SidebarSlotContext {
709
+ collapsedSections: Readonly<Record<string, boolean>>;
710
+ setSectionCollapsed: (key: string, collapsed: boolean) => void;
711
+ undockedPanels?: readonly string[];
712
+ undockPanel?: (sectionId: string, as?: 'tile' | 'floating') => void;
713
+ }
714
+ /**
715
+ * A contribution as a region renderer sees it. A renderer checks the region
716
+ * name against its own and narrows `item` itself, which is what lets one
717
+ * renderer serve both chromes — the trial's and the lab's — without knowing
718
+ * which context it was handed.
719
+ */
720
+ interface RegionContribution<TCtx> extends ContributionBase {
721
+ region: string;
722
+ item?: unknown;
723
+ render?: (ctx: TCtx) => ReactNode;
724
+ }
725
+ /**
726
+ * Everything a contribution can read about the trial it is being rendered
727
+ * into. Replaces the three separate slot contexts, which each carried a
728
+ * hand-picked subset.
729
+ */
730
+ interface TrialChromeContext extends ToolSlotContext, SidebarSlotContext {
731
+ trialId: string;
732
+ instrumentName: string;
733
+ /** What the title bar reads, which is the instrument's title (or, lacking
734
+ * one, its name) until something calls `setTitle`. */
735
+ title: string;
736
+ /** Retitle this trial; `null` restores the instrument's title. Persisted with
737
+ * the trial, so a title survives a reload. */
738
+ setTitle: (title: string | null) => void;
739
+ isLastTrial: boolean;
740
+ /** Null when the trial holds a view that is not the 2D one. */
741
+ zoom: number | null;
742
+ setZoom: (z: number) => void;
743
+ canUndo: boolean;
744
+ canRedo: boolean;
745
+ undo: () => void;
746
+ redo: () => void;
747
+ /** Whether the trial's loupe is turned on. False for an instrument that
748
+ * declares none, whose chrome offers no way to turn one on. */
749
+ loupeOn: boolean;
750
+ toggleLoupe: () => void;
751
+ /** The instrument's controls, resolved against the lab's rules. Always
752
+ * populated: a legacy `configSchema()` is adapted into the same shape. */
753
+ configSchema: ResolvedConfig;
754
+ /** @deprecated Read `configSchema`. Empty for an instrument declaring
755
+ * `config`, since a builder schema has no `ConfigField[]` form. */
756
+ configFields: ConfigField[];
757
+ config: unknown;
758
+ /** The trial's unpinned paths. `config` stays raw, so a control drawn from
759
+ * it sits at the value un-pinning writes back. */
760
+ auto: ReadonlySet<string>;
761
+ setConfig: (key: string, value: unknown) => void;
762
+ /** Which of this trial's collapsible sections are folded. A sidebar
763
+ * section's key is its contribution id; a section *inside* a contribution —
764
+ * a control panel's property group — is keyed `<contribution id>/<label>`.
765
+ * A section absent here is at its own default. Persisted with the trial. */
766
+ collapsedSections: Readonly<Record<string, boolean>>;
767
+ /** Fold or unfold one section, keyed as `collapsedSections` is. */
768
+ setSectionCollapsed: (key: string, collapsed: boolean) => void;
769
+ /** Section ids this trial currently has torn out of its sidebar. */
770
+ undockedPanels: readonly string[];
771
+ /** Tear a sidebar section out into the workspace. */
772
+ undockPanel: (sectionId: string, as?: 'tile' | 'floating') => void;
773
+ /** Put a torn-out section back in the sidebar. */
774
+ dockPanel: (sectionId: string) => void;
775
+ savedSnapshots: SavedSnapshot[];
776
+ saveSnapshot: (name?: string) => void;
777
+ loadSnapshot: (snapshotId: string) => void;
778
+ clone: () => void;
779
+ reset: () => void;
780
+ close: () => void;
781
+ }
782
+
783
+ /** A point on the magnified surface, in its own CSS pixels. */
784
+ interface LoupePoint {
785
+ x: number;
786
+ y: number;
787
+ }
788
+
789
+ /** How a loupe magnifies. `'vector'` re-renders the source through a zoomed-in
790
+ * view, so content stays sharp at any factor; `'pixel'` blows up the actual
791
+ * pixels the surface presented. */
792
+ type LoupeMode = 'vector' | 'pixel';
793
+
794
+ /** What a DOM instrument's loupe `render` is handed: the same data its own
795
+ * `render` gets, and the camera to draw it through. */
796
+ interface LoupeRenderArgs<TS = unknown, TC = unknown> {
797
+ state: TS;
798
+ config: TC;
799
+ /** The trial's own camera composed with the magnification, about the aimed
800
+ * point — so drawing the same content through it magnifies in place. */
801
+ view: ViewTransform;
802
+ /** The magnification on its own, for whatever must not scale with the
803
+ * camera. */
804
+ factor: number;
805
+ mode: LoupeMode;
806
+ /** The size of the viewport `view` is written for: the trial's content well,
807
+ * not the lens. The lens shows a circle cut out of it. */
808
+ size: ViewportSize;
809
+ }
810
+ /**
811
+ * Declares that an instrument can be magnified.
812
+ *
813
+ * With no `render`, the loupe re-draws the instrument's canvas layers through a
814
+ * zoomed camera, so it stays sharp at any factor. An instrument whose content is
815
+ * DOM supplies `render` instead: given a camera, draw me again.
816
+ */
817
+ interface LoupeCapability<TS = unknown, TC = unknown> {
818
+ render?: (args: LoupeRenderArgs<TS, TC>) => ReactNode;
819
+ /** Opening magnification, clamped to the bounds below. Default 6. */
820
+ factor?: number;
821
+ /** What the wheel clamps to. Defaults 2 and 32. */
822
+ minFactor?: number;
823
+ maxFactor?: number;
824
+ /** `'vector'` (default) re-renders the content magnified; `'pixel'` blows up
825
+ * the pixels the instrument presented. A `render` loupe is always vector —
826
+ * DOM has no framebuffer to enlarge. */
827
+ mode?: LoupeMode;
828
+ /** Lens diameter in CSS px. Default 200. */
829
+ diameter?: number;
830
+ /** Held for a momentary peek while the loupe is off. Default `'Alt'`; `null`
831
+ * turns hold-to-peek off. Matched against `KeyboardEvent.key`. */
832
+ peekKey?: string | null;
833
+ /** Called with the color under the aim, wherever the surface can say. The
834
+ * canvas painter reads it back; a DOM loupe has no pixels to sample. */
835
+ onColorChange?: (hex: string) => void;
836
+ }
837
+ /** An instrument's loupe declaration. `true` takes every default. */
838
+ type LoupeDeclaration<TS = unknown, TC = unknown> = true | LoupeCapability<TS, TC>;
839
+ /** A {@link LoupeCapability} with every default filled in. */
840
+ interface ResolvedLoupe<TS = unknown, TC = unknown> {
841
+ render?: (args: LoupeRenderArgs<TS, TC>) => ReactNode;
842
+ onColorChange?: (hex: string) => void;
843
+ factor: number;
844
+ minFactor: number;
845
+ maxFactor: number;
846
+ mode: LoupeMode;
847
+ diameter: number;
848
+ peekKey: string | null;
849
+ }
850
+ declare const LOUPE_DEFAULTS: {
851
+ readonly factor: 6;
852
+ readonly minFactor: 2;
853
+ readonly maxFactor: 32;
854
+ readonly mode: "vector";
855
+ readonly diameter: 200;
856
+ readonly peekKey: "Alt";
857
+ };
858
+ declare function resolveLoupe<TS, TC>(declared: LoupeDeclaration<TS, TC>): ResolvedLoupe<TS, TC>;
859
+
860
+ /**
861
+ * A tool a trial can be in. labkit's own — core's `ToolsApi` carries hotkey
862
+ * slots, ambient tools, eligibility tiers and canvas overlay layers, all bound
863
+ * to the gesture dispatcher, and a labkit instrument is an arbitrary canvas or
864
+ * DOM tree rather than a weasel scene.
865
+ */
866
+ interface TrialTool {
867
+ id: string;
868
+ label: string;
869
+ icon: IconComponent;
870
+ /** Shown in the tooltip. Not bound here — the instrument owns its keymap. */
871
+ shortcut?: string;
872
+ /** Presentation grouping in the palette. Ungrouped tools sort after grouped. */
873
+ group?: string;
874
+ }
875
+ /** What an instrument declares to get a palette region. */
876
+ interface ToolCapability {
877
+ tools: TrialTool[];
878
+ /** Which tool a fresh trial starts in. Defaults to the first. */
879
+ initial?: string;
880
+ }
881
+
882
+ /** What an instrument's `render` is handed: its state and config, the setters
883
+ * for both, the trial it is mounted in, and a way to emit named events. */
884
+ interface RenderContext<TS = unknown, TC = unknown> {
885
+ state: TS;
886
+ config: TC;
887
+ setState: (next: TS | ((prev: TS) => TS)) => void;
888
+ /** Write one config value, by dotted path — `'grid.size'` for a leaf under
889
+ * an `f.group`, `'cellSize'` for one at the root. */
890
+ setConfig: (path: ConfigPath<TC>, value: unknown) => void;
891
+ /** labkit persists `view` and restores it on Reset without ever reading
892
+ * into it. */
893
+ trial: TrialInfo & {
894
+ setView: (next: unknown) => void;
895
+ /** 2D convenience over `view`. Reads 1 and writes nothing when the trial holds
896
+ * a view that is not the 2D one. */
897
+ zoom: number;
898
+ setZoom: (z: number) => void;
899
+ /** Resolved active tool: this trial's slot, or the lab's. Null when neither
900
+ * holds one. */
901
+ activeToolId: string | null;
902
+ /** The canvas layers currently shown, in declaration order. labkit skips a
903
+ * hidden layer's `draw`, so this is the only way to know what was
904
+ * painted — a legend listing rows the view did not draw needs it. */
905
+ visibleLayers: readonly string[];
906
+ /**
907
+ * Where the pointer is over this trial, in the instrument's world:
908
+ * `get()` answers `{ worldX, worldY, viewId }`, where `viewId` is
909
+ * `'stage'` over the trial's own camera and `'overview'` over its
910
+ * `<TrialOverview>`, or `null` when it is over neither. A key handler
911
+ * reading it works over either view.
912
+ */
913
+ pointer: PointerContextValue;
914
+ };
915
+ emit: (event: string) => void;
916
+ /** Present only when the instrument declares a `job`. */
917
+ job?: JobHandle;
918
+ }
919
+ /** One 2D canvas layer of an instrument, drawn in declaration order.
1020
920
  *
1021
- * A pane measured before layout is 0×0, and dividing by that would seed every
1022
- * stored position with `NaN` — which compares false against everything and so
1023
- * fails silently rather than loudly. Zero sides give zeros. */
1024
- declare function worldToFrac(r: WorldRect, content: {
1025
- w: number;
1026
- h: number;
1027
- }): FracRect;
1028
- /** Snap to 4dp. Positions are diffed by humans in stored documents, so the
1029
- * last digits of a float are noise that hides the change that matters. */
1030
- declare function roundFrac(f: FracRect): FracRect;
1031
- /** Whether `pt` falls in `box`, widened by `tol` on every side so a hairline
1032
- * mark stays reachable. */
1033
- declare function fracContains(box: FracRect, pt: FracPoint, tol?: number): boolean;
1034
- /** Whether `outer` wholly encloses `inner`. A marquee takes what it encloses,
1035
- * not what it grazes — brushing selection is a different gesture, and this
1036
- * answers false for two rects that merely overlap. */
1037
- declare function fracEncloses(outer: FracRect, inner: FracRect): boolean;
921
+ * `draw` is called with the camera already applied, so it works in world
922
+ * coordinates. `zoom` is passed for the things that must not scale with it —
923
+ * set `ctx.lineWidth = 1 / zoom` to keep a hairline hairline. */
924
+ interface CanvasLayer<TS = unknown, TC = unknown> {
925
+ id: string;
926
+ draw: (ctx: CanvasRenderingContext2D, args: {
927
+ state: TS;
928
+ config: TC;
929
+ zoom: number;
930
+ }) => void;
931
+ }
932
+ /** Declares that an instrument draws to a canvas: its layers, and where the
933
+ * view starts. */
934
+ interface CanvasCapability<TS = unknown, TC = unknown> {
935
+ layers: CanvasLayer<TS, TC>[];
936
+ /** The coordinate system this instrument's world is in. Omitted, world (0,0)
937
+ * sits at the canvas top-left with y running down — what labkit assumed
938
+ * before an instrument could say otherwise. */
939
+ worldSpec?: WorldSpec;
940
+ /** Where the view starts. A function is called once the canvas knows its
941
+ * size, so an instrument can frame content it can only place in terms of
942
+ * the viewport; until then the trial's view is `null`. */
943
+ initialView?: ViewTransform | ((size: ViewportSize) => ViewTransform);
944
+ /** Widens the camera's default zoom clamp; the opening zoom stays reachable
945
+ * regardless of these. */
946
+ minZoom?: number;
947
+ maxZoom?: number;
948
+ }
949
+ /** Declares that an instrument's DOM is content of a fixed size, which the
950
+ * trial pans and zooms the way it does a canvas's layers. Not combined with
951
+ * `canvas`: there the DOM is an unzoomed overlay, and `canvas` wins. */
952
+ interface StageCapability<TS = unknown, TC = unknown> {
953
+ /** The content's own size in CSS pixels, at zoom 1. */
954
+ size: ViewportSize;
955
+ /** Where the view starts. Omitted, the content opens centered and shrunk to
956
+ * fit (`fitStage`). A function is called once the stage knows its size. */
957
+ initialView?: ViewTransform | ((viewport: ViewportSize) => ViewTransform);
958
+ minZoom?: number;
959
+ maxZoom?: number;
960
+ /** Drawn over the content in viewport pixels, outside the camera — a legend,
961
+ * a floating panel, anything that must not zoom with the picture. */
962
+ overlay?: (ctx: RenderContext<TS, TC>) => ReactNode;
963
+ }
964
+ /** Declares which of an instrument's layers the trial should offer
965
+ * show/hide controls for. */
966
+ interface LayerCapability {
967
+ /** In list order. A bare string is a layer id that doubles as its own label;
968
+ * give a descriptor instead to label a layer or mark it `alwaysOn`. */
969
+ ids: readonly (string | LayerDescriptor)[];
970
+ }
971
+ /** Declares that an instrument accepts items dragged from a palette: what the
972
+ * palette offers, what a drop does to the state, and — optionally — live
973
+ * feedback during the drag and the ability to drag existing items back out. */
974
+ interface DragDropCapability<TS = unknown, TC = unknown> {
975
+ palette: PaletteItem[] | ((state: TS, config: TC) => PaletteItem[]);
976
+ onDrop: (worldPos: Point, item: PaletteItem, state: TS, config: TC) => TS;
977
+ onDragOver?: (worldPos: Point, item: PaletteItem, state: TS, config: TC) => DragFeedback | null;
978
+ pickUp?: (hit: HitResult, state: TS, config: TC) => {
979
+ item: PaletteItem;
980
+ state: TS;
981
+ } | null;
982
+ }
983
+ /** Declares that an instrument's state is undoable: which emitted events
984
+ * snapshot it, and how many snapshots to keep. */
985
+ interface UndoCapability {
986
+ snapshotOn?: string[];
987
+ maxDepth?: number;
988
+ }
989
+ /** The name of an event an instrument emits through `RenderContext.emit`. */
990
+ type SystemEvent = string;
991
+ /** A point in world coordinates. */
992
+ type Point = {
993
+ x: number;
994
+ y: number;
995
+ };
996
+ /** What a hit-test found, and where. */
997
+ type HitResult = {
998
+ hit: boolean;
999
+ layerId?: string;
1000
+ pointId?: string;
1001
+ };
1002
+ /** A trial's camera. `zoom` is always positive and finite once labkit holds
1003
+ * it — see `normalize2DView`. */
1004
+ type ViewTransform = {
1005
+ zoom: number;
1006
+ pan: Point;
1007
+ };
1008
+ /** A layer as the layer list shows it. `alwaysOn` layers cannot be hidden. */
1009
+ type LayerDescriptor = {
1010
+ id: string;
1011
+ label: string;
1012
+ alwaysOn?: boolean;
1013
+ };
1014
+ /** One draggable entry in an instrument's palette. */
1015
+ type PaletteItem = {
1016
+ id: string;
1017
+ label: string;
1018
+ data?: unknown;
1019
+ };
1020
+ /** Whether a drop would be accepted at the current position, and why not if
1021
+ * it would not. */
1022
+ type DragFeedback = {
1023
+ ok: boolean;
1024
+ reason?: string;
1025
+ };
1026
+ /**
1027
+ * An instrument: one self-contained interactive experiment a lab can host.
1028
+ *
1029
+ * It owns two pieces of data — `config`, the settings the control panel edits,
1030
+ * and `state`, what the experiment is currently doing — and renders from both.
1031
+ * The optional capability fields declare what else it wants from the runtime:
1032
+ * a canvas, a layer list, palette drag-and-drop, undo. Declaring a capability
1033
+ * is what makes the trial provide the corresponding chrome.
1034
+ */
1035
+ interface Instrument<TS = unknown, TC = unknown, TItem = unknown> {
1036
+ /** The id a trial record names its instrument by. */
1037
+ name: string;
1038
+ /** What a trial of this instrument and the add-trial menu read. Default: `name`. */
1039
+ title?: string;
1040
+ defaultConfig: () => TC;
1041
+ initialState: (config: TC) => TS;
1042
+ /** The instrument's config, declared once: values, types and controls.
1043
+ * Supplying this makes `defaultConfig` optional — `defineInstrument`
1044
+ * synthesizes it. Prefer it over `defaultConfig` + `configSchema`. */
1045
+ config?: ConfigSchema<TC>;
1046
+ /** @deprecated Declare `config` instead; this repeats what `TC` already
1047
+ * says and nothing holds the two to one answer. */
1048
+ configSchema?: () => ConfigField[];
1049
+ /** The instrument's DOM. With `canvas`, this renders as an overlay above the
1050
+ * layers rather than instead of them; return `null` for canvas only. */
1051
+ render: (ctx: RenderContext<TS, TC>) => ReactNode;
1052
+ onConfigChange?: (config: TC, prev: TC, state: TS) => TS;
1053
+ /** Moves a stored config's values to where the current schema keeps them —
1054
+ * `gridSize` to `grid.size` — before the defaults fill its gaps, so it
1055
+ * returns only what it moved and leaves the rest to them. It runs on every
1056
+ * read of a stored config, including one already moved, so it must hand
1057
+ * back a current config unchanged. */
1058
+ migrateConfig?: (stored: unknown) => unknown;
1059
+ serialize?: (state: TS) => unknown;
1060
+ deserialize?: (data: unknown, config: TC) => TS;
1061
+ canvas?: CanvasCapability<TS, TC>;
1062
+ stage?: StageCapability<TS, TC>;
1063
+ layers?: LayerCapability;
1064
+ dragDrop?: DragDropCapability<TS, TC>;
1065
+ undo?: UndoCapability;
1066
+ /** Tools this instrument offers. Declaring them gives the trial a palette
1067
+ * region and its own tool slot. */
1068
+ tools?: ToolCapability;
1069
+ /** Regions of this instrument that accept marks, and optionally what a mark
1070
+ * is allowed to mean. Declaring it is what makes the trial provide the
1071
+ * annotation overlay and its chrome. */
1072
+ annotations?: AnnotationsCapability<TS, TC>;
1073
+ /** Magnification. `true` takes every default and re-draws the instrument's
1074
+ * canvas layers through a zoomed camera; an instrument whose content is DOM
1075
+ * gives a `render` that draws it again at a camera it is handed. A function
1076
+ * is re-read as the config changes, so a setting can drive the lens. */
1077
+ loupe?: LoupeDeclaration<TS, TC> | ((config: TC) => LoupeDeclaration<TS, TC>);
1078
+ /** Chrome this instrument contributes beyond what its capabilities imply. */
1079
+ chrome?: TrialContribution[];
1080
+ /** Work too slow to do during a render. The runtime starts it, aborts it on
1081
+ * unmount and on a `key` change, and renders progress into the trial. */
1082
+ job?: JobCapability<TS, TC, TItem>;
1083
+ }
1084
+ /** Instruments as a lab receives them. `any` rather than `unknown` because
1085
+ * parameter contravariance keeps a `defineInstrument<TS, TC>` result out of
1086
+ * an `Instrument<unknown, unknown>[]`; it is contained to this alias. */
1087
+ type InstrumentList = readonly Instrument<any, any, any>[];
1038
1088
 
1039
- export { DEFAULT_FRAME as D, fracContains as aA, fracEncloses as aB, fracToWorld as aC, isAuto as aD, resolveLoupe as aE, roundFrac as aF, worldToFrac as aG, LOUPE_DEFAULTS as ab, annotationsFromJSON as aw, auto as ax, createAnnotationScene as ay, createAnnotationStore as az, applyCamera as c, dockPanel as l, panelKey as p, resolveFrame as r, undockPanel as u };
1040
- export type { AnnotationStatus as $, AnnotationData as A, CaptureOptions as B, CreateLabStoreOptions as C, CaptureResult as E, AnnotationTarget as F, AnnotationsApi as G, AnnotationsCapability as H, InstrumentSerializers as I, TrialInfo as J, TrialTool as K, LayerCapability as L, Migration as M, AnnotationKind as N, Instrument as O, PaletteItem as P, LabDensity as Q, TrialContribution as R, SerializedTrial as S, TrialRecord as T, UndoStack as U, ViewportSize as V, WorldFrame as W, Annotation as X, AnnotationInit as Y, AnnotationPatch as Z, AnnotationQuery as _, LayerDescriptor as a, AnnotationStoreOptions as a0, AnnotationTargetInfo as a1, Auto as a2, CanvasCapability as a3, CanvasLayer as a4, CaptureDeps as a5, ContributionBase as a6, FracPoint as a7, FracRect as a8, HitResult as a9, LoupePoint as aH, LoupeMode as aI, IconComponent as aa, LoupeCapability as ac, LoupeDeclaration as ad, LoupeRenderArgs as ae, RegionContribution as af, RenderContext as ag, ResolvedLoupe as ah, SerializedAnnotations as ai, SidebarSection as aj, SidebarSlotContext as ak, StageCapability as al, StatusReadout as am, SystemEvent as an, ToolCapability as ao, ToolItem as ap, ToolSlotContext as aq, ToolbarItem as ar, TrialChromeContext as as, TrialRegion as at, UndoCapability as au, ViewportControl as av, WorldSpec as b, StorageAdapter as d, LabDocument as e, LabStoreState as f, SavedSnapshot as g, StorageChange as h, TrialStateHandle as i, UndockedPanel as j, UndockedPanels as k, ViewTransform as m, Point as n, DragFeedback as o, DragDropCapability as q, LabMode as s, InstrumentList as t, InstrumentHooks as v, WorldRect as w, AnnotationMeaning as x, MarkScene as y, CaptureSource as z };
1089
+ export { DEFAULT_FRAME as D, annotationsFromJSON as aB, auto as aC, createAnnotationScene as aD, createAnnotationStore as aE, fracContains as aF, fracEncloses as aG, fracToWorld as aH, isAuto as aI, markCommands as aJ, resolveLoupe as aK, roundFrac as aL, worldToFrac as aM, LOUPE_DEFAULTS as ae, dockPanel as k, applyCamera as m, panelKey as p, resolveFrame as r, undockPanel as u };
1090
+ export type { AnnotationPatch as $, AnnotationMeaning as A, CaptureSource as B, CreateLabStoreOptions as C, CaptureOptions as E, CaptureResult as F, PaintableMark as G, AnnotationTarget as H, InstrumentSerializers as I, AnnotationsApi as J, AnnotationsCapability as K, LayerCapability as L, Migration as M, TrialInfo as N, TrialTool as O, Point as P, AnnotationKind as Q, Instrument as R, SerializedTrial as S, TrialRecord as T, UndoStack as U, ViewportSize as V, WorldFrame as W, LabDensity as X, TrialContribution as Y, Annotation as Z, AnnotationInit as _, LayerDescriptor as a, AnnotationQuery as a0, AnnotationStatus as a1, AnnotationStoreOptions as a2, AnnotationTargetInfo as a3, AnnotationToolId as a4, Auto as a5, CanvasCapability as a6, CanvasLayer as a7, CaptureDeps as a8, ContributionBase as a9, ViewportControl as aA, FracPoint as aa, FracRect as ab, HitResult as ac, IconComponent as ad, LoupeCapability as af, LoupeDeclaration as ag, LoupeMode as ah, LoupePoint as ai, LoupeRenderArgs as aj, RegionContribution as ak, RenderContext as al, ResolvedLoupe as am, SerializedAnnotations as an, SidebarSection as ao, SidebarSlotContext as ap, StageCapability as aq, StatusReadout as ar, SystemEvent as as, ToolCapability as at, ToolItem as au, ToolSlotContext as av, ToolbarItem as aw, TrialChromeContext as ax, TrialRegion as ay, UndoCapability as az, ViewTransform as b, StorageAdapter as c, LabDocument as d, LabStoreState as e, SavedSnapshot as f, StorageChange as g, TrialStateHandle as h, UndockedPanel as i, UndockedPanels as j, WorldSpec as l, PaletteItem as n, DragFeedback as o, DragDropCapability as q, LabMode as s, InstrumentList as t, InstrumentHooks as v, AnnotationData as w, WorldRect as x, MarkStyle as y, MarkScene as z };