@weasel-js/labkit 1.4.1 → 1.4.3

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 (145) hide show
  1. package/README.md +28 -2
  2. package/dist/_dts/{CanvasStackContext-BIRUVVGH.d.ts → CanvasStackContext-Dde8_1my.d.ts} +1 -1
  3. package/dist/_dts/{PrefsForm.d-CgxUequc.d.ts → PrefsForm.d-DDHFANkc.d.ts} +1 -1
  4. package/dist/_dts/{frac-X7mWgd7y.d.ts → frac-B5mB8LsJ.d.ts} +33 -13
  5. package/dist/_dts/{index-FEa1MudK.d.ts → index-DelFfeV0.d.ts} +99 -31
  6. package/dist/_dts/types-B9_zrHmb.d.ts +166 -0
  7. package/dist/_dts/{types-ChVJJHvk.d.ts → types-BP2OCcpg.d.ts} +41 -4
  8. package/dist/_dts/types-x92Kfeme.d.ts +62 -0
  9. package/dist/_dts/{useTrialState-Cdm13fvl.d.ts → useTrialState-D6Vb3T-g.d.ts} +11 -7
  10. package/dist/canvas/index.d.ts +9 -8
  11. package/dist/canvas/index.js +1 -2
  12. package/dist/chrome/index.d.ts +16 -9
  13. package/dist/chrome/index.js +5 -5
  14. package/dist/{chunk-MSR7EVS6.js → chunk-2RQPGOQF.js} +6 -5
  15. package/dist/chunk-2RQPGOQF.js.map +1 -0
  16. package/dist/{chunk-LJSIUFYD.js → chunk-5TROQVQ2.js} +46 -21
  17. package/dist/chunk-5TROQVQ2.js.map +1 -0
  18. package/dist/{chunk-53AZ2O2N.js → chunk-ASWKLRKJ.js} +92 -45
  19. package/dist/chunk-ASWKLRKJ.js.map +1 -0
  20. package/dist/{chunk-BMQWLKOL.js → chunk-C42T6DJT.js} +94 -83
  21. package/dist/chunk-C42T6DJT.js.map +1 -0
  22. package/dist/chunk-E44SU6XS.js +50 -0
  23. package/dist/chunk-E44SU6XS.js.map +1 -0
  24. package/dist/{chunk-OFCBEZMU.js → chunk-FQMJUVHJ.js} +3 -3
  25. package/dist/{chunk-OFCBEZMU.js.map → chunk-FQMJUVHJ.js.map} +1 -1
  26. package/dist/{chunk-PP7ZWAMV.js → chunk-I6JCVE24.js} +3 -3
  27. package/dist/chunk-I6JCVE24.js.map +1 -0
  28. package/dist/chunk-ISCOFAUO.js +170 -0
  29. package/dist/chunk-ISCOFAUO.js.map +1 -0
  30. package/dist/chunk-MHAGC6VD.js +7320 -0
  31. package/dist/chunk-MHAGC6VD.js.map +1 -0
  32. package/dist/{chunk-7TRFMIWX.js → chunk-QAGAHJKZ.js} +4 -4
  33. package/dist/{chunk-7TRFMIWX.js.map → chunk-QAGAHJKZ.js.map} +1 -1
  34. package/dist/{chunk-D7QIPGEB.js → chunk-RS3HRQWU.js} +62 -60
  35. package/dist/chunk-RS3HRQWU.js.map +1 -0
  36. package/dist/{chunk-RLOXQZW3.js → chunk-TJ7QY3OC.js} +3 -3
  37. package/dist/{chunk-RLOXQZW3.js.map → chunk-TJ7QY3OC.js.map} +1 -1
  38. package/dist/controls/index.d.ts +5 -4
  39. package/dist/controls/index.js +3 -3
  40. package/dist/dragdrop/index.d.ts +9 -8
  41. package/dist/dragdrop/index.js +1 -2
  42. package/dist/index.d.ts +152 -57
  43. package/dist/index.js +315 -200
  44. package/dist/index.js.map +1 -1
  45. package/dist/layers/index.d.ts +27 -11
  46. package/dist/layers/index.js +2 -3
  47. package/dist/loupe/index.d.ts +8 -7
  48. package/dist/loupe/index.js +1 -2
  49. package/dist/passthrough/weasel-canvas.d.ts +1 -3
  50. package/dist/passthrough/weasel-canvas.js +1 -1
  51. package/dist/passthrough/weasel-canvas.js.map +1 -1
  52. package/dist/passthrough/weasel-ui.d.ts +171 -22
  53. package/dist/passthrough/weasel-ui.js +1 -2
  54. package/dist/primitives/index.js +3 -4
  55. package/dist/state/index.d.ts +6 -3
  56. package/dist/state/index.js +3 -2
  57. package/dist/state/index.js.map +1 -1
  58. package/dist/styles.css +21 -4
  59. package/dist/surface/index.js +1 -2
  60. package/dist/ui/layers/index.d.ts +22 -9
  61. package/dist/ui/layers/index.js +1 -2
  62. package/dist/undo/index.d.ts +6 -6
  63. package/package.json +7 -7
  64. package/src/annotations/AnnotationTargets.tsx +6 -1
  65. package/src/annotations/Annotations.overlay.test.tsx +2 -0
  66. package/src/annotations/ExportMenu.test.tsx +16 -0
  67. package/src/annotations/ExportMenu.tsx +0 -5
  68. package/src/annotations/types.ts +4 -1
  69. package/src/canvas/useOrbit.test.ts +7 -3
  70. package/src/canvas/usePanZoom.test.ts +7 -3
  71. package/src/chrome/ChromeRegions.stories.tsx +42 -1
  72. package/src/chrome/builtins.test.ts +4 -0
  73. package/src/chrome/builtins.tsx +18 -0
  74. package/src/chrome/index.ts +1 -1
  75. package/src/chrome/regions/SidebarRegion.tsx +7 -6
  76. package/src/chrome/regions/TitleBarRegion.test.tsx +64 -0
  77. package/src/chrome/regions/TitleBarRegion.tsx +16 -7
  78. package/src/chrome/regions/ToolbarRegion.test.tsx +14 -0
  79. package/src/chrome/regions/ToolbarRegion.tsx +2 -2
  80. package/src/chrome/regions/ViewportRegion.tsx +2 -2
  81. package/src/chrome/regions/regions.test.tsx +45 -1
  82. package/src/chrome/types.ts +21 -3
  83. package/src/config/builder.test.ts +56 -2
  84. package/src/config/builder.ts +73 -13
  85. package/src/config/index.ts +16 -1
  86. package/src/config/path.test.ts +109 -0
  87. package/src/config/path.ts +76 -0
  88. package/src/config/resolve.test.ts +93 -2
  89. package/src/config/resolve.ts +72 -29
  90. package/src/config/types.ts +99 -9
  91. package/src/config/visible.ts +6 -4
  92. package/src/controls/ControlPanel.stories.tsx +38 -4
  93. package/src/controls/ControlPanel.test.tsx +152 -0
  94. package/src/controls/ControlPanel.tsx +134 -40
  95. package/src/dragdrop/DragDropRuntime.tsx +74 -61
  96. package/src/dragdrop/Palette.tsx +4 -3
  97. package/src/dragdrop/dragDrop.test.tsx +35 -12
  98. package/src/index.ts +18 -2
  99. package/src/instrument/serializers.ts +36 -0
  100. package/src/instrument/types.ts +8 -7
  101. package/src/lab/Lab.test.tsx +36 -1
  102. package/src/lab/Lab.tsx +12 -3
  103. package/src/lab/LabContext.ts +5 -1
  104. package/src/lab/LabShell.less +17 -2
  105. package/src/lab/LabShell.test.tsx +8 -0
  106. package/src/lab/LabShell.tsx +3 -1
  107. package/src/layers/AGENTS.md +29 -7
  108. package/src/layers/LayerList.less +16 -0
  109. package/src/layers/LayerList.stories.tsx +66 -0
  110. package/src/layers/LayerList.test.tsx +117 -5
  111. package/src/layers/LayerList.tsx +192 -53
  112. package/src/layers/index.ts +1 -1
  113. package/src/passthrough/weasel-ui.ts +5 -0
  114. package/src/primitives/FloatingPanel.test.tsx +8 -3
  115. package/src/primitives/ZoomControl.less +6 -2
  116. package/src/primitives/ZoomControl.tsx +1 -0
  117. package/src/state/helpers.ts +1 -1
  118. package/src/state/store.test.ts +233 -1
  119. package/src/state/store.ts +57 -24
  120. package/src/state/types.ts +44 -3
  121. package/src/state/useTrialState.ts +1 -1
  122. package/src/trial/Trial.config.test.tsx +44 -0
  123. package/src/trial/Trial.less +4 -0
  124. package/src/trial/Trial.targets.test.tsx +96 -0
  125. package/src/trial/Trial.test.tsx +171 -14
  126. package/src/trial/Trial.tsx +7 -4
  127. package/src/trial/TrialChrome.tsx +32 -7
  128. package/src/trial/TrialTitleBar.tsx +7 -3
  129. package/src/trial/index.ts +1 -0
  130. package/src/trial/trialOps.test.ts +49 -1
  131. package/src/trial/trialOps.ts +24 -6
  132. package/dist/_dts/types-lg4TSCb2.d.ts +0 -164
  133. package/dist/_dts/weasel-canvas-FJTFi3ZZ.d.ts +0 -5041
  134. package/dist/chunk-53AZ2O2N.js.map +0 -1
  135. package/dist/chunk-BMQWLKOL.js.map +0 -1
  136. package/dist/chunk-D7QIPGEB.js.map +0 -1
  137. package/dist/chunk-DG2IFMLA.js +0 -29970
  138. package/dist/chunk-DG2IFMLA.js.map +0 -1
  139. package/dist/chunk-J6SBFZYT.js +0 -7121
  140. package/dist/chunk-J6SBFZYT.js.map +0 -1
  141. package/dist/chunk-LJSIUFYD.js.map +0 -1
  142. package/dist/chunk-MIT3ISW4.js +0 -100
  143. package/dist/chunk-MIT3ISW4.js.map +0 -1
  144. package/dist/chunk-MSR7EVS6.js.map +0 -1
  145. package/dist/chunk-PP7ZWAMV.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,35 +1,38 @@
1
1
  import { createEventBus, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
2
2
  export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
3
- import { useDragDrop, DragOverlay, Palette } from './chunk-D7QIPGEB.js';
4
- import { useSurfaceOptional, useSurfaceCanvas, useTileId, useTiledSurface, SurfaceContext, SurfaceCanvasContext } from './chunk-OFCBEZMU.js';
5
- export { SurfaceCanvasContext, SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTileId, useTiledSurface } from './chunk-OFCBEZMU.js';
3
+ import { useDragDrop, DragOverlay, Palette } from './chunk-RS3HRQWU.js';
4
+ import { useSurfaceOptional, useSurfaceCanvas, useTileId, useTiledSurface, SurfaceContext, SurfaceCanvasContext } from './chunk-FQMJUVHJ.js';
5
+ export { SurfaceCanvasContext, SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTileId, useTiledSurface } from './chunk-FQMJUVHJ.js';
6
6
  import { useJob } from './chunk-HXXTULDN.js';
7
7
  export { useJob } from './chunk-HXXTULDN.js';
8
- import { ANNOTATION_WEASEL_TOOLS, annotationToolInfo, suppressContributions, mergeContributions, builtinContributions, TitleBarRegion, ToolbarRegion, PaletteRegion, ViewportRegion, SidebarRegion, StatusRegion, AnnotationsContext } from './chunk-BMQWLKOL.js';
9
- export { ANNOTATION_TOOLS, ANNOTATION_WEASEL_TOOLS, AnnotationsContext, ExportMenu, MarkList, PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, annotationToolInfo, builtinContributions, mergeContributions, suppressContributions, useAnnotations, useAnnotationsOptional } from './chunk-BMQWLKOL.js';
10
- import { JobProgress } from './chunk-7TRFMIWX.js';
11
- export { FloatingPanel, JobProgress, Legend, Sidebar } from './chunk-7TRFMIWX.js';
12
- export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-MSR7EVS6.js';
13
- import { createLabStore, panelKey, noneAdapter } from './chunk-LJSIUFYD.js';
14
- export { CURRENT_DOCUMENT_VERSION, createMemoryAdapter, dockPanel, labDocumentKey, localStorageAdapter, noneAdapter, panelKey, quarantineKey, sessionStorageAdapter, undockPanel, urlHashAdapter, useTrialState } from './chunk-LJSIUFYD.js';
8
+ import { ANNOTATION_WEASEL_TOOLS, annotationToolInfo, suppressContributions, mergeContributions, builtinContributions, TitleBarRegion, ToolbarRegion, PaletteRegion, ViewportRegion, SidebarRegion, StatusRegion, AnnotationsContext } from './chunk-C42T6DJT.js';
9
+ export { ANNOTATION_TOOLS, ANNOTATION_WEASEL_TOOLS, AnnotationsContext, ExportMenu, MarkList, PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, annotationToolInfo, builtinContributions, mergeContributions, suppressContributions, useAnnotations, useAnnotationsOptional } from './chunk-C42T6DJT.js';
10
+ import { JobProgress } from './chunk-QAGAHJKZ.js';
11
+ export { FloatingPanel, JobProgress, Legend, Sidebar } from './chunk-QAGAHJKZ.js';
12
+ export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-2RQPGOQF.js';
13
+ import { createLabStore, panelKey, noneAdapter } from './chunk-5TROQVQ2.js';
14
+ export { CURRENT_DOCUMENT_VERSION, createMemoryAdapter, dockPanel, labDocumentKey, localStorageAdapter, noneAdapter, panelKey, quarantineKey, sessionStorageAdapter, undockPanel, urlHashAdapter, useTrialState } from './chunk-5TROQVQ2.js';
15
15
  import { LabStoreContext, TrialIdProvider } from './chunk-2YILQ7Y5.js';
16
16
  export { LabStoreContext, LabStoreProvider, TrialIdContext, TrialIdProvider, useLabStore, useTrialId } from './chunk-2YILQ7Y5.js';
17
- import { fromConfigFields } from './chunk-53AZ2O2N.js';
18
- export { ControlPanel, fromConfigFields, isLeafVisible } from './chunk-53AZ2O2N.js';
19
- import { CanvasStack } from './chunk-PP7ZWAMV.js';
20
- export { CanvasStack, PITCH_LIMIT, clampPitch, orbitAfterDrag, orbitAfterWheel, useOrbit, usePanZoom, wrapYaw } from './chunk-PP7ZWAMV.js';
21
- import { LayerList } from './chunk-MIT3ISW4.js';
22
- export { LayerList } from './chunk-MIT3ISW4.js';
17
+ import { fromConfigFields } from './chunk-ASWKLRKJ.js';
18
+ export { ControlPanel, fromConfigFields, isLeafVisible } from './chunk-ASWKLRKJ.js';
19
+ import { hasConfigPath, schemaNodeAtPath, withValueAtPath, fillConfigDefaults } from './chunk-E44SU6XS.js';
20
+ export { fillConfigDefaults, hasConfigPath, schemaNodeAtPath, valueAtPath, withValueAtPath } from './chunk-E44SU6XS.js';
21
+ import { CanvasStack } from './chunk-I6JCVE24.js';
22
+ export { CanvasStack, PITCH_LIMIT, clampPitch, orbitAfterDrag, orbitAfterWheel, useOrbit, usePanZoom, wrapYaw } from './chunk-I6JCVE24.js';
23
+ import { LayerList } from './chunk-ISCOFAUO.js';
24
+ export { LayerList } from './chunk-ISCOFAUO.js';
23
25
  import './chunk-SBXNQW4G.js';
24
- import { E, w, T, C, p, r3 } from './chunk-J6SBFZYT.js';
25
- export { A2 as CheckboxRow, k as ColorRow, h2 as CurveField, v3 as EffectCard, b3 as EffectCardList, c3 as LayerStack, M2 as NumberRow, x3 as PropertyGroup, C2 as PropertyList, S2 as PropertyPanel, T2 as PropertyRow, w2 as PropertySpan, N2 as SelectRow, D as SliderRow, _2 as Subpanel, j2 as TextRow, P2 as ToggleRow } from './chunk-J6SBFZYT.js';
26
- import { resolveLoupe, TrialLoupe } from './chunk-RLOXQZW3.js';
27
- export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LoupeBubble, TrialLoupe, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe } from './chunk-RLOXQZW3.js';
26
+ import { E, w, T, C, g, r6 } from './chunk-MHAGC6VD.js';
27
+ export { M2 as CheckboxRow, j2 as ColorRow, y3 as CurveField, R as EffectCard, B as EffectCardList, c3 as LayerStack, P2 as NumberRow, V as PropertyGroup, T2 as PropertyList, w2 as PropertyPanel, D as PropertyRow, E2 as PropertySpan, F as SelectRow, k as SliderRow, L2 as Subpanel, N2 as TextRow, I as ToggleRow } from './chunk-MHAGC6VD.js';
28
+ import { resolveLoupe, TrialLoupe } from './chunk-TJ7QY3OC.js';
29
+ export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LoupeBubble, TrialLoupe, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe } from './chunk-TJ7QY3OC.js';
28
30
  export { centerOn, zoomAt } from './chunk-CTRKTLYZ.js';
29
31
  import { applyCamera } from './chunk-2AYGEN57.js';
30
32
  export { DEFAULT_FRAME, applyCamera, resolveFrame, screenToWorld, worldToScreen } from './chunk-2AYGEN57.js';
31
33
  export { CanvasStackContext } from './chunk-CQLQPQ4P.js';
32
- import { textCommand, linePath, markerDrawCommands, ellipsePath, rectPath, createScene, asNodeId as asNodeId$1, WeaselProvider, SceneCanvas, sceneFromJSON, PathBuilder, renderSceneToPixels, useActiveToolContext, resolveStrokeWidth, getMarker, getPaintKind, boundsOfPath, tileGeometry, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, SCRIPT_METRICS } from './chunk-DG2IFMLA.js';
34
+ import { textCommand, linePath, markerDrawCommands, ellipsePath, rectPath, createScene, asNodeId as asNodeId$1, WeaselProvider, SceneCanvas, sceneFromJSON, PathBuilder, renderSceneToPixels, useActiveToolContext, resolveStrokeWidth, getMarker, getPaintKind, boundsOfPath, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, SCRIPT_METRICS } from '@weasel-js/core';
35
+ import { tileGeometry } from '@weasel-js/core/patterns-builtin';
33
36
  import { createContext, useState, useRef, useEffect, useContext, useMemo, Children, useLayoutEffect, useCallback, useSyncExternalStore } from 'react';
34
37
  import { createPortal } from 'react-dom';
35
38
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -37,16 +40,15 @@ import { useStore } from 'zustand/react';
37
40
  import { asNodeId, Store, createNode, stripStrategy, floatingStrategy, gridStrategy } from 'windease';
38
41
  import { Provider, StrategyRegistryProvider, Container, DragProvider, useDragHandle } from 'windease/react';
39
42
 
40
- // ../svg/dist/index.js
41
43
  var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
42
- function multiply(a, b2) {
44
+ function multiply(a, b) {
43
45
  return [
44
- a[0] * b2[0] + a[2] * b2[1],
45
- a[1] * b2[0] + a[3] * b2[1],
46
- a[0] * b2[2] + a[2] * b2[3],
47
- a[1] * b2[2] + a[3] * b2[3],
48
- a[0] * b2[4] + a[2] * b2[5] + a[4],
49
- a[1] * b2[4] + a[3] * b2[5] + a[5]
46
+ a[0] * b[0] + a[2] * b[1],
47
+ a[1] * b[0] + a[3] * b[1],
48
+ a[0] * b[2] + a[2] * b[3],
49
+ a[1] * b[2] + a[3] * b[3],
50
+ a[0] * b[4] + a[2] * b[5] + a[4],
51
+ a[1] * b[4] + a[3] * b[5] + a[5]
50
52
  ];
51
53
  }
52
54
  function isIdentity(m, eps = 1e-9) {
@@ -108,8 +110,8 @@ function escapeAttr(s) {
108
110
  }
109
111
  function serializePathD(path) {
110
112
  if (path.kind === "rect") {
111
- const { x: x2, y, width, height } = path;
112
- return `M${trimNumber(x2)} ${trimNumber(y)}h${trimNumber(width)}v${trimNumber(height)}h${trimNumber(-width)}Z`;
113
+ const { x, y: y2, width, height } = path;
114
+ return `M${trimNumber(x)} ${trimNumber(y2)}h${trimNumber(width)}v${trimNumber(height)}h${trimNumber(-width)}Z`;
113
115
  }
114
116
  const cmds = path.commands;
115
117
  const xs = path.coords;
@@ -395,9 +397,9 @@ function pathXml(node, registry, namespaces) {
395
397
  attrs.push(`opacity="${trimNumber(node.opacity)}"`);
396
398
  }
397
399
  if (node.rotation != null && node.rotation !== 0) {
398
- const b2 = pathBounds(node.path);
399
- const cx = b2.minX + (b2.maxX - b2.minX) / 2;
400
- const cy = b2.minY + (b2.maxY - b2.minY) / 2;
400
+ const b = pathBounds(node.path);
401
+ const cx = b.minX + (b.maxX - b.minX) / 2;
402
+ const cy = b.minY + (b.maxY - b.minY) / 2;
401
403
  const deg = node.rotation * 180 / Math.PI;
402
404
  attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
403
405
  }
@@ -421,17 +423,18 @@ function paintAttrs(paint, name, registry, includeOpacity = true) {
421
423
  return [`${name}="url(#${id})"`];
422
424
  }
423
425
  function coreStrokeAttrs(stroke, registry) {
424
- if (!stroke) return [];
426
+ const paint = stroke?.paint;
427
+ if (!stroke || !paint) return [];
425
428
  const width = resolveStrokeWidth(stroke.width ?? 1, 1);
426
429
  if (!(width > 0)) return [];
427
430
  const attrs = [];
428
- if ("color" in stroke.paint) {
429
- attrs.push(`stroke="${stroke.paint.color}"`);
430
- if (stroke.paint.opacity != null && stroke.paint.opacity !== 1) {
431
- attrs.push(`stroke-opacity="${trimNumber(stroke.paint.opacity)}"`);
431
+ if ("color" in paint) {
432
+ attrs.push(`stroke="${paint.color}"`);
433
+ if (paint.opacity != null && paint.opacity !== 1) {
434
+ attrs.push(`stroke-opacity="${trimNumber(paint.opacity)}"`);
432
435
  }
433
436
  } else {
434
- attrs.push(`stroke="url(#${registry.register(stroke.paint)})"`);
437
+ attrs.push(`stroke="url(#${registry.register(paint)})"`);
435
438
  }
436
439
  attrs.push(`stroke-width="${trimNumber(width)}"`);
437
440
  if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
@@ -494,11 +497,11 @@ function computeBounds(nodes) {
494
497
  n.children.forEach((c2) => visit(c2, childCtm));
495
498
  return;
496
499
  }
497
- const b2 = n.kind === "text" || n.kind === "image" ? { minX: n.x, minY: n.y, maxX: n.x + n.width, maxY: n.y + n.height } : pathBounds(n.path);
498
- const local = n.rotation ? rotateAboutCenter(ctm, b2, n.rotation) : ctm;
499
- for (const [x2, y] of corners(b2)) {
500
- const px = local[0] * x2 + local[2] * y + local[4];
501
- const py = local[1] * x2 + local[3] * y + local[5];
500
+ const b = n.kind === "text" || n.kind === "image" ? { minX: n.x, minY: n.y, maxX: n.x + n.width, maxY: n.y + n.height } : pathBounds(n.path);
501
+ const local = n.rotation ? rotateAboutCenter(ctm, b, n.rotation) : ctm;
502
+ for (const [x, y2] of corners(b)) {
503
+ const px = local[0] * x + local[2] * y2 + local[4];
504
+ const py = local[1] * x + local[3] * y2 + local[5];
502
505
  if (px < minX) minX = px;
503
506
  if (py < minY) minY = py;
504
507
  if (px > maxX) maxX = px;
@@ -511,12 +514,12 @@ function computeBounds(nodes) {
511
514
  }
512
515
  return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
513
516
  }
514
- function corners(b2) {
515
- return [[b2.minX, b2.minY], [b2.maxX, b2.minY], [b2.maxX, b2.maxY], [b2.minX, b2.maxY]];
517
+ function corners(b) {
518
+ return [[b.minX, b.minY], [b.maxX, b.minY], [b.maxX, b.maxY], [b.minX, b.maxY]];
516
519
  }
517
- function rotateAboutCenter(ctm, b2, angle) {
518
- const cx = (b2.minX + b2.maxX) / 2;
519
- const cy = (b2.minY + b2.maxY) / 2;
520
+ function rotateAboutCenter(ctm, b, angle) {
521
+ const cx = (b.minX + b.maxX) / 2;
522
+ const cy = (b.minY + b.maxY) / 2;
520
523
  const c2 = Math.cos(angle);
521
524
  const s = Math.sin(angle);
522
525
  return multiply(ctm, [c2, s, -s, c2, cx - cx * c2 + cy * s, cy - cx * s - cy * c2]);
@@ -549,7 +552,9 @@ function textXml(node, registry, namespaces) {
549
552
  }
550
553
  const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
551
554
  if (decoration) attrs.push(`text-decoration="${decoration}"`);
552
- if (node.fill) {
555
+ if (node.fill === null) {
556
+ attrs.push('fill="none"');
557
+ } else if (node.fill) {
553
558
  if ("color" in node.fill) {
554
559
  attrs.push(`fill="${node.fill.color}"`);
555
560
  if (node.fill.opacity != null && node.fill.opacity !== 1) {
@@ -628,34 +633,32 @@ function pathBounds(path) {
628
633
  maxY: path.y + path.height
629
634
  };
630
635
  }
631
- const b2 = boundsOfPath(path);
632
- return { minX: b2.x, minY: b2.y, maxX: b2.x + b2.width, maxY: b2.y + b2.height };
636
+ const b = boundsOfPath(path);
637
+ return { minX: b.x, minY: b.y, maxX: b.x + b.width, maxY: b.y + b.height };
633
638
  }
634
-
635
- // src/annotations/paint.ts
636
639
  var MARK_COLOR = "#e5484d";
637
640
  var MARK_WIDTH = 2;
638
641
  var TEXT_SIZE = 14;
639
- var toWorld = (p2, content) => ({
640
- x: p2.x * content.w,
641
- y: p2.y * content.h
642
+ var toWorld = (p, content) => ({
643
+ x: p.x * content.w,
644
+ y: p.y * content.h
642
645
  });
643
646
  function vertices(m, content) {
644
647
  const stored = m.data.points;
645
- if (stored && stored.length >= 2) return stored.map((p2) => toWorld(p2, content));
646
- const { x: x2, y, width, height } = m.pose;
648
+ if (stored && stored.length >= 2) return stored.map((p) => toWorld(p, content));
649
+ const { x, y: y2, width, height } = m.pose;
647
650
  return [
648
- { x: x2, y },
649
- { x: x2 + width, y: y + height }
651
+ { x, y: y2 },
652
+ { x: x + width, y: y2 + height }
650
653
  ];
651
654
  }
652
655
  function polyline(points) {
653
- const b2 = new PathBuilder();
656
+ const b = new PathBuilder();
654
657
  const first = points[0];
655
658
  if (!first) return rectPath(0, 0, 0, 0);
656
- b2.moveTo(first.x, first.y);
657
- for (const p2 of points.slice(1)) b2.lineTo(p2.x, p2.y);
658
- return b2.build();
659
+ b.moveTo(first.x, first.y);
660
+ for (const p of points.slice(1)) b.lineTo(p.x, p.y);
661
+ return b.build();
659
662
  }
660
663
  function markCommands(m, content, style = {}) {
661
664
  const color = style.color ?? MARK_COLOR;
@@ -674,13 +677,13 @@ function markCommands(m, content, style = {}) {
674
677
  case "ellipse":
675
678
  return [{ kind: "path", path: ellipsePath(m.pose), stroke }];
676
679
  case "line": {
677
- const [a, b2] = vertices(m, content);
678
- return [{ kind: "path", path: linePath(a, b2), stroke }];
680
+ const [a, b] = vertices(m, content);
681
+ return [{ kind: "path", path: linePath(a, b), stroke }];
679
682
  }
680
683
  case "arrow": {
681
- const [a, b2] = vertices(m, content);
684
+ const [a, b] = vertices(m, content);
682
685
  const marked = { ...stroke, markerEnd: "arrow" };
683
- const path = linePath(a, b2);
686
+ const path = linePath(a, b);
684
687
  return [
685
688
  { kind: "path", path, stroke: marked },
686
689
  ...markerDrawCommands(path, marked, MARK_WIDTH, void 0)
@@ -742,8 +745,6 @@ function resolveMarkStyle(data, opts) {
742
745
  stale: isStale(data.seen, opts.config, opts.positionDependsOn ?? [])
743
746
  };
744
747
  }
745
-
746
- // src/annotations/svgNodes.ts
747
748
  var NO_PAINT = { kind: "none" };
748
749
  function toSvgPaint(paint) {
749
750
  if (!paint) return NO_PAINT;
@@ -799,7 +800,7 @@ function capturePlan(base, format) {
799
800
  if (format === "svg") return "svg-document";
800
801
  return base?.kind === "svg" ? "svg-document" : "raster-stack";
801
802
  }
802
- function nestBaseSvg(markup, w3, h2) {
803
+ function nestBaseSvg(markup, w3, h) {
803
804
  const doc = new DOMParser().parseFromString(markup, "image/svg+xml");
804
805
  const root = doc.documentElement;
805
806
  if (doc.getElementsByTagName("parsererror").length > 0 || root.localName !== "svg") {
@@ -808,16 +809,16 @@ function nestBaseSvg(markup, w3, h2) {
808
809
  root.setAttribute("x", "0");
809
810
  root.setAttribute("y", "0");
810
811
  root.setAttribute("width", String(w3));
811
- root.setAttribute("height", String(h2));
812
+ root.setAttribute("height", String(h));
812
813
  return new XMLSerializer().serializeToString(root);
813
814
  }
814
- function embedRasterBase(base, w3, h2, onWarn) {
815
+ function embedRasterBase(base, w3, h, onWarn) {
815
816
  const href = base.kind === "canvas" ? base.canvas.toDataURL() : base.kind === "image" ? base.src : "";
816
817
  if (base.kind === "image" && !href.startsWith("data:")) {
817
818
  onWarn?.(`base image '${href}' is a reference, so this SVG is not self-contained`);
818
819
  }
819
- return serializeSvg([{ kind: "image", href, x: 0, y: 0, width: w3, height: h2 }], {
820
- viewBox: { x: 0, y: 0, width: w3, height: h2 }
820
+ return serializeSvg([{ kind: "image", href, x: 0, y: 0, width: w3, height: h }], {
821
+ viewBox: { x: 0, y: 0, width: w3, height: h }
821
822
  });
822
823
  }
823
824
  function markNodesOf(scene, draw) {
@@ -833,15 +834,15 @@ function markNodesOf(scene, draw) {
833
834
  }
834
835
  function composeCaptureSvg(args) {
835
836
  const { base, scene, draw, scale, onWarn } = args;
836
- const { w: w3, h: h2 } = draw.content;
837
- const baseXml = base === void 0 ? "" : base.kind === "svg" ? nestBaseSvg(base.markup, w3, h2) : embedRasterBase(base, w3, h2, onWarn);
837
+ const { w: w3, h } = draw.content;
838
+ const baseXml = base === void 0 ? "" : base.kind === "svg" ? nestBaseSvg(base.markup, w3, h) : embedRasterBase(base, w3, h, onWarn);
838
839
  const marksXml = serializeSvg(markNodesOf(scene, draw), {
839
- viewBox: { x: 0, y: 0, width: w3, height: h2 },
840
+ viewBox: { x: 0, y: 0, width: w3, height: h },
840
841
  onWarn
841
842
  });
842
843
  const outW = Math.max(1, Math.round(w3 * scale));
843
- const outH = Math.max(1, Math.round(h2 * scale));
844
- return `<svg xmlns="${SVG_NS}" width="${outW}" height="${outH}" viewBox="0 0 ${w3} ${h2}">${baseXml}${marksXml}</svg>`;
844
+ const outH = Math.max(1, Math.round(h * scale));
845
+ return `<svg xmlns="${SVG_NS}" width="${outW}" height="${outH}" viewBox="0 0 ${w3} ${h}">${baseXml}${marksXml}</svg>`;
845
846
  }
846
847
  function context2d(width, height) {
847
848
  const canvas = document.createElement("canvas");
@@ -1041,8 +1042,6 @@ function drop(stack, value) {
1041
1042
  }
1042
1043
  }
1043
1044
  }
1044
-
1045
- // src/annotations/store.ts
1046
1045
  function createAnnotationScene() {
1047
1046
  return createScene({ systemLayers: [{ id: "marks" }] });
1048
1047
  }
@@ -1265,17 +1264,17 @@ function annotationsFromJSON(raw, targets, rest = {}) {
1265
1264
  }
1266
1265
 
1267
1266
  // src/annotations/view.ts
1268
- function toWeaselView(v2) {
1269
- return { x: v2.pan.x, y: v2.pan.y, scale: { x: v2.zoom, y: v2.zoom } };
1267
+ function toWeaselView(v) {
1268
+ return { x: v.pan.x, y: v.pan.y, scale: { x: v.zoom, y: v.zoom } };
1270
1269
  }
1271
- function fromWeaselView(v2) {
1272
- return { zoom: v2.scale.x, pan: { x: v2.x, y: v2.y } };
1270
+ function fromWeaselView(v) {
1271
+ return { zoom: v.scale.x, pan: { x: v.x, y: v.y } };
1273
1272
  }
1274
1273
  function fitView(content, pane) {
1275
1274
  const zoom = content.w > 0 && content.h > 0 ? Math.min(pane.w / content.w, pane.h / content.h) : 1;
1276
1275
  return { zoom: Number.isFinite(zoom) && zoom > 0 ? zoom : 1, pan: { x: 0, y: 0 } };
1277
1276
  }
1278
- var sameRect = (a, b2) => a !== null && a.x === b2.x && a.y === b2.y && a.w === b2.w && a.h === b2.h;
1277
+ var sameRect = (a, b) => a !== null && a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;
1279
1278
  function ToolBridge({ toolId }) {
1280
1279
  const tools = useActiveToolContext();
1281
1280
  const setActive = tools.setActive;
@@ -1285,9 +1284,9 @@ function ToolBridge({ toolId }) {
1285
1284
  return null;
1286
1285
  }
1287
1286
  function pointsOf(extras, content) {
1288
- const toFrac = (p2) => ({
1289
- x: content.w === 0 ? 0 : p2.x / content.w,
1290
- y: content.h === 0 ? 0 : p2.y / content.h
1287
+ const toFrac = (p) => ({
1288
+ x: content.w === 0 ? 0 : p.x / content.w,
1289
+ y: content.h === 0 ? 0 : p.y / content.h
1291
1290
  });
1292
1291
  if (extras.kind === "line") {
1293
1292
  return [toFrac(extras.a), toFrac(extras.b)];
@@ -1411,10 +1410,11 @@ function AnnotationTargets({
1411
1410
  capability,
1412
1411
  state,
1413
1412
  config,
1413
+ trial,
1414
1414
  annotations,
1415
1415
  activeToolId
1416
1416
  }) {
1417
- const targets = capability.targets(state, config);
1417
+ const targets = capability.targets(state, config, trial);
1418
1418
  return /* @__PURE__ */ jsx(Fragment, { children: targets.map((t) => /* @__PURE__ */ jsx(
1419
1419
  AnnotationOverlay,
1420
1420
  {
@@ -1429,6 +1429,9 @@ function AnnotationTargets({
1429
1429
  }
1430
1430
 
1431
1431
  // src/config/builder.ts
1432
+ function isConfigBranch(entry) {
1433
+ return "children" in entry;
1434
+ }
1432
1435
  var BaseNode = class {
1433
1436
  constructor(defaultValue, annotations = {}, options = {}) {
1434
1437
  this.annotations = annotations;
@@ -1468,9 +1471,10 @@ var BaseNode = class {
1468
1471
  pair(pair) {
1469
1472
  return this.ann({ pair });
1470
1473
  }
1471
- /** Render under a named section heading. */
1472
- section(section) {
1473
- return this.opt({ section });
1474
+ /** Render under a named section heading. `collapsed` opens the section
1475
+ * folded — say it on any one of the section's leaves. */
1476
+ section(label, opts = {}) {
1477
+ return this.opt({ section: { label, ...opts } });
1474
1478
  }
1475
1479
  /** Show this row only while the predicate holds. Presentational — the value
1476
1480
  * stays in config and the instrument still reads it. */
@@ -1554,6 +1558,45 @@ var CustomNode = class _CustomNode extends BaseNode {
1554
1558
  return new _CustomNode(this.kind, this.default, annotations, options);
1555
1559
  }
1556
1560
  };
1561
+ var GroupNode = class _GroupNode {
1562
+ constructor(children, annotations = {}, options = {}) {
1563
+ this.children = children;
1564
+ this.annotations = annotations;
1565
+ this.options = options;
1566
+ }
1567
+ children;
1568
+ annotations;
1569
+ options;
1570
+ with(annotations, options) {
1571
+ return new _GroupNode(this.children, annotations, options);
1572
+ }
1573
+ /** Heading for the group's rows. Defaults to the key, title-cased. */
1574
+ label(name) {
1575
+ return this.with({ ...this.annotations, name }, this.options);
1576
+ }
1577
+ /** Longer help text. Carried on the resolved `PrefGroup`, where weasel-ui's
1578
+ * `PrefsForm` draws it under the heading; `ControlPanel` has no place for
1579
+ * it yet and shows the heading alone. */
1580
+ describe(description) {
1581
+ return this.with({ ...this.annotations, description }, this.options);
1582
+ }
1583
+ /** Render this whole group under a named section heading. */
1584
+ section(label, opts = {}) {
1585
+ return this.with(this.annotations, { ...this.options, section: { label, ...opts } });
1586
+ }
1587
+ /** Show this group only while the predicate holds. Presentational — every
1588
+ * value beneath it stays in config and the instrument still reads it. */
1589
+ showIf(predicate) {
1590
+ return this.with(this.annotations, { ...this.options, showIf: predicate });
1591
+ }
1592
+ };
1593
+ function shapeDefaults(shape) {
1594
+ const out = {};
1595
+ for (const [key, entry] of Object.entries(shape)) {
1596
+ out[key] = isConfigBranch(entry) ? shapeDefaults(entry.children) : entry.default;
1597
+ }
1598
+ return out;
1599
+ }
1557
1600
  var expand = (options) => options.map((o) => typeof o === "string" ? { value: o, label: o } : o);
1558
1601
  var f = {
1559
1602
  number: (def) => new NumberNode(def),
@@ -1567,18 +1610,15 @@ var f = {
1567
1610
  /** A leaf with no declared kind: the rule chain decides, which is how a lab
1568
1611
  * states a convention like "every `*Color` key is a color picker". */
1569
1612
  value: (def) => new ValueNode(def),
1613
+ /** A branch: its children nest under this key, so `grid: f.group({ size })`
1614
+ * puts the value at `config.grid.size` and addresses it as `'grid.size'`.
1615
+ * Groups nest as deeply as the schema wants. */
1616
+ group: (children) => new GroupNode(children),
1570
1617
  /** A leaf of a kind a lab supplies the control for, through `controls`. */
1571
1618
  custom: (kind, def, validate) => new CustomNode(kind, def, {}, validate ? { validate } : {}),
1572
- /** Collect leaves into an instrument's config. */
1619
+ /** Collect leaves and groups into an instrument's config. */
1573
1620
  schema(nodes) {
1574
- return {
1575
- nodes,
1576
- defaults: () => {
1577
- const out = {};
1578
- for (const [key, node] of Object.entries(nodes)) out[key] = node.default;
1579
- return out;
1580
- }
1581
- };
1621
+ return { nodes, defaults: () => shapeDefaults(nodes) };
1582
1622
  }
1583
1623
  };
1584
1624
 
@@ -1610,9 +1650,9 @@ function applyRules(seed, ctx, rules) {
1610
1650
  for (const rule of rules) {
1611
1651
  const patch = rule({ ...ctx, leaf: acc });
1612
1652
  if (patch === null) continue;
1613
- for (const [k2, v2] of Object.entries(patch)) {
1614
- if (v2 !== void 0 && acc[k2] === void 0) {
1615
- acc[k2] = v2;
1653
+ for (const [k2, v] of Object.entries(patch)) {
1654
+ if (v !== void 0 && acc[k2] === void 0) {
1655
+ acc[k2] = v;
1616
1656
  }
1617
1657
  }
1618
1658
  }
@@ -1622,40 +1662,59 @@ function applyRules(seed, ctx, rules) {
1622
1662
  // src/config/resolve.ts
1623
1663
  function defined(annotations) {
1624
1664
  const out = {};
1625
- for (const [k2, v2] of Object.entries(annotations)) if (v2 !== void 0) out[k2] = v2;
1665
+ for (const [k2, v] of Object.entries(annotations)) if (v !== void 0) out[k2] = v;
1626
1666
  return out;
1627
1667
  }
1628
1668
  function resolveConfigSchema(schema, rules = []) {
1669
+ const sink = {
1670
+ sections: [],
1671
+ showIf: /* @__PURE__ */ new Map(),
1672
+ renderers: {},
1673
+ chain: [...rules, ...builtinRules]
1674
+ };
1675
+ const group = resolveShape(schema.nodes, "", "", sink);
1676
+ return { group, sections: sink.sections, showIf: sink.showIf, renderers: sink.renderers };
1677
+ }
1678
+ function resolveShape(shape, at, name, sink) {
1629
1679
  const children = {};
1630
- const sectionOrder = [];
1631
- const sectionPaths = /* @__PURE__ */ new Map();
1632
- const showIf = /* @__PURE__ */ new Map();
1633
- const renderers = {};
1634
- const chain2 = [...rules, ...builtinRules];
1635
- for (const [key, node] of Object.entries(schema.nodes)) {
1636
- const seed = defined({
1637
- ...node.annotations,
1638
- ...node.kind === null ? {} : { kind: node.kind }
1639
- });
1640
- const patch = applyRules(seed, { key, path: key, default: node.default }, chain2);
1641
- children[key] = { ...patch, default: node.default };
1642
- const { section, showIf: predicate, render } = node.options;
1680
+ const sections = /* @__PURE__ */ new Map();
1681
+ for (const [key, entry] of Object.entries(shape)) {
1682
+ const path = at === "" ? key : `${at}.${key}`;
1683
+ children[key] = resolveEntry(entry, key, path, sink);
1684
+ const { section, showIf: predicate } = entry.options;
1643
1685
  if (section !== void 0) {
1644
- if (!sectionPaths.has(section)) {
1645
- sectionOrder.push(section);
1646
- sectionPaths.set(section, []);
1686
+ let spec = sections.get(section.label);
1687
+ if (!spec) {
1688
+ spec = { at, label: section.label, paths: [] };
1689
+ sections.set(section.label, spec);
1690
+ sink.sections.push(spec);
1691
+ }
1692
+ spec.paths.push(path);
1693
+ if (section.collapsed !== void 0) {
1694
+ spec.collapsed = (spec.collapsed ?? false) || section.collapsed;
1647
1695
  }
1648
- sectionPaths.get(section)?.push(key);
1649
1696
  }
1650
- if (predicate) showIf.set(key, predicate);
1651
- if (render) renderers[key] = render;
1697
+ if (predicate) sink.showIf.set(path, predicate);
1698
+ if (!isConfigBranch(entry) && entry.options.render) sink.renderers[path] = entry.options.render;
1699
+ }
1700
+ return { name, children };
1701
+ }
1702
+ function resolveEntry(entry, key, path, sink) {
1703
+ if (isConfigBranch(entry)) {
1704
+ const group = resolveShape(
1705
+ entry.children,
1706
+ path,
1707
+ entry.annotations.name ?? titleCase(key),
1708
+ sink
1709
+ );
1710
+ return entry.annotations.description === void 0 ? group : { ...group, description: entry.annotations.description };
1652
1711
  }
1653
- const group = { name: "", children };
1654
- const sections = sectionOrder.map((label) => ({
1655
- label,
1656
- paths: sectionPaths.get(label) ?? []
1657
- }));
1658
- return { group, sections, showIf, renderers };
1712
+ const seed = defined({
1713
+ ...entry.annotations,
1714
+ ...entry.kind === null ? {} : { kind: entry.kind }
1715
+ });
1716
+ const patch = applyRules(seed, { key, path, default: entry.default }, sink.chain);
1717
+ return { ...patch, default: entry.default };
1659
1718
  }
1660
1719
  var LabContext = createContext(null);
1661
1720
  function useLabContext() {
@@ -2303,9 +2362,9 @@ function hexToRgba(hex, alpha) {
2303
2362
  const body = m[1];
2304
2363
  const full = body.length === 3 ? body.replace(/./g, (c2) => c2 + c2) : body;
2305
2364
  const r2 = Number.parseInt(full.slice(0, 2), 16);
2306
- const g = Number.parseInt(full.slice(2, 4), 16);
2307
- const b2 = Number.parseInt(full.slice(4, 6), 16);
2308
- return `rgba(${r2}, ${g}, ${b2}, ${alpha})`;
2365
+ const g2 = Number.parseInt(full.slice(2, 4), 16);
2366
+ const b = Number.parseInt(full.slice(4, 6), 16);
2367
+ return `rgba(${r2}, ${g2}, ${b}, ${alpha})`;
2309
2368
  }
2310
2369
  var REF = /^\{([^}]+)\}$/;
2311
2370
  function refTarget(value) {
@@ -2443,17 +2502,36 @@ function useThemeOptional() {
2443
2502
  return useContext(ThemeContext);
2444
2503
  }
2445
2504
 
2505
+ // src/instrument/serializers.ts
2506
+ function configDefaultsOf(instruments) {
2507
+ const out = {};
2508
+ for (const instrument of instruments) out[instrument.name] = () => instrument.defaultConfig();
2509
+ return out;
2510
+ }
2511
+ function serializersOf(instruments) {
2512
+ const out = {};
2513
+ for (const instrument of instruments) {
2514
+ const { name, serialize: serialize2, deserialize } = instrument;
2515
+ if (!serialize2 && !deserialize) continue;
2516
+ out[name] = {
2517
+ ...serialize2 ? { serialize: (state) => serialize2(state) } : {},
2518
+ ...deserialize ? { deserialize: (data, config) => deserialize(data, config) } : {}
2519
+ };
2520
+ }
2521
+ return out;
2522
+ }
2523
+
2446
2524
  // ../theme/dist/index.js
2447
2525
  var ALPHA_EXT = "com.weasel.alpha";
2448
- var isToken = (v2) => typeof v2 === "object" && v2 !== null && "$value" in v2;
2526
+ var isToken = (v) => typeof v === "object" && v !== null && "$value" in v;
2449
2527
  function flattenTokens(doc) {
2450
2528
  const out = {};
2451
2529
  for (const [groupName, group] of Object.entries(doc)) {
2452
2530
  if (groupName.startsWith("$")) continue;
2453
2531
  if (typeof group !== "object" || group === null) continue;
2454
- const g = group;
2455
- const groupType = g.$type;
2456
- for (const [leaf, node] of Object.entries(g)) {
2532
+ const g2 = group;
2533
+ const groupType = g2.$type;
2534
+ for (const [leaf, node] of Object.entries(g2)) {
2457
2535
  if (leaf.startsWith("$")) continue;
2458
2536
  if (!isToken(node)) continue;
2459
2537
  const type = node.$type ?? groupType;
@@ -2563,11 +2641,11 @@ var interstellarTheme = loadDTCG(interstellar_tokens_default);
2563
2641
  var DEFAULT_VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
2564
2642
  function as2DView(view) {
2565
2643
  if (typeof view !== "object" || view === null) return null;
2566
- const v2 = view;
2567
- if (typeof v2.zoom !== "number") return null;
2568
- if (typeof v2.pan !== "object" || v2.pan === null) return null;
2569
- if (typeof v2.pan.x !== "number" || typeof v2.pan.y !== "number") return null;
2570
- return { zoom: v2.zoom, pan: { x: v2.pan.x, y: v2.pan.y } };
2644
+ const v = view;
2645
+ if (typeof v.zoom !== "number") return null;
2646
+ if (typeof v.pan !== "object" || v.pan === null) return null;
2647
+ if (typeof v.pan.x !== "number" || typeof v.pan.y !== "number") return null;
2648
+ return { zoom: v.zoom, pan: { x: v.pan.x, y: v.pan.y } };
2571
2649
  }
2572
2650
  var ZONE_ID = asNodeId("lk-trial-body");
2573
2651
  var SIDEBAR_ID = asNodeId("lk-trial-sidebar");
@@ -2671,18 +2749,20 @@ var TrialDragContext = createContext(null);
2671
2749
  function useTrialDrag() {
2672
2750
  return useContext(TrialDragContext);
2673
2751
  }
2674
- function Draggable({ nodeId, title, children }) {
2752
+ function Draggable({ nodeId, title, lead, children }) {
2675
2753
  const handlers = useDragHandle(nodeId);
2676
2754
  return /* @__PURE__ */ jsxs("div", { className: "lk-trial__titlebar lk-trial__titlebar--draggable", ...handlers, children: [
2755
+ lead,
2677
2756
  /* @__PURE__ */ jsx("span", { className: "lk-trial__title", children: title }),
2678
2757
  children
2679
2758
  ] });
2680
2759
  }
2681
- function TrialTitleBar({ title, children }) {
2760
+ function TrialTitleBar({ title, lead, children }) {
2682
2761
  const drag = useTrialDrag();
2683
2762
  if (drag)
2684
- return /* @__PURE__ */ jsx(Draggable, { nodeId: drag.nodeId, title, children });
2763
+ return /* @__PURE__ */ jsx(Draggable, { nodeId: drag.nodeId, title, lead, children });
2685
2764
  return /* @__PURE__ */ jsxs("div", { className: "lk-trial__titlebar", children: [
2765
+ lead,
2686
2766
  /* @__PURE__ */ jsx("span", { className: "lk-trial__title", children: title }),
2687
2767
  children
2688
2768
  ] });
@@ -2752,6 +2832,7 @@ function UndockedSections({ trialId, sections, onDock }) {
2752
2832
  }
2753
2833
  var NO_OP = () => {
2754
2834
  };
2835
+ var NO_SECTIONS = Object.freeze({});
2755
2836
  function TrialChrome({
2756
2837
  trialId,
2757
2838
  record,
@@ -2772,17 +2853,21 @@ function TrialChrome({
2772
2853
  if (!storeCtx) throw new Error("[labkit] TrialChrome requires <LabStoreProvider>");
2773
2854
  const updateTrialView = useStore(storeCtx.store, (s) => s.updateTrialView);
2774
2855
  const updateTrialSidebarWidth = useStore(storeCtx.store, (s) => s.updateTrialSidebarWidth);
2856
+ const setTrialTitle = useStore(storeCtx.store, (s) => s.setTrialTitle);
2857
+ const setTrialSectionCollapsed = useStore(storeCtx.store, (s) => s.setTrialSectionCollapsed);
2775
2858
  const updateTrialConfig = useStore(storeCtx.store, (s) => s.updateTrialConfig);
2776
2859
  const updateTrialState = useStore(storeCtx.store, (s) => s.updateTrialState);
2777
2860
  const undockPanelAction = useStore(storeCtx.store, (s) => s.undockPanel);
2778
2861
  const dockPanelAction = useStore(storeCtx.store, (s) => s.dockPanel);
2779
2862
  const undockedPanels = useStore(storeCtx.store, (s) => s.undockedPanels);
2780
2863
  const undockedIds = useMemo(
2781
- () => Object.values(undockedPanels).filter((p2) => p2.trialId === trialId).map((p2) => p2.sectionId),
2864
+ () => Object.values(undockedPanels).filter((p) => p.trialId === trialId).map((p) => p.sectionId),
2782
2865
  [undockedPanels, trialId]
2783
2866
  );
2784
2867
  const view2d = as2DView(record.view);
2785
2868
  const configSchema = useConfigSchema(instrument);
2869
+ const title = record.title ?? record.instrumentName;
2870
+ const collapsedSections = record.collapsedSections ?? NO_SECTIONS;
2786
2871
  const ctx = useMemo(() => {
2787
2872
  const setZoom = (z) => {
2788
2873
  if (!view2d) return;
@@ -2791,6 +2876,8 @@ function TrialChrome({
2791
2876
  return {
2792
2877
  trialId,
2793
2878
  instrumentName: record.instrumentName,
2879
+ title,
2880
+ setTitle: (next) => setTrialTitle(trialId, next),
2794
2881
  isLastTrial,
2795
2882
  zoom: view2d ? view2d.zoom : null,
2796
2883
  setZoom,
@@ -2803,20 +2890,22 @@ function TrialChrome({
2803
2890
  configSchema,
2804
2891
  configFields: instrument.config ? [] : instrument.configSchema?.() ?? [],
2805
2892
  config: record.config,
2806
- setConfig: (key, value) => {
2893
+ setConfig: (path, value) => {
2807
2894
  const prevConfig = record.config;
2808
- if (process.env.NODE_ENV !== "production" && !(key in prevConfig)) {
2895
+ if (process.env.NODE_ENV !== "production" && !hasConfigPath(prevConfig, path) && schemaNodeAtPath(configSchema.group, path) === void 0) {
2809
2896
  console.warn(
2810
- `[labkit] setConfig: unknown key "${key}" for instrument "${record.instrumentName}"`
2897
+ `[labkit] setConfig: unknown path "${path}" for instrument "${record.instrumentName}"`
2811
2898
  );
2812
2899
  }
2813
- updateTrialConfig(trialId, key, value);
2900
+ updateTrialConfig(trialId, path, value);
2814
2901
  if (instrument.onConfigChange) {
2815
- const nextConfig = { ...prevConfig, [key]: value };
2902
+ const nextConfig = withValueAtPath(prevConfig, path, value);
2816
2903
  const nextState = instrument.onConfigChange(nextConfig, prevConfig, record.state);
2817
2904
  updateTrialState(trialId, nextState);
2818
2905
  }
2819
2906
  },
2907
+ collapsedSections,
2908
+ setSectionCollapsed: (key, collapsed) => setTrialSectionCollapsed(trialId, key, collapsed),
2820
2909
  undockedPanels: undockedIds,
2821
2910
  undockPanel: (sectionId, as) => undockPanelAction(trialId, sectionId, as),
2822
2911
  dockPanel: (sectionId) => dockPanelAction(trialId, sectionId),
@@ -2834,6 +2923,7 @@ function TrialChrome({
2834
2923
  record,
2835
2924
  instrument,
2836
2925
  lab,
2926
+ title,
2837
2927
  isLastTrial,
2838
2928
  updateTrialView,
2839
2929
  updateTrialConfig,
@@ -2846,7 +2936,10 @@ function TrialChrome({
2846
2936
  configSchema,
2847
2937
  undockedIds,
2848
2938
  undockPanelAction,
2849
- dockPanelAction
2939
+ dockPanelAction,
2940
+ setTrialTitle,
2941
+ setTrialSectionCollapsed,
2942
+ collapsedSections
2850
2943
  ]);
2851
2944
  const contributions = useMemo(
2852
2945
  () => suppressContributions(
@@ -2877,11 +2970,18 @@ function TrialChrome({
2877
2970
  "section",
2878
2971
  {
2879
2972
  className: "lk-trial",
2880
- "aria-label": `Trial ${record.instrumentName}`,
2973
+ "aria-label": `Trial ${title}`,
2881
2974
  tabIndex: -1,
2882
2975
  onKeyDown: handleKeyDown,
2883
2976
  children: [
2884
- /* @__PURE__ */ jsx(TrialTitleBar, { title: record.instrumentName, children: /* @__PURE__ */ jsx(TitleBarRegion, { contributions: inRegion("titlebar"), ctx }) }),
2977
+ /* @__PURE__ */ jsx(
2978
+ TrialTitleBar,
2979
+ {
2980
+ title,
2981
+ lead: /* @__PURE__ */ jsx(TitleBarRegion, { placement: "lead", contributions: inRegion("titlebar"), ctx }),
2982
+ children: /* @__PURE__ */ jsx(TitleBarRegion, { contributions: inRegion("titlebar"), ctx })
2983
+ }
2984
+ ),
2885
2985
  /* @__PURE__ */ jsx("div", { className: "lk-trial__toolbar", children: /* @__PURE__ */ jsx(ToolbarRegion, { contributions: inRegion("toolbar"), ctx }) }),
2886
2986
  /* @__PURE__ */ jsxs("div", { className: "lk-trial__body", children: [
2887
2987
  /* @__PURE__ */ jsx(PaletteRegion, { contributions: inRegion("palette"), ctx }),
@@ -2978,7 +3078,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
2978
3078
  const snap = structuredClone(current.state);
2979
3079
  updateTrialUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
2980
3080
  };
2981
- const setView = (v2) => updateTrialView(record.id, v2);
3081
+ const setView = (v) => updateTrialView(record.id, v);
2982
3082
  const placeView = (size) => {
2983
3083
  const declared = instrument.canvas?.initialView;
2984
3084
  if (typeof declared !== "function") return;
@@ -2987,7 +3087,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
2987
3087
  };
2988
3088
  const annotationsCap = instrument.annotations;
2989
3089
  const targetsRef = useRef(() => []);
2990
- targetsRef.current = () => annotationsCap ? annotationsCap.targets(record.state, record.config) : [];
3090
+ targetsRef.current = () => annotationsCap ? annotationsCap.targets(record.state, record.config, { id: record.id, view: record.view }) : [];
2991
3091
  const annotationsRef = useRef(null);
2992
3092
  const configRef = useRef(record.config);
2993
3093
  configRef.current = record.config;
@@ -3063,11 +3163,11 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3063
3163
  updateTrialState(record.id, next);
3064
3164
  bus.emit("state.change");
3065
3165
  },
3066
- setConfig: (key, value) => {
3067
- const evt = `config.change:${String(key)}`;
3166
+ setConfig: (path, value) => {
3167
+ const evt = `config.change:${path}`;
3068
3168
  snapshotIfNeeded("config.change");
3069
3169
  snapshotIfNeeded(evt);
3070
- updateTrialConfig(record.id, key, value);
3170
+ updateTrialConfig(record.id, path, value);
3071
3171
  bus.emit("config.change");
3072
3172
  bus.emit(evt);
3073
3173
  },
@@ -3118,11 +3218,11 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3118
3218
  if (declared == null) return null;
3119
3219
  return resolveLoupe(typeof declared === "function" ? declared(record.config) : declared);
3120
3220
  }, [instrument.loupe, record.config]);
3121
- const loupeBindings = loupeCap ? { on: loupeOn, toggle: () => setLoupeOn((v2) => !v2) } : void 0;
3221
+ const loupeBindings = loupeCap ? { on: loupeOn, toggle: () => setLoupeOn((v) => !v) } : void 0;
3122
3222
  const canvasLayers = useMemo(() => {
3123
3223
  if (!instrument.canvas) return [];
3124
3224
  const baseLayers = instrument.canvas.layers;
3125
- const ordered = layerOrder ? [...baseLayers].sort((a, b2) => layerOrder.indexOf(a.id) - layerOrder.indexOf(b2.id)) : baseLayers;
3225
+ const ordered = layerOrder ? [...baseLayers].sort((a, b) => layerOrder.indexOf(a.id) - layerOrder.indexOf(b.id)) : baseLayers;
3126
3226
  return ordered.map((layer) => ({
3127
3227
  id: layer.id,
3128
3228
  visible: layerVisibility[layer.id] !== false,
@@ -3154,8 +3254,8 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3154
3254
  });
3155
3255
  const paletteItems = useMemo(() => {
3156
3256
  if (!instrument.dragDrop) return [];
3157
- const p2 = instrument.dragDrop.palette;
3158
- return typeof p2 === "function" ? p2(record.state, record.config) : p2;
3257
+ const p = instrument.dragDrop.palette;
3258
+ return typeof p === "function" ? p(record.state, record.config) : p;
3159
3259
  }, [instrument.dragDrop, record.state, record.config]);
3160
3260
  const layersWithFeedback = useMemo(() => {
3161
3261
  if (!dragDropResult.drag?.feedback) return canvasLayers;
@@ -3269,6 +3369,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3269
3369
  capability: annotationsCap,
3270
3370
  state: record.state,
3271
3371
  config: record.config,
3372
+ trial: { id: record.id, view: record.view },
3272
3373
  annotations,
3273
3374
  activeToolId: resolvedToolId
3274
3375
  }
@@ -3310,9 +3411,12 @@ function initialView(instrument) {
3310
3411
  if (typeof declared === "function") return null;
3311
3412
  return declared ? structuredClone(declared) : { ...DEFAULT_VIEW2, pan: { ...DEFAULT_VIEW2.pan } };
3312
3413
  }
3313
- function addTrial(trials, instruments, instrumentName) {
3414
+ function seedConfig(defaults, seed) {
3415
+ return seed ? fillConfigDefaults(seed, defaults) : defaults;
3416
+ }
3417
+ function addTrial(trials, instruments, instrumentName, options = {}) {
3314
3418
  const instrument = findInstrument(instruments, instrumentName);
3315
- const config = instrument.defaultConfig();
3419
+ const config = seedConfig(instrument.defaultConfig(), options.config);
3316
3420
  const state = instrument.initialState(config);
3317
3421
  const record = {
3318
3422
  id: crypto.randomUUID(),
@@ -3322,6 +3426,7 @@ function addTrial(trials, instruments, instrumentName) {
3322
3426
  view: initialView(instrument),
3323
3427
  undoStack: { past: [], future: [] }
3324
3428
  };
3429
+ if (options.config) record.configSeed = options.config;
3325
3430
  return [...trials, record];
3326
3431
  }
3327
3432
  function cloneTrial(trials, id) {
@@ -3348,7 +3453,7 @@ function resetTrial(trials, id, instruments) {
3348
3453
  const current = trials[idx];
3349
3454
  if (!current) return trials;
3350
3455
  const instrument = findInstrument(instruments, current.instrumentName);
3351
- const config = instrument.defaultConfig();
3456
+ const config = seedConfig(instrument.defaultConfig(), current.configSeed);
3352
3457
  const state = instrument.initialState(config);
3353
3458
  const reset = {
3354
3459
  ...current,
@@ -3394,7 +3499,7 @@ function LabHeader() {
3394
3499
  // Held at null so the control stays an "add one" action rather than
3395
3500
  // reading as the current instrument.
3396
3501
  /* @__PURE__ */ jsx(
3397
- p,
3502
+ g,
3398
3503
  {
3399
3504
  className: "lk-lab-header__add-select",
3400
3505
  "aria-label": "Add trial",
@@ -3408,7 +3513,7 @@ function LabHeader() {
3408
3513
  )
3409
3514
  ),
3410
3515
  /* @__PURE__ */ jsx(
3411
- r3,
3516
+ r6,
3412
3517
  {
3413
3518
  className: "lk-lab-header__mode",
3414
3519
  ariaLabel: "Color mode",
@@ -3461,14 +3566,18 @@ function useResolvedMode(mode) {
3461
3566
  function LabShell({ title, children, header, footer, mode = "auto" }) {
3462
3567
  const resolved = useResolvedMode(mode);
3463
3568
  const outer = useThemeOptional();
3464
- const shell = /* @__PURE__ */ jsxs("div", { className: "lk-root lk-shell", children: [
3465
- /* @__PURE__ */ jsxs("header", { className: "lk-shell-header", children: [
3466
- /* @__PURE__ */ jsx("h1", { className: "lk-shell-title", children: title }),
3467
- header && /* @__PURE__ */ jsx("div", { className: "lk-shell-header-actions", children: header })
3468
- ] }),
3469
- /* @__PURE__ */ jsx("main", { className: "lk-shell-body", children }),
3470
- footer && /* @__PURE__ */ jsx("footer", { className: "lk-shell-footer", children: footer })
3471
- ] });
3569
+ const shell = (
3570
+ // Overlays portal here rather than to the themed wrapper above: `.lk-root`
3571
+ // carries the element defaults a lab's bare markup is styled by.
3572
+ /* @__PURE__ */ jsxs("div", { className: "lk-root lk-shell", "data-wzl-portal-host": "", children: [
3573
+ /* @__PURE__ */ jsxs("header", { className: "lk-shell-header", children: [
3574
+ /* @__PURE__ */ jsx("h1", { className: "lk-shell-title", children: title }),
3575
+ header && /* @__PURE__ */ jsx("div", { className: "lk-shell-header-actions", children: header })
3576
+ ] }),
3577
+ /* @__PURE__ */ jsx("main", { className: "lk-shell-body", children }),
3578
+ footer && /* @__PURE__ */ jsx("footer", { className: "lk-shell-footer", children: footer })
3579
+ ] })
3580
+ );
3472
3581
  return outer ? shell : /* @__PURE__ */ jsx(ThemeProvider, { theme: interstellarTheme, mode: resolved, children: shell });
3473
3582
  }
3474
3583
  var ZONE_ID2 = asNodeId("lk-workspace");
@@ -3477,11 +3586,11 @@ var FLOAT_ZONE_ID = asNodeId("lk-workspace-floating");
3477
3586
  var PANEL_KIND = "panel";
3478
3587
  var KIND = "trial";
3479
3588
  function extentOf(store, id) {
3480
- const p2 = store.getNode(id)?.membership?.placement;
3481
- if (!p2) return null;
3589
+ const p = store.getNode(id)?.membership?.placement;
3590
+ if (!p) return null;
3482
3591
  const out = {};
3483
- if (p2.size) out.size = p2.size;
3484
- if (p2.span) out.span = p2.span;
3592
+ if (p.size) out.size = p.size;
3593
+ if (p.span) out.span = p.span;
3485
3594
  return Object.keys(out).length > 0 ? out : null;
3486
3595
  }
3487
3596
  function PanelFrame({ panel, floating = false }) {
@@ -3502,11 +3611,11 @@ function Workspace({
3502
3611
  viewport
3503
3612
  }) {
3504
3613
  const items = Children.toArray(children);
3505
- const tilePanels = useMemo(() => (panels ?? []).filter((p2) => p2.as === "tile"), [panels]);
3506
- const floatPanels = useMemo(() => (panels ?? []).filter((p2) => p2.as === "floating"), [panels]);
3614
+ const tilePanels = useMemo(() => (panels ?? []).filter((p) => p.as === "tile"), [panels]);
3615
+ const floatPanels = useMemo(() => (panels ?? []).filter((p) => p.as === "floating"), [panels]);
3507
3616
  const wantedIds = [
3508
- ...items.map((_2, i) => ids?.[i] ?? `lk-ws-${i}`),
3509
- ...tilePanels.map((p2) => `lk-panel-${p2.key}`)
3617
+ ...items.map((_, i) => ids?.[i] ?? `lk-ws-${i}`),
3618
+ ...tilePanels.map((p) => `lk-panel-${p.key}`)
3510
3619
  ];
3511
3620
  const idKey = wantedIds.join(",");
3512
3621
  const idKeyRef = useRef(null);
@@ -3583,7 +3692,7 @@ function Workspace({
3583
3692
  const chrome = useMemo(() => {
3584
3693
  const byId = new Map(nodeIds.map((id, i) => [id, items[i]]));
3585
3694
  const panelByNode = new Map(
3586
- tilePanels.map((p2) => [`lk-panel-${p2.key}`, p2])
3695
+ tilePanels.map((p) => [`lk-panel-${p.key}`, p])
3587
3696
  );
3588
3697
  return {
3589
3698
  [PANEL_KIND]: ({ node }) => {
@@ -3601,7 +3710,7 @@ function Workspace({
3601
3710
  }
3602
3711
  };
3603
3712
  }, [nodeIds, items, reorderable, tilePanels]);
3604
- const floatingLayer = floatPanels.length === 0 ? null : /* @__PURE__ */ jsx("div", { className: "lk-workspace__floating", children: floatPanels.map((p2) => /* @__PURE__ */ jsx(PanelFrame, { panel: p2, floating: true }, p2.key)) });
3713
+ const floatingLayer = floatPanels.length === 0 ? null : /* @__PURE__ */ jsx("div", { className: "lk-workspace__floating", children: floatPanels.map((p) => /* @__PURE__ */ jsx(PanelFrame, { panel: p, floating: true }, p.key)) });
3605
3714
  const grid = /* @__PURE__ */ jsx(
3606
3715
  Container,
3607
3716
  {
@@ -3624,7 +3733,13 @@ function Workspace({
3624
3733
  ) : reorderable ? /* @__PURE__ */ jsx(DragProvider, { children: grid }) : grid }) });
3625
3734
  }
3626
3735
  function buildStore(instruments, defaultInstrument, storage, storageKey, initialMode) {
3627
- const store = createLabStore({ storageKey, storage, initialMode });
3736
+ const store = createLabStore({
3737
+ storageKey,
3738
+ storage,
3739
+ initialMode,
3740
+ serializers: serializersOf(instruments),
3741
+ configDefaults: configDefaultsOf(instruments)
3742
+ });
3628
3743
  if (store.getState().trials.length === 0) {
3629
3744
  const seeded = addTrial([], instruments, defaultInstrument);
3630
3745
  const record = seeded[0];
@@ -3644,8 +3759,8 @@ var NEBULA_SLOTS = [
3644
3759
  ];
3645
3760
  function buildNebula(colors) {
3646
3761
  const blobs = colors.map((c2, i) => {
3647
- const p2 = NEBULA_SLOTS[i % NEBULA_SLOTS.length];
3648
- return `radial-gradient(ellipse ${p2.sx} ${p2.sy} at ${p2.cx} ${p2.cy}, color-mix(in srgb, ${c2} 22%, transparent), transparent ${p2.stop})`;
3762
+ const p = NEBULA_SLOTS[i % NEBULA_SLOTS.length];
3763
+ return `radial-gradient(ellipse ${p.sx} ${p.sy} at ${p.cx} ${p.cy}, color-mix(in srgb, ${c2} 22%, transparent), transparent ${p.stop})`;
3649
3764
  });
3650
3765
  blobs.push("radial-gradient(ellipse at center, #0a0a18 0%, #02020a 100%)");
3651
3766
  return blobs.join(", ");
@@ -3697,11 +3812,11 @@ function Lab({
3697
3812
  const c2 = ownCanvasRef.current;
3698
3813
  if (!c2) return;
3699
3814
  const w3 = Math.round(frame.size.width * frame.dpr);
3700
- const h2 = Math.round(frame.size.height * frame.dpr);
3701
- if (bufferRef.current.w === w3 && bufferRef.current.h === h2) return;
3702
- bufferRef.current = { w: w3, h: h2 };
3815
+ const h = Math.round(frame.size.height * frame.dpr);
3816
+ if (bufferRef.current.w === w3 && bufferRef.current.h === h) return;
3817
+ bufferRef.current = { w: w3, h };
3703
3818
  c2.width = w3;
3704
- c2.height = h2;
3819
+ c2.height = h;
3705
3820
  c2.style.width = `${frame.size.width}px`;
3706
3821
  c2.style.height = `${frame.size.height}px`;
3707
3822
  surfaceRef.current?.invalidateAll();
@@ -3734,8 +3849,8 @@ function Lab({
3734
3849
  return {
3735
3850
  instruments,
3736
3851
  trials,
3737
- addTrial: (instrumentName) => {
3738
- const next = addTrial(store.getState().trials, instruments, instrumentName);
3852
+ addTrial: (instrumentName, options) => {
3853
+ const next = addTrial(store.getState().trials, instruments, instrumentName, options);
3739
3854
  replaceTrials(next);
3740
3855
  },
3741
3856
  cloneTrial: (id) => {
@@ -3841,6 +3956,6 @@ function Lab({
3841
3956
  ) }) });
3842
3957
  }
3843
3958
 
3844
- export { AnnotationOverlay, AnnotationTargets, DEFAULT_VIEW, Lab, LabContext, LabPalette, LabShell, Trial, TrialBody, TrialChrome, Workspace, addTrial, annotationsFromJSON, applyRules, as2DView, builtinRules, capturePlan, captureTarget, cloneTrial, closeTrial, composeCaptureSvg, createAnnotationScene, createAnnotationStore, createMarkDrawOne, defineInstrument, f, fitView, fracContains, fracIntersects, fracToWorld, fromWeaselView, interstellarTheme, isStale, markCommands, markSvgNodes, reorderTrials, resetTrial, resolveConfigSchema, resolveMarkStyle, roundFrac, seenFrom, titleCase, toWeaselView, useConfigSchema, useLabContext, validateConfigSchema, worldToFrac };
3959
+ export { AnnotationOverlay, AnnotationTargets, DEFAULT_VIEW, Lab, LabContext, LabPalette, LabShell, Trial, TrialBody, TrialChrome, Workspace, addTrial, annotationsFromJSON, applyRules, as2DView, builtinRules, capturePlan, captureTarget, cloneTrial, closeTrial, composeCaptureSvg, createAnnotationScene, createAnnotationStore, createMarkDrawOne, defineInstrument, f, fitView, fracContains, fracIntersects, fracToWorld, fromWeaselView, interstellarTheme, isConfigBranch, isStale, markCommands, markSvgNodes, reorderTrials, resetTrial, resolveConfigSchema, resolveMarkStyle, roundFrac, seenFrom, titleCase, toWeaselView, useConfigSchema, useLabContext, validateConfigSchema, worldToFrac };
3845
3960
  //# sourceMappingURL=index.js.map
3846
3961
  //# sourceMappingURL=index.js.map