@weasel-js/labkit 1.0.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +33 -6
  2. package/dist/_dts/{DrawCommand-BkZztJsW.d.ts → DrawCommand-C_XboUpz.d.ts} +48 -6
  3. package/dist/_dts/{index-CFlDPeZh.d.ts → index-JFAYj5Tv.d.ts} +29 -7
  4. package/dist/_dts/types-DJ79Tg5J.d.ts +56 -0
  5. package/dist/_dts/useTrialState-BMNIx3Cy.d.ts +171 -0
  6. package/dist/canvas/index.d.ts +2 -1
  7. package/dist/{chunk-BAPZPDDA.js → chunk-73KA7WBO.js} +425 -394
  8. package/dist/chunk-73KA7WBO.js.map +1 -0
  9. package/dist/{chunk-DWV7SFKR.js → chunk-BOHF3PQO.js} +4397 -3726
  10. package/dist/chunk-BOHF3PQO.js.map +1 -0
  11. package/dist/{chunk-DEWXYFEU.js → chunk-G5TJVQQT.js} +3 -3
  12. package/dist/{chunk-DEWXYFEU.js.map → chunk-G5TJVQQT.js.map} +1 -1
  13. package/dist/chunk-LN6JDUGB.js +106 -0
  14. package/dist/chunk-LN6JDUGB.js.map +1 -0
  15. package/dist/chunk-THBG7FQZ.js +167 -0
  16. package/dist/chunk-THBG7FQZ.js.map +1 -0
  17. package/dist/chunk-VUU5UXHE.js +491 -0
  18. package/dist/chunk-VUU5UXHE.js.map +1 -0
  19. package/dist/dragdrop/index.d.ts +2 -1
  20. package/dist/index.d.ts +211 -151
  21. package/dist/index.js +317 -169
  22. package/dist/index.js.map +1 -1
  23. package/dist/job/index.d.ts +13 -0
  24. package/dist/job/index.js +3 -0
  25. package/dist/job/index.js.map +1 -0
  26. package/dist/layers/index.d.ts +3 -2
  27. package/dist/passthrough/weasel-canvas.d.ts +1 -1
  28. package/dist/passthrough/weasel-canvas.js +1 -1
  29. package/dist/passthrough/weasel-ui.d.ts +29 -6
  30. package/dist/passthrough/weasel-ui.js +2 -2
  31. package/dist/state/index.d.ts +12 -12
  32. package/dist/state/index.js +5 -5
  33. package/dist/state/index.js.map +1 -1
  34. package/dist/styles.css +31 -13
  35. package/dist/surface/index.d.ts +77 -0
  36. package/dist/surface/index.js +3 -0
  37. package/dist/surface/index.js.map +1 -0
  38. package/dist/ui/layers/index.js +3 -3
  39. package/dist/undo/index.d.ts +2 -1
  40. package/package.json +9 -1
  41. package/src/canvas/AGENTS.md +14 -2
  42. package/src/canvas/useOrbit.test.ts +71 -0
  43. package/src/canvas/useOrbit.ts +149 -0
  44. package/src/dragdrop/dragDrop.test.tsx +4 -7
  45. package/src/index.test.ts +70 -0
  46. package/src/index.ts +24 -7
  47. package/src/instrument/SineWave.smoke.test.tsx +2 -1
  48. package/src/instrument/capabilityDetector.ts +1 -1
  49. package/src/instrument/types.ts +30 -6
  50. package/src/job/index.ts +3 -0
  51. package/src/job/types.ts +47 -0
  52. package/src/job/useJob.test.tsx +210 -0
  53. package/src/job/useJob.ts +134 -0
  54. package/src/lab/Lab.less +16 -0
  55. package/src/lab/Lab.stories.tsx +6 -7
  56. package/src/lab/Lab.test.tsx +25 -25
  57. package/src/lab/Lab.tsx +50 -50
  58. package/src/lab/LabContext.ts +12 -12
  59. package/src/lab/LabShell.tsx +1 -1
  60. package/src/lab/{WorkspaceGrid.less → Workspace.less} +2 -2
  61. package/src/lab/{WorkspaceGrid.stories.tsx → Workspace.stories.tsx} +13 -13
  62. package/src/lab/Workspace.surface.test.tsx +49 -0
  63. package/src/lab/{WorkspaceGrid.test.tsx → Workspace.test.tsx} +24 -24
  64. package/src/lab/{WorkspaceGrid.tsx → Workspace.tsx} +32 -19
  65. package/src/lab/index.ts +2 -2
  66. package/src/layers/AGENTS.md +3 -3
  67. package/src/primitives/Toolbar.stories.tsx +2 -2
  68. package/src/state/SingletonExperiment.test.tsx +8 -8
  69. package/src/state/SingletonExperiment.tsx +9 -9
  70. package/src/state/context.tsx +12 -12
  71. package/src/state/document.test.ts +369 -0
  72. package/src/state/document.ts +194 -0
  73. package/src/state/helpers.test.ts +74 -46
  74. package/src/state/helpers.ts +21 -29
  75. package/src/state/index.ts +16 -8
  76. package/src/state/store.test.ts +440 -41
  77. package/src/state/store.ts +165 -102
  78. package/src/state/types.ts +32 -13
  79. package/src/state/{useExperimentState.test.tsx → useTrialState.test.tsx} +18 -18
  80. package/src/state/useTrialState.ts +29 -0
  81. package/src/state/view.test.ts +127 -0
  82. package/src/state/view.ts +18 -0
  83. package/src/styles.less +2 -2
  84. package/src/surface/AGENTS.md +64 -0
  85. package/src/surface/SurfaceContext.ts +5 -0
  86. package/src/surface/composeRects.test.ts +50 -0
  87. package/src/surface/composeRects.ts +19 -0
  88. package/src/surface/deviceRect.test.ts +40 -0
  89. package/src/surface/deviceRect.ts +19 -0
  90. package/src/surface/index.ts +7 -0
  91. package/src/surface/rect.ts +16 -0
  92. package/src/surface/useSurfaceTile.test.tsx +67 -0
  93. package/src/surface/useSurfaceTile.ts +32 -0
  94. package/src/surface/useTiledSurface.test.tsx +231 -0
  95. package/src/surface/useTiledSurface.ts +157 -0
  96. package/src/theme/base.less +1 -1
  97. package/src/{workspace → trial}/DefaultSidebar.tsx +3 -3
  98. package/src/trial/DefaultStatusBar.tsx +19 -0
  99. package/src/{workspace → trial}/DefaultToolbar.tsx +9 -8
  100. package/src/trial/Trial.canvas.test.tsx +80 -0
  101. package/src/trial/Trial.job.test.tsx +75 -0
  102. package/src/{workspace/Workspace.less → trial/Trial.less} +13 -1
  103. package/src/{workspace/Workspace.stories.tsx → trial/Trial.stories.tsx} +20 -18
  104. package/src/{workspace/Workspace.test.tsx → trial/Trial.test.tsx} +26 -26
  105. package/src/{workspace/Workspace.tsx → trial/Trial.tsx} +77 -46
  106. package/src/{workspace/WorkspaceChrome.tsx → trial/TrialChrome.tsx} +73 -46
  107. package/src/{workspace → trial}/index.ts +13 -13
  108. package/src/{workspace → trial}/slotTypes.ts +18 -17
  109. package/src/{workspace/workspaceOps.test.ts → trial/trialOps.test.ts} +38 -44
  110. package/src/trial/trialOps.ts +99 -0
  111. package/src/ui/format.test.ts +33 -0
  112. package/src/ui/format.ts +11 -0
  113. package/src/ui/properties/CurveField.tsx +6 -6
  114. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +3 -3
  115. package/dist/_dts/useExperimentState-D7EQnnwJ.d.ts +0 -151
  116. package/dist/chunk-BAPZPDDA.js.map +0 -1
  117. package/dist/chunk-DWV7SFKR.js.map +0 -1
  118. package/dist/chunk-KSTEW2AF.js +0 -367
  119. package/dist/chunk-KSTEW2AF.js.map +0 -1
  120. package/src/state/useExperimentState.ts +0 -31
  121. package/src/workspace/DefaultStatusBar.tsx +0 -18
  122. package/src/workspace/workspaceOps.ts +0 -102
package/dist/index.js CHANGED
@@ -1,10 +1,14 @@
1
- export { LayerStack } from './chunk-DEWXYFEU.js';
2
- import { p, F } from './chunk-BAPZPDDA.js';
3
- import './chunk-DWV7SFKR.js';
1
+ import { useSurfaceOptional } from './chunk-THBG7FQZ.js';
2
+ export { SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceOptional, useSurfaceTile, useTiledSurface } from './chunk-THBG7FQZ.js';
3
+ import { useJob } from './chunk-LN6JDUGB.js';
4
+ export { useJob } from './chunk-LN6JDUGB.js';
5
+ export { LayerStack } from './chunk-G5TJVQQT.js';
6
+ import { p, F } from './chunk-73KA7WBO.js';
7
+ import './chunk-BOHF3PQO.js';
4
8
  import { Sidebar, StatusBar, Toolbar } from './chunk-PMAU3SEE.js';
5
9
  export { FpsMeter, ScaleIndicator, Sidebar, StatusBar, Toolbar } from './chunk-PMAU3SEE.js';
6
- import { LabStoreContext, createLabStore, noneAdapter } from './chunk-KSTEW2AF.js';
7
- export { LabStoreContext, LabStoreProvider, WorkspaceIdContext, WorkspaceIdProvider, createMemoryAdapter, localStorageAdapter, noneAdapter, sessionStorageAdapter, urlHashAdapter, useExperimentState, useLabStore, useWorkspaceId } from './chunk-KSTEW2AF.js';
10
+ import { LabStoreContext, createLabStore, noneAdapter } from './chunk-VUU5UXHE.js';
11
+ export { CURRENT_DOCUMENT_VERSION, LabStoreContext, LabStoreProvider, TrialIdContext, TrialIdProvider, createMemoryAdapter, labDocumentKey, localStorageAdapter, noneAdapter, quarantineKey, sessionStorageAdapter, urlHashAdapter, useLabStore, useTrialId, useTrialState } from './chunk-VUU5UXHE.js';
8
12
  import { ControlPanel } from './chunk-5R2ATYPJ.js';
9
13
  export { ControlPanel } from './chunk-5R2ATYPJ.js';
10
14
  import { CanvasStack, screenToWorld } from './chunk-574LJAV4.js';
@@ -14,13 +18,94 @@ import { LayerList } from './chunk-SFL7NFKN.js';
14
18
  export { LayerList } from './chunk-SFL7NFKN.js';
15
19
  import { createEventBus, redo, undo, pushSnapshot } from './chunk-RL2LOLNI.js';
16
20
  export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
17
- import { createContext, useContext, useMemo, useRef, useState, Children, useLayoutEffect, useEffect, useCallback } from 'react';
21
+ import { createContext, useRef, useCallback, useContext, useMemo, useState, Children, useLayoutEffect, useEffect } from 'react';
18
22
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
19
23
  import { useStore } from 'zustand/react';
20
24
  import { createPortal } from 'react-dom';
21
25
  import { asNodeId, Store, createNode, gridStrategy } from 'windease';
22
26
  import { DragHandle, Provider, StrategyRegistryProvider, DragProvider, Container } from 'windease/react';
23
27
 
28
+ var PITCH_LIMIT = Math.PI / 2 - 0.01;
29
+ var YAW_PER_PX = 8e-3;
30
+ var PITCH_PER_PX = 8e-3;
31
+ var DISTANCE_PER_NOTCH = 15e-4;
32
+ var DRAG_THRESHOLD = 3;
33
+ function clampPitch(pitch) {
34
+ return Math.min(PITCH_LIMIT, Math.max(-PITCH_LIMIT, pitch));
35
+ }
36
+ function wrapYaw(yaw) {
37
+ const wrapped = ((yaw + Math.PI) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI) - Math.PI;
38
+ return wrapped === -Math.PI ? Math.PI : wrapped;
39
+ }
40
+ function orbitAfterDrag(start, dx, dy) {
41
+ return {
42
+ ...start,
43
+ yaw: wrapYaw(start.yaw + dx * YAW_PER_PX),
44
+ pitch: clampPitch(start.pitch + dy * PITCH_PER_PX)
45
+ };
46
+ }
47
+ function orbitAfterWheel(view, deltaY, minDistance, maxDistance) {
48
+ const factor = Math.exp(deltaY * DISTANCE_PER_NOTCH);
49
+ return {
50
+ ...view,
51
+ distance: Math.min(maxDistance, Math.max(minDistance, view.distance * factor))
52
+ };
53
+ }
54
+ function useOrbit({
55
+ view,
56
+ onViewChange,
57
+ home,
58
+ minDistance = 0.1,
59
+ maxDistance = 1e3
60
+ }) {
61
+ const dragRef = useRef(null);
62
+ const viewRef = useRef(view);
63
+ viewRef.current = view;
64
+ const homeRef = useRef(home ?? view);
65
+ if (home) homeRef.current = home;
66
+ const onWheel = useCallback(
67
+ (e) => {
68
+ e.preventDefault();
69
+ onViewChange(orbitAfterWheel(viewRef.current, e.deltaY, minDistance, maxDistance));
70
+ },
71
+ [onViewChange, minDistance, maxDistance]
72
+ );
73
+ const onPointerDown = useCallback((e) => {
74
+ if (e.button !== 0) return;
75
+ e.currentTarget.setPointerCapture(e.pointerId);
76
+ dragRef.current = {
77
+ pointerId: e.pointerId,
78
+ startX: e.clientX,
79
+ startY: e.clientY,
80
+ startView: viewRef.current,
81
+ moved: false
82
+ };
83
+ }, []);
84
+ const onPointerMove = useCallback(
85
+ (e) => {
86
+ const drag = dragRef.current;
87
+ if (!drag || drag.pointerId !== e.pointerId) return;
88
+ const dx = e.clientX - drag.startX;
89
+ const dy = e.clientY - drag.startY;
90
+ if (!drag.moved && Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
91
+ drag.moved = true;
92
+ onViewChange(orbitAfterDrag(drag.startView, dx, dy));
93
+ },
94
+ [onViewChange]
95
+ );
96
+ const onPointerUp = useCallback((e) => {
97
+ const drag = dragRef.current;
98
+ if (!drag || drag.pointerId !== e.pointerId) return;
99
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) {
100
+ e.currentTarget.releasePointerCapture(e.pointerId);
101
+ }
102
+ dragRef.current = null;
103
+ }, []);
104
+ const onDoubleClick = useCallback(() => onViewChange(homeRef.current), [onViewChange]);
105
+ const isDragging = useCallback(() => dragRef.current?.moved === true, []);
106
+ return { onWheel, onPointerDown, onPointerMove, onPointerUp, onDoubleClick, isDragging };
107
+ }
108
+
24
109
  // src/instrument/capabilityDetector.ts
25
110
  function detectCapabilities(instrument) {
26
111
  return {
@@ -90,7 +175,7 @@ function validateConfigSchema(fields) {
90
175
  return { valid: errors.length === 0, errors };
91
176
  }
92
177
 
93
- // ../theme/dist/chunk-STFENPNX.js
178
+ // ../theme/dist/chunk-E3FOFGMD.js
94
179
  var THEME_SOURCES = {
95
180
  "weasel": {
96
181
  "primitives": {
@@ -630,19 +715,20 @@ function resolveTheme(theme, mode) {
630
715
  return out;
631
716
  }
632
717
  var STYLE_ID = "wzl-themes";
633
- var emitted = /* @__PURE__ */ new Set();
718
+ var emitted = /* @__PURE__ */ new Map();
634
719
  var sheet = null;
635
720
  var styleEl = null;
636
721
  function canAdopt() {
637
722
  return typeof CSSStyleSheet !== "undefined" && "replaceSync" in CSSStyleSheet.prototype && Array.isArray(document.adoptedStyleSheets);
638
723
  }
639
- function appendRule(css) {
724
+ function flushRules() {
725
+ const css = [...emitted.values()].join("\n");
640
726
  if (canAdopt()) {
641
727
  if (!sheet) {
642
728
  sheet = new CSSStyleSheet();
643
729
  document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
644
730
  }
645
- sheet.insertRule(css, sheet.cssRules.length);
731
+ sheet.replaceSync(css);
646
732
  return;
647
733
  }
648
734
  if (!styleEl) {
@@ -650,17 +736,17 @@ function appendRule(css) {
650
736
  styleEl.id = STYLE_ID;
651
737
  document.head.appendChild(styleEl);
652
738
  }
653
- styleEl.textContent += `${css}
654
- `;
739
+ styleEl.textContent = css;
655
740
  }
656
741
  function applyTheme(el, theme, mode) {
657
742
  if (typeof document === "undefined") return;
658
743
  const key = `${theme.name}::${mode}`;
659
- if (!emitted.has(key)) {
660
- const resolved = resolveTheme(theme, mode);
661
- const body = Object.entries(resolved).map(([name, value]) => `${name}: ${value};`).join(" ");
662
- appendRule(`[data-wzl-theme='${theme.name}'][data-wzl-mode='${mode}'] { ${body} }`);
663
- emitted.add(key);
744
+ const resolved = resolveTheme(theme, mode);
745
+ const body = Object.entries(resolved).map(([name, value]) => `${name}: ${value};`).join(" ");
746
+ const rule = `[data-wzl-theme='${theme.name}'][data-wzl-mode='${mode}'] { ${body} }`;
747
+ if (emitted.get(key) !== rule) {
748
+ emitted.set(key, rule);
749
+ flushRules();
664
750
  }
665
751
  el.setAttribute("data-wzl-theme", theme.name);
666
752
  el.setAttribute("data-wzl-mode", mode);
@@ -933,6 +1019,17 @@ function useLabContext() {
933
1019
  }
934
1020
  return ctx;
935
1021
  }
1022
+
1023
+ // src/state/view.ts
1024
+ var DEFAULT_VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
1025
+ function as2DView(view) {
1026
+ if (typeof view !== "object" || view === null) return null;
1027
+ const v = view;
1028
+ if (typeof v.zoom !== "number") return null;
1029
+ if (typeof v.pan !== "object" || v.pan === null) return null;
1030
+ if (typeof v.pan.x !== "number" || typeof v.pan.y !== "number") return null;
1031
+ return { zoom: v.zoom, pan: { x: v.pan.x, y: v.pan.y } };
1032
+ }
936
1033
  function DefaultSidebar({ ctx }) {
937
1034
  if (ctx.configFields.length === 0) {
938
1035
  return /* @__PURE__ */ jsx(Sidebar, { title: ctx.instrumentName, children: /* @__PURE__ */ jsxs("div", { className: "lk-sidebar__placeholder", children: [
@@ -949,13 +1046,27 @@ function DefaultSidebar({ ctx }) {
949
1046
  }
950
1047
  ) });
951
1048
  }
1049
+
1050
+ // src/ui/format.ts
1051
+ var MINUS = "\u2212";
1052
+ function formatNumber(n, fractionDigits) {
1053
+ if (!Number.isFinite(n)) return String(n);
1054
+ const s = fractionDigits == null ? String(n) : n.toFixed(fractionDigits);
1055
+ return s.startsWith("-") ? MINUS + s.slice(1) : s;
1056
+ }
1057
+ function parseSignedNumber(s) {
1058
+ return Number(s.replace(MINUS, "-"));
1059
+ }
1060
+ function formatZoom(zoom) {
1061
+ if (!Number.isFinite(zoom)) return String(zoom);
1062
+ if (zoom <= 2) return `${formatNumber(Math.round(zoom * 100))}%`;
1063
+ if (zoom >= 100) return `${Math.round(zoom).toLocaleString("en-US")}x`;
1064
+ return `${formatNumber(Math.round(zoom * 10) / 10)}x`;
1065
+ }
952
1066
  function DefaultStatusBar({ ctx }) {
953
1067
  return /* @__PURE__ */ jsxs(StatusBar, { children: [
954
1068
  /* @__PURE__ */ jsx(StatusBar.Section, { children: ctx.instrumentName }),
955
- /* @__PURE__ */ jsxs(StatusBar.Section, { children: [
956
- Math.round(ctx.zoom * 100),
957
- "%"
958
- ] })
1069
+ ctx.zoom === null ? null : /* @__PURE__ */ jsx(StatusBar.Section, { children: formatZoom(ctx.zoom) })
959
1070
  ] });
960
1071
  }
961
1072
  function DefaultToolbar({ ctx }) {
@@ -975,10 +1086,7 @@ function DefaultToolbar({ ctx }) {
975
1086
  ] }),
976
1087
  ctx.hasCanvas && /* @__PURE__ */ jsxs(Fragment, { children: [
977
1088
  /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.zoomOut, title: "Zoom out", children: "\u2212" }),
978
- /* @__PURE__ */ jsxs("span", { className: "lk-toolbar__zoom-label", children: [
979
- Math.round(ctx.zoom * 100),
980
- "%"
981
- ] }),
1089
+ /* @__PURE__ */ jsx("span", { className: "lk-toolbar__zoom-label", children: formatZoom(ctx.zoom) }),
982
1090
  /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.zoomIn, title: "Zoom in", children: "+" }),
983
1091
  /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.resetZoom, title: "Reset zoom", children: "1:1" }),
984
1092
  /* @__PURE__ */ jsx(Toolbar.Spacer, {})
@@ -1004,43 +1112,46 @@ function DefaultToolbar({ ctx }) {
1004
1112
  }
1005
1113
  ),
1006
1114
  /* @__PURE__ */ jsx(Toolbar.Spacer, {}),
1007
- /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.clone, title: "Clone workspace", children: "Clone" }),
1008
- /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.reset, title: "Reset workspace", children: "Reset" }),
1115
+ /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.clone, title: "Clone trial", children: "Clone" }),
1116
+ /* @__PURE__ */ jsx(Toolbar.Button, { onClick: ctx.reset, title: "Reset trial", children: "Reset" }),
1009
1117
  /* @__PURE__ */ jsx(
1010
1118
  Toolbar.Button,
1011
1119
  {
1012
1120
  onClick: ctx.close,
1013
- disabled: ctx.isLastWorkspace,
1014
- title: ctx.isLastWorkspace ? "Cannot close the last workspace" : "Close workspace",
1121
+ disabled: ctx.isLastTrial,
1122
+ title: ctx.isLastTrial ? "Cannot close the last trial" : "Close trial",
1015
1123
  children: "Close"
1016
1124
  }
1017
1125
  )
1018
1126
  ] });
1019
1127
  }
1020
- function WorkspaceChrome({
1021
- workspaceId,
1128
+ function TrialChrome({
1129
+ trialId,
1022
1130
  record,
1023
1131
  instrument,
1024
- isLastWorkspace,
1132
+ isLastTrial,
1025
1133
  toolbar,
1026
1134
  sidebar,
1027
1135
  statusBar,
1028
1136
  undoBindings,
1137
+ job,
1029
1138
  sidebarExtras,
1030
1139
  children
1031
1140
  }) {
1032
1141
  const lab = useLabContext();
1033
1142
  const storeCtx = useContext(LabStoreContext);
1034
- if (!storeCtx) throw new Error("[labkit] WorkspaceChrome requires <LabStoreProvider>");
1035
- const updateWorkspaceView = useStore(storeCtx.store, (s) => s.updateWorkspaceView);
1036
- const updateWorkspaceConfig = useStore(storeCtx.store, (s) => s.updateWorkspaceConfig);
1037
- const updateWorkspaceState = useStore(storeCtx.store, (s) => s.updateWorkspaceState);
1143
+ if (!storeCtx) throw new Error("[labkit] TrialChrome requires <LabStoreProvider>");
1144
+ const updateTrialView = useStore(storeCtx.store, (s) => s.updateTrialView);
1145
+ const updateTrialConfig = useStore(storeCtx.store, (s) => s.updateTrialConfig);
1146
+ const updateTrialState = useStore(storeCtx.store, (s) => s.updateTrialState);
1147
+ const view2d = as2DView(record.view);
1038
1148
  const toolbarCtx = useMemo(() => {
1039
1149
  const setZoom = (z) => {
1040
- updateWorkspaceView(workspaceId, { ...record.view, zoom: z });
1150
+ if (!view2d) return;
1151
+ updateTrialView(trialId, { ...view2d, zoom: z });
1041
1152
  };
1042
1153
  return {
1043
- workspaceId,
1154
+ trialId,
1044
1155
  instrumentName: record.instrumentName,
1045
1156
  hasUndo: instrument.undo != null,
1046
1157
  canUndo: undoBindings?.canUndo ?? false,
@@ -1049,24 +1160,24 @@ function WorkspaceChrome({
1049
1160
  }),
1050
1161
  redo: undoBindings?.redo ?? (() => {
1051
1162
  }),
1052
- zoom: record.view.zoom,
1163
+ zoom: view2d?.zoom ?? 1,
1053
1164
  setZoom,
1054
- zoomIn: () => setZoom(record.view.zoom * 1.25),
1055
- zoomOut: () => setZoom(record.view.zoom * 0.8),
1165
+ zoomIn: () => setZoom((view2d?.zoom ?? 1) * 1.25),
1166
+ zoomOut: () => setZoom((view2d?.zoom ?? 1) * 0.8),
1056
1167
  resetZoom: () => setZoom(1),
1057
1168
  hasCanvas: instrument.canvas != null,
1058
- savedSnapshots: lab.savedSnapshots.filter((s) => s.workspaceId === workspaceId),
1059
- saveSnapshot: (name) => lab.saveSnapshot(workspaceId, name),
1060
- loadSnapshot: (snapshotId) => lab.loadSnapshot(workspaceId, snapshotId),
1061
- clone: () => lab.cloneWorkspace(workspaceId),
1062
- reset: () => lab.resetWorkspace(workspaceId),
1063
- close: () => lab.closeWorkspace(workspaceId),
1064
- isLastWorkspace
1169
+ savedSnapshots: lab.savedSnapshots.filter((s) => s.trialId === trialId),
1170
+ saveSnapshot: (name) => lab.saveSnapshot(trialId, name),
1171
+ loadSnapshot: (snapshotId) => lab.loadSnapshot(trialId, snapshotId),
1172
+ clone: () => lab.cloneTrial(trialId),
1173
+ reset: () => lab.resetTrial(trialId),
1174
+ close: () => lab.closeTrial(trialId),
1175
+ isLastTrial
1065
1176
  };
1066
- }, [workspaceId, record, instrument, lab, isLastWorkspace, updateWorkspaceView, undoBindings]);
1177
+ }, [trialId, record, instrument, lab, isLastTrial, updateTrialView, undoBindings, view2d]);
1067
1178
  const sidebarCtx = useMemo(
1068
1179
  () => ({
1069
- workspaceId,
1180
+ trialId,
1070
1181
  instrumentName: record.instrumentName,
1071
1182
  configFields: instrument.configSchema?.() ?? [],
1072
1183
  config: record.config,
@@ -1077,20 +1188,20 @@ function WorkspaceChrome({
1077
1188
  `[labkit] setConfig: unknown key "${key}" for instrument "${record.instrumentName}"`
1078
1189
  );
1079
1190
  }
1080
- updateWorkspaceConfig(workspaceId, key, value);
1191
+ updateTrialConfig(trialId, key, value);
1081
1192
  if (instrument.onConfigChange) {
1082
1193
  const nextConfig = { ...prevConfig, [key]: value };
1083
1194
  const nextState = instrument.onConfigChange(nextConfig, prevConfig, record.state);
1084
- updateWorkspaceState(workspaceId, nextState);
1195
+ updateTrialState(trialId, nextState);
1085
1196
  }
1086
1197
  }
1087
1198
  }),
1088
- [workspaceId, record, instrument, updateWorkspaceConfig, updateWorkspaceState]
1199
+ [trialId, record, instrument, updateTrialConfig, updateTrialState]
1089
1200
  );
1090
1201
  const statusCtx = {
1091
- workspaceId,
1202
+ trialId,
1092
1203
  instrumentName: record.instrumentName,
1093
- zoom: record.view.zoom
1204
+ zoom: view2d ? view2d.zoom : null
1094
1205
  };
1095
1206
  const handleKeyDown = (e) => {
1096
1207
  const mod = e.metaKey || e.ctrlKey;
@@ -1107,57 +1218,71 @@ function WorkspaceChrome({
1107
1218
  return /* @__PURE__ */ jsxs(
1108
1219
  "section",
1109
1220
  {
1110
- className: "lk-workspace",
1111
- "aria-label": `Workspace ${record.instrumentName}`,
1221
+ className: "lk-trial",
1222
+ "aria-label": `Trial ${record.instrumentName}`,
1112
1223
  onKeyDown: handleKeyDown,
1113
1224
  children: [
1114
- /* @__PURE__ */ jsx("div", { className: "lk-workspace__toolbar", children: toolbar ? toolbar(toolbarCtx) : /* @__PURE__ */ jsx(DefaultToolbar, { ctx: toolbarCtx }) }),
1115
- /* @__PURE__ */ jsxs("div", { className: "lk-workspace__body", children: [
1116
- /* @__PURE__ */ jsxs("div", { className: "lk-workspace__sidebar", children: [
1225
+ /* @__PURE__ */ jsx("div", { className: "lk-trial__toolbar", children: toolbar ? toolbar(toolbarCtx) : /* @__PURE__ */ jsx(DefaultToolbar, { ctx: toolbarCtx }) }),
1226
+ /* @__PURE__ */ jsxs("div", { className: "lk-trial__body", children: [
1227
+ /* @__PURE__ */ jsxs("div", { className: "lk-trial__sidebar", children: [
1117
1228
  sidebar ? sidebar(sidebarCtx) : /* @__PURE__ */ jsx(DefaultSidebar, { ctx: sidebarCtx }),
1118
1229
  sidebarExtras
1119
1230
  ] }),
1120
- /* @__PURE__ */ jsx("div", { className: "lk-workspace__content", children })
1231
+ /* @__PURE__ */ jsx("div", { className: "lk-trial__content", children })
1121
1232
  ] }),
1122
- /* @__PURE__ */ jsx("div", { className: "lk-workspace__status", children: statusBar ? statusBar(statusCtx) : /* @__PURE__ */ jsx(DefaultStatusBar, { ctx: statusCtx }) })
1233
+ /* @__PURE__ */ jsxs("div", { className: "lk-trial__status", children: [
1234
+ job ? /* @__PURE__ */ jsxs("div", { className: "lk-trial__job", children: [
1235
+ /* @__PURE__ */ jsxs("span", { className: "lk-trial__job-count", children: [
1236
+ job.done,
1237
+ job.total === null ? "" : ` / ${job.total}`
1238
+ ] }),
1239
+ job.failures.length > 0 ? /* @__PURE__ */ jsxs("span", { className: "lk-trial__job-failures", children: [
1240
+ job.failures.length,
1241
+ " failed"
1242
+ ] }) : null,
1243
+ job.error ? /* @__PURE__ */ jsx("span", { className: "lk-trial__job-error", children: job.error }) : null,
1244
+ job.status === "running" ? /* @__PURE__ */ jsx("button", { type: "button", className: "lk-trial__job-cancel", onClick: job.cancel, children: "Cancel" }) : null
1245
+ ] }) : null,
1246
+ statusBar ? statusBar(statusCtx) : /* @__PURE__ */ jsx(DefaultStatusBar, { ctx: statusCtx })
1247
+ ] })
1123
1248
  ]
1124
1249
  }
1125
1250
  );
1126
1251
  }
1127
- function Workspace({ id }) {
1252
+ function Trial({ id }) {
1128
1253
  const lab = useLabContext();
1129
1254
  const storeCtx = useContext(LabStoreContext);
1130
- if (!storeCtx) throw new Error("[labkit] <Workspace> requires <LabStoreProvider>");
1131
- const record = useStore(storeCtx.store, (s) => s.workspaces.find((w) => w.id === id));
1255
+ if (!storeCtx) throw new Error("[labkit] <Trial> requires <LabStoreProvider>");
1256
+ const record = useStore(storeCtx.store, (s) => s.trials.find((w) => w.id === id));
1132
1257
  if (!record) {
1133
- return /* @__PURE__ */ jsxs("div", { className: "lk-workspace lk-workspace--unknown", children: [
1134
- "Workspace not found: ",
1258
+ return /* @__PURE__ */ jsxs("div", { className: "lk-trial lk-trial--unknown", children: [
1259
+ "Trial not found: ",
1135
1260
  id
1136
1261
  ] });
1137
1262
  }
1138
1263
  const instrument = lab.instruments.find((i) => i.name === record.instrumentName);
1139
1264
  if (!instrument) {
1140
- return /* @__PURE__ */ jsxs("div", { className: "lk-workspace lk-workspace--unknown", children: [
1265
+ return /* @__PURE__ */ jsxs("div", { className: "lk-trial lk-trial--unknown", children: [
1141
1266
  "Unknown instrument: ",
1142
1267
  record.instrumentName
1143
1268
  ] });
1144
1269
  }
1145
1270
  return /* @__PURE__ */ jsx(
1146
- WorkspaceRuntime,
1271
+ TrialRuntime,
1147
1272
  {
1148
1273
  record,
1149
1274
  instrument,
1150
1275
  store: storeCtx.store,
1151
- isLast: lab.workspaces.length <= 1
1276
+ isLast: lab.trials.length <= 1
1152
1277
  }
1153
1278
  );
1154
1279
  }
1155
- function WorkspaceRuntime({ record, instrument, store, isLast }) {
1280
+ function TrialRuntime({ record, instrument, store, isLast }) {
1156
1281
  const canvasContainerRef = useRef(null);
1157
- const updateWorkspaceState = useStore(store, (s) => s.updateWorkspaceState);
1158
- const updateWorkspaceConfig = useStore(store, (s) => s.updateWorkspaceConfig);
1159
- const updateWorkspaceView = useStore(store, (s) => s.updateWorkspaceView);
1160
- const updateWorkspaceUndoStack = useStore(store, (s) => s.updateWorkspaceUndoStack);
1282
+ const updateTrialState = useStore(store, (s) => s.updateTrialState);
1283
+ const updateTrialConfig = useStore(store, (s) => s.updateTrialConfig);
1284
+ const updateTrialView = useStore(store, (s) => s.updateTrialView);
1285
+ const updateTrialUndoStack = useStore(store, (s) => s.updateTrialUndoStack);
1161
1286
  const busRef = useRef(null);
1162
1287
  if (busRef.current === null) busRef.current = createEventBus();
1163
1288
  const bus = busRef.current;
@@ -1168,37 +1293,52 @@ function WorkspaceRuntime({ record, instrument, store, isLast }) {
1168
1293
  const maxDepth = undoCap?.maxDepth ?? 50;
1169
1294
  const snapshotIfNeeded = (event) => {
1170
1295
  if (!undoCap || !undoEvents.has(event)) return;
1171
- const current = store.getState().workspaces.find((w) => w.id === record.id);
1296
+ const current = store.getState().trials.find((w) => w.id === record.id);
1172
1297
  if (!current) return;
1173
1298
  const snap = structuredClone(current.state);
1174
- updateWorkspaceUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
1299
+ updateTrialUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
1175
1300
  };
1176
- const setView = (v) => updateWorkspaceView(record.id, v);
1301
+ const setView = (v) => updateTrialView(record.id, v);
1302
+ const view2d = as2DView(record.view);
1303
+ const jobCap = instrument.job;
1304
+ const job = useJob({
1305
+ capability: jobCap ?? { run: async function* () {
1306
+ }, onItem: (_i, st) => st },
1307
+ config: record.config,
1308
+ state: record.state,
1309
+ setState: (next) => updateTrialState(record.id, next)
1310
+ });
1177
1311
  const renderCtx = {
1178
1312
  state: record.state,
1179
1313
  config: record.config,
1180
1314
  setState: (next) => {
1181
1315
  snapshotIfNeeded("state.change");
1182
- updateWorkspaceState(record.id, next);
1316
+ updateTrialState(record.id, next);
1183
1317
  bus.emit("state.change");
1184
1318
  },
1185
1319
  setConfig: (key, value) => {
1186
1320
  const evt = `config.change:${String(key)}`;
1187
1321
  snapshotIfNeeded("config.change");
1188
1322
  snapshotIfNeeded(evt);
1189
- updateWorkspaceConfig(record.id, key, value);
1323
+ updateTrialConfig(record.id, key, value);
1190
1324
  bus.emit("config.change");
1191
1325
  bus.emit(evt);
1192
1326
  },
1193
- workspace: {
1327
+ trial: {
1194
1328
  id: record.id,
1195
- zoom: record.view.zoom,
1196
- setZoom: (z) => updateWorkspaceView(record.id, { ...record.view, zoom: z })
1329
+ view: record.view,
1330
+ setView,
1331
+ zoom: view2d?.zoom ?? 1,
1332
+ setZoom: (z) => {
1333
+ if (!view2d) return;
1334
+ updateTrialView(record.id, { ...view2d, zoom: z });
1335
+ }
1197
1336
  },
1198
1337
  emit: (event) => {
1199
1338
  snapshotIfNeeded(event);
1200
1339
  bus.emit(event);
1201
- }
1340
+ },
1341
+ job: jobCap ? job : void 0
1202
1342
  };
1203
1343
  const undoBindings = undoCap ? {
1204
1344
  canUndo: record.undoStack.past.length > 0,
@@ -1206,14 +1346,14 @@ function WorkspaceRuntime({ record, instrument, store, isLast }) {
1206
1346
  undo: () => {
1207
1347
  const result = undo(record.undoStack, structuredClone(record.state));
1208
1348
  if (!result) return;
1209
- updateWorkspaceState(record.id, result.snapshot);
1210
- updateWorkspaceUndoStack(record.id, result.stack);
1349
+ updateTrialState(record.id, result.snapshot);
1350
+ updateTrialUndoStack(record.id, result.stack);
1211
1351
  },
1212
1352
  redo: () => {
1213
1353
  const result = redo(record.undoStack, structuredClone(record.state));
1214
1354
  if (!result) return;
1215
- updateWorkspaceState(record.id, result.snapshot);
1216
- updateWorkspaceUndoStack(record.id, result.stack);
1355
+ updateTrialState(record.id, result.snapshot);
1356
+ updateTrialUndoStack(record.id, result.stack);
1217
1357
  }
1218
1358
  } : void 0;
1219
1359
  const canvasLayers = useMemo(() => {
@@ -1223,7 +1363,11 @@ function WorkspaceRuntime({ record, instrument, store, isLast }) {
1223
1363
  return ordered.map((layer) => ({
1224
1364
  id: layer.id,
1225
1365
  visible: layerVisibility[layer.id] !== false,
1226
- render: (ctx, view) => layer.draw(ctx, { state: record.state, config: record.config, zoom: view.zoom })
1366
+ render: (ctx, view) => {
1367
+ ctx.translate(view.pan.x, view.pan.y);
1368
+ ctx.scale(view.zoom, view.zoom);
1369
+ layer.draw(ctx, { state: record.state, config: record.config, zoom: view.zoom });
1370
+ }
1227
1371
  }));
1228
1372
  }, [instrument.canvas, record.state, record.config, layerVisibility, layerOrder]);
1229
1373
  const layerDescriptors = useMemo(() => {
@@ -1233,12 +1377,12 @@ function WorkspaceRuntime({ record, instrument, store, isLast }) {
1233
1377
  const dragDropResult = useDragDrop({
1234
1378
  capability: instrument.dragDrop ?? { palette: [], onDrop: (_p, _i, s) => s },
1235
1379
  canvasContainerRef,
1236
- view: record.view,
1380
+ view: view2d ?? DEFAULT_VIEW,
1237
1381
  state: record.state,
1238
1382
  config: record.config,
1239
1383
  setState: (next) => {
1240
1384
  snapshotIfNeeded("canvas.itemAdded");
1241
- updateWorkspaceState(record.id, next);
1385
+ updateTrialState(record.id, next);
1242
1386
  },
1243
1387
  emit: (evt) => {
1244
1388
  snapshotIfNeeded(evt);
@@ -1280,10 +1424,18 @@ function WorkspaceRuntime({ record, instrument, store, isLast }) {
1280
1424
  "div",
1281
1425
  {
1282
1426
  ref: canvasContainerRef,
1283
- className: "lk-workspace__canvas-host",
1427
+ className: "lk-trial__canvas-host",
1284
1428
  style: { width: "100%", height: "100%", position: "relative" },
1285
1429
  children: [
1286
- /* @__PURE__ */ jsx(CanvasStack, { layers: layersWithFeedback, view: record.view, onViewChange: setView }),
1430
+ /* @__PURE__ */ jsx(
1431
+ CanvasStack,
1432
+ {
1433
+ layers: layersWithFeedback,
1434
+ view: view2d ?? DEFAULT_VIEW,
1435
+ onViewChange: setView,
1436
+ children: instrument.render(renderCtx)
1437
+ }
1438
+ ),
1287
1439
  /* @__PURE__ */ jsx(DragOverlay, { drag: dragDropResult.drag })
1288
1440
  ]
1289
1441
  }
@@ -1308,12 +1460,13 @@ function WorkspaceRuntime({ record, instrument, store, isLast }) {
1308
1460
  }
1309
1461
  ) : null;
1310
1462
  return /* @__PURE__ */ jsx(
1311
- WorkspaceChrome,
1463
+ TrialChrome,
1312
1464
  {
1313
- workspaceId: record.id,
1465
+ job: jobCap ? job : void 0,
1466
+ trialId: record.id,
1314
1467
  record,
1315
1468
  instrument,
1316
- isLastWorkspace: isLast,
1469
+ isLastTrial: isLast,
1317
1470
  undoBindings,
1318
1471
  sidebarExtras: /* @__PURE__ */ jsxs(Fragment, { children: [
1319
1472
  paletteNode,
@@ -1324,8 +1477,8 @@ function WorkspaceRuntime({ record, instrument, store, isLast }) {
1324
1477
  );
1325
1478
  }
1326
1479
 
1327
- // src/workspace/workspaceOps.ts
1328
- var DEFAULT_VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
1480
+ // src/trial/trialOps.ts
1481
+ var DEFAULT_VIEW2 = { zoom: 1, pan: { x: 0, y: 0 } };
1329
1482
  function findInstrument(instruments, name) {
1330
1483
  const found = instruments.find((i) => i.name === name);
1331
1484
  if (!found) {
@@ -1334,9 +1487,9 @@ function findInstrument(instruments, name) {
1334
1487
  return found;
1335
1488
  }
1336
1489
  function initialView(instrument) {
1337
- return instrument.canvas?.initialView ? structuredClone(instrument.canvas.initialView) : { ...DEFAULT_VIEW, pan: { ...DEFAULT_VIEW.pan } };
1490
+ return instrument.canvas?.initialView ? structuredClone(instrument.canvas.initialView) : { ...DEFAULT_VIEW2, pan: { ...DEFAULT_VIEW2.pan } };
1338
1491
  }
1339
- function addWorkspace(workspaces, instruments, instrumentName) {
1492
+ function addTrial(trials, instruments, instrumentName) {
1340
1493
  const instrument = findInstrument(instruments, instrumentName);
1341
1494
  const config = instrument.defaultConfig();
1342
1495
  const state = instrument.initialState(config);
@@ -1348,12 +1501,12 @@ function addWorkspace(workspaces, instruments, instrumentName) {
1348
1501
  view: initialView(instrument),
1349
1502
  undoStack: { past: [], future: [] }
1350
1503
  };
1351
- return [...workspaces, record];
1504
+ return [...trials, record];
1352
1505
  }
1353
- function cloneWorkspace(workspaces, id) {
1354
- const sourceIdx = workspaces.findIndex((w) => w.id === id);
1355
- const source = workspaces[sourceIdx];
1356
- if (!source) return workspaces;
1506
+ function cloneTrial(trials, id) {
1507
+ const sourceIdx = trials.findIndex((w) => w.id === id);
1508
+ const source = trials[sourceIdx];
1509
+ if (!source) return trials;
1357
1510
  const clone = {
1358
1511
  ...source,
1359
1512
  id: crypto.randomUUID(),
@@ -1362,17 +1515,17 @@ function cloneWorkspace(workspaces, id) {
1362
1515
  view: structuredClone(source.view),
1363
1516
  undoStack: { past: [], future: [] }
1364
1517
  };
1365
- return [...workspaces.slice(0, sourceIdx + 1), clone, ...workspaces.slice(sourceIdx + 1)];
1518
+ return [...trials.slice(0, sourceIdx + 1), clone, ...trials.slice(sourceIdx + 1)];
1366
1519
  }
1367
- function closeWorkspace(workspaces, id) {
1368
- if (workspaces.length <= 1) return workspaces;
1369
- const next = workspaces.filter((w) => w.id !== id);
1370
- return next.length === workspaces.length ? workspaces : next;
1520
+ function closeTrial(trials, id) {
1521
+ if (trials.length <= 1) return trials;
1522
+ const next = trials.filter((w) => w.id !== id);
1523
+ return next.length === trials.length ? trials : next;
1371
1524
  }
1372
- function resetWorkspace(workspaces, id, instruments) {
1373
- const idx = workspaces.findIndex((w) => w.id === id);
1374
- const current = workspaces[idx];
1375
- if (!current) return workspaces;
1525
+ function resetTrial(trials, id, instruments) {
1526
+ const idx = trials.findIndex((w) => w.id === id);
1527
+ const current = trials[idx];
1528
+ if (!current) return trials;
1376
1529
  const instrument = findInstrument(instruments, current.instrumentName);
1377
1530
  const config = instrument.defaultConfig();
1378
1531
  const state = instrument.initialState(config);
@@ -1382,13 +1535,13 @@ function resetWorkspace(workspaces, id, instruments) {
1382
1535
  state,
1383
1536
  view: initialView(instrument)
1384
1537
  };
1385
- return [...workspaces.slice(0, idx), reset, ...workspaces.slice(idx + 1)];
1538
+ return [...trials.slice(0, idx), reset, ...trials.slice(idx + 1)];
1386
1539
  }
1387
- function reorderWorkspaces(workspaces, ids) {
1388
- const byId = new Map(workspaces.map((w) => [w.id, w]));
1540
+ function reorderTrials(trials, ids) {
1541
+ const byId = new Map(trials.map((w) => [w.id, w]));
1389
1542
  const named = ids.map((id) => byId.get(id)).filter((w) => w !== void 0);
1390
1543
  const seen = new Set(named.map((w) => w.id));
1391
- const rest = workspaces.filter((w) => !seen.has(w.id));
1544
+ const rest = trials.filter((w) => !seen.has(w.id));
1392
1545
  return [...named, ...rest];
1393
1546
  }
1394
1547
  function query() {
@@ -1422,9 +1575,9 @@ function LabShell({ title, children, header, footer, mode = "auto" }) {
1422
1575
  ] });
1423
1576
  return outer ? shell : /* @__PURE__ */ jsx(ThemeProvider, { theme: interstellarTheme, mode: resolved, children: shell });
1424
1577
  }
1425
- var ZONE_ID = asNodeId("lk-workspaces");
1578
+ var ZONE_ID = asNodeId("lk-workspace");
1426
1579
  var STRATEGIES = { grid: gridStrategy };
1427
- var KIND = "workspace";
1580
+ var KIND = "trial";
1428
1581
  function extentOf(store, id) {
1429
1582
  const p2 = store.getNode(id)?.membership?.placement;
1430
1583
  if (!p2) return null;
@@ -1433,7 +1586,7 @@ function extentOf(store, id) {
1433
1586
  if (p2.span) out.span = p2.span;
1434
1587
  return Object.keys(out).length > 0 ? out : null;
1435
1588
  }
1436
- function WorkspaceGrid({
1589
+ function Workspace({
1437
1590
  children,
1438
1591
  ids,
1439
1592
  resizable = false,
@@ -1465,6 +1618,7 @@ function WorkspaceGrid({
1465
1618
  storeRef.current = store2;
1466
1619
  }
1467
1620
  const store = storeRef.current;
1621
+ const surface = useSurfaceOptional();
1468
1622
  useLayoutEffect(() => {
1469
1623
  store.updateContainerConfig(ZONE_ID, { resizable, gap, padding });
1470
1624
  }, [store, resizable, gap, padding]);
@@ -1483,6 +1637,11 @@ function WorkspaceGrid({
1483
1637
  }
1484
1638
  store.setChildOrder(ZONE_ID, [...nodeIds]);
1485
1639
  }, [store, nodeIds]);
1640
+ useEffect(() => {
1641
+ if (!surface) return;
1642
+ surface.invalidateRects();
1643
+ return store.events.on("node.placementChanged", () => surface.invalidateRects());
1644
+ }, [store, surface, nodeIds]);
1486
1645
  useEffect(() => {
1487
1646
  if (!onLayoutChange) return;
1488
1647
  return store.events.on("node.placementChanged", () => {
@@ -1504,9 +1663,9 @@ function WorkspaceGrid({
1504
1663
  [KIND]: ({ node }) => {
1505
1664
  const content = byId.get(node.id) ?? null;
1506
1665
  if (!reorderable) return content;
1507
- return /* @__PURE__ */ jsxs("div", { className: "lk-workspace-tile", children: [
1508
- /* @__PURE__ */ jsx(DragHandle, { nodeId: node.id, className: "lk-workspace-tile__grip", children: /* @__PURE__ */ jsx("span", { className: "lk-workspace-tile__grip-dots", "aria-hidden": "true" }) }),
1509
- /* @__PURE__ */ jsx("div", { className: "lk-workspace-tile__body", children: content })
1666
+ return /* @__PURE__ */ jsxs("div", { className: "lk-trial-tile", children: [
1667
+ /* @__PURE__ */ jsx(DragHandle, { nodeId: node.id, className: "lk-trial-tile__grip", children: /* @__PURE__ */ jsx("span", { className: "lk-trial-tile__grip-dots", "aria-hidden": "true" }) }),
1668
+ /* @__PURE__ */ jsx("div", { className: "lk-trial-tile__body", children: content })
1510
1669
  ] });
1511
1670
  }
1512
1671
  };
@@ -1516,7 +1675,7 @@ function WorkspaceGrid({
1516
1675
  {
1517
1676
  parentId: ZONE_ID,
1518
1677
  chrome,
1519
- className: "lk-workspace-grid windease-zone",
1678
+ className: "lk-workspace windease-zone",
1520
1679
  affordances: resizable,
1521
1680
  viewport,
1522
1681
  onChildOrderChange: reorderable ? commitOrder : void 0
@@ -1526,12 +1685,12 @@ function WorkspaceGrid({
1526
1685
  }
1527
1686
  function buildStore(instruments, defaultInstrument, storage, storageKey, initialMode) {
1528
1687
  const store = createLabStore({ storageKey, storage, initialMode });
1529
- if (store.getState().workspaces.length === 0) {
1530
- const seeded = addWorkspace([], instruments, defaultInstrument);
1688
+ if (store.getState().trials.length === 0) {
1689
+ const seeded = addTrial([], instruments, defaultInstrument);
1531
1690
  const record = seeded[0];
1532
1691
  if (record) {
1533
1692
  const { undoStack: _undoStack, ...rest } = record;
1534
- store.getState().addWorkspace(rest);
1693
+ store.getState().addTrial(rest);
1535
1694
  }
1536
1695
  }
1537
1696
  return store;
@@ -1575,7 +1734,7 @@ function Lab({
1575
1734
  );
1576
1735
  }
1577
1736
  const store = storeRef.current;
1578
- const workspaces = useStore(store, (s) => s.workspaces);
1737
+ const trials = useStore(store, (s) => s.trials);
1579
1738
  const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
1580
1739
  const modeValue = useStore(store, (s) => s.mode);
1581
1740
  const layout = useStore(store, (s) => s.layout);
@@ -1586,47 +1745,47 @@ function Lab({
1586
1745
  }
1587
1746
  }, [mode, store]);
1588
1747
  const contextValue = useMemo(() => {
1589
- const replaceWorkspaces = (next) => {
1590
- const currentById = new Map(store.getState().workspaces.map((w) => [w.id, w]));
1748
+ const replaceTrials = (next) => {
1749
+ const currentById = new Map(store.getState().trials.map((w) => [w.id, w]));
1591
1750
  const merged = next.map((w) => currentById.get(w.id) ?? w);
1592
- store.setState({ workspaces: merged });
1751
+ store.setState({ trials: merged });
1593
1752
  store.getState().setMode(store.getState().mode);
1594
1753
  };
1595
1754
  return {
1596
1755
  instruments,
1597
- workspaces,
1598
- addWorkspace: (instrumentName) => {
1599
- const next = addWorkspace(store.getState().workspaces, instruments, instrumentName);
1600
- replaceWorkspaces(next);
1756
+ trials,
1757
+ addTrial: (instrumentName) => {
1758
+ const next = addTrial(store.getState().trials, instruments, instrumentName);
1759
+ replaceTrials(next);
1601
1760
  },
1602
- cloneWorkspace: (id) => {
1603
- const next = cloneWorkspace(store.getState().workspaces, id);
1604
- replaceWorkspaces(next);
1761
+ cloneTrial: (id) => {
1762
+ const next = cloneTrial(store.getState().trials, id);
1763
+ replaceTrials(next);
1605
1764
  },
1606
- closeWorkspace: (id) => {
1607
- const next = closeWorkspace(store.getState().workspaces, id);
1608
- replaceWorkspaces(next);
1765
+ closeTrial: (id) => {
1766
+ const next = closeTrial(store.getState().trials, id);
1767
+ replaceTrials(next);
1609
1768
  },
1610
- reorderWorkspaces: (ids) => {
1611
- replaceWorkspaces(reorderWorkspaces(store.getState().workspaces, ids));
1769
+ reorderTrials: (ids) => {
1770
+ replaceTrials(reorderTrials(store.getState().trials, ids));
1612
1771
  },
1613
- resetWorkspace: (id) => {
1614
- const next = resetWorkspace(store.getState().workspaces, id, instruments);
1772
+ resetTrial: (id) => {
1773
+ const next = resetTrial(store.getState().trials, id, instruments);
1615
1774
  const record = next.find((w) => w.id === id);
1616
1775
  if (!record) return;
1617
1776
  store.setState((s) => ({
1618
- workspaces: s.workspaces.map(
1777
+ trials: s.trials.map(
1619
1778
  (w) => w.id === id ? { ...w, config: record.config, state: record.state, view: record.view } : w
1620
1779
  )
1621
1780
  }));
1622
- store.getState().updateWorkspaceView(id, record.view);
1781
+ store.getState().updateTrialView(id, record.view);
1623
1782
  },
1624
1783
  savedSnapshots,
1625
- saveSnapshot: (workspaceId, name) => {
1626
- store.getState().saveSnapshot(workspaceId, name ?? `Save ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
1784
+ saveSnapshot: (trialId, name) => {
1785
+ store.getState().saveSnapshot(trialId, name ?? `Save ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
1627
1786
  },
1628
- loadSnapshot: (workspaceId, snapshotId) => {
1629
- store.getState().loadSnapshot(snapshotId, workspaceId);
1787
+ loadSnapshot: (trialId, snapshotId) => {
1788
+ store.getState().loadSnapshot(snapshotId, trialId);
1630
1789
  },
1631
1790
  deleteSnapshot: (snapshotId) => {
1632
1791
  store.getState().deleteSnapshot(snapshotId);
@@ -1636,7 +1795,7 @@ function Lab({
1636
1795
  store.getState().setMode(m);
1637
1796
  }
1638
1797
  };
1639
- }, [instruments, workspaces, savedSnapshots, modeValue, store]);
1798
+ }, [instruments, trials, savedSnapshots, modeValue, store]);
1640
1799
  const backdropStyle = resolvedMode === "dark" && nebula && nebula.length > 0 ? { ["--wzl-backdrop"]: buildNebula(nebula) } : void 0;
1641
1800
  return /* @__PURE__ */ jsx(LabStoreContext.Provider, { value: { store }, children: /* @__PURE__ */ jsx(LabContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
1642
1801
  ThemeProvider,
@@ -1646,15 +1805,15 @@ function Lab({
1646
1805
  className: "lk-lab",
1647
1806
  style: backdropStyle,
1648
1807
  children: /* @__PURE__ */ jsx(LabShell, { title: title ?? "Labkit", mode: modeValue, header: children, children: /* @__PURE__ */ jsx(
1649
- WorkspaceGrid,
1808
+ Workspace,
1650
1809
  {
1651
- ids: workspaces.map((w) => w.id),
1810
+ ids: trials.map((w) => w.id),
1652
1811
  resizable: true,
1653
1812
  reorderable: true,
1654
- onReorder: (ids) => contextValue.reorderWorkspaces(ids),
1813
+ onReorder: (ids) => contextValue.reorderTrials(ids),
1655
1814
  layout,
1656
1815
  onLayoutChange: (next) => store.getState().setLayout(next),
1657
- children: workspaces.map((w) => /* @__PURE__ */ jsx(Workspace, { id: w.id }, w.id))
1816
+ children: trials.map((w) => /* @__PURE__ */ jsx(Trial, { id: w.id }, w.id))
1658
1817
  }
1659
1818
  ) })
1660
1819
  }
@@ -2024,17 +2183,6 @@ function PropertyGroup({
2024
2183
  /* @__PURE__ */ jsx("div", { className: "lk-property-group__body", children })
2025
2184
  ] });
2026
2185
  }
2027
-
2028
- // src/ui/format.ts
2029
- var MINUS = "\u2212";
2030
- function formatNumber(n, fractionDigits) {
2031
- if (!Number.isFinite(n)) return String(n);
2032
- const s = fractionDigits == null ? String(n) : n.toFixed(fractionDigits);
2033
- return s.startsWith("-") ? MINUS + s.slice(1) : s;
2034
- }
2035
- function parseSignedNumber(s) {
2036
- return Number(s.replace(MINUS, "-"));
2037
- }
2038
2186
  function PropertyPanel({ title, children, className }) {
2039
2187
  const cls = className ? `lk-property-panel ${className}` : "lk-property-panel";
2040
2188
  return /* @__PURE__ */ jsxs("div", { className: cls, children: [
@@ -2278,6 +2426,6 @@ function ToggleRow({
2278
2426
  }) }) });
2279
2427
  }
2280
2428
 
2281
- export { CheckboxRow, ColorRow, CurveField, DefaultSidebar, DefaultStatusBar, DefaultToolbar, EffectCard, EffectCardList, Lab, LabContext, LabShell, NumberRow, PropertyGroup, PropertyList, PropertyPanel, PropertyRow, SelectRow, SliderRow, Subpanel, TextRow, ToggleRow, Workspace, WorkspaceChrome, WorkspaceGrid, addWorkspace, cloneWorkspace, closeWorkspace, defineInstrument, detectCapabilities, interstellarTheme, reorderWorkspaces, resetWorkspace, useLabContext, validateConfigSchema };
2429
+ export { CheckboxRow, ColorRow, CurveField, DEFAULT_VIEW, DefaultSidebar, DefaultStatusBar, DefaultToolbar, EffectCard, EffectCardList, Lab, LabContext, LabShell, NumberRow, PITCH_LIMIT, PropertyGroup, PropertyList, PropertyPanel, PropertyRow, SelectRow, SliderRow, Subpanel, TextRow, ToggleRow, Trial, TrialChrome, Workspace, addTrial, as2DView, clampPitch, cloneTrial, closeTrial, defineInstrument, detectCapabilities, interstellarTheme, orbitAfterDrag, orbitAfterWheel, reorderTrials, resetTrial, useLabContext, useOrbit, validateConfigSchema, wrapYaw };
2282
2430
  //# sourceMappingURL=index.js.map
2283
2431
  //# sourceMappingURL=index.js.map