@weasel-js/labkit 1.4.2 → 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 (105) hide show
  1. package/README.md +20 -2
  2. package/dist/_dts/{CanvasStackContext-kjILVnPj.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-A4R6v4ld.d.ts → frac-B5mB8LsJ.d.ts} +7 -4
  5. package/dist/_dts/{index-CsU9WhjM.d.ts → index-DelFfeV0.d.ts} +15 -7
  6. package/dist/_dts/types-B9_zrHmb.d.ts +166 -0
  7. package/dist/_dts/{types-BbXpvQa8.d.ts → types-BP2OCcpg.d.ts} +21 -3
  8. package/dist/_dts/types-x92Kfeme.d.ts +62 -0
  9. package/dist/_dts/{useTrialState-DKjqpv20.d.ts → useTrialState-D6Vb3T-g.d.ts} +7 -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 +7 -6
  13. package/dist/chrome/index.js +5 -5
  14. package/dist/{chunk-UTOEDPNU.js → chunk-2RQPGOQF.js} +7 -6
  15. package/dist/chunk-2RQPGOQF.js.map +1 -0
  16. package/dist/{chunk-ULDW42CR.js → chunk-5TROQVQ2.js} +25 -21
  17. package/dist/chunk-5TROQVQ2.js.map +1 -0
  18. package/dist/{chunk-XKENZTNE.js → chunk-ASWKLRKJ.js} +65 -49
  19. package/dist/chunk-ASWKLRKJ.js.map +1 -0
  20. package/dist/{chunk-W2FJR5FF.js → chunk-C42T6DJT.js} +74 -76
  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-WS6ZRV75.js → chunk-FQMJUVHJ.js} +3 -3
  25. package/dist/{chunk-WS6ZRV75.js.map → chunk-FQMJUVHJ.js.map} +1 -1
  26. package/dist/{chunk-FJG4PHTL.js → chunk-I6JCVE24.js} +3 -3
  27. package/dist/chunk-I6JCVE24.js.map +1 -0
  28. package/dist/{chunk-NSOVI3AZ.js → chunk-ISCOFAUO.js} +6 -6
  29. package/dist/{chunk-NSOVI3AZ.js.map → chunk-ISCOFAUO.js.map} +1 -1
  30. package/dist/chunk-MHAGC6VD.js +7320 -0
  31. package/dist/chunk-MHAGC6VD.js.map +1 -0
  32. package/dist/{chunk-UDXOYZEC.js → chunk-QAGAHJKZ.js} +4 -4
  33. package/dist/{chunk-UDXOYZEC.js.map → chunk-QAGAHJKZ.js.map} +1 -1
  34. package/dist/{chunk-TN7YSJVU.js → chunk-RS3HRQWU.js} +3 -3
  35. package/dist/{chunk-TN7YSJVU.js.map → chunk-RS3HRQWU.js.map} +1 -1
  36. package/dist/{chunk-D5KQ5OY6.js → chunk-TJ7QY3OC.js} +3 -3
  37. package/dist/{chunk-D5KQ5OY6.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 +6 -5
  41. package/dist/dragdrop/index.js +1 -2
  42. package/dist/index.d.ts +90 -23
  43. package/dist/index.js +199 -117
  44. package/dist/index.js.map +1 -1
  45. package/dist/layers/index.d.ts +7 -6
  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 +88 -20
  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 +2 -2
  59. package/dist/surface/index.js +1 -2
  60. package/dist/ui/layers/index.js +1 -2
  61. package/dist/undo/index.d.ts +6 -6
  62. package/package.json +7 -7
  63. package/src/annotations/ExportMenu.test.tsx +16 -0
  64. package/src/annotations/ExportMenu.tsx +0 -5
  65. package/src/config/builder.test.ts +56 -2
  66. package/src/config/builder.ts +73 -13
  67. package/src/config/index.ts +16 -1
  68. package/src/config/path.test.ts +109 -0
  69. package/src/config/path.ts +76 -0
  70. package/src/config/resolve.test.ts +93 -2
  71. package/src/config/resolve.ts +72 -29
  72. package/src/config/types.ts +99 -9
  73. package/src/config/visible.ts +6 -4
  74. package/src/controls/ControlPanel.stories.tsx +38 -4
  75. package/src/controls/ControlPanel.test.tsx +114 -0
  76. package/src/controls/ControlPanel.tsx +99 -50
  77. package/src/index.ts +16 -1
  78. package/src/instrument/serializers.ts +36 -0
  79. package/src/instrument/types.ts +4 -2
  80. package/src/lab/Lab.test.tsx +29 -1
  81. package/src/lab/Lab.tsx +10 -1
  82. package/src/lab/LabShell.test.tsx +8 -0
  83. package/src/lab/LabShell.tsx +3 -1
  84. package/src/primitives/ZoomControl.less +6 -2
  85. package/src/primitives/ZoomControl.tsx +1 -0
  86. package/src/state/helpers.ts +1 -1
  87. package/src/state/store.test.ts +155 -1
  88. package/src/state/store.ts +30 -24
  89. package/src/state/types.ts +24 -3
  90. package/src/state/useTrialState.ts +1 -1
  91. package/src/trial/Trial.config.test.tsx +44 -0
  92. package/src/trial/Trial.tsx +3 -3
  93. package/src/trial/TrialChrome.tsx +10 -5
  94. package/src/trial/trialOps.ts +4 -8
  95. package/dist/_dts/types-lg4TSCb2.d.ts +0 -164
  96. package/dist/_dts/weasel-canvas-FJTFi3ZZ.d.ts +0 -5041
  97. package/dist/chunk-54ZWZ5FQ.js +0 -29997
  98. package/dist/chunk-54ZWZ5FQ.js.map +0 -1
  99. package/dist/chunk-FJG4PHTL.js.map +0 -1
  100. package/dist/chunk-ISSVF5PT.js +0 -7263
  101. package/dist/chunk-ISSVF5PT.js.map +0 -1
  102. package/dist/chunk-ULDW42CR.js.map +0 -1
  103. package/dist/chunk-UTOEDPNU.js.map +0 -1
  104. package/dist/chunk-W2FJR5FF.js.map +0 -1
  105. package/dist/chunk-XKENZTNE.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-TN7YSJVU.js';
4
- import { useSurfaceOptional, useSurfaceCanvas, useTileId, useTiledSurface, SurfaceContext, SurfaceCanvasContext } from './chunk-WS6ZRV75.js';
5
- export { SurfaceCanvasContext, SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTileId, useTiledSurface } from './chunk-WS6ZRV75.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-W2FJR5FF.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-W2FJR5FF.js';
10
- import { JobProgress } from './chunk-UDXOYZEC.js';
11
- export { FloatingPanel, JobProgress, Legend, Sidebar } from './chunk-UDXOYZEC.js';
12
- export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-UTOEDPNU.js';
13
- import { createLabStore, panelKey, noneAdapter } from './chunk-ULDW42CR.js';
14
- export { CURRENT_DOCUMENT_VERSION, createMemoryAdapter, dockPanel, labDocumentKey, localStorageAdapter, noneAdapter, panelKey, quarantineKey, sessionStorageAdapter, undockPanel, urlHashAdapter, useTrialState } from './chunk-ULDW42CR.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-XKENZTNE.js';
18
- export { ControlPanel, fromConfigFields, isLeafVisible } from './chunk-XKENZTNE.js';
19
- import { CanvasStack } from './chunk-FJG4PHTL.js';
20
- export { CanvasStack, PITCH_LIMIT, clampPitch, orbitAfterDrag, orbitAfterWheel, useOrbit, usePanZoom, wrapYaw } from './chunk-FJG4PHTL.js';
21
- import { LayerList } from './chunk-NSOVI3AZ.js';
22
- export { LayerList } from './chunk-NSOVI3AZ.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, h, r4 } from './chunk-ISSVF5PT.js';
25
- export { M2 as CheckboxRow, j2 as ColorRow, y4 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-ISSVF5PT.js';
26
- import { resolveLoupe, TrialLoupe } from './chunk-D5KQ5OY6.js';
27
- export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LoupeBubble, TrialLoupe, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe } from './chunk-D5KQ5OY6.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-54ZWZ5FQ.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,7 +40,6 @@ 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
44
  function multiply(a, b) {
43
45
  return [
@@ -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}"`);
@@ -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) {
@@ -631,8 +636,6 @@ function pathBounds(path) {
631
636
  const b = boundsOfPath(path);
632
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;
@@ -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
  }
@@ -1430,6 +1429,9 @@ function AnnotationTargets({
1430
1429
  }
1431
1430
 
1432
1431
  // src/config/builder.ts
1432
+ function isConfigBranch(entry) {
1433
+ return "children" in entry;
1434
+ }
1433
1435
  var BaseNode = class {
1434
1436
  constructor(defaultValue, annotations = {}, options = {}) {
1435
1437
  this.annotations = annotations;
@@ -1469,9 +1471,10 @@ var BaseNode = class {
1469
1471
  pair(pair) {
1470
1472
  return this.ann({ pair });
1471
1473
  }
1472
- /** Render under a named section heading. */
1473
- section(section) {
1474
- 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 } });
1475
1478
  }
1476
1479
  /** Show this row only while the predicate holds. Presentational — the value
1477
1480
  * stays in config and the instrument still reads it. */
@@ -1555,6 +1558,45 @@ var CustomNode = class _CustomNode extends BaseNode {
1555
1558
  return new _CustomNode(this.kind, this.default, annotations, options);
1556
1559
  }
1557
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
+ }
1558
1600
  var expand = (options) => options.map((o) => typeof o === "string" ? { value: o, label: o } : o);
1559
1601
  var f = {
1560
1602
  number: (def) => new NumberNode(def),
@@ -1568,18 +1610,15 @@ var f = {
1568
1610
  /** A leaf with no declared kind: the rule chain decides, which is how a lab
1569
1611
  * states a convention like "every `*Color` key is a color picker". */
1570
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),
1571
1617
  /** A leaf of a kind a lab supplies the control for, through `controls`. */
1572
1618
  custom: (kind, def, validate) => new CustomNode(kind, def, {}, validate ? { validate } : {}),
1573
- /** Collect leaves into an instrument's config. */
1619
+ /** Collect leaves and groups into an instrument's config. */
1574
1620
  schema(nodes) {
1575
- return {
1576
- nodes,
1577
- defaults: () => {
1578
- const out = {};
1579
- for (const [key, node] of Object.entries(nodes)) out[key] = node.default;
1580
- return out;
1581
- }
1582
- };
1621
+ return { nodes, defaults: () => shapeDefaults(nodes) };
1583
1622
  }
1584
1623
  };
1585
1624
 
@@ -1627,36 +1666,55 @@ function defined(annotations) {
1627
1666
  return out;
1628
1667
  }
1629
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) {
1630
1679
  const children = {};
1631
- const sectionOrder = [];
1632
- const sectionPaths = /* @__PURE__ */ new Map();
1633
- const showIf = /* @__PURE__ */ new Map();
1634
- const renderers = {};
1635
- const chain2 = [...rules, ...builtinRules];
1636
- for (const [key, node] of Object.entries(schema.nodes)) {
1637
- const seed = defined({
1638
- ...node.annotations,
1639
- ...node.kind === null ? {} : { kind: node.kind }
1640
- });
1641
- const patch = applyRules(seed, { key, path: key, default: node.default }, chain2);
1642
- children[key] = { ...patch, default: node.default };
1643
- 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;
1644
1685
  if (section !== void 0) {
1645
- if (!sectionPaths.has(section)) {
1646
- sectionOrder.push(section);
1647
- 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;
1648
1695
  }
1649
- sectionPaths.get(section)?.push(key);
1650
1696
  }
1651
- if (predicate) showIf.set(key, predicate);
1652
- 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 };
1653
1711
  }
1654
- const group = { name: "", children };
1655
- const sections = sectionOrder.map((label) => ({
1656
- label,
1657
- paths: sectionPaths.get(label) ?? []
1658
- }));
1659
- 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 };
1660
1718
  }
1661
1719
  var LabContext = createContext(null);
1662
1720
  function useLabContext() {
@@ -2304,9 +2362,9 @@ function hexToRgba(hex, alpha) {
2304
2362
  const body = m[1];
2305
2363
  const full = body.length === 3 ? body.replace(/./g, (c2) => c2 + c2) : body;
2306
2364
  const r2 = Number.parseInt(full.slice(0, 2), 16);
2307
- const g = Number.parseInt(full.slice(2, 4), 16);
2365
+ const g2 = Number.parseInt(full.slice(2, 4), 16);
2308
2366
  const b = Number.parseInt(full.slice(4, 6), 16);
2309
- return `rgba(${r2}, ${g}, ${b}, ${alpha})`;
2367
+ return `rgba(${r2}, ${g2}, ${b}, ${alpha})`;
2310
2368
  }
2311
2369
  var REF = /^\{([^}]+)\}$/;
2312
2370
  function refTarget(value) {
@@ -2444,6 +2502,25 @@ function useThemeOptional() {
2444
2502
  return useContext(ThemeContext);
2445
2503
  }
2446
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
+
2447
2524
  // ../theme/dist/index.js
2448
2525
  var ALPHA_EXT = "com.weasel.alpha";
2449
2526
  var isToken = (v) => typeof v === "object" && v !== null && "$value" in v;
@@ -2452,9 +2529,9 @@ function flattenTokens(doc) {
2452
2529
  for (const [groupName, group] of Object.entries(doc)) {
2453
2530
  if (groupName.startsWith("$")) continue;
2454
2531
  if (typeof group !== "object" || group === null) continue;
2455
- const g = group;
2456
- const groupType = g.$type;
2457
- 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)) {
2458
2535
  if (leaf.startsWith("$")) continue;
2459
2536
  if (!isToken(node)) continue;
2460
2537
  const type = node.$type ?? groupType;
@@ -2813,16 +2890,16 @@ function TrialChrome({
2813
2890
  configSchema,
2814
2891
  configFields: instrument.config ? [] : instrument.configSchema?.() ?? [],
2815
2892
  config: record.config,
2816
- setConfig: (key, value) => {
2893
+ setConfig: (path, value) => {
2817
2894
  const prevConfig = record.config;
2818
- 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) {
2819
2896
  console.warn(
2820
- `[labkit] setConfig: unknown key "${key}" for instrument "${record.instrumentName}"`
2897
+ `[labkit] setConfig: unknown path "${path}" for instrument "${record.instrumentName}"`
2821
2898
  );
2822
2899
  }
2823
- updateTrialConfig(trialId, key, value);
2900
+ updateTrialConfig(trialId, path, value);
2824
2901
  if (instrument.onConfigChange) {
2825
- const nextConfig = { ...prevConfig, [key]: value };
2902
+ const nextConfig = withValueAtPath(prevConfig, path, value);
2826
2903
  const nextState = instrument.onConfigChange(nextConfig, prevConfig, record.state);
2827
2904
  updateTrialState(trialId, nextState);
2828
2905
  }
@@ -3086,11 +3163,11 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
3086
3163
  updateTrialState(record.id, next);
3087
3164
  bus.emit("state.change");
3088
3165
  },
3089
- setConfig: (key, value) => {
3090
- const evt = `config.change:${String(key)}`;
3166
+ setConfig: (path, value) => {
3167
+ const evt = `config.change:${path}`;
3091
3168
  snapshotIfNeeded("config.change");
3092
3169
  snapshotIfNeeded(evt);
3093
- updateTrialConfig(record.id, key, value);
3170
+ updateTrialConfig(record.id, path, value);
3094
3171
  bus.emit("config.change");
3095
3172
  bus.emit(evt);
3096
3173
  },
@@ -3335,12 +3412,7 @@ function initialView(instrument) {
3335
3412
  return declared ? structuredClone(declared) : { ...DEFAULT_VIEW2, pan: { ...DEFAULT_VIEW2.pan } };
3336
3413
  }
3337
3414
  function seedConfig(defaults, seed) {
3338
- if (!seed) return defaults;
3339
- const out = { ...defaults };
3340
- for (const [key, value] of Object.entries(seed)) {
3341
- if (value !== void 0) out[key] = value;
3342
- }
3343
- return out;
3415
+ return seed ? fillConfigDefaults(seed, defaults) : defaults;
3344
3416
  }
3345
3417
  function addTrial(trials, instruments, instrumentName, options = {}) {
3346
3418
  const instrument = findInstrument(instruments, instrumentName);
@@ -3427,7 +3499,7 @@ function LabHeader() {
3427
3499
  // Held at null so the control stays an "add one" action rather than
3428
3500
  // reading as the current instrument.
3429
3501
  /* @__PURE__ */ jsx(
3430
- h,
3502
+ g,
3431
3503
  {
3432
3504
  className: "lk-lab-header__add-select",
3433
3505
  "aria-label": "Add trial",
@@ -3441,7 +3513,7 @@ function LabHeader() {
3441
3513
  )
3442
3514
  ),
3443
3515
  /* @__PURE__ */ jsx(
3444
- r4,
3516
+ r6,
3445
3517
  {
3446
3518
  className: "lk-lab-header__mode",
3447
3519
  ariaLabel: "Color mode",
@@ -3494,14 +3566,18 @@ function useResolvedMode(mode) {
3494
3566
  function LabShell({ title, children, header, footer, mode = "auto" }) {
3495
3567
  const resolved = useResolvedMode(mode);
3496
3568
  const outer = useThemeOptional();
3497
- const shell = /* @__PURE__ */ jsxs("div", { className: "lk-root lk-shell", children: [
3498
- /* @__PURE__ */ jsxs("header", { className: "lk-shell-header", children: [
3499
- /* @__PURE__ */ jsx("h1", { className: "lk-shell-title", children: title }),
3500
- header && /* @__PURE__ */ jsx("div", { className: "lk-shell-header-actions", children: header })
3501
- ] }),
3502
- /* @__PURE__ */ jsx("main", { className: "lk-shell-body", children }),
3503
- footer && /* @__PURE__ */ jsx("footer", { className: "lk-shell-footer", children: footer })
3504
- ] });
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
+ );
3505
3581
  return outer ? shell : /* @__PURE__ */ jsx(ThemeProvider, { theme: interstellarTheme, mode: resolved, children: shell });
3506
3582
  }
3507
3583
  var ZONE_ID2 = asNodeId("lk-workspace");
@@ -3657,7 +3733,13 @@ function Workspace({
3657
3733
  ) : reorderable ? /* @__PURE__ */ jsx(DragProvider, { children: grid }) : grid }) });
3658
3734
  }
3659
3735
  function buildStore(instruments, defaultInstrument, storage, storageKey, initialMode) {
3660
- 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
+ });
3661
3743
  if (store.getState().trials.length === 0) {
3662
3744
  const seeded = addTrial([], instruments, defaultInstrument);
3663
3745
  const record = seeded[0];
@@ -3730,11 +3812,11 @@ function Lab({
3730
3812
  const c2 = ownCanvasRef.current;
3731
3813
  if (!c2) return;
3732
3814
  const w3 = Math.round(frame.size.width * frame.dpr);
3733
- const h2 = Math.round(frame.size.height * frame.dpr);
3734
- if (bufferRef.current.w === w3 && bufferRef.current.h === h2) return;
3735
- 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 };
3736
3818
  c2.width = w3;
3737
- c2.height = h2;
3819
+ c2.height = h;
3738
3820
  c2.style.width = `${frame.size.width}px`;
3739
3821
  c2.style.height = `${frame.size.height}px`;
3740
3822
  surfaceRef.current?.invalidateAll();
@@ -3874,6 +3956,6 @@ function Lab({
3874
3956
  ) }) });
3875
3957
  }
3876
3958
 
3877
- 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 };
3878
3960
  //# sourceMappingURL=index.js.map
3879
3961
  //# sourceMappingURL=index.js.map