@weasel-js/labkit 1.1.0 → 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 (79) hide show
  1. package/README.md +27 -0
  2. package/dist/_dts/{DrawCommand-BkZztJsW.d.ts → DrawCommand-C_XboUpz.d.ts} +48 -6
  3. package/dist/_dts/{index-iAP6XbH3.d.ts → index-JFAYj5Tv.d.ts} +12 -0
  4. package/dist/_dts/types-DJ79Tg5J.d.ts +56 -0
  5. package/dist/_dts/{useTrialState-DYe2vUwN.d.ts → useTrialState-BMNIx3Cy.d.ts} +5 -9
  6. package/dist/canvas/index.d.ts +2 -1
  7. package/dist/{chunk-C6GJKPUI.js → chunk-73KA7WBO.js} +69 -63
  8. package/dist/chunk-73KA7WBO.js.map +1 -0
  9. package/dist/{chunk-DJLDIRFN.js → chunk-BOHF3PQO.js} +4375 -3720
  10. package/dist/chunk-BOHF3PQO.js.map +1 -0
  11. package/dist/{chunk-NRKWVTVT.js → chunk-G5TJVQQT.js} +3 -3
  12. package/dist/{chunk-NRKWVTVT.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-3TYUJR7Z.js → chunk-VUU5UXHE.js} +6 -3
  18. package/dist/chunk-VUU5UXHE.js.map +1 -0
  19. package/dist/dragdrop/index.d.ts +2 -1
  20. package/dist/index.d.ts +71 -11
  21. package/dist/index.js +165 -22
  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 +20 -4
  30. package/dist/passthrough/weasel-ui.js +2 -2
  31. package/dist/state/index.d.ts +2 -2
  32. package/dist/state/index.js +2 -2
  33. package/dist/styles.css +10 -0
  34. package/dist/surface/index.d.ts +77 -0
  35. package/dist/surface/index.js +3 -0
  36. package/dist/surface/index.js.map +1 -0
  37. package/dist/ui/layers/index.js +3 -3
  38. package/dist/undo/index.d.ts +2 -1
  39. package/package.json +9 -1
  40. package/src/canvas/AGENTS.md +8 -0
  41. package/src/canvas/useOrbit.test.ts +71 -0
  42. package/src/canvas/useOrbit.ts +149 -0
  43. package/src/index.test.ts +18 -0
  44. package/src/index.ts +13 -0
  45. package/src/instrument/SineWave.smoke.test.tsx +2 -1
  46. package/src/instrument/types.ts +13 -1
  47. package/src/job/index.ts +3 -0
  48. package/src/job/types.ts +47 -0
  49. package/src/job/useJob.test.tsx +210 -0
  50. package/src/job/useJob.ts +134 -0
  51. package/src/lab/Lab.stories.tsx +1 -2
  52. package/src/lab/Workspace.surface.test.tsx +49 -0
  53. package/src/lab/Workspace.tsx +14 -1
  54. package/src/state/store.ts +8 -2
  55. package/src/state/types.ts +4 -2
  56. package/src/state/view.test.ts +127 -0
  57. package/src/state/view.ts +18 -0
  58. package/src/surface/AGENTS.md +64 -0
  59. package/src/surface/SurfaceContext.ts +5 -0
  60. package/src/surface/composeRects.test.ts +50 -0
  61. package/src/surface/composeRects.ts +19 -0
  62. package/src/surface/deviceRect.test.ts +40 -0
  63. package/src/surface/deviceRect.ts +19 -0
  64. package/src/surface/index.ts +7 -0
  65. package/src/surface/rect.ts +16 -0
  66. package/src/surface/useSurfaceTile.test.tsx +67 -0
  67. package/src/surface/useSurfaceTile.ts +32 -0
  68. package/src/surface/useTiledSurface.test.tsx +231 -0
  69. package/src/surface/useTiledSurface.ts +157 -0
  70. package/src/trial/DefaultStatusBar.tsx +2 -2
  71. package/src/trial/Trial.job.test.tsx +75 -0
  72. package/src/trial/Trial.less +12 -0
  73. package/src/trial/Trial.stories.tsx +8 -1
  74. package/src/trial/Trial.tsx +32 -6
  75. package/src/trial/TrialChrome.tsx +33 -6
  76. package/src/trial/slotTypes.ts +2 -1
  77. package/dist/chunk-3TYUJR7Z.js.map +0 -1
  78. package/dist/chunk-C6GJKPUI.js.map +0 -1
  79. package/dist/chunk-DJLDIRFN.js.map +0 -1
package/dist/index.d.ts CHANGED
@@ -1,20 +1,69 @@
1
1
  export { CanvasLayerDescriptor, CanvasStack, CanvasStackContext, CanvasStackContextValue, CanvasStackProps, screenToWorld, worldToScreen } from './canvas/index.js';
2
+ import * as react from 'react';
3
+ import { WheelEvent, PointerEvent, ReactNode } from 'react';
2
4
  export { ControlPanel } from './controls/index.js';
3
5
  import { C as ConfigField } from './_dts/types-x92Kfeme.js';
4
6
  export { a as CheckboxField, b as ColorField, c as ConfigFieldBase, d as ConfigFieldType, N as NumberField, S as SelectField, e as SelectOption, f as SliderField, T as TextField } from './_dts/types-x92Kfeme.js';
5
- import { I as Instrument, a as InstrumentList } from './_dts/index-iAP6XbH3.js';
6
- export { C as CanvasCapability, b as CanvasLayer, D as DragDropCapability, c as DragFeedback, H as HitResult, L as LayerCapability, d as LayerDescriptor, P as PaletteItem, e as Point, R as RenderContext, S as SystemEvent, U as UndoCapability, V as ViewTransform } from './_dts/index-iAP6XbH3.js';
7
+ import { I as Instrument, a as InstrumentList, V as ViewTransform } from './_dts/index-JFAYj5Tv.js';
8
+ export { C as CanvasCapability, b as CanvasLayer, D as DragDropCapability, c as DragFeedback, H as HitResult, L as LayerCapability, d as LayerDescriptor, P as PaletteItem, e as Point, R as RenderContext, S as SystemEvent, U as UndoCapability } from './_dts/index-JFAYj5Tv.js';
9
+ import { J as JobHandle } from './_dts/types-DJ79Tg5J.js';
10
+ export { a as JobCapability, b as JobEvent, c as JobFailure, d as JobStatus } from './_dts/types-DJ79Tg5J.js';
11
+ export { UseJobOptions, useJob } from './job/index.js';
7
12
  import * as react_jsx_runtime from 'react/jsx-runtime';
8
- import * as react from 'react';
9
- import { ReactNode } from 'react';
10
- import { a as StorageAdapter, w as LabMode, T as TrialRecord, h as SavedSnapshot } from './_dts/useTrialState-DYe2vUwN.js';
11
- export { C as CURRENT_DOCUMENT_VERSION, b as CreateLabStoreOptions, L as LabDocument, e as LabStoreContext, f as LabStoreProvider, g as LabStoreState, S as SerializedTrial, i as TrialIdContext, j as TrialIdProvider, k as TrialStateHandle, U as UndoStack, m as createMemoryAdapter, n as labDocumentKey, o as localStorageAdapter, p as noneAdapter, q as quarantineKey, s as sessionStorageAdapter, u as urlHashAdapter, r as useLabStore, t as useTrialId, v as useTrialState } from './_dts/useTrialState-DYe2vUwN.js';
13
+ import { a as StorageAdapter, w as LabMode, T as TrialRecord, h as SavedSnapshot } from './_dts/useTrialState-BMNIx3Cy.js';
14
+ export { C as CURRENT_DOCUMENT_VERSION, b as CreateLabStoreOptions, L as LabDocument, e as LabStoreContext, f as LabStoreProvider, g as LabStoreState, S as SerializedTrial, i as TrialIdContext, j as TrialIdProvider, k as TrialStateHandle, U as UndoStack, m as createMemoryAdapter, n as labDocumentKey, o as localStorageAdapter, p as noneAdapter, q as quarantineKey, s as sessionStorageAdapter, u as urlHashAdapter, r as useLabStore, t as useTrialId, v as useTrialState } from './_dts/useTrialState-BMNIx3Cy.js';
12
15
  export { LayerList, LayerListProps } from './layers/index.js';
13
16
  export { FpsMeter, ScaleIndicator, ScaleIndicatorProps, Sidebar, SidebarProps, StatusBar, StatusBarProps, Toolbar, ToolbarProps } from './primitives/index.js';
17
+ export { Box, Rect, SurfaceContext, SurfaceFrame, SurfaceHandle, UseTiledSurfaceOptions, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceOptional, useSurfaceTile, useTiledSurface } from './surface/index.js';
14
18
  export { LayerStack, LayerStackItem, LayerStackProps } from './ui/layers/index.js';
15
19
  export { EventBus, EventListener, clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from './undo/index.js';
16
20
  import 'zustand/vanilla';
17
21
 
22
+ /** A point in the space the instrument works in. */
23
+ interface Vec3 {
24
+ x: number;
25
+ y: number;
26
+ z: number;
27
+ }
28
+ /** An orbit camera as a trial view: where it looks from, and what it looks at.
29
+ * labkit does not turn this into a matrix — the host's renderer does. */
30
+ interface OrbitView {
31
+ yaw: number;
32
+ pitch: number;
33
+ distance: number;
34
+ target: Vec3;
35
+ }
36
+ /** Just short of the pole. At exactly ±PI/2 the azimuth is undefined and the
37
+ * camera rolls, which reads as the model jumping rather than as a limit. */
38
+ declare const PITCH_LIMIT: number;
39
+ declare function clampPitch(pitch: number): number;
40
+ declare function wrapYaw(yaw: number): number;
41
+ /** The view a drag of (dx, dy) from `start` produces. Absolute against the drag
42
+ * start, so it can be re-applied any number of times without compounding. */
43
+ declare function orbitAfterDrag(start: OrbitView, dx: number, dy: number): OrbitView;
44
+ /** Multiplicative, so one notch covers the same proportion of the distance
45
+ * whether the camera is near or far. */
46
+ declare function orbitAfterWheel(view: OrbitView, deltaY: number, minDistance: number, maxDistance: number): OrbitView;
47
+ interface UseOrbitOptions {
48
+ view: OrbitView;
49
+ onViewChange: (v: OrbitView) => void;
50
+ /** Restored on double-click. Defaults to the view the hook first saw. */
51
+ home?: OrbitView;
52
+ minDistance?: number;
53
+ maxDistance?: number;
54
+ }
55
+ interface OrbitHandlers {
56
+ onWheel: (e: WheelEvent<HTMLElement>) => void;
57
+ onPointerDown: (e: PointerEvent<HTMLElement>) => void;
58
+ onPointerMove: (e: PointerEvent<HTMLElement>) => void;
59
+ onPointerUp: (e: PointerEvent<HTMLElement>) => void;
60
+ onDoubleClick: () => void;
61
+ isDragging: () => boolean;
62
+ }
63
+ /** Pointer gestures over an orbit view: drag to turn, wheel or pinch to dolly,
64
+ * double-click to go home. The 3D peer of `usePanZoom`. */
65
+ declare function useOrbit({ view, onViewChange, home, minDistance, maxDistance, }: UseOrbitOptions): OrbitHandlers;
66
+
18
67
  /** Which optional capabilities an instrument declared — what the trial
19
68
  * consults to decide which chrome to show. */
20
69
  interface CapabilityFlags {
@@ -158,6 +207,14 @@ interface WorkspaceProps {
158
207
  */
159
208
  declare function Workspace({ children, ids, resizable, reorderable, onReorder, layout, onLayoutChange, gap, padding, viewport, }: WorkspaceProps): react_jsx_runtime.JSX.Element;
160
209
 
210
+ /** The 2D view labkit has always shipped, and what a trial gets when it names no
211
+ * other. Re-exported under its own name so a consumer can say which it means. */
212
+ type ViewTransform2D = ViewTransform;
213
+ declare const DEFAULT_VIEW: ViewTransform2D;
214
+ /** A trial's view is opaque to labkit, so anything that needs the 2D shape — the
215
+ * zoom chrome, `CanvasStack` — asks for it and handles not getting it. */
216
+ declare function as2DView(view: unknown): ViewTransform2D | null;
217
+
161
218
  type TokenValue = string | number | readonly (string | number)[];
162
219
  /**
163
220
  * A single design token as authored, before aliases are resolved: a DTCG-shaped
@@ -233,7 +290,8 @@ interface TrialSidebarContext {
233
290
  interface TrialStatusBarContext {
234
291
  trialId: string;
235
292
  instrumentName: string;
236
- zoom: number;
293
+ /** Null when the trial holds a view that is not the 2D one. */
294
+ zoom: number | null;
237
295
  }
238
296
  /** Replaces a trial's toolbar. Receives everything the default one uses,
239
297
  * so a custom toolbar need not reach into the store. */
@@ -256,7 +314,7 @@ interface DefaultStatusBarProps {
256
314
  ctx: TrialStatusBarContext;
257
315
  }
258
316
  /** The status bar a trial renders when no `statusBar` slot is supplied:
259
- * the instrument's name and the current zoom. */
317
+ * the instrument's name, and the zoom when the trial's view has one. */
260
318
  declare function DefaultStatusBar({ ctx }: DefaultStatusBarProps): react_jsx_runtime.JSX.Element;
261
319
 
262
320
  /** Props for `<DefaultToolbar>`. */
@@ -291,13 +349,15 @@ interface TrialChromeProps {
291
349
  sidebar?: SidebarSlot;
292
350
  statusBar?: StatusBarSlot;
293
351
  undoBindings?: UndoBindings;
352
+ /** Supplied when the instrument declares a `job`. */
353
+ job?: JobHandle;
294
354
  sidebarExtras?: ReactNode;
295
355
  children: ReactNode;
296
356
  }
297
357
  /** The frame around a running instrument — toolbar, sidebar, status bar —
298
358
  * each replaceable by a slot. Assembles the slot contexts and wires the undo
299
359
  * keyboard shortcuts. */
300
- declare function TrialChrome({ trialId, record, instrument, isLastTrial, toolbar, sidebar, statusBar, undoBindings, sidebarExtras, children, }: TrialChromeProps): react_jsx_runtime.JSX.Element;
360
+ declare function TrialChrome({ trialId, record, instrument, isLastTrial, toolbar, sidebar, statusBar, undoBindings, job, sidebarExtras, children, }: TrialChromeProps): react_jsx_runtime.JSX.Element;
301
361
 
302
362
  /** Append a new trial running `instrumentName`, at that instrument's
303
363
  * default config and initial state. */
@@ -595,5 +655,5 @@ interface ToggleRowProps<T extends string> {
595
655
  * visible at once. */
596
656
  declare function ToggleRow<T extends string>({ label, value, options, onChange, layout, }: ToggleRowProps<T>): react_jsx_runtime.JSX.Element;
597
657
 
598
- export { CheckboxRow, ColorRow, ConfigField, CurveField, DefaultSidebar, DefaultStatusBar, DefaultToolbar, EffectCard, EffectCardList, Instrument, InstrumentList, Lab, LabContext, LabMode, LabShell, NumberRow, PropertyGroup, PropertyList, PropertyPanel, PropertyRow, SavedSnapshot, SelectRow, SliderRow, StorageAdapter, Subpanel, TextRow, ToggleRow, Trial, TrialChrome, TrialRecord, Workspace, addTrial, cloneTrial, closeTrial, defineInstrument, detectCapabilities, interstellarTheme, reorderTrials, resetTrial, useLabContext, validateConfigSchema };
599
- export type { CapabilityFlags, CheckboxRowProps, ColorRowProps, CurveFieldProps, CurveMark, DefaultSidebarProps, DefaultStatusBarProps, DefaultToolbarProps, EffectCardListItem, EffectCardListProps, EffectCardProps, LabContextValue, LabProps, LabShellProps, NumberRowProps, PropertyGroupProps, PropertyListPack, PropertyListProps, PropertyPanelProps, PropertyRowLayout, PropertyRowProps, PropertyRowVariant, SelectRowProps, SidebarSlot, SliderRowProps, StatusBarSlot, SubpanelProps, TextRowProps, ToggleRowProps, ToolbarSlot, TrialChromeProps, TrialProps, TrialSidebarContext, TrialStatusBarContext, TrialToolbarContext, ValidationResult, WorkspaceProps };
658
+ export { CheckboxRow, ColorRow, ConfigField, CurveField, DEFAULT_VIEW, DefaultSidebar, DefaultStatusBar, DefaultToolbar, EffectCard, EffectCardList, Instrument, InstrumentList, JobHandle, Lab, LabContext, LabMode, LabShell, NumberRow, PITCH_LIMIT, PropertyGroup, PropertyList, PropertyPanel, PropertyRow, SavedSnapshot, SelectRow, SliderRow, StorageAdapter, Subpanel, TextRow, ToggleRow, Trial, TrialChrome, TrialRecord, ViewTransform, Workspace, addTrial, as2DView, clampPitch, cloneTrial, closeTrial, defineInstrument, detectCapabilities, interstellarTheme, orbitAfterDrag, orbitAfterWheel, reorderTrials, resetTrial, useLabContext, useOrbit, validateConfigSchema, wrapYaw };
659
+ export type { CapabilityFlags, CheckboxRowProps, ColorRowProps, CurveFieldProps, CurveMark, DefaultSidebarProps, DefaultStatusBarProps, DefaultToolbarProps, EffectCardListItem, EffectCardListProps, EffectCardProps, LabContextValue, LabProps, LabShellProps, NumberRowProps, OrbitHandlers, OrbitView, PropertyGroupProps, PropertyListPack, PropertyListProps, PropertyPanelProps, PropertyRowLayout, PropertyRowProps, PropertyRowVariant, SelectRowProps, SidebarSlot, SliderRowProps, StatusBarSlot, SubpanelProps, TextRowProps, ToggleRowProps, ToolbarSlot, TrialChromeProps, TrialProps, TrialSidebarContext, TrialStatusBarContext, TrialToolbarContext, UseOrbitOptions, ValidationResult, Vec3, ViewTransform2D, WorkspaceProps };
package/dist/index.js CHANGED
@@ -1,10 +1,14 @@
1
- export { LayerStack } from './chunk-NRKWVTVT.js';
2
- import { p, F } from './chunk-C6GJKPUI.js';
3
- import './chunk-DJLDIRFN.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-3TYUJR7Z.js';
7
- export { CURRENT_DOCUMENT_VERSION, LabStoreContext, LabStoreProvider, TrialIdContext, TrialIdProvider, createMemoryAdapter, labDocumentKey, localStorageAdapter, noneAdapter, quarantineKey, sessionStorageAdapter, urlHashAdapter, useLabStore, useTrialId, useTrialState } from './chunk-3TYUJR7Z.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 {
@@ -934,6 +1019,17 @@ function useLabContext() {
934
1019
  }
935
1020
  return ctx;
936
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
+ }
937
1033
  function DefaultSidebar({ ctx }) {
938
1034
  if (ctx.configFields.length === 0) {
939
1035
  return /* @__PURE__ */ jsx(Sidebar, { title: ctx.instrumentName, children: /* @__PURE__ */ jsxs("div", { className: "lk-sidebar__placeholder", children: [
@@ -970,7 +1066,7 @@ function formatZoom(zoom) {
970
1066
  function DefaultStatusBar({ ctx }) {
971
1067
  return /* @__PURE__ */ jsxs(StatusBar, { children: [
972
1068
  /* @__PURE__ */ jsx(StatusBar.Section, { children: ctx.instrumentName }),
973
- /* @__PURE__ */ jsx(StatusBar.Section, { children: formatZoom(ctx.zoom) })
1069
+ ctx.zoom === null ? null : /* @__PURE__ */ jsx(StatusBar.Section, { children: formatZoom(ctx.zoom) })
974
1070
  ] });
975
1071
  }
976
1072
  function DefaultToolbar({ ctx }) {
@@ -1038,6 +1134,7 @@ function TrialChrome({
1038
1134
  sidebar,
1039
1135
  statusBar,
1040
1136
  undoBindings,
1137
+ job,
1041
1138
  sidebarExtras,
1042
1139
  children
1043
1140
  }) {
@@ -1047,9 +1144,11 @@ function TrialChrome({
1047
1144
  const updateTrialView = useStore(storeCtx.store, (s) => s.updateTrialView);
1048
1145
  const updateTrialConfig = useStore(storeCtx.store, (s) => s.updateTrialConfig);
1049
1146
  const updateTrialState = useStore(storeCtx.store, (s) => s.updateTrialState);
1147
+ const view2d = as2DView(record.view);
1050
1148
  const toolbarCtx = useMemo(() => {
1051
1149
  const setZoom = (z) => {
1052
- updateTrialView(trialId, { ...record.view, zoom: z });
1150
+ if (!view2d) return;
1151
+ updateTrialView(trialId, { ...view2d, zoom: z });
1053
1152
  };
1054
1153
  return {
1055
1154
  trialId,
@@ -1061,10 +1160,10 @@ function TrialChrome({
1061
1160
  }),
1062
1161
  redo: undoBindings?.redo ?? (() => {
1063
1162
  }),
1064
- zoom: record.view.zoom,
1163
+ zoom: view2d?.zoom ?? 1,
1065
1164
  setZoom,
1066
- zoomIn: () => setZoom(record.view.zoom * 1.25),
1067
- zoomOut: () => setZoom(record.view.zoom * 0.8),
1165
+ zoomIn: () => setZoom((view2d?.zoom ?? 1) * 1.25),
1166
+ zoomOut: () => setZoom((view2d?.zoom ?? 1) * 0.8),
1068
1167
  resetZoom: () => setZoom(1),
1069
1168
  hasCanvas: instrument.canvas != null,
1070
1169
  savedSnapshots: lab.savedSnapshots.filter((s) => s.trialId === trialId),
@@ -1075,7 +1174,7 @@ function TrialChrome({
1075
1174
  close: () => lab.closeTrial(trialId),
1076
1175
  isLastTrial
1077
1176
  };
1078
- }, [trialId, record, instrument, lab, isLastTrial, updateTrialView, undoBindings]);
1177
+ }, [trialId, record, instrument, lab, isLastTrial, updateTrialView, undoBindings, view2d]);
1079
1178
  const sidebarCtx = useMemo(
1080
1179
  () => ({
1081
1180
  trialId,
@@ -1102,7 +1201,7 @@ function TrialChrome({
1102
1201
  const statusCtx = {
1103
1202
  trialId,
1104
1203
  instrumentName: record.instrumentName,
1105
- zoom: record.view.zoom
1204
+ zoom: view2d ? view2d.zoom : null
1106
1205
  };
1107
1206
  const handleKeyDown = (e) => {
1108
1207
  const mod = e.metaKey || e.ctrlKey;
@@ -1131,7 +1230,21 @@ function TrialChrome({
1131
1230
  ] }),
1132
1231
  /* @__PURE__ */ jsx("div", { className: "lk-trial__content", children })
1133
1232
  ] }),
1134
- /* @__PURE__ */ jsx("div", { className: "lk-trial__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
+ ] })
1135
1248
  ]
1136
1249
  }
1137
1250
  );
@@ -1186,6 +1299,15 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1186
1299
  updateTrialUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
1187
1300
  };
1188
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
+ });
1189
1311
  const renderCtx = {
1190
1312
  state: record.state,
1191
1313
  config: record.config,
@@ -1204,13 +1326,19 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1204
1326
  },
1205
1327
  trial: {
1206
1328
  id: record.id,
1207
- zoom: record.view.zoom,
1208
- setZoom: (z) => updateTrialView(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
+ }
1209
1336
  },
1210
1337
  emit: (event) => {
1211
1338
  snapshotIfNeeded(event);
1212
1339
  bus.emit(event);
1213
- }
1340
+ },
1341
+ job: jobCap ? job : void 0
1214
1342
  };
1215
1343
  const undoBindings = undoCap ? {
1216
1344
  canUndo: record.undoStack.past.length > 0,
@@ -1249,7 +1377,7 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1249
1377
  const dragDropResult = useDragDrop({
1250
1378
  capability: instrument.dragDrop ?? { palette: [], onDrop: (_p, _i, s) => s },
1251
1379
  canvasContainerRef,
1252
- view: record.view,
1380
+ view: view2d ?? DEFAULT_VIEW,
1253
1381
  state: record.state,
1254
1382
  config: record.config,
1255
1383
  setState: (next) => {
@@ -1299,7 +1427,15 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1299
1427
  className: "lk-trial__canvas-host",
1300
1428
  style: { width: "100%", height: "100%", position: "relative" },
1301
1429
  children: [
1302
- /* @__PURE__ */ jsx(CanvasStack, { layers: layersWithFeedback, view: record.view, onViewChange: setView, children: instrument.render(renderCtx) }),
1430
+ /* @__PURE__ */ jsx(
1431
+ CanvasStack,
1432
+ {
1433
+ layers: layersWithFeedback,
1434
+ view: view2d ?? DEFAULT_VIEW,
1435
+ onViewChange: setView,
1436
+ children: instrument.render(renderCtx)
1437
+ }
1438
+ ),
1303
1439
  /* @__PURE__ */ jsx(DragOverlay, { drag: dragDropResult.drag })
1304
1440
  ]
1305
1441
  }
@@ -1326,6 +1462,7 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1326
1462
  return /* @__PURE__ */ jsx(
1327
1463
  TrialChrome,
1328
1464
  {
1465
+ job: jobCap ? job : void 0,
1329
1466
  trialId: record.id,
1330
1467
  record,
1331
1468
  instrument,
@@ -1341,7 +1478,7 @@ function TrialRuntime({ record, instrument, store, isLast }) {
1341
1478
  }
1342
1479
 
1343
1480
  // src/trial/trialOps.ts
1344
- var DEFAULT_VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
1481
+ var DEFAULT_VIEW2 = { zoom: 1, pan: { x: 0, y: 0 } };
1345
1482
  function findInstrument(instruments, name) {
1346
1483
  const found = instruments.find((i) => i.name === name);
1347
1484
  if (!found) {
@@ -1350,7 +1487,7 @@ function findInstrument(instruments, name) {
1350
1487
  return found;
1351
1488
  }
1352
1489
  function initialView(instrument) {
1353
- 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 } };
1354
1491
  }
1355
1492
  function addTrial(trials, instruments, instrumentName) {
1356
1493
  const instrument = findInstrument(instruments, instrumentName);
@@ -1481,6 +1618,7 @@ function Workspace({
1481
1618
  storeRef.current = store2;
1482
1619
  }
1483
1620
  const store = storeRef.current;
1621
+ const surface = useSurfaceOptional();
1484
1622
  useLayoutEffect(() => {
1485
1623
  store.updateContainerConfig(ZONE_ID, { resizable, gap, padding });
1486
1624
  }, [store, resizable, gap, padding]);
@@ -1499,6 +1637,11 @@ function Workspace({
1499
1637
  }
1500
1638
  store.setChildOrder(ZONE_ID, [...nodeIds]);
1501
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]);
1502
1645
  useEffect(() => {
1503
1646
  if (!onLayoutChange) return;
1504
1647
  return store.events.on("node.placementChanged", () => {
@@ -2283,6 +2426,6 @@ function ToggleRow({
2283
2426
  }) }) });
2284
2427
  }
2285
2428
 
2286
- export { CheckboxRow, ColorRow, CurveField, DefaultSidebar, DefaultStatusBar, DefaultToolbar, EffectCard, EffectCardList, Lab, LabContext, LabShell, NumberRow, PropertyGroup, PropertyList, PropertyPanel, PropertyRow, SelectRow, SliderRow, Subpanel, TextRow, ToggleRow, Trial, TrialChrome, Workspace, addTrial, cloneTrial, closeTrial, defineInstrument, detectCapabilities, interstellarTheme, reorderTrials, resetTrial, 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 };
2287
2430
  //# sourceMappingURL=index.js.map
2288
2431
  //# sourceMappingURL=index.js.map