@weasel-js/labkit 1.4.4 → 1.5.1
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.
- package/README.md +54 -6
- package/dist/_dts/{CanvasStackContext-kTEZvEgE.d.ts → CanvasStackContext-CJNMS6mz.d.ts} +1 -1
- package/dist/_dts/{PrefsForm.d-DDHFANkc.d.ts → PrefsForm.d-DPy9I8It.d.ts} +4 -0
- package/dist/_dts/Select.d-Gwr8fLpK.d.ts +453 -0
- package/dist/_dts/{frac-z7ker2Vx.d.ts → frac-B4okC2lX.d.ts} +291 -31
- package/dist/_dts/{index-Blv9uQzu.d.ts → index-BNdz-tNd.d.ts} +105 -17
- package/dist/_dts/index-CSJoj4LC.d.ts +248 -0
- package/dist/_dts/{types-B9_zrHmb.d.ts → types-DzoNY1pO.d.ts} +91 -4
- package/dist/_dts/usePanZoom-Pzf9f2jh.d.ts +160 -0
- package/dist/_dts/useTrialState-uLlmaccn.d.ts +212 -0
- package/dist/canvas/index.d.ts +18 -129
- package/dist/canvas/index.js +4 -2
- package/dist/chrome/index.d.ts +5 -93
- package/dist/chrome/index.js +10 -6
- package/dist/chunk-7PFH3CNM.js +150 -0
- package/dist/chunk-7PFH3CNM.js.map +1 -0
- package/dist/{chunk-I6JCVE24.js → chunk-CEIUBPPW.js} +143 -17
- package/dist/chunk-CEIUBPPW.js.map +1 -0
- package/dist/chunk-EH7UEQV7.js +590 -0
- package/dist/chunk-EH7UEQV7.js.map +1 -0
- package/dist/{chunk-MOM3GOVY.js → chunk-EHGVROVV.js} +125 -27
- package/dist/chunk-EHGVROVV.js.map +1 -0
- package/dist/{chunk-TJ7QY3OC.js → chunk-ERFMC6WU.js} +160 -78
- package/dist/chunk-ERFMC6WU.js.map +1 -0
- package/dist/{chunk-CTRKTLYZ.js → chunk-H6ZAOWNE.js} +11 -11
- package/dist/chunk-H6ZAOWNE.js.map +1 -0
- package/dist/{chunk-64ZCN3DA.js → chunk-IHUVQ3TX.js} +250 -160
- package/dist/chunk-IHUVQ3TX.js.map +1 -0
- package/dist/{chunk-FQMJUVHJ.js → chunk-IJJUK4FU.js} +53 -46
- package/dist/chunk-IJJUK4FU.js.map +1 -0
- package/dist/chunk-IXTQI7JX.js +737 -0
- package/dist/chunk-IXTQI7JX.js.map +1 -0
- package/dist/{chunk-BKFVHKJH.js → chunk-MDVTK5U3.js} +4 -4
- package/dist/{chunk-BKFVHKJH.js.map → chunk-MDVTK5U3.js.map} +1 -1
- package/dist/chunk-PHK4W562.js +72 -0
- package/dist/chunk-PHK4W562.js.map +1 -0
- package/dist/chunk-PJ7WEQBJ.js +18 -0
- package/dist/chunk-PJ7WEQBJ.js.map +1 -0
- package/dist/chunk-RWNDDKOH.js +682 -0
- package/dist/chunk-RWNDDKOH.js.map +1 -0
- package/dist/chunk-T3EC2DXD.js +12628 -0
- package/dist/chunk-T3EC2DXD.js.map +1 -0
- package/dist/{chunk-SMHP6XZ4.js → chunk-UN4U4P63.js} +10 -8
- package/dist/chunk-UN4U4P63.js.map +1 -0
- package/dist/chunk-WIG6XHJ7.js +35 -0
- package/dist/chunk-WIG6XHJ7.js.map +1 -0
- package/dist/chunk-Z76KEZAX.js +15 -0
- package/dist/chunk-Z76KEZAX.js.map +1 -0
- package/dist/chunk-ZDHY47EV.js +363 -0
- package/dist/chunk-ZDHY47EV.js.map +1 -0
- package/dist/config/index.d.ts +276 -0
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controls/index.d.ts +3 -4
- package/dist/controls/index.js +4 -3
- package/dist/dragdrop/index.d.ts +3 -5
- package/dist/index.d.ts +266 -413
- package/dist/index.js +1775 -2251
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +4 -6
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +69 -20
- package/dist/loupe/index.js +2 -2
- package/dist/passthrough/weasel-ui.d.ts +175 -309
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +55 -6
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +13 -8
- package/dist/state/index.js +18 -13
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +223 -76
- package/dist/surface/index.d.ts +44 -7
- package/dist/surface/index.js +2 -1
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +3 -5
- package/package.json +15 -8
- package/src/annotations/AnnotationOverlay.tsx +56 -5
- package/src/annotations/Annotations.less +4 -0
- package/src/annotations/Annotations.overlay.test.tsx +105 -6
- package/src/annotations/drawOne.test.ts +1 -1
- package/src/annotations/drawOne.ts +2 -2
- package/src/annotations/paint.test.ts +2 -2
- package/src/annotations/paint.ts +1 -1
- package/src/annotations/preload.ts +6 -0
- package/src/annotations/store.test.ts +18 -0
- package/src/annotations/store.ts +5 -4
- package/src/annotations/svgNodes.test.ts +2 -2
- package/src/annotations/svgNodes.ts +1 -0
- package/src/annotations/toolMap.test.ts +9 -5
- package/src/annotations/toolMap.ts +6 -3
- package/src/annotations/types.ts +6 -3
- package/src/canvas/CameraWheelContext.ts +12 -0
- package/src/canvas/CanvasStack.stories.tsx +1 -1
- package/src/canvas/CanvasStack.test.tsx +25 -0
- package/src/canvas/CanvasStack.tsx +31 -4
- package/src/canvas/Stage.less +18 -0
- package/src/canvas/Stage.test.tsx +186 -0
- package/src/canvas/Stage.tsx +134 -0
- package/src/canvas/camera.test.ts +17 -2
- package/src/canvas/camera.ts +9 -9
- package/src/canvas/index.ts +4 -0
- package/src/canvas/usePanZoom.test.ts +28 -0
- package/src/canvas/usePanZoom.ts +21 -9
- package/src/chrome/ChromeRegions.stories.tsx +1 -4
- package/src/chrome/LabChrome.tsx +130 -0
- package/src/chrome/builtins.test.ts +1 -0
- package/src/chrome/builtins.tsx +21 -31
- package/src/chrome/index.ts +16 -0
- package/src/chrome/labRegions.test.tsx +208 -0
- package/src/chrome/labTypes.ts +50 -0
- package/src/chrome/merge.ts +7 -9
- package/src/chrome/regions/PaletteRegion.test.tsx +6 -4
- package/src/chrome/regions/PaletteRegion.tsx +16 -9
- package/src/chrome/regions/SidebarRegion.stories.tsx +44 -0
- package/src/chrome/regions/SidebarRegion.tsx +25 -13
- package/src/chrome/regions/StatusRegion.tsx +15 -8
- package/src/chrome/regions/ToolbarRegion.tsx +34 -16
- package/src/chrome/types.ts +50 -20
- package/src/config/auto.test.ts +12 -0
- package/src/config/auto.ts +15 -0
- package/src/config/autoConfig.test.ts +107 -0
- package/src/config/autoConfig.ts +153 -0
- package/src/config/builder.test.ts +42 -0
- package/src/config/builder.ts +68 -17
- package/src/config/declarationEmit.fixture.ts +11 -0
- package/src/config/declarationEmit.test.ts +51 -0
- package/src/config/entry.test.ts +46 -0
- package/src/config/index.ts +16 -1
- package/src/config/nodeClasses.test.ts +18 -0
- package/src/config/path.ts +7 -2
- package/src/config/resolve.test.ts +25 -0
- package/src/config/resolve.ts +17 -1
- package/src/config/types.ts +34 -2
- package/src/config/useResolvedConfig.ts +26 -0
- package/src/controls/AutoControls.stories.tsx +88 -0
- package/src/controls/ControlPanel.stories.tsx +42 -1
- package/src/controls/ControlPanel.test.tsx +530 -4
- package/src/controls/ControlPanel.tsx +562 -82
- package/src/fake-indexeddb-auto.d.ts +5 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +66 -5
- package/src/instrument/serializers.test.ts +22 -0
- package/src/instrument/serializers.ts +40 -1
- package/src/instrument/types.ts +25 -1
- package/src/lab/Lab.chrome.test.tsx +16 -0
- package/src/lab/Lab.less +4 -13
- package/src/lab/Lab.persist.test.tsx +147 -0
- package/src/lab/Lab.stories.tsx +1 -3
- package/src/lab/Lab.surface.test.tsx +71 -3
- package/src/lab/Lab.test.tsx +168 -16
- package/src/lab/Lab.tsx +434 -129
- package/src/lab/LabContext.ts +8 -0
- package/src/lab/LabFit.stories.less +18 -0
- package/src/lab/LabFit.stories.tsx +219 -0
- package/src/lab/LabFullChrome.stories.tsx +1 -4
- package/src/lab/LabHeader.test.tsx +40 -9
- package/src/lab/LabHeader.tsx +7 -14
- package/src/lab/LabPalette.tsx +13 -26
- package/src/lab/LabRoot.test.tsx +56 -0
- package/src/lab/LabRoot.tsx +47 -0
- package/src/lab/LabShell.less +73 -15
- package/src/lab/LabShell.stories.tsx +50 -1
- package/src/lab/LabShell.tsx +18 -27
- package/src/lab/LabSwitcher.less +18 -11
- package/src/lab/LabSwitcher.test.tsx +14 -9
- package/src/lab/LabSwitcher.tsx +8 -4
- package/src/lab/Workspace.less +1 -1
- package/src/lab/Workspace.stories.tsx +1 -1
- package/src/lab/Workspace.surface.test.tsx +1 -0
- package/src/lab/fitCheck.test.ts +57 -0
- package/src/lab/fitCheck.ts +125 -0
- package/src/lab/index.ts +2 -0
- package/src/layers/LayerList.stories.tsx +1 -1
- package/src/loupe/AGENTS.md +17 -5
- package/src/loupe/Loupe.less +1 -1
- package/src/loupe/LoupeGestures.tsx +67 -0
- package/src/loupe/TrialLoupe.tsx +43 -28
- package/src/loupe/canvasLens.ts +1 -1
- package/src/loupe/index.ts +4 -0
- package/src/loupe/loupeActions.ts +98 -0
- package/src/loupe/types.ts +1 -1
- package/src/loupe/useLoupe.test.tsx +60 -3
- package/src/loupe/useLoupe.ts +29 -49
- package/src/passthrough/weasel-ui.ts +9 -0
- package/src/primitives/FloatingPanel.stories.tsx +14 -2
- package/src/primitives/FloatingPanel.test.tsx +47 -16
- package/src/primitives/FloatingPanel.tsx +11 -25
- package/src/primitives/FpsMeter.stories.tsx +1 -1
- package/src/primitives/JobProgress.stories.tsx +1 -1
- package/src/primitives/Legend.stories.tsx +1 -1
- package/src/primitives/ScaleIndicator.stories.tsx +1 -1
- package/src/primitives/Sidebar.less +7 -0
- package/src/primitives/Sidebar.stories.tsx +1 -1
- package/src/primitives/Split.less +14 -0
- package/src/primitives/Split.test.tsx +133 -0
- package/src/primitives/Split.tsx +175 -0
- package/src/primitives/StatusBar.stories.tsx +1 -1
- package/src/primitives/Toolbar.stories.tsx +1 -1
- package/src/primitives/ZoomControl.tsx +5 -2
- package/src/primitives/index.ts +2 -0
- package/src/specimen/Specimen.less +95 -0
- package/src/specimen/Specimen.stories.tsx +13 -0
- package/src/specimen/Specimen.test.tsx +13 -0
- package/src/specimen/Specimen.tsx +888 -0
- package/src/state/Persistence.stories.tsx +75 -0
- package/src/state/Persistence.tsx +33 -0
- package/src/state/SingletonExperiment.test.tsx +47 -53
- package/src/state/SingletonExperiment.tsx +24 -13
- package/src/state/adapterContract.ts +116 -0
- package/src/state/adapters.test.ts +113 -61
- package/src/state/adapters.ts +328 -69
- package/src/state/document.test.ts +71 -96
- package/src/state/document.ts +66 -52
- package/src/state/helpers.test.ts +1 -1
- package/src/state/index.ts +8 -0
- package/src/state/labRecords.test.ts +105 -0
- package/src/state/labRecords.ts +160 -0
- package/src/state/openLabStore.test.ts +658 -0
- package/src/state/openLabStore.ts +403 -0
- package/src/state/records.test.ts +234 -0
- package/src/state/records.ts +219 -0
- package/src/state/store.test.ts +169 -635
- package/src/state/store.ts +146 -163
- package/src/state/toolSlot.test.ts +1 -1
- package/src/state/types.ts +65 -17
- package/src/state/useOpenOnce.ts +68 -0
- package/src/state/usePersistedState.test.tsx +125 -0
- package/src/state/usePersistedState.ts +66 -0
- package/src/state/useTrialState.test.tsx +87 -3
- package/src/state/useTrialState.ts +9 -1
- package/src/state/view.test.ts +49 -15
- package/src/state/view.ts +18 -1
- package/src/styles.less +3 -0
- package/src/surface/AGENTS.md +39 -7
- package/src/surface/SurfaceContext.ts +26 -3
- package/src/surface/index.ts +2 -0
- package/src/surface/useSurfaceTile.test.tsx +1 -0
- package/src/surface/useSurfaceTile.ts +6 -5
- package/src/surface/useTiledSurface.test.tsx +231 -1
- package/src/surface/useTiledSurface.ts +76 -15
- package/src/theme/Interstellar.stories.tsx +9 -9
- package/src/theme/interstellar.test.ts +17 -6
- package/src/theme/interstellar.theme.json +180 -0
- package/src/theme/interstellar.ts +8 -5
- package/src/tools/labTool.ts +11 -0
- package/src/trial/Trial.annotations.persist.test.tsx +100 -58
- package/src/trial/Trial.annotations.test.tsx +59 -7
- package/src/trial/Trial.canvas.test.tsx +32 -0
- package/src/trial/Trial.config.test.tsx +75 -2
- package/src/trial/Trial.job.test.tsx +3 -3
- package/src/trial/Trial.less +5 -26
- package/src/trial/Trial.stories.tsx +5 -3
- package/src/trial/Trial.test.tsx +31 -4
- package/src/trial/Trial.trialId.test.tsx +23 -1
- package/src/trial/Trial.tsx +139 -52
- package/src/trial/TrialBody.tsx +17 -141
- package/src/trial/TrialChrome.tsx +24 -7
- package/src/trial/trialOps.test.ts +101 -1
- package/src/trial/trialOps.ts +67 -7
- package/dist/_dts/types-BP2OCcpg.d.ts +0 -155
- package/dist/_dts/types-x92Kfeme.d.ts +0 -62
- package/dist/_dts/useTrialState-D6Vb3T-g.d.ts +0 -99
- package/dist/chunk-2P6PP5N4.js +0 -553
- package/dist/chunk-2P6PP5N4.js.map +0 -1
- package/dist/chunk-64ZCN3DA.js.map +0 -1
- package/dist/chunk-CTRKTLYZ.js.map +0 -1
- package/dist/chunk-E44SU6XS.js +0 -50
- package/dist/chunk-E44SU6XS.js.map +0 -1
- package/dist/chunk-FQMJUVHJ.js.map +0 -1
- package/dist/chunk-I6JCVE24.js.map +0 -1
- package/dist/chunk-MOM3GOVY.js.map +0 -1
- package/dist/chunk-SMHP6XZ4.js.map +0 -1
- package/dist/chunk-TJ7QY3OC.js.map +0 -1
- package/dist/chunk-U3IYHIAE.js +0 -343
- package/dist/chunk-U3IYHIAE.js.map +0 -1
- package/dist/chunk-W3ECWC2K.js +0 -7335
- package/dist/chunk-W3ECWC2K.js.map +0 -1
- package/src/theme/interstellar.tokens.json +0 -61
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* `SingletonExperiment` — so `useTrialState()` threw everywhere else, which
|
|
5
5
|
* made the documented hook pattern unreachable from an instrument's `render`.
|
|
6
6
|
*/
|
|
7
|
-
import { render, screen } from '@testing-library/react';
|
|
7
|
+
import { act, render, screen } from '@testing-library/react';
|
|
8
8
|
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
9
9
|
import { defineInstrument } from '../instrument/defineInstrument';
|
|
10
10
|
import { Lab } from '../lab/Lab';
|
|
11
|
+
import { LabContext, type LabContextValue } from '../lab/LabContext';
|
|
11
12
|
import { useTrialState } from '../state/useTrialState';
|
|
12
13
|
|
|
13
14
|
beforeAll(() => {
|
|
@@ -34,3 +35,24 @@ describe('<Trial> trial-scoped hooks', () => {
|
|
|
34
35
|
expect(screen.getByTestId('probe')).toHaveTextContent('hits:7');
|
|
35
36
|
});
|
|
36
37
|
});
|
|
38
|
+
|
|
39
|
+
describe('<Trial> root element', () => {
|
|
40
|
+
it('carries its trial id, so a lab can find a trial from outside', () => {
|
|
41
|
+
let labRef: LabContextValue | null = null;
|
|
42
|
+
const { container } = render(
|
|
43
|
+
<Lab instruments={[reader]} defaultInstrument="Reader">
|
|
44
|
+
<LabContext.Consumer>
|
|
45
|
+
{(value) => {
|
|
46
|
+
if (value) labRef = value;
|
|
47
|
+
return null;
|
|
48
|
+
}}
|
|
49
|
+
</LabContext.Consumer>
|
|
50
|
+
</Lab>,
|
|
51
|
+
);
|
|
52
|
+
act(() => (labRef as LabContextValue | null)?.addTrial('Reader'));
|
|
53
|
+
const ids = (labRef as LabContextValue | null)?.trials.map((t) => t.id) ?? [];
|
|
54
|
+
expect(ids).toHaveLength(2);
|
|
55
|
+
const roots = container.querySelectorAll<HTMLElement>('.lk-trial[data-trial-id]');
|
|
56
|
+
expect([...roots].map((el) => el.dataset.trialId)).toEqual(ids);
|
|
57
|
+
});
|
|
58
|
+
});
|
package/src/trial/Trial.tsx
CHANGED
|
@@ -2,15 +2,26 @@ import { type ReactNode, useContext, useEffect, useMemo, useRef, useState } from
|
|
|
2
2
|
import { useStore } from 'zustand/react';
|
|
3
3
|
import { AnnotationsContext } from '../annotations/AnnotationsContext';
|
|
4
4
|
import { AnnotationTargets } from '../annotations/AnnotationTargets';
|
|
5
|
+
import { AnnotationPreloadContext } from '../annotations/preload';
|
|
5
6
|
import { annotationsFromJSON } from '../annotations/store';
|
|
6
|
-
import type { AnnotationTargetInfo } from '../annotations/types';
|
|
7
|
+
import type { AnnotationStorage, AnnotationTargetInfo } from '../annotations/types';
|
|
8
|
+
import { CameraWheelContext, type CameraWheelSlot } from '../canvas/CameraWheelContext';
|
|
7
9
|
import { CanvasStack } from '../canvas/CanvasStack';
|
|
10
|
+
import { fitStage, Stage } from '../canvas/Stage';
|
|
8
11
|
import type { CanvasLayerDescriptor } from '../canvas/useLayerScheduler';
|
|
9
12
|
import { applyCamera, type ViewportSize } from '../canvas/worldSpec';
|
|
10
13
|
import type { TrialContribution } from '../chrome/types';
|
|
14
|
+
import { useConfigSchema } from '../config/useConfigSchema';
|
|
15
|
+
import { useResolvedConfig } from '../config/useResolvedConfig';
|
|
11
16
|
import { DragOverlay, useDragDrop } from '../dragdrop/DragDropRuntime';
|
|
12
17
|
import { Palette } from '../dragdrop/Palette';
|
|
13
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
Instrument,
|
|
20
|
+
InstrumentList,
|
|
21
|
+
LayerDescriptor,
|
|
22
|
+
PaletteItem,
|
|
23
|
+
RenderContext,
|
|
24
|
+
} from '../instrument/types';
|
|
14
25
|
import { useJob } from '../job/useJob';
|
|
15
26
|
import { useLabContext } from '../lab/LabContext';
|
|
16
27
|
import { LayerList } from '../layers/LayerList';
|
|
@@ -19,7 +30,8 @@ import { resolveLoupe } from '../loupe/types';
|
|
|
19
30
|
import { LabStoreContext, TrialIdProvider } from '../state/context';
|
|
20
31
|
import type { LabStore } from '../state/store';
|
|
21
32
|
import type { TrialRecord } from '../state/types';
|
|
22
|
-
import { as2DView, DEFAULT_VIEW } from '../state/view';
|
|
33
|
+
import { as2DView, DEFAULT_VIEW, withZoom } from '../state/view';
|
|
34
|
+
import { resolveLabTool } from '../tools/labTool';
|
|
23
35
|
import { createEventBus, type EventBus } from '../undo/eventBus';
|
|
24
36
|
import { pushSnapshot, redo as undoRedo, undo as undoUndo } from '../undo/undoStack';
|
|
25
37
|
import type { LoupeBindings, UndoBindings } from './TrialChrome';
|
|
@@ -41,20 +53,25 @@ export function Trial({ id, chrome, suppress }: TrialProps) {
|
|
|
41
53
|
const lab = useLabContext();
|
|
42
54
|
const storeCtx = useContext(LabStoreContext);
|
|
43
55
|
if (!storeCtx) throw new Error('[labkit] <Trial> requires <LabStoreProvider>');
|
|
56
|
+
const instruments = useStore(storeCtx.store, (s) => s.instruments) ?? lab.instruments;
|
|
44
57
|
const record = useStore(storeCtx.store, (s) => s.trials.find((w) => w.id === id));
|
|
58
|
+
const instrument = record ? instruments.find((i) => i.name === record.instrumentName) : undefined;
|
|
59
|
+
const kept = useKeptMarks(instrument?.annotations?.storage, id);
|
|
45
60
|
if (!record) {
|
|
46
61
|
return <div className="lk-trial lk-trial--unknown">Trial not found: {id}</div>;
|
|
47
62
|
}
|
|
48
|
-
const instrument = lab.instruments.find((i) => i.name === record.instrumentName);
|
|
49
63
|
if (!instrument) {
|
|
50
64
|
return (
|
|
51
65
|
<div className="lk-trial lk-trial--unknown">Unknown instrument: {record.instrumentName}</div>
|
|
52
66
|
);
|
|
53
67
|
}
|
|
68
|
+
if (!kept.ready) return <div className="lk-trial lk-trial--loading" />;
|
|
54
69
|
return (
|
|
55
70
|
<TrialRuntime
|
|
71
|
+
keptMarks={kept.value}
|
|
56
72
|
record={record}
|
|
57
73
|
instrument={instrument}
|
|
74
|
+
instruments={instruments}
|
|
58
75
|
store={storeCtx.store}
|
|
59
76
|
isLast={lab.trials.length <= 1}
|
|
60
77
|
chrome={chrome}
|
|
@@ -63,16 +80,60 @@ export function Trial({ id, chrome, suppress }: TrialProps) {
|
|
|
63
80
|
);
|
|
64
81
|
}
|
|
65
82
|
|
|
83
|
+
/** The marks an instrument keeps in its own storage: preloaded by the lab for
|
|
84
|
+
* the trials it opened with, loaded here for any added since. */
|
|
85
|
+
function useKeptMarks(
|
|
86
|
+
storage: AnnotationStorage | undefined,
|
|
87
|
+
trialId: string,
|
|
88
|
+
): { ready: boolean; value: unknown } {
|
|
89
|
+
const preload = useContext(AnnotationPreloadContext);
|
|
90
|
+
const [loaded, setLoaded] = useState<{ value: unknown } | null>(() =>
|
|
91
|
+
storage && preload?.has(trialId) ? { value: preload.get(trialId) } : null,
|
|
92
|
+
);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!storage || loaded) return;
|
|
95
|
+
let live = true;
|
|
96
|
+
storage.load().then(
|
|
97
|
+
(value) => {
|
|
98
|
+
if (live) setLoaded({ value });
|
|
99
|
+
},
|
|
100
|
+
(error) => {
|
|
101
|
+
console.warn(`[labkit] could not load the marks of trial "${trialId}"`, error);
|
|
102
|
+
if (live) setLoaded({ value: null });
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
return () => {
|
|
106
|
+
live = false;
|
|
107
|
+
};
|
|
108
|
+
}, [storage, loaded, trialId]);
|
|
109
|
+
if (!storage) return { ready: true, value: undefined };
|
|
110
|
+
return loaded ? { ready: true, value: loaded.value } : { ready: false, value: undefined };
|
|
111
|
+
}
|
|
112
|
+
|
|
66
113
|
interface TrialRuntimeProps {
|
|
114
|
+
/** The marks an instrument's own `AnnotationStorage` loaded. */
|
|
115
|
+
keptMarks: unknown;
|
|
67
116
|
record: TrialRecord;
|
|
68
117
|
instrument: Instrument;
|
|
118
|
+
/** The lab's instruments as the store holds them now. */
|
|
119
|
+
instruments: InstrumentList;
|
|
69
120
|
store: LabStore;
|
|
70
121
|
isLast: boolean;
|
|
71
122
|
chrome?: readonly TrialContribution[];
|
|
72
123
|
suppress?: readonly string[];
|
|
73
124
|
}
|
|
74
125
|
|
|
75
|
-
function TrialRuntime({
|
|
126
|
+
function TrialRuntime({
|
|
127
|
+
keptMarks,
|
|
128
|
+
record,
|
|
129
|
+
instrument,
|
|
130
|
+
instruments,
|
|
131
|
+
store,
|
|
132
|
+
isLast,
|
|
133
|
+
chrome,
|
|
134
|
+
suppress,
|
|
135
|
+
}: TrialRuntimeProps) {
|
|
136
|
+
const wheelSlot = useRef<CameraWheelSlot['current']>(null);
|
|
76
137
|
const canvasContainerRef = useRef<HTMLDivElement | null>(null);
|
|
77
138
|
const loupeHostRef = useRef<HTMLDivElement | null>(null);
|
|
78
139
|
const updateTrialState = useStore(store, (s) => s.updateTrialState);
|
|
@@ -84,6 +145,11 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
84
145
|
const setLabTool = useStore(store, (s) => s.setLabTool);
|
|
85
146
|
const setTrialTool = useStore(store, (s) => s.setTrialTool);
|
|
86
147
|
|
|
148
|
+
// Everything below reads `config`; `record.config` is the panel's view, and
|
|
149
|
+
// reaches it through `record` on `TrialChrome`.
|
|
150
|
+
const schema = useConfigSchema(instrument);
|
|
151
|
+
const { config } = useResolvedConfig(schema, record.config, record.auto);
|
|
152
|
+
|
|
87
153
|
const busRef = useRef<EventBus | null>(null);
|
|
88
154
|
if (busRef.current === null) busRef.current = createEventBus();
|
|
89
155
|
const bus = busRef.current;
|
|
@@ -117,8 +183,11 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
117
183
|
// A function `initialView` needs the canvas size, so `trialOps` leaves the
|
|
118
184
|
// view null and the first measurement resolves it. Reset nulls it again,
|
|
119
185
|
// which re-frames — so the guard is the null itself, not a "have I run" flag.
|
|
186
|
+
const stage = instrument.canvas ? undefined : instrument.stage;
|
|
120
187
|
const placeView = (size: ViewportSize): void => {
|
|
121
|
-
const declared =
|
|
188
|
+
const declared =
|
|
189
|
+
instrument.canvas?.initialView ??
|
|
190
|
+
(stage ? (stage.initialView ?? ((vp: ViewportSize) => fitStage(stage.size, vp))) : undefined);
|
|
122
191
|
if (typeof declared !== 'function') return;
|
|
123
192
|
if (record.view != null) return;
|
|
124
193
|
setView(declared(size));
|
|
@@ -131,24 +200,26 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
131
200
|
const targetsRef = useRef<() => readonly AnnotationTargetInfo[]>(() => []);
|
|
132
201
|
targetsRef.current = () =>
|
|
133
202
|
annotationsCap
|
|
134
|
-
? annotationsCap.targets(record.state,
|
|
203
|
+
? annotationsCap.targets(record.state, config, { id: record.id, view: record.view })
|
|
135
204
|
: [];
|
|
136
205
|
|
|
137
|
-
// One store for the trial's lifetime.
|
|
138
|
-
// the storage slot is 3d.
|
|
206
|
+
// One store for the trial's lifetime.
|
|
139
207
|
const annotationsRef = useRef<ReturnType<typeof annotationsFromJSON> | null>(null);
|
|
140
208
|
// Read through refs for the same reason `targets` is: the store is built
|
|
141
209
|
// once, and an export must draw against the config the trial holds now.
|
|
142
|
-
const configRef = useRef<unknown>(
|
|
143
|
-
configRef.current =
|
|
210
|
+
const configRef = useRef<unknown>(config);
|
|
211
|
+
configRef.current = config;
|
|
212
|
+
// The instrument can be swapped under a live trial, so the capability is read at use too.
|
|
213
|
+
const capRef = useRef(annotationsCap);
|
|
214
|
+
capRef.current = annotationsCap;
|
|
144
215
|
if (annotationsRef.current === null) {
|
|
145
216
|
// Seeded from wherever the marks were kept: the instrument's own store if
|
|
146
217
|
// it declared one, else this trial's slot.
|
|
147
|
-
const kept = annotationsCap?.storage ?
|
|
218
|
+
const kept = annotationsCap?.storage ? keptMarks : record.annotations;
|
|
148
219
|
annotationsRef.current = annotationsFromJSON(kept, () => targetsRef.current(), {
|
|
149
|
-
meaning:
|
|
220
|
+
meaning: () => capRef.current?.meaning,
|
|
150
221
|
config: () => configRef.current,
|
|
151
|
-
onCapture: (result) =>
|
|
222
|
+
onCapture: (result) => capRef.current?.onCapture?.(result),
|
|
152
223
|
});
|
|
153
224
|
}
|
|
154
225
|
const annotations = annotationsRef.current;
|
|
@@ -203,15 +274,14 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
203
274
|
|
|
204
275
|
// A trial gets its own slot when its instrument declares tools; otherwise it
|
|
205
276
|
// reads the lab's. Which slot a change writes follows from the same thing.
|
|
206
|
-
// Annotation tools
|
|
207
|
-
//
|
|
208
|
-
|
|
277
|
+
// Annotation tools always read the lab's: a tool is what the hand holds, so
|
|
278
|
+
// picking one arms it in every trial, the way artboards share a tool. They
|
|
279
|
+
// were once per trial, back when one trial compared several pictures.
|
|
280
|
+
const declaresTools = instrument.tools != null;
|
|
281
|
+
const labTool = resolveLabTool(labToolId, instruments);
|
|
209
282
|
const resolvedToolId = declaresTools
|
|
210
|
-
? (record.activeToolId ??
|
|
211
|
-
|
|
212
|
-
instrument.tools?.tools[0]?.id ??
|
|
213
|
-
(instrument.annotations ? 'select' : null))
|
|
214
|
-
: labToolId;
|
|
283
|
+
? (record.activeToolId ?? instrument.tools?.initial ?? instrument.tools?.tools[0]?.id ?? null)
|
|
284
|
+
: labTool;
|
|
215
285
|
const setActiveTool = (id: string): void => {
|
|
216
286
|
if (declaresTools) setTrialTool(record.id, id);
|
|
217
287
|
else setLabTool(id);
|
|
@@ -226,14 +296,14 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
226
296
|
// this — with a runner that yields nothing and is never started.
|
|
227
297
|
const job = useJob({
|
|
228
298
|
capability: jobCap ?? { run: async function* () {}, onItem: (_i, st) => st },
|
|
229
|
-
config
|
|
299
|
+
config,
|
|
230
300
|
state: record.state,
|
|
231
301
|
setState: (next) => updateTrialState(record.id, next as never),
|
|
232
302
|
});
|
|
233
303
|
|
|
234
304
|
const renderCtx: RenderContext<unknown, unknown> = {
|
|
235
305
|
state: record.state,
|
|
236
|
-
config
|
|
306
|
+
config,
|
|
237
307
|
setState: (next) => {
|
|
238
308
|
snapshotIfNeeded('state.change');
|
|
239
309
|
updateTrialState(record.id, next);
|
|
@@ -254,7 +324,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
254
324
|
zoom: view2d?.zoom ?? 1,
|
|
255
325
|
setZoom: (z) => {
|
|
256
326
|
if (!view2d) return;
|
|
257
|
-
updateTrialView(record.id,
|
|
327
|
+
updateTrialView(record.id, withZoom(view2d, z));
|
|
258
328
|
},
|
|
259
329
|
activeToolId: resolvedToolId,
|
|
260
330
|
visibleLayers,
|
|
@@ -302,8 +372,8 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
302
372
|
const loupeCap = useMemo(() => {
|
|
303
373
|
const declared = instrument.loupe;
|
|
304
374
|
if (declared == null) return null;
|
|
305
|
-
return resolveLoupe(typeof declared === 'function' ? declared(
|
|
306
|
-
}, [instrument.loupe,
|
|
375
|
+
return resolveLoupe(typeof declared === 'function' ? declared(config) : declared);
|
|
376
|
+
}, [instrument.loupe, config]);
|
|
307
377
|
const loupeBindings: LoupeBindings | undefined = loupeCap
|
|
308
378
|
? { on: loupeOn, toggle: () => setLoupeOn((v) => !v) }
|
|
309
379
|
: undefined;
|
|
@@ -321,10 +391,10 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
321
391
|
// Camera applied here, so a layer draws in world coordinates. `zoom`
|
|
322
392
|
// stays in the args for line widths, which must not scale with it.
|
|
323
393
|
applyCamera(ctx, view, frame);
|
|
324
|
-
layer.draw(ctx, { state: record.state, config
|
|
394
|
+
layer.draw(ctx, { state: record.state, config, zoom: view.zoom });
|
|
325
395
|
},
|
|
326
396
|
}));
|
|
327
|
-
}, [instrument.canvas, record.state,
|
|
397
|
+
}, [instrument.canvas, record.state, config, layerVisibility, layerOrder]);
|
|
328
398
|
|
|
329
399
|
const layerDescriptors: LayerDescriptor[] = useMemo(() => {
|
|
330
400
|
if (!instrument.layers) return [];
|
|
@@ -337,7 +407,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
337
407
|
view: view2d ?? DEFAULT_VIEW,
|
|
338
408
|
worldSpec: instrument.canvas?.worldSpec,
|
|
339
409
|
state: record.state,
|
|
340
|
-
config
|
|
410
|
+
config,
|
|
341
411
|
setState: (next) => {
|
|
342
412
|
snapshotIfNeeded('canvas.itemAdded');
|
|
343
413
|
updateTrialState(record.id, next as never);
|
|
@@ -351,8 +421,8 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
351
421
|
const paletteItems: PaletteItem[] = useMemo(() => {
|
|
352
422
|
if (!instrument.dragDrop) return [];
|
|
353
423
|
const p = instrument.dragDrop.palette;
|
|
354
|
-
return typeof p === 'function' ? p(record.state,
|
|
355
|
-
}, [instrument.dragDrop, record.state,
|
|
424
|
+
return typeof p === 'function' ? p(record.state, config) : p;
|
|
425
|
+
}, [instrument.dragDrop, record.state, config]);
|
|
356
426
|
|
|
357
427
|
const layersWithFeedback: CanvasLayerDescriptor[] = useMemo(() => {
|
|
358
428
|
if (!dragDropResult.drag?.feedback) return canvasLayers;
|
|
@@ -386,7 +456,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
386
456
|
capability={loupeCap}
|
|
387
457
|
enabled={loupeOn}
|
|
388
458
|
state={record.state}
|
|
389
|
-
config={
|
|
459
|
+
config={config}
|
|
390
460
|
view={view2d ?? DEFAULT_VIEW}
|
|
391
461
|
worldSpec={instrument.canvas?.worldSpec}
|
|
392
462
|
hostRef={loupeHostRef}
|
|
@@ -412,6 +482,21 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
412
482
|
<DragOverlay drag={dragDropResult.drag} />
|
|
413
483
|
</div>
|
|
414
484
|
);
|
|
485
|
+
} else if (stage) {
|
|
486
|
+
body = (
|
|
487
|
+
<Stage
|
|
488
|
+
size={stage.size}
|
|
489
|
+
view={view2d ?? DEFAULT_VIEW}
|
|
490
|
+
onViewChange={setView}
|
|
491
|
+
onResize={placeView}
|
|
492
|
+
minZoom={stage.minZoom}
|
|
493
|
+
maxZoom={stage.maxZoom}
|
|
494
|
+
hostRef={loupeHostRef}
|
|
495
|
+
overlay={lens}
|
|
496
|
+
>
|
|
497
|
+
{instrument.render(renderCtx)}
|
|
498
|
+
</Stage>
|
|
499
|
+
);
|
|
415
500
|
} else if (lens) {
|
|
416
501
|
body = (
|
|
417
502
|
<div ref={loupeHostRef} className="lk-trial__loupe-host">
|
|
@@ -475,33 +560,35 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }: T
|
|
|
475
560
|
<AnnotationTargets
|
|
476
561
|
capability={annotationsCap}
|
|
477
562
|
state={record.state}
|
|
478
|
-
config={
|
|
563
|
+
config={config}
|
|
479
564
|
trial={{ id: record.id, view: record.view }}
|
|
480
565
|
annotations={annotations}
|
|
481
|
-
activeToolId={
|
|
566
|
+
activeToolId={labTool}
|
|
482
567
|
/>
|
|
483
568
|
) : null;
|
|
484
569
|
|
|
485
570
|
return (
|
|
486
571
|
<TrialIdProvider trialId={record.id}>
|
|
487
572
|
<AnnotationsContext.Provider value={annotationsCap ? annotations : null}>
|
|
488
|
-
<
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
573
|
+
<CameraWheelContext.Provider value={wheelSlot}>
|
|
574
|
+
<TrialChrome
|
|
575
|
+
job={jobCap ? job : undefined}
|
|
576
|
+
loupe={loupeBindings}
|
|
577
|
+
trialId={record.id}
|
|
578
|
+
record={record}
|
|
579
|
+
instrument={instrument}
|
|
580
|
+
isLastTrial={isLast}
|
|
581
|
+
undoBindings={undoBindings}
|
|
582
|
+
trialChrome={extraChrome}
|
|
583
|
+
chrome={chrome}
|
|
584
|
+
suppress={suppress}
|
|
585
|
+
activeToolId={resolvedToolId}
|
|
586
|
+
setActiveTool={setActiveTool}
|
|
587
|
+
>
|
|
588
|
+
{body}
|
|
589
|
+
{annotationOverlays}
|
|
590
|
+
</TrialChrome>
|
|
591
|
+
</CameraWheelContext.Provider>
|
|
505
592
|
</AnnotationsContext.Provider>
|
|
506
593
|
</TrialIdProvider>
|
|
507
594
|
);
|
package/src/trial/TrialBody.tsx
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { type ChromeMap, Container, Provider, StrategyRegistryProvider } from 'windease/react';
|
|
4
|
-
|
|
5
|
-
const ZONE_ID = asNodeId('lk-trial-body');
|
|
6
|
-
const SIDEBAR_ID = asNodeId('lk-trial-sidebar');
|
|
7
|
-
const CONTENT_ID = asNodeId('lk-trial-content');
|
|
8
|
-
const STRATEGIES = { strip: stripStrategy as never };
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Split, type SplitProps } from '../primitives/Split';
|
|
9
3
|
|
|
10
4
|
/** Props for `<TrialBody>`. */
|
|
11
|
-
export interface TrialBodyProps
|
|
5
|
+
export interface TrialBodyProps
|
|
6
|
+
extends Omit<SplitProps, 'sidebar' | 'children' | 'className' | 'sidebarClassName' | 'label'> {
|
|
12
7
|
/** The sidebar region's sections. */
|
|
13
8
|
sidebar: ReactNode;
|
|
14
9
|
/** The instrument's own output. */
|
|
@@ -16,140 +11,21 @@ export interface TrialBodyProps {
|
|
|
16
11
|
/** Extra class on the content pane — `Trial` marks a canvas instrument's
|
|
17
12
|
* well flush. */
|
|
18
13
|
contentClassName?: string;
|
|
19
|
-
/** The sidebar's current extent in pixels. Omit for an unpersisted lab: the
|
|
20
|
-
* seam still works, its result just does not outlive the mount. */
|
|
21
|
-
width?: number;
|
|
22
|
-
/** Where the sidebar starts before anyone drags it. */
|
|
23
|
-
defaultWidth?: number;
|
|
24
|
-
minWidth?: number;
|
|
25
|
-
maxWidth?: number;
|
|
26
|
-
/** How little the instrument may be squeezed to. Reported as the seam's
|
|
27
|
-
* reachable maximum, so the drag stops there rather than clamping silently. */
|
|
28
|
-
contentMinWidth?: number;
|
|
29
|
-
/** Fires through the drag, not on release: the pane is already there. */
|
|
30
|
-
onWidthChange?: (width: number) => void;
|
|
31
|
-
/**
|
|
32
|
-
* Fixed extent. Omit in an app — the strip measures its own box. Supply it
|
|
33
|
-
* where nothing measures, notably jsdom, or both panes lay out at zero.
|
|
34
|
-
*/
|
|
35
|
-
viewport?: { w: number; h: number };
|
|
36
14
|
}
|
|
37
15
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
*
|
|
41
|
-
* The seam is `stripStrategy`'s own resize affordance, so it arrives as a
|
|
42
|
-
* `role="separator"` carrying the reachable range, operable by pointer and by
|
|
43
|
-
* arrows / Home / End, and clamped against the content pane's floor rather
|
|
44
|
-
* than against the sidebar's alone.
|
|
45
|
-
*/
|
|
46
|
-
export function TrialBody({
|
|
47
|
-
sidebar,
|
|
48
|
-
children,
|
|
49
|
-
contentClassName,
|
|
50
|
-
width,
|
|
51
|
-
defaultWidth = 320,
|
|
52
|
-
minWidth = 140,
|
|
53
|
-
maxWidth = 720,
|
|
54
|
-
contentMinWidth = 160,
|
|
55
|
-
onWidthChange,
|
|
56
|
-
viewport,
|
|
57
|
-
}: TrialBodyProps) {
|
|
58
|
-
// The cross-axis half of every size hint below is inert: a horizontal strip
|
|
59
|
-
// stretches its panes to the container's height and never reads `h`.
|
|
60
|
-
const storeRef = useRef<Store | null>(null);
|
|
61
|
-
if (storeRef.current === null) {
|
|
62
|
-
const store = new Store();
|
|
63
|
-
store.registerNode(
|
|
64
|
-
createNode({
|
|
65
|
-
kind: 'zone',
|
|
66
|
-
id: ZONE_ID,
|
|
67
|
-
container: { strategyId: 'strip', config: { axis: 'x', resizable: true } },
|
|
68
|
-
}),
|
|
69
|
-
);
|
|
70
|
-
store.registerNode(
|
|
71
|
-
createNode({
|
|
72
|
-
kind: 'sidebar',
|
|
73
|
-
id: SIDEBAR_ID,
|
|
74
|
-
parentId: ZONE_ID,
|
|
75
|
-
meta: { title: 'Sidebar' },
|
|
76
|
-
placement: { size: { w: width ?? defaultWidth } },
|
|
77
|
-
hints: { minSize: { w: minWidth, h: 0 }, maxSize: { w: maxWidth, h: 0 } },
|
|
78
|
-
}),
|
|
79
|
-
);
|
|
80
|
-
store.registerNode(
|
|
81
|
-
createNode({
|
|
82
|
-
kind: 'content',
|
|
83
|
-
id: CONTENT_ID,
|
|
84
|
-
parentId: ZONE_ID,
|
|
85
|
-
meta: { title: 'Content' },
|
|
86
|
-
hints: { minSize: { w: contentMinWidth, h: 0 } },
|
|
87
|
-
}),
|
|
88
|
-
);
|
|
89
|
-
store.showNode(SIDEBAR_ID);
|
|
90
|
-
store.showNode(CONTENT_ID);
|
|
91
|
-
storeRef.current = store;
|
|
92
|
-
}
|
|
93
|
-
const store = storeRef.current;
|
|
94
|
-
|
|
95
|
-
// What the store last held, so the placement event can tell a drag from the
|
|
96
|
-
// echo of our own write and a controlled `width` does not fight the seam.
|
|
97
|
-
const widthRef = useRef(width ?? defaultWidth);
|
|
98
|
-
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
store.setHints(SIDEBAR_ID, {
|
|
101
|
-
minSize: { w: minWidth, h: 0 },
|
|
102
|
-
maxSize: { w: maxWidth, h: 0 },
|
|
103
|
-
});
|
|
104
|
-
store.setHints(CONTENT_ID, { minSize: { w: contentMinWidth, h: 0 } });
|
|
105
|
-
}, [store, minWidth, maxWidth, contentMinWidth]);
|
|
106
|
-
|
|
107
|
-
useEffect(() => {
|
|
108
|
-
if (width === undefined || width === widthRef.current) return;
|
|
109
|
-
widthRef.current = width;
|
|
110
|
-
store.patchPlacement(SIDEBAR_ID, { size: { w: width } });
|
|
111
|
-
}, [store, width]);
|
|
112
|
-
|
|
113
|
-
const onWidthChangeRef = useRef(onWidthChange);
|
|
114
|
-
onWidthChangeRef.current = onWidthChange;
|
|
115
|
-
useEffect(
|
|
116
|
-
() =>
|
|
117
|
-
store.events.on('node.placementChanged', () => {
|
|
118
|
-
const next = (store.getPlacement(SIDEBAR_ID)?.size as { w?: number } | undefined)?.w;
|
|
119
|
-
if (typeof next !== 'number' || next === widthRef.current) return;
|
|
120
|
-
widthRef.current = next;
|
|
121
|
-
onWidthChangeRef.current?.(next);
|
|
122
|
-
}),
|
|
123
|
-
[store],
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
const chrome = useMemo<ChromeMap>(
|
|
127
|
-
() => ({
|
|
128
|
-
sidebar: () => <div className="lk-trial__sidebar">{sidebar}</div>,
|
|
129
|
-
content: () => (
|
|
130
|
-
<div
|
|
131
|
-
className={
|
|
132
|
-
contentClassName ? `lk-trial__content ${contentClassName}` : 'lk-trial__content'
|
|
133
|
-
}
|
|
134
|
-
>
|
|
135
|
-
{children}
|
|
136
|
-
</div>
|
|
137
|
-
),
|
|
138
|
-
}),
|
|
139
|
-
[sidebar, children, contentClassName],
|
|
140
|
-
);
|
|
141
|
-
|
|
16
|
+
/** A trial's sidebar and content as a `Split`, wearing the trial's classes. */
|
|
17
|
+
export function TrialBody({ sidebar, children, contentClassName, ...rest }: TrialBodyProps) {
|
|
142
18
|
return (
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
</
|
|
19
|
+
<Split
|
|
20
|
+
{...rest}
|
|
21
|
+
sidebar={sidebar}
|
|
22
|
+
className="lk-trial__panes"
|
|
23
|
+
sidebarClassName="lk-trial__sidebar"
|
|
24
|
+
contentClassName={
|
|
25
|
+
contentClassName ? `lk-trial__content ${contentClassName}` : 'lk-trial__content'
|
|
26
|
+
}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</Split>
|
|
154
30
|
);
|
|
155
31
|
}
|
|
@@ -9,15 +9,17 @@ import { TitleBarRegion } from '../chrome/regions/TitleBarRegion';
|
|
|
9
9
|
import { ToolbarRegion } from '../chrome/regions/ToolbarRegion';
|
|
10
10
|
import { ViewportRegion } from '../chrome/regions/ViewportRegion';
|
|
11
11
|
import type { TrialChromeContext, TrialContribution, TrialRegion } from '../chrome/types';
|
|
12
|
-
import {
|
|
12
|
+
import { applyConfigWrite, resolveAutoConfig } from '../config/autoConfig';
|
|
13
|
+
import { hasConfigPath, schemaNodeAtPath } from '../config/path';
|
|
13
14
|
import { useConfigSchema } from '../config/useConfigSchema';
|
|
15
|
+
import { useResolvedConfig } from '../config/useResolvedConfig';
|
|
14
16
|
import type { Instrument } from '../instrument/types';
|
|
15
17
|
import type { JobHandle } from '../job/types';
|
|
16
18
|
import { useLabContext } from '../lab/LabContext';
|
|
17
19
|
import { JobProgress } from '../primitives/JobProgress';
|
|
18
20
|
import { LabStoreContext } from '../state/context';
|
|
19
21
|
import type { TrialRecord } from '../state/types';
|
|
20
|
-
import { as2DView } from '../state/view';
|
|
22
|
+
import { as2DView, withZoom } from '../state/view';
|
|
21
23
|
import { TrialBody } from './TrialBody';
|
|
22
24
|
import { TrialTitleBar } from './TrialTitleBar';
|
|
23
25
|
import { UndockedSections } from './UndockedSections';
|
|
@@ -110,14 +112,21 @@ export function TrialChrome({
|
|
|
110
112
|
const view2d = as2DView(record.view);
|
|
111
113
|
|
|
112
114
|
const configSchema = useConfigSchema(instrument);
|
|
115
|
+
// What the instrument is looking at right now. The panel reads `record.config`
|
|
116
|
+
// — the pinned values — but anything instrument-facing gets the resolved one.
|
|
117
|
+
const { config: resolvedConfig, auto } = useResolvedConfig(
|
|
118
|
+
configSchema,
|
|
119
|
+
record.config,
|
|
120
|
+
record.auto,
|
|
121
|
+
);
|
|
113
122
|
|
|
114
|
-
const title = record.title ??
|
|
123
|
+
const title = record.title ?? instrument.title ?? instrument.name;
|
|
115
124
|
const collapsedSections = record.collapsedSections ?? NO_SECTIONS;
|
|
116
125
|
|
|
117
126
|
const ctx = useMemo<TrialChromeContext>(() => {
|
|
118
127
|
const setZoom = (z: number): void => {
|
|
119
128
|
if (!view2d) return;
|
|
120
|
-
updateTrialView(trialId,
|
|
129
|
+
updateTrialView(trialId, withZoom(view2d, z));
|
|
121
130
|
};
|
|
122
131
|
return {
|
|
123
132
|
trialId,
|
|
@@ -136,6 +145,7 @@ export function TrialChrome({
|
|
|
136
145
|
configSchema,
|
|
137
146
|
configFields: instrument.config ? [] : (instrument.configSchema?.() ?? []),
|
|
138
147
|
config: record.config,
|
|
148
|
+
auto,
|
|
139
149
|
setConfig: (path, value) => {
|
|
140
150
|
const prevConfig = record.config as Record<string, unknown>;
|
|
141
151
|
if (
|
|
@@ -149,8 +159,9 @@ export function TrialChrome({
|
|
|
149
159
|
}
|
|
150
160
|
updateTrialConfig(trialId, path, value);
|
|
151
161
|
if (instrument.onConfigChange) {
|
|
152
|
-
const
|
|
153
|
-
const
|
|
162
|
+
const next = applyConfigWrite(prevConfig, record.auto ?? [], path, value);
|
|
163
|
+
const nextConfig = resolveAutoConfig(configSchema, next.config, new Set(next.autoPaths));
|
|
164
|
+
const nextState = instrument.onConfigChange(nextConfig, resolvedConfig, record.state);
|
|
154
165
|
updateTrialState(trialId, nextState as never);
|
|
155
166
|
}
|
|
156
167
|
},
|
|
@@ -184,6 +195,8 @@ export function TrialChrome({
|
|
|
184
195
|
activeToolId,
|
|
185
196
|
setActiveTool,
|
|
186
197
|
configSchema,
|
|
198
|
+
resolvedConfig,
|
|
199
|
+
auto,
|
|
187
200
|
undockedIds,
|
|
188
201
|
undockPanelAction,
|
|
189
202
|
dockPanelAction,
|
|
@@ -225,6 +238,8 @@ export function TrialChrome({
|
|
|
225
238
|
return (
|
|
226
239
|
<section
|
|
227
240
|
className="lk-trial"
|
|
241
|
+
data-trial-id={trialId}
|
|
242
|
+
data-focused={lab.trials.length > 1 && lab.focusedTrialId === trialId ? 'true' : undefined}
|
|
228
243
|
aria-label={`Trial ${title}`}
|
|
229
244
|
tabIndex={-1}
|
|
230
245
|
onKeyDown={handleKeyDown}
|
|
@@ -243,7 +258,9 @@ export function TrialChrome({
|
|
|
243
258
|
<TrialBody
|
|
244
259
|
width={record.sidebarWidth}
|
|
245
260
|
onWidthChange={(w) => updateTrialSidebarWidth(trialId, w)}
|
|
246
|
-
contentClassName={
|
|
261
|
+
contentClassName={
|
|
262
|
+
instrument.canvas || instrument.stage ? 'lk-trial__content--flush' : undefined
|
|
263
|
+
}
|
|
247
264
|
sidebar={
|
|
248
265
|
<>
|
|
249
266
|
<SidebarRegion contributions={inRegion('sidebar')} ctx={ctx} />
|