@weasel-js/labkit 1.6.1 → 1.7.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 +3 -3
- package/dist/_dts/{CanvasStackContext-Wo6OwIVM.d.ts → CanvasStackContext-Cjy6I-Iv.d.ts} +4 -1
- package/dist/_dts/{Stage-DWBBi8IU.d.ts → Stage-D6t7o0pF.d.ts} +59 -18
- package/dist/_dts/{index-CKyX0ME_.d.ts → index-BTmaRy6a.d.ts} +10 -6
- package/dist/_dts/{types-B7yBTTF9.d.ts → index-DCMA4Fy1.d.ts} +59 -113
- package/dist/_dts/{types-DTuViBvc.d.ts → types-BUBSIZuV.d.ts} +18 -8
- package/dist/_dts/{useTrialState-CMaV6UD4.d.ts → useTrialState-Bh8bzFgE.d.ts} +1 -1
- package/dist/canvas/index.d.ts +8 -6
- package/dist/canvas/index.js +6 -6
- package/dist/chrome/index.d.ts +6 -4
- package/dist/chrome/index.js +11 -10
- package/dist/chunk-2ES4W47N.js +8 -0
- package/dist/chunk-2ES4W47N.js.map +1 -0
- package/dist/{chunk-WPLAANH4.js → chunk-2N7UBSS2.js} +12 -12
- package/dist/chunk-2N7UBSS2.js.map +1 -0
- package/dist/chunk-4KSC7NXL.js +15491 -0
- package/dist/chunk-4KSC7NXL.js.map +1 -0
- package/dist/{chunk-SLAQPUI4.js → chunk-CEGFXTBD.js} +10 -63
- package/dist/chunk-CEGFXTBD.js.map +1 -0
- package/dist/chunk-CZZPXUOU.js +3 -0
- package/dist/{chunk-SBXNQW4G.js.map → chunk-CZZPXUOU.js.map} +1 -1
- package/dist/{chunk-QGMOA2YE.js → chunk-E23Q6A3Y.js} +296 -104
- package/dist/chunk-E23Q6A3Y.js.map +1 -0
- package/dist/{chunk-GHGO52SO.js → chunk-E6NARGU7.js} +5 -5
- package/dist/{chunk-GHGO52SO.js.map → chunk-E6NARGU7.js.map} +1 -1
- package/dist/{chunk-IJJUK4FU.js → chunk-EESJUSYR.js} +5 -6
- package/dist/chunk-EESJUSYR.js.map +1 -0
- package/dist/{chunk-HXXTULDN.js → chunk-EFXRM6XQ.js} +8 -11
- package/dist/chunk-EFXRM6XQ.js.map +1 -0
- package/dist/chunk-F4ACIK7Z.js +210 -0
- package/dist/chunk-F4ACIK7Z.js.map +1 -0
- package/dist/{chunk-6FLXL3I7.js → chunk-FB4MIZOO.js} +77 -45
- package/dist/chunk-FB4MIZOO.js.map +1 -0
- package/dist/{chunk-AWQG7FCC.js → chunk-FG3EXZDV.js} +3 -3
- package/dist/{chunk-AWQG7FCC.js.map → chunk-FG3EXZDV.js.map} +1 -1
- package/dist/{chunk-4TT3PNPR.js → chunk-GAHELLJT.js} +95 -95
- package/dist/chunk-GAHELLJT.js.map +1 -0
- package/dist/{chunk-HKV7LRHY.js → chunk-IBVRMHW5.js} +5 -5
- package/dist/{chunk-HKV7LRHY.js.map → chunk-IBVRMHW5.js.map} +1 -1
- package/dist/{chunk-ASOK76ZH.js → chunk-JFOJESJF.js} +72 -29
- package/dist/chunk-JFOJESJF.js.map +1 -0
- package/dist/{chunk-RWNDDKOH.js → chunk-PMLZP7RT.js} +18 -15
- package/dist/chunk-PMLZP7RT.js.map +1 -0
- package/dist/{chunk-TJ77A4A3.js → chunk-READTDQX.js} +5 -9
- package/dist/chunk-READTDQX.js.map +1 -0
- package/dist/{chunk-JVBYV6TL.js → chunk-ROGJWDQA.js} +3 -3
- package/dist/{chunk-JVBYV6TL.js.map → chunk-ROGJWDQA.js.map} +1 -1
- package/dist/{chunk-CGHF6RZV.js → chunk-T6DLGEJB.js} +18 -18
- package/dist/chunk-T6DLGEJB.js.map +1 -0
- package/dist/{chunk-24W5VEJW.js → chunk-ULYYNWGP.js} +2 -2
- package/dist/{chunk-24W5VEJW.js.map → chunk-ULYYNWGP.js.map} +1 -1
- package/dist/{chunk-DWMSB4CA.js → chunk-VYVGVBGY.js} +95 -32
- package/dist/chunk-VYVGVBGY.js.map +1 -0
- package/dist/{chunk-IRTGUQ5O.js → chunk-WLMDTI2Q.js} +16 -6
- package/dist/chunk-WLMDTI2Q.js.map +1 -0
- package/dist/{chunk-NEXR37YJ.js → chunk-XE5HJOPL.js} +6 -8
- package/dist/chunk-XE5HJOPL.js.map +1 -0
- package/dist/chunk-XVUKDSYV.js +38 -0
- package/dist/chunk-XVUKDSYV.js.map +1 -0
- package/dist/{chunk-O6FFFOHS.js → chunk-YCFKW5RX.js} +16 -8
- package/dist/chunk-YCFKW5RX.js.map +1 -0
- package/dist/{chunk-CQLQPQ4P.js → chunk-ZQIHJXP4.js} +2 -2
- package/dist/chunk-ZQIHJXP4.js.map +1 -0
- package/dist/config/index.d.ts +28 -13
- package/dist/config/index.js +4 -4
- package/dist/controls/index.d.ts +4 -7
- package/dist/controls/index.js +6 -5
- package/dist/dragdrop/index.d.ts +23 -3
- package/dist/dragdrop/index.js +4 -3
- package/dist/fonts/oswald-tabular-digits.woff2 +0 -0
- package/dist/index.d.ts +72 -487
- package/dist/index.js +603 -441
- package/dist/index.js.map +1 -1
- package/dist/job/index.d.ts +8 -0
- package/dist/job/index.js +1 -1
- package/dist/layers/index.d.ts +6 -5
- package/dist/layers/index.js +3 -2
- package/dist/loupe/index.d.ts +100 -30
- package/dist/loupe/index.js +585 -6
- package/dist/loupe/index.js.map +1 -1
- package/dist/overview/index.d.ts +4 -2
- package/dist/overview/index.js +20 -18
- package/dist/overview/index.js.map +1 -1
- package/dist/passthrough/weasel-canvas.d.ts +2 -0
- package/dist/passthrough/weasel-ui.d.ts +2 -2701
- package/dist/passthrough/weasel-ui.js +4 -3
- package/dist/primitives/index.d.ts +37 -2
- package/dist/primitives/index.js +9 -8
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +6 -6
- package/dist/styles.css +150 -126
- package/dist/surface/index.d.ts +10 -2
- package/dist/surface/index.js +1 -1
- package/dist/undo/index.d.ts +5 -3
- package/package.json +14 -13
- package/src/annotations/AnnotationOverlay.abandon.test.tsx +143 -0
- package/src/annotations/AnnotationOverlay.tsx +52 -33
- package/src/annotations/Annotations.less +0 -10
- package/src/annotations/Annotations.overlay.test.tsx +31 -0
- package/src/annotations/MarkList.tsx +4 -8
- package/src/annotations/capture.browser.test.ts +41 -0
- package/src/annotations/capture.ts +14 -7
- package/src/annotations/drawOne.ts +2 -2
- package/src/annotations/frac.test.ts +44 -1
- package/src/annotations/frac.ts +46 -18
- package/src/annotations/index.ts +1 -0
- package/src/annotations/paint.test.ts +76 -46
- package/src/annotations/paint.ts +65 -43
- package/src/annotations/store.test.ts +212 -2
- package/src/annotations/store.ts +130 -27
- package/src/annotations/svgNodes.test.ts +28 -19
- package/src/annotations/svgNodes.ts +6 -6
- package/src/annotations/types.ts +29 -10
- package/src/canvas/AGENTS.md +1 -0
- package/src/canvas/CameraInput.test.tsx +30 -0
- package/src/canvas/CameraInput.tsx +60 -48
- package/src/canvas/CanvasStack.tsx +11 -6
- package/src/canvas/CanvasStackContext.ts +4 -1
- package/src/canvas/LinkedCursor.less +1 -1
- package/src/canvas/Stage.test.tsx +33 -4
- package/src/canvas/Stage.tsx +9 -4
- package/src/canvas/cameraRegistry.test.ts +55 -0
- package/src/canvas/cameraRegistry.ts +87 -0
- package/src/canvas/cameraZoom.test.ts +46 -0
- package/src/canvas/cameraZoom.ts +24 -0
- package/src/canvas/index.ts +14 -2
- package/src/canvas/sharedScope.test.tsx +94 -0
- package/src/canvas/worldSpec.ts +3 -1
- package/src/chrome/ChromeRegions.stories.tsx +1 -0
- package/src/chrome/builtins.test.ts +7 -6
- package/src/chrome/builtins.tsx +5 -1
- package/src/chrome/index.ts +1 -1
- package/src/chrome/merge.test.ts +5 -0
- package/src/chrome/merge.ts +4 -2
- package/src/chrome/regions/PaletteRegion.less +0 -7
- package/src/chrome/regions/PaletteRegion.test.tsx +2 -3
- package/src/chrome/regions/PaletteRegion.tsx +3 -4
- package/src/chrome/regions/SidebarRegion.less +12 -2
- package/src/chrome/regions/ViewportRegion.browser.test.less +6 -0
- package/src/chrome/regions/ViewportRegion.browser.test.tsx +38 -0
- package/src/chrome/regions/ViewportRegion.less +6 -0
- package/src/chrome/regions/ViewportRegion.tsx +10 -4
- package/src/chrome/types.ts +4 -2
- package/src/config/auto.ts +1 -0
- package/src/config/builder.test.ts +2 -1
- package/src/config/builder.ts +18 -5
- package/src/config/fromConfigField.test.ts +14 -0
- package/src/config/fromConfigField.ts +18 -4
- package/src/config/path.ts +2 -5
- package/src/config/resolve.ts +4 -0
- package/src/config/types.ts +14 -9
- package/src/controls/ControlPanel.less +1 -1
- package/src/controls/ControlPanel.stories.tsx +3 -2
- package/src/controls/ControlPanel.test.tsx +98 -11
- package/src/controls/ControlPanel.tsx +61 -2
- package/src/dragdrop/DragDropRuntime.tsx +22 -13
- package/src/dragdrop/DragGhost.tsx +3 -0
- package/src/dragdrop/Palette.tsx +2 -0
- package/src/index.test.ts +5 -0
- package/src/index.ts +16 -2
- package/src/instrument/types.ts +0 -6
- package/src/job/useJob.ts +14 -9
- package/src/lab/Lab.tsx +88 -73
- package/src/lab/{LabFit.stories.less → LabFit.browser.test.less} +1 -1
- package/src/lab/{LabFit.stories.tsx → LabFit.browser.test.tsx} +41 -63
- package/src/lab/LabFullChrome.stories.tsx +1 -0
- package/src/lab/LabHeader.test.tsx +24 -14
- package/src/lab/LabHeader.tsx +11 -12
- package/src/lab/LabShell.less +9 -17
- package/src/lab/LabSwitcher.browser.test.tsx +27 -0
- package/src/lab/LabSwitcher.less +8 -6
- package/src/lab/LabZoom.test.tsx +223 -0
- package/src/lab/LabZoom.tsx +170 -0
- package/src/lab/Workspace.tsx +48 -52
- package/src/lab/index.ts +1 -0
- package/src/loupe/AGENTS.md +28 -15
- package/src/loupe/CanvasLoupe.tsx +2 -3
- package/src/loupe/DomLoupe.tsx +1 -7
- package/src/loupe/Loupe.less +16 -0
- package/src/loupe/LoupeGestures.tsx +1 -1
- package/src/loupe/TrialLoupe.tsx +120 -57
- package/src/loupe/index.ts +1 -6
- package/src/loupe/types.test.ts +3 -3
- package/src/loupe/types.ts +24 -27
- package/src/loupe/useLoupe.test.tsx +29 -4
- package/src/loupe/useLoupe.ts +20 -17
- package/src/overview/OverviewMarks.tsx +12 -4
- package/src/overview/TrialOverview.tsx +10 -8
- package/src/passthrough/weasel-ui.test.ts +2 -0
- package/src/passthrough/weasel-ui.ts +17 -5
- package/src/primitives/FloatingPanel.test.tsx +11 -0
- package/src/primitives/FloatingPanel.tsx +8 -7
- package/src/primitives/JobProgress.less +2 -1
- package/src/primitives/Legend.stories.tsx +1 -0
- package/src/primitives/Readout.browser.test.tsx +137 -0
- package/src/primitives/Readout.less +12 -0
- package/src/primitives/Readout.stories.tsx +58 -0
- package/src/primitives/Readout.test.tsx +93 -0
- package/src/primitives/Readout.tsx +56 -0
- package/src/primitives/Split.tsx +69 -41
- package/src/primitives/Toolbar.tsx +3 -3
- package/src/primitives/ZoomControl.less +1 -2
- package/src/primitives/index.ts +2 -0
- package/src/specimen/Specimen.stories.tsx +1 -0
- package/src/specimen/Specimen.tsx +2 -3
- package/src/state/Persistence.browser.test.tsx +47 -0
- package/src/state/store.test.ts +5 -1
- package/src/state/undock.ts +3 -0
- package/src/state/useOpenOnce.ts +20 -16
- package/src/state/usePersistedState.test.tsx +1 -1
- package/src/state/view.ts +1 -0
- package/src/styles.less +1 -0
- package/src/surface/index.ts +1 -1
- package/src/surface/useTiledSurface.test.tsx +14 -0
- package/src/surface/useTiledSurface.ts +11 -3
- package/src/theme/Interstellar.stories.tsx +3 -1
- package/src/theme/base.less +21 -47
- package/src/theme/interstellar.test.ts +1 -1
- package/src/theme/interstellar.theme.json +10 -3
- package/src/trial/Trial.annotations.test.tsx +73 -0
- package/src/trial/Trial.less +0 -29
- package/src/trial/Trial.loupe.test.tsx +28 -11
- package/src/trial/Trial.tsx +85 -67
- package/src/trial/TrialChrome.tsx +27 -6
- package/src/trial/index.ts +2 -0
- package/src/trial/loupeSwitch.ts +15 -0
- package/dist/_dts/LayerList.d-BoELAMsM.d.ts +0 -83
- package/dist/_dts/PrefsRow.d-DFpPIeIC.d.ts +0 -39
- package/dist/_dts/PropertyField.d-D0ndfIIt.d.ts +0 -404
- package/dist/_dts/Select.d-Cr8-zoup.d.ts +0 -569
- package/dist/chunk-4TT3PNPR.js.map +0 -1
- package/dist/chunk-6FLXL3I7.js.map +0 -1
- package/dist/chunk-ASOK76ZH.js.map +0 -1
- package/dist/chunk-CGHF6RZV.js.map +0 -1
- package/dist/chunk-CQLQPQ4P.js.map +0 -1
- package/dist/chunk-DWMSB4CA.js.map +0 -1
- package/dist/chunk-HXXTULDN.js.map +0 -1
- package/dist/chunk-IJJUK4FU.js.map +0 -1
- package/dist/chunk-IRTGUQ5O.js.map +0 -1
- package/dist/chunk-KHK5FGER.js +0 -145
- package/dist/chunk-KHK5FGER.js.map +0 -1
- package/dist/chunk-NEXR37YJ.js.map +0 -1
- package/dist/chunk-O6FFFOHS.js.map +0 -1
- package/dist/chunk-QGMOA2YE.js.map +0 -1
- package/dist/chunk-RWNDDKOH.js.map +0 -1
- package/dist/chunk-S4NCI32V.js +0 -549
- package/dist/chunk-S4NCI32V.js.map +0 -1
- package/dist/chunk-SBXNQW4G.js +0 -3
- package/dist/chunk-SLAQPUI4.js.map +0 -1
- package/dist/chunk-TJ77A4A3.js.map +0 -1
- package/dist/chunk-TYN6MLXX.js +0 -13864
- package/dist/chunk-TYN6MLXX.js.map +0 -1
- package/dist/chunk-WPLAANH4.js.map +0 -1
- package/src/primitives/useRovingTabIndex.test.ts +0 -48
- package/src/primitives/useRovingTabIndex.ts +0 -88
- package/src/state/Persistence.stories.tsx +0 -75
package/src/trial/Trial.tsx
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
createPointerStore,
|
|
3
|
+
PointerContextProvider,
|
|
4
|
+
useLatest,
|
|
5
|
+
WeaselProvider,
|
|
6
|
+
} from '@weasel-js/core';
|
|
7
|
+
import {
|
|
8
|
+
type ReactNode,
|
|
9
|
+
useCallback,
|
|
10
|
+
useContext,
|
|
11
|
+
useEffect,
|
|
12
|
+
useLayoutEffect,
|
|
13
|
+
useMemo,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
} from 'react';
|
|
3
17
|
import { useStore } from 'zustand/react';
|
|
4
18
|
import { AnnotationsContext } from '../annotations/AnnotationsContext';
|
|
5
19
|
import { AnnotationTargets } from '../annotations/AnnotationTargets';
|
|
6
20
|
import { AnnotationPreloadContext } from '../annotations/preload';
|
|
7
21
|
import { annotationsFromJSON } from '../annotations/store';
|
|
8
|
-
import type { AnnotationStorage, AnnotationTargetInfo } from '../annotations/types';
|
|
9
|
-
import {
|
|
22
|
+
import type { AnnotationStorage, AnnotationsApi, AnnotationTargetInfo } from '../annotations/types';
|
|
23
|
+
import type { CameraView } from '../canvas/CameraInput';
|
|
10
24
|
import { CameraWheelContext, type CameraWheelSlot } from '../canvas/CameraWheelContext';
|
|
11
25
|
import { CanvasStack } from '../canvas/CanvasStack';
|
|
26
|
+
import { CameraPublishContext, CameraRegistryContext } from '../canvas/cameraRegistry';
|
|
12
27
|
import { fitStage, Stage } from '../canvas/Stage';
|
|
13
28
|
import type { CanvasLayerDescriptor } from '../canvas/useLayerScheduler';
|
|
14
29
|
import { applyCamera, type ViewportSize } from '../canvas/worldSpec';
|
|
@@ -26,8 +41,6 @@ import type {
|
|
|
26
41
|
} from '../instrument/types';
|
|
27
42
|
import { useJob } from '../job/useJob';
|
|
28
43
|
import { useLabContext } from '../lab/LabContext';
|
|
29
|
-
import { TrialLoupe } from '../loupe/TrialLoupe';
|
|
30
|
-
import { resolveLoupe } from '../loupe/types';
|
|
31
44
|
import { LayerList, type LayerListItem, moveLayers } from '../passthrough/weasel-ui';
|
|
32
45
|
import { LabStoreContext, TrialIdProvider } from '../state/context';
|
|
33
46
|
import type { LabStore } from '../state/store';
|
|
@@ -36,6 +49,7 @@ import { as2DView, DEFAULT_VIEW, withZoom } from '../state/view';
|
|
|
36
49
|
import { resolveLabTool } from '../tools/labTool';
|
|
37
50
|
import { createEventBus, type EventBus } from '../undo/eventBus';
|
|
38
51
|
import { pushSnapshot, redo as undoRedo, undo as undoUndo } from '../undo/undoStack';
|
|
52
|
+
import { LoupeSwitchContext } from './loupeSwitch';
|
|
39
53
|
import type { LoupeBindings, UndoBindings } from './TrialChrome';
|
|
40
54
|
import { TrialChrome } from './TrialChrome';
|
|
41
55
|
|
|
@@ -136,9 +150,14 @@ function TrialRuntime({
|
|
|
136
150
|
suppress,
|
|
137
151
|
}: TrialRuntimeProps) {
|
|
138
152
|
const wheelSlot = useRef<CameraWheelSlot['current']>(null);
|
|
153
|
+
const cameras = useContext(CameraRegistryContext);
|
|
154
|
+
const trialId = record.id;
|
|
155
|
+
const publishCamera = useMemo(
|
|
156
|
+
() => (cameras ? (camera: CameraView) => cameras.register(trialId, camera) : null),
|
|
157
|
+
[cameras, trialId],
|
|
158
|
+
);
|
|
139
159
|
const [pointer] = useState(createPointerStore);
|
|
140
160
|
const canvasContainerRef = useRef<HTMLDivElement | null>(null);
|
|
141
|
-
const loupeHostRef = useRef<HTMLDivElement | null>(null);
|
|
142
161
|
const updateTrialState = useStore(store, (s) => s.updateTrialState);
|
|
143
162
|
const updateTrialConfig = useStore(store, (s) => s.updateTrialConfig);
|
|
144
163
|
const updateTrialView = useStore(store, (s) => s.updateTrialView);
|
|
@@ -160,6 +179,12 @@ function TrialRuntime({
|
|
|
160
179
|
const [layerVisibility, setLayerVisibility] = useState<Record<string, boolean>>({});
|
|
161
180
|
const [layerOrder, setLayerOrder] = useState<string[] | null>(null);
|
|
162
181
|
const [loupeOn, setLoupeOn] = useState(false);
|
|
182
|
+
const [lenses, setLenses] = useState(0);
|
|
183
|
+
const mountLens = useCallback(() => {
|
|
184
|
+
setLenses((n) => n + 1);
|
|
185
|
+
return () => setLenses((n) => n - 1);
|
|
186
|
+
}, []);
|
|
187
|
+
const loupeSwitch = useMemo(() => ({ on: loupeOn, mount: mountLens }), [loupeOn, mountLens]);
|
|
163
188
|
|
|
164
189
|
const visibleLayers = useMemo(
|
|
165
190
|
() =>
|
|
@@ -200,43 +225,69 @@ function TrialRuntime({
|
|
|
200
225
|
// resizing or gaining a dependency must not need the store rebuilt. Held in
|
|
201
226
|
// a ref because the store is built once and closes over it.
|
|
202
227
|
const annotationsCap = instrument.annotations;
|
|
203
|
-
const targetsRef =
|
|
204
|
-
targetsRef.current = () =>
|
|
228
|
+
const targetsRef = useLatest((): readonly AnnotationTargetInfo[] =>
|
|
205
229
|
annotationsCap
|
|
206
230
|
? annotationsCap.targets(record.state, config, { id: record.id, view: record.view })
|
|
207
|
-
: []
|
|
231
|
+
: [],
|
|
232
|
+
);
|
|
208
233
|
|
|
209
234
|
// One store for the trial's lifetime.
|
|
210
235
|
const annotationsRef = useRef<ReturnType<typeof annotationsFromJSON> | null>(null);
|
|
211
236
|
// Read through refs for the same reason `targets` is: the store is built
|
|
212
237
|
// once, and an export must draw against the config the trial holds now.
|
|
213
|
-
const configRef =
|
|
214
|
-
configRef.current = config;
|
|
238
|
+
const configRef = useLatest(config);
|
|
215
239
|
// The instrument can be swapped under a live trial, so the capability is read at use too.
|
|
216
|
-
const capRef =
|
|
217
|
-
capRef.current = annotationsCap;
|
|
240
|
+
const capRef = useLatest(annotationsCap);
|
|
218
241
|
if (annotationsRef.current === null) {
|
|
219
242
|
// Seeded from wherever the marks were kept: the instrument's own store if
|
|
220
243
|
// it declared one, else this trial's slot.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
244
|
+
annotationsRef.current = annotationsFromJSON(
|
|
245
|
+
annotationsCap?.storage ? keptMarks : record.annotations,
|
|
246
|
+
() => targetsRef.current(),
|
|
247
|
+
{
|
|
248
|
+
meaning: () => capRef.current?.meaning,
|
|
249
|
+
config: () => configRef.current,
|
|
250
|
+
onCapture: (result) => capRef.current?.onCapture?.(result),
|
|
251
|
+
selection: () => capRef.current?.selection,
|
|
252
|
+
},
|
|
253
|
+
);
|
|
227
254
|
}
|
|
228
255
|
const annotations = annotationsRef.current;
|
|
229
256
|
|
|
257
|
+
// Chrome reads the store while it renders — a mark list, the export menu,
|
|
258
|
+
// the overview — and the store reads the refs above, which hold the last
|
|
259
|
+
// commit. So once a change to them commits, those readers are told to read
|
|
260
|
+
// again. Through the context's `subscribe` alone: the store's own would have
|
|
261
|
+
// the debounce below save marks that did not change.
|
|
262
|
+
const inputsCommitted = useRef(new Set<() => void>());
|
|
263
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: these are what `targets` and `config` answer from — the signal, not values this reads
|
|
264
|
+
useLayoutEffect(() => {
|
|
265
|
+
for (const fn of inputsCommitted.current) fn();
|
|
266
|
+
}, [record.state, record.view, config, annotationsCap]);
|
|
267
|
+
const provided = useMemo<AnnotationsApi>(
|
|
268
|
+
() => ({
|
|
269
|
+
...annotations,
|
|
270
|
+
subscribe: (fn) => {
|
|
271
|
+
const off = annotations.subscribe(fn);
|
|
272
|
+
inputsCommitted.current.add(fn);
|
|
273
|
+
return () => {
|
|
274
|
+
off();
|
|
275
|
+
inputsCommitted.current.delete(fn);
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
}),
|
|
279
|
+
[annotations],
|
|
280
|
+
);
|
|
281
|
+
|
|
230
282
|
// Written on a trailing debounce rather than per notification: a scene
|
|
231
283
|
// mutates every frame of a drag, and a zustand write per frame re-renders
|
|
232
284
|
// every trial in the lab. The unmount flush is not an optimization — the
|
|
233
285
|
// last mark before a trial closes is otherwise lost.
|
|
234
286
|
const saveCap = annotationsCap?.storage;
|
|
235
|
-
const saveRef =
|
|
236
|
-
saveRef.current = (doc) => {
|
|
287
|
+
const saveRef = useLatest((doc: unknown): void => {
|
|
237
288
|
if (saveCap) saveCap.save(doc as never);
|
|
238
289
|
else updateTrialAnnotations(record.id, doc);
|
|
239
|
-
};
|
|
290
|
+
});
|
|
240
291
|
// Only when the pair flips, which is rare: bumping React state on every
|
|
241
292
|
// scene notification would re-render the trial on every frame of a drag,
|
|
242
293
|
// which is the thing the debounce below exists to avoid.
|
|
@@ -273,7 +324,7 @@ function TrialRuntime({
|
|
|
273
324
|
if (timer) clearTimeout(timer);
|
|
274
325
|
flush();
|
|
275
326
|
};
|
|
276
|
-
}, [annotations, annotationsCap]);
|
|
327
|
+
}, [annotations, annotationsCap, saveRef]);
|
|
277
328
|
|
|
278
329
|
// A trial gets its own slot when its instrument declares tools; otherwise it
|
|
279
330
|
// reads the lab's. Which slot a change writes follows from the same thing.
|
|
@@ -373,14 +424,8 @@ function TrialRuntime({
|
|
|
373
424
|
}
|
|
374
425
|
: undefined;
|
|
375
426
|
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
if (declared == null) return null;
|
|
379
|
-
return resolveLoupe(typeof declared === 'function' ? declared(config) : declared);
|
|
380
|
-
}, [instrument.loupe, config]);
|
|
381
|
-
const loupeBindings: LoupeBindings | undefined = loupeCap
|
|
382
|
-
? { on: loupeOn, toggle: () => setLoupeOn((v) => !v) }
|
|
383
|
-
: undefined;
|
|
427
|
+
const loupeBindings: LoupeBindings | undefined =
|
|
428
|
+
lenses > 0 ? { on: loupeOn, toggle: () => setLoupeOn((v) => !v) } : undefined;
|
|
384
429
|
|
|
385
430
|
const canvasLayers: CanvasLayerDescriptor[] = useMemo(() => {
|
|
386
431
|
if (!instrument.canvas) return [];
|
|
@@ -471,20 +516,6 @@ function TrialRuntime({
|
|
|
471
516
|
];
|
|
472
517
|
}, [canvasLayers, dragDropResult.drag]);
|
|
473
518
|
|
|
474
|
-
// The lens tracks the pointer over whatever box holds the content: the canvas
|
|
475
|
-
// stack's own element, which it takes from context, or the wrapper below.
|
|
476
|
-
const lens = loupeCap ? (
|
|
477
|
-
<TrialLoupe
|
|
478
|
-
capability={loupeCap}
|
|
479
|
-
enabled={loupeOn}
|
|
480
|
-
state={record.state}
|
|
481
|
-
config={config}
|
|
482
|
-
view={view2d ?? DEFAULT_VIEW}
|
|
483
|
-
worldSpec={instrument.canvas?.worldSpec}
|
|
484
|
-
hostRef={loupeHostRef}
|
|
485
|
-
/>
|
|
486
|
-
) : null;
|
|
487
|
-
|
|
488
519
|
let body: ReactNode;
|
|
489
520
|
if (instrument.canvas) {
|
|
490
521
|
body = (
|
|
@@ -499,7 +530,6 @@ function TrialRuntime({
|
|
|
499
530
|
maxZoom={instrument.canvas.maxZoom}
|
|
500
531
|
>
|
|
501
532
|
{instrument.render(renderCtx)}
|
|
502
|
-
{lens}
|
|
503
533
|
</CanvasStack>
|
|
504
534
|
<DragOverlay drag={dragDropResult.drag} />
|
|
505
535
|
</div>
|
|
@@ -513,24 +543,11 @@ function TrialRuntime({
|
|
|
513
543
|
onResize={placeView}
|
|
514
544
|
minZoom={stage.minZoom}
|
|
515
545
|
maxZoom={stage.maxZoom}
|
|
516
|
-
|
|
517
|
-
overlay={
|
|
518
|
-
<>
|
|
519
|
-
{stage.overlay?.(renderCtx)}
|
|
520
|
-
{lens}
|
|
521
|
-
</>
|
|
522
|
-
}
|
|
546
|
+
overlay={stage.overlay?.(renderCtx)}
|
|
523
547
|
>
|
|
524
548
|
{instrument.render(renderCtx)}
|
|
525
549
|
</Stage>
|
|
526
550
|
);
|
|
527
|
-
} else if (lens) {
|
|
528
|
-
body = (
|
|
529
|
-
<div ref={loupeHostRef} className="lk-trial__loupe-host">
|
|
530
|
-
{instrument.render(renderCtx)}
|
|
531
|
-
{lens}
|
|
532
|
-
</div>
|
|
533
|
-
);
|
|
534
551
|
} else {
|
|
535
552
|
body = instrument.render(renderCtx);
|
|
536
553
|
}
|
|
@@ -593,21 +610,22 @@ function TrialRuntime({
|
|
|
593
610
|
/>
|
|
594
611
|
) : null;
|
|
595
612
|
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
//
|
|
599
|
-
// its own.
|
|
613
|
+
// The trial's root registries, isolated from the trial beside it. Each
|
|
614
|
+
// camera inside mounts an input scope under them; chrome here reaches the
|
|
615
|
+
// one last used. The pointer store is the trial's, shared by all of them.
|
|
600
616
|
const scopedBody = (
|
|
601
617
|
<WeaselProvider isolate>
|
|
602
618
|
<PointerContextProvider store={pointer}>
|
|
603
|
-
<
|
|
619
|
+
<CameraPublishContext.Provider value={publishCamera}>
|
|
620
|
+
<LoupeSwitchContext.Provider value={loupeSwitch}>{body}</LoupeSwitchContext.Provider>
|
|
621
|
+
</CameraPublishContext.Provider>
|
|
604
622
|
</PointerContextProvider>
|
|
605
623
|
</WeaselProvider>
|
|
606
624
|
);
|
|
607
625
|
|
|
608
626
|
return (
|
|
609
627
|
<TrialIdProvider trialId={record.id}>
|
|
610
|
-
<AnnotationsContext.Provider value={annotationsCap ?
|
|
628
|
+
<AnnotationsContext.Provider value={annotationsCap ? provided : null}>
|
|
611
629
|
<CameraWheelContext.Provider value={wheelSlot}>
|
|
612
630
|
<TrialChrome
|
|
613
631
|
job={jobCap ? job : undefined}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type KeyboardEvent,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
useContext,
|
|
5
|
+
useMemo,
|
|
6
|
+
useSyncExternalStore,
|
|
7
|
+
} from 'react';
|
|
2
8
|
import { useStore } from 'zustand/react';
|
|
3
|
-
import {
|
|
9
|
+
import { CameraRegistryContext } from '../canvas/cameraRegistry';
|
|
10
|
+
import { zoomCameraTo } from '../canvas/cameraZoom';
|
|
11
|
+
import { builtinContributions, TRANSIENT_BUILTINS } from '../chrome/builtins';
|
|
4
12
|
import { mergeContributions, suppressContributions } from '../chrome/merge';
|
|
5
13
|
import { PaletteRegion } from '../chrome/regions/PaletteRegion';
|
|
6
14
|
import { SidebarRegion } from '../chrome/regions/SidebarRegion';
|
|
@@ -30,6 +38,7 @@ export interface LoupeBindings {
|
|
|
30
38
|
toggle: () => void;
|
|
31
39
|
}
|
|
32
40
|
|
|
41
|
+
/** The trial's undo history as its chrome sees it. */
|
|
33
42
|
export interface UndoBindings {
|
|
34
43
|
canUndo: boolean;
|
|
35
44
|
canRedo: boolean;
|
|
@@ -46,8 +55,8 @@ export interface TrialChromeProps {
|
|
|
46
55
|
undoBindings?: UndoBindings;
|
|
47
56
|
/** Supplied when the instrument declares a `job`. */
|
|
48
57
|
job?: JobHandle;
|
|
49
|
-
/** Supplied
|
|
50
|
-
*
|
|
58
|
+
/** Supplied while a lens is mounted in the trial. The lens itself is the
|
|
59
|
+
* instrument's to mount; the chrome only offers the switch. */
|
|
51
60
|
loupe?: LoupeBindings;
|
|
52
61
|
/** Contributions the trial runtime itself adds — the drag palette and the
|
|
53
62
|
* layer list, which depend on runtime state the instrument cannot reach. */
|
|
@@ -68,6 +77,7 @@ const NO_OP = (): void => {};
|
|
|
68
77
|
/** One frozen empty map, so a trial that has folded nothing keeps a stable
|
|
69
78
|
* context identity across renders. */
|
|
70
79
|
const NO_SECTIONS: Readonly<Record<string, boolean>> = Object.freeze({});
|
|
80
|
+
const noSubscribe = (): (() => void) => NO_OP;
|
|
71
81
|
|
|
72
82
|
/** The frame around a running instrument. Builds one chrome context, assembles
|
|
73
83
|
* the contributions the instrument, the runtime and the lab declare, and hands
|
|
@@ -123,10 +133,18 @@ export function TrialChrome({
|
|
|
123
133
|
const title = record.title ?? instrument.title ?? instrument.name;
|
|
124
134
|
const collapsedSections = record.collapsedSections ?? NO_SECTIONS;
|
|
125
135
|
|
|
136
|
+
// The trial's own camera, where it has one: zoom goes through it, about the
|
|
137
|
+
// middle of the view and inside the instrument's range, as the lab's does.
|
|
138
|
+
const cameras = useContext(CameraRegistryContext);
|
|
139
|
+
const camera = useSyncExternalStore(
|
|
140
|
+
cameras?.subscribe ?? noSubscribe,
|
|
141
|
+
() => cameras?.get(trialId) ?? null,
|
|
142
|
+
);
|
|
143
|
+
|
|
126
144
|
const ctx = useMemo<TrialChromeContext>(() => {
|
|
127
145
|
const setZoom = (z: number): void => {
|
|
128
|
-
if (
|
|
129
|
-
updateTrialView(trialId, withZoom(view2d, z));
|
|
146
|
+
if (camera) zoomCameraTo(camera, z);
|
|
147
|
+
else if (view2d) updateTrialView(trialId, withZoom(view2d, z));
|
|
130
148
|
};
|
|
131
149
|
return {
|
|
132
150
|
trialId,
|
|
@@ -140,6 +158,7 @@ export function TrialChrome({
|
|
|
140
158
|
canRedo: undoBindings?.canRedo ?? false,
|
|
141
159
|
undo: undoBindings?.undo ?? NO_OP,
|
|
142
160
|
redo: undoBindings?.redo ?? NO_OP,
|
|
161
|
+
hasLoupe: loupe != null,
|
|
143
162
|
loupeOn: loupe?.on ?? false,
|
|
144
163
|
toggleLoupe: loupe?.toggle ?? NO_OP,
|
|
145
164
|
configSchema,
|
|
@@ -203,6 +222,7 @@ export function TrialChrome({
|
|
|
203
222
|
setTrialTitle,
|
|
204
223
|
setTrialSectionCollapsed,
|
|
205
224
|
collapsedSections,
|
|
225
|
+
camera,
|
|
206
226
|
]);
|
|
207
227
|
|
|
208
228
|
const contributions = useMemo(
|
|
@@ -215,6 +235,7 @@ export function TrialChrome({
|
|
|
215
235
|
[...(chrome ?? [])],
|
|
216
236
|
),
|
|
217
237
|
suppress ?? [],
|
|
238
|
+
TRANSIENT_BUILTINS,
|
|
218
239
|
),
|
|
219
240
|
[instrument, ctx, trialChrome, chrome, suppress, lab.controls],
|
|
220
241
|
);
|
package/src/trial/index.ts
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A trial's loupe switch, as a lens mounted inside the trial finds it. The
|
|
5
|
+
* trial offers its toolbar toggle while at least one lens has called `mount`,
|
|
6
|
+
* and every lens reads `on` as whether it is turned on.
|
|
7
|
+
*/
|
|
8
|
+
export interface LoupeSwitch {
|
|
9
|
+
on: boolean;
|
|
10
|
+
/** Reports a lens mounted; the returned function reports it gone. */
|
|
11
|
+
mount: () => () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The loupe switch of the trial around the caller, or `null` outside one. */
|
|
15
|
+
export const LoupeSwitchContext = createContext<LoupeSwitch | null>(null);
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import * as react from 'react';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
3
|
-
import { S as StanceProps } from './PrefsRow.d-DFpPIeIC.js';
|
|
4
|
-
|
|
5
|
-
/** One layer. With a body (see `renderBody`) it draws as a card that folds its
|
|
6
|
-
* body away; without one, as a one-line row. Either can hold child layers. */
|
|
7
|
-
interface LayerListItem {
|
|
8
|
-
id: string;
|
|
9
|
-
/** The layer's name. Also what its controls are labeled by. */
|
|
10
|
-
label: string;
|
|
11
|
-
/** Shown in the header in place of `label` — a select that switches the
|
|
12
|
-
* layer's kind, say. */
|
|
13
|
-
title?: ReactNode;
|
|
14
|
-
/** Fixed-size content before the label: a color swatch, a kind icon. */
|
|
15
|
-
leading?: ReactNode;
|
|
16
|
-
/** Drawn in the drag handle in place of the grip — an ordinal. */
|
|
17
|
-
badge?: ReactNode;
|
|
18
|
-
/** Which of its list this layer is: an index into the theme's tone list, or
|
|
19
|
-
* a color. A card draws its edge and handle in it and recolors its controls. */
|
|
20
|
-
tone?: StanceProps['tone'];
|
|
21
|
-
/** Cannot be dragged, hidden or removed, walls off drops on either side, and
|
|
22
|
-
* selects alone rather than joining a multi-selection. */
|
|
23
|
-
locked?: boolean;
|
|
24
|
-
/** Shown or not, when the list has `onVisibilityChange`. Default true. */
|
|
25
|
-
visible?: boolean;
|
|
26
|
-
children?: LayerListItem[];
|
|
27
|
-
/** Folds the body and children away until the user opens them.
|
|
28
|
-
* `collapsedIds` overrides it. */
|
|
29
|
-
defaultCollapsed?: boolean;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* A reorder: `ids`, siblings under `parentId` (`null` at the top level), now
|
|
33
|
-
* sit as one block in list order starting at `index` of the parent's children.
|
|
34
|
-
* A drag never moves a layer to another parent.
|
|
35
|
-
*/
|
|
36
|
-
interface LayerMove {
|
|
37
|
-
ids: string[];
|
|
38
|
-
parentId: string | null;
|
|
39
|
-
index: number;
|
|
40
|
-
}
|
|
41
|
-
/** Props for `<LayerList>`. */
|
|
42
|
-
interface LayerListProps {
|
|
43
|
-
items: LayerListItem[];
|
|
44
|
-
/** Omit for a list the user cannot reorder. */
|
|
45
|
-
onReorder?: (move: LayerMove) => void;
|
|
46
|
-
/** Giving both makes rows selectable: a click selects one, shift-click adds
|
|
47
|
-
* or removes one, and dragging a selected row drags its selected siblings. */
|
|
48
|
-
selectedIds?: readonly string[];
|
|
49
|
-
onSelect?: (ids: string[]) => void;
|
|
50
|
-
/** Adds a visibility checkbox to every unlocked layer. */
|
|
51
|
-
onVisibilityChange?: (id: string, visible: boolean) => void;
|
|
52
|
-
/** Adds a ✕ to every unlocked layer. */
|
|
53
|
-
onRemove?: (id: string) => void;
|
|
54
|
-
/** A layer's controls. Returning `null` leaves that layer a one-line row. */
|
|
55
|
-
renderBody?: (item: LayerListItem) => ReactNode;
|
|
56
|
-
/** Ids whose body and children are folded away. Giving it makes folding
|
|
57
|
-
* controlled; `onCollapsedChange` fires either way. */
|
|
58
|
-
collapsedIds?: readonly string[];
|
|
59
|
-
onCollapsedChange?: (ids: string[]) => void;
|
|
60
|
-
/** Heading above the list. */
|
|
61
|
-
title?: string;
|
|
62
|
-
/** Names the list for assistive tech when it has no `title`. */
|
|
63
|
-
label?: string;
|
|
64
|
-
/** Kinds the user can add, as buttons beside the title. Shown only with `onAdd`. */
|
|
65
|
-
addKinds?: string[];
|
|
66
|
-
onAdd?: (kind: string) => void;
|
|
67
|
-
/** Shown in place of the list when `items` is empty. */
|
|
68
|
-
empty?: ReactNode;
|
|
69
|
-
/** Appended to the root's class list — the module's own names are hashed. */
|
|
70
|
-
className?: string;
|
|
71
|
-
}
|
|
72
|
-
/** Returns `items` with `move` applied, for a consumer that holds the tree as state. */
|
|
73
|
-
declare function moveLayers(items: LayerListItem[], move: LayerMove): LayerListItem[];
|
|
74
|
-
/**
|
|
75
|
-
* An ordered, drag-reorderable list of layers — a document's objects, a lab's
|
|
76
|
-
* canvases, a fill's effects. Each layer is a one-line row or, with a body, a
|
|
77
|
-
* card whose controls fold away; either can nest child layers. Selection,
|
|
78
|
-
* visibility, removal and an add palette are each on when their handler is given.
|
|
79
|
-
*/
|
|
80
|
-
declare function LayerList(props: LayerListProps): react.JSX.Element;
|
|
81
|
-
|
|
82
|
-
export { LayerList as L, moveLayers as m };
|
|
83
|
-
export type { LayerListItem as a, LayerListProps as b, LayerMove as c };
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Stance } from '@weasel-js/theme';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
3
|
-
import { ToolPrefLeaf } from '@weasel-js/core';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* What a surface holds, and which of its peers it is. Every surface that takes
|
|
7
|
-
* these takes both, and the theme decides how each looks.
|
|
8
|
-
*/
|
|
9
|
-
interface StanceProps {
|
|
10
|
-
/** The class of content the surface holds. Omitted, it draws as it always has. */
|
|
11
|
-
stance?: Stance;
|
|
12
|
-
/**
|
|
13
|
-
* Which of its peers this is: an index into the theme's tone list, wrapping,
|
|
14
|
-
* or a color given directly. Independent of `stance`, and allowed without one.
|
|
15
|
-
*/
|
|
16
|
-
tone?: number | string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** What a {@link PrefRenderer} is given for the leaf it is rendering. */
|
|
20
|
-
interface PrefRenderContext {
|
|
21
|
-
/** Dotted path of the leaf within the schema root. */
|
|
22
|
-
path: string;
|
|
23
|
-
/** The schema node. App renderers narrow this to their own kind shape. */
|
|
24
|
-
pref: ToolPrefLeaf;
|
|
25
|
-
/** Current value — `values` at `path`, falling back to `pref.default`. */
|
|
26
|
-
value: unknown;
|
|
27
|
-
setValue: (value: unknown) => void;
|
|
28
|
-
/** Whether this leaf is currently auto — not pinned, computed by the owner. */
|
|
29
|
-
auto: boolean;
|
|
30
|
-
/** Toggle this leaf's auto state. */
|
|
31
|
-
setAuto: (next: boolean) => void;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Renders the control cell for one preference leaf. Returning `null`
|
|
35
|
-
* collapses the row.
|
|
36
|
-
*/
|
|
37
|
-
type PrefRenderer = (ctx: PrefRenderContext) => ReactNode;
|
|
38
|
-
|
|
39
|
-
export type { PrefRenderer as P, StanceProps as S, PrefRenderContext as a };
|