@weasel-js/labkit 1.6.0 → 1.6.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/dist/_dts/{CanvasStackContext-DQgcH1ny.d.ts → CanvasStackContext-Wo6OwIVM.d.ts} +1 -1
- package/dist/_dts/FloatingPanel-7j0IcXwz.d.ts +30 -0
- package/dist/_dts/{LayerList.d-2c9yup5Y.d.ts → LayerList.d-BoELAMsM.d.ts} +2 -2
- package/dist/_dts/{index-DINmCuLA.d.ts → PropertyField.d-D0ndfIIt.d.ts} +34 -83
- package/dist/_dts/{Select.d-BchGMC2Y.d.ts → Select.d-Cr8-zoup.d.ts} +4 -5
- package/dist/_dts/{usePanZoom-Bq73pwFe.d.ts → Stage-DWBBi8IU.d.ts} +80 -35
- package/dist/_dts/{index-B0--8PEM.d.ts → index-CKyX0ME_.d.ts} +12 -13
- package/dist/_dts/{frac-DTRq7Xpb.d.ts → types-B7yBTTF9.d.ts} +699 -649
- package/dist/_dts/{types-B7y8hX1D.d.ts → types-DTuViBvc.d.ts} +20 -1
- package/dist/_dts/{useTrialState-CP4jfuKl.d.ts → useTrialState-CMaV6UD4.d.ts} +2 -3
- package/dist/canvas/index.d.ts +39 -7
- package/dist/canvas/index.js +5 -3
- package/dist/chrome/index.d.ts +4 -5
- package/dist/chrome/index.js +8 -6
- package/dist/chunk-24W5VEJW.js +18 -0
- package/dist/chunk-24W5VEJW.js.map +1 -0
- package/dist/chunk-3RY6WL5T.js +38 -0
- package/dist/chunk-3RY6WL5T.js.map +1 -0
- package/dist/{chunk-H5VIV7MQ.js → chunk-4TT3PNPR.js} +60 -51
- package/dist/chunk-4TT3PNPR.js.map +1 -0
- package/dist/{chunk-ZORVNOKX.js → chunk-6FLXL3I7.js} +9 -145
- package/dist/chunk-6FLXL3I7.js.map +1 -0
- package/dist/{chunk-HFRE4QQV.js → chunk-ASOK76ZH.js} +53 -22
- package/dist/chunk-ASOK76ZH.js.map +1 -0
- package/dist/chunk-AWQG7FCC.js +19 -0
- package/dist/chunk-AWQG7FCC.js.map +1 -0
- package/dist/chunk-CGHF6RZV.js +300 -0
- package/dist/chunk-CGHF6RZV.js.map +1 -0
- package/dist/chunk-DWMSB4CA.js +228 -0
- package/dist/chunk-DWMSB4CA.js.map +1 -0
- package/dist/{chunk-USO6TERP.js → chunk-GHGO52SO.js} +11 -9
- package/dist/chunk-GHGO52SO.js.map +1 -0
- package/dist/{chunk-MOLIUNVH.js → chunk-HKV7LRHY.js} +4 -4
- package/dist/{chunk-MOLIUNVH.js.map → chunk-HKV7LRHY.js.map} +1 -1
- package/dist/{chunk-VQ47KWD5.js → chunk-IRTGUQ5O.js} +5 -4
- package/dist/chunk-IRTGUQ5O.js.map +1 -0
- package/dist/{chunk-H6ZAOWNE.js → chunk-JVBYV6TL.js} +3 -3
- package/dist/{chunk-H6ZAOWNE.js.map → chunk-JVBYV6TL.js.map} +1 -1
- package/dist/chunk-KHK5FGER.js +145 -0
- package/dist/chunk-KHK5FGER.js.map +1 -0
- package/dist/chunk-NEXR37YJ.js +144 -0
- package/dist/chunk-NEXR37YJ.js.map +1 -0
- package/dist/{chunk-BVL7Y574.js → chunk-O6FFFOHS.js} +14 -6
- package/dist/chunk-O6FFFOHS.js.map +1 -0
- package/dist/chunk-QGMOA2YE.js +731 -0
- package/dist/chunk-QGMOA2YE.js.map +1 -0
- package/dist/{chunk-ERFMC6WU.js → chunk-S4NCI32V.js} +19 -10
- package/dist/chunk-S4NCI32V.js.map +1 -0
- package/dist/{chunk-3FI7Y4R4.js → chunk-SLAQPUI4.js} +4 -4
- package/dist/{chunk-3FI7Y4R4.js.map → chunk-SLAQPUI4.js.map} +1 -1
- package/dist/{chunk-AWH3SRPW.js → chunk-TJ77A4A3.js} +3 -31
- package/dist/chunk-TJ77A4A3.js.map +1 -0
- package/dist/{chunk-WUJXJC5O.js → chunk-TYN6MLXX.js} +691 -1395
- package/dist/chunk-TYN6MLXX.js.map +1 -0
- package/dist/{chunk-RS3HRQWU.js → chunk-WPLAANH4.js} +4 -3
- package/dist/chunk-WPLAANH4.js.map +1 -0
- package/dist/config/index.d.ts +36 -35
- package/dist/config/index.js +4 -5
- package/dist/controls/index.d.ts +91 -7
- package/dist/controls/index.js +5 -4
- package/dist/dragdrop/index.d.ts +8 -6
- package/dist/dragdrop/index.js +3 -2
- package/dist/index.d.ts +50 -77
- package/dist/index.js +138 -254
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +3 -4
- package/dist/layers/index.js +2 -1
- package/dist/loupe/index.d.ts +19 -16
- package/dist/loupe/index.js +5 -3
- package/dist/overview/index.d.ts +56 -0
- package/dist/overview/index.js +288 -0
- package/dist/overview/index.js.map +1 -0
- package/dist/passthrough/weasel-ui.d.ts +38 -38
- package/dist/passthrough/weasel-ui.js +2 -1
- package/dist/primitives/index.d.ts +19 -43
- package/dist/primitives/index.js +5 -3
- package/dist/state/index.d.ts +7 -7
- package/dist/state/index.js +4 -5
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +63 -1
- package/dist/undo/index.d.ts +3 -3
- package/package.json +13 -9
- package/src/annotations/index.ts +1 -0
- package/src/annotations/store.test.ts +26 -0
- package/src/annotations/store.ts +25 -6
- package/src/annotations/toolMap.test.ts +37 -2
- package/src/annotations/toolMap.ts +13 -0
- package/src/annotations/types.ts +27 -0
- package/src/canvas/AGENTS.md +8 -10
- package/src/canvas/CameraInput.test.tsx +136 -0
- package/src/canvas/CameraInput.tsx +311 -0
- package/src/canvas/CanvasStack.tsx +42 -58
- package/src/canvas/LinkedCursor.less +16 -0
- package/src/canvas/LinkedCursor.tsx +54 -0
- package/src/canvas/Stage.tsx +42 -37
- package/src/canvas/cameraView.test.ts +60 -0
- package/src/canvas/cameraView.ts +61 -0
- package/src/canvas/index.ts +11 -2
- package/src/chrome/regions/SidebarRegion.test.tsx +9 -2
- package/src/config/builder.ts +3 -3
- package/src/config/entry.test.ts +3 -1
- package/src/config/fromConfigField.ts +1 -0
- package/src/config/index.ts +4 -1
- package/src/config/resolve.ts +11 -1
- package/src/config/sectionTree.test.ts +12 -6
- package/src/config/sectionTree.ts +15 -5
- package/src/config/types.ts +21 -0
- package/src/controls/ControlPanel.test.tsx +12 -2
- package/src/controls/ControlPanel.tsx +28 -9
- package/src/controls/inDialog.test.tsx +18 -0
- package/src/controls/inDialog.tsx +2 -10
- package/src/index.test.ts +1 -1
- package/src/index.ts +8 -4
- package/src/instrument/SineWave.smoke.test.tsx +2 -0
- package/src/instrument/types.ts +10 -1
- package/src/lab/Lab.nebula.test.tsx +11 -2
- package/src/lab/Lab.test.tsx +3 -1
- package/src/lab/Lab.tsx +4 -4
- package/src/loupe/AGENTS.md +7 -6
- package/src/loupe/LoupeGestures.tsx +14 -9
- package/src/loupe/TrialLoupe.tsx +5 -5
- package/src/loupe/index.ts +1 -0
- package/src/loupe/loupeActions.ts +6 -2
- package/src/overview/AGENTS.md +38 -0
- package/src/overview/OverviewMarks.test.tsx +100 -0
- package/src/overview/OverviewMarks.tsx +93 -0
- package/src/overview/TrialOverview.less +53 -0
- package/src/overview/TrialOverview.stories.tsx +50 -0
- package/src/overview/TrialOverview.test.tsx +116 -0
- package/src/overview/TrialOverview.tsx +374 -0
- package/src/overview/index.ts +2 -0
- package/src/passthrough/weasel-ui.ts +7 -0
- package/src/specimen/Specimen.tsx +19 -2
- package/src/state/labRecords.test.ts +4 -1
- package/src/state/store.test.ts +4 -4
- package/src/state/useTrialState.test.tsx +1 -1
- package/src/styles.less +2 -0
- package/src/theme/Interstellar.stories.tsx +1 -5
- package/src/tools/labTool.ts +6 -2
- package/src/trial/Trial.annotations.test.tsx +16 -0
- package/src/trial/Trial.loupe.test.tsx +73 -1
- package/src/trial/Trial.stories.tsx +2 -0
- package/src/trial/Trial.tsx +17 -1
- package/src/trial/trialOps.test.ts +4 -4
- package/dist/_dts/inDialog-D5ZKGjiD.d.ts +0 -23
- package/dist/_dts/number.d-OeQYQoUO.d.ts +0 -28
- package/dist/chunk-2AYGEN57.js +0 -32
- package/dist/chunk-2AYGEN57.js.map +0 -1
- package/dist/chunk-AWH3SRPW.js.map +0 -1
- package/dist/chunk-BVL7Y574.js.map +0 -1
- package/dist/chunk-ERFMC6WU.js.map +0 -1
- package/dist/chunk-H5VIV7MQ.js.map +0 -1
- package/dist/chunk-HFRE4QQV.js.map +0 -1
- package/dist/chunk-RS3HRQWU.js.map +0 -1
- package/dist/chunk-TT5C5S6R.js +0 -368
- package/dist/chunk-TT5C5S6R.js.map +0 -1
- package/dist/chunk-USO6TERP.js.map +0 -1
- package/dist/chunk-VQ47KWD5.js.map +0 -1
- package/dist/chunk-WUJXJC5O.js.map +0 -1
- package/dist/chunk-ZORVNOKX.js.map +0 -1
- package/src/canvas/usePanZoom.test.ts +0 -244
- package/src/canvas/usePanZoom.ts +0 -147
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* labkit's camera as a weasel `View`, so core's viewport actions drive it.
|
|
3
|
+
*
|
|
4
|
+
* labkit places a world point on screen at
|
|
5
|
+
* `originPx + pan + zoom * (x, yDir * y)`. A weasel `View` places one at
|
|
6
|
+
* `(p - (x, y)) * scale`. Over *frame-local* coordinates — the world with `y`
|
|
7
|
+
* multiplied by `yDir` — the two are the same affine map, which is all a pan
|
|
8
|
+
* or a zoom about a screen point needs.
|
|
9
|
+
*/
|
|
10
|
+
import type { View } from '@weasel-js/core';
|
|
11
|
+
import type { Point, ViewTransform } from '../instrument/types';
|
|
12
|
+
import { normalize2DView } from '../state/view';
|
|
13
|
+
import { DEFAULT_FRAME, type WorldFrame } from './worldSpec';
|
|
14
|
+
|
|
15
|
+
/** The weasel `View` equal to `view` under `frame`. */
|
|
16
|
+
export function toCameraView(view: ViewTransform, frame: WorldFrame = DEFAULT_FRAME): View {
|
|
17
|
+
const v = normalize2DView(view);
|
|
18
|
+
return {
|
|
19
|
+
x: -(frame.originPx.x + v.pan.x) / v.zoom,
|
|
20
|
+
y: -(frame.originPx.y + v.pan.y) / v.zoom,
|
|
21
|
+
scale: { x: v.zoom, y: v.zoom },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The labkit camera equal to weasel `view` under `frame`. */
|
|
26
|
+
export function fromCameraView(view: View, frame: WorldFrame = DEFAULT_FRAME): ViewTransform {
|
|
27
|
+
const zoom = view.scale.x;
|
|
28
|
+
return {
|
|
29
|
+
zoom,
|
|
30
|
+
pan: { x: -view.x * zoom - frame.originPx.x, y: -view.y * zoom - frame.originPx.y },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A frame-local point in the instrument's world, and back — the map is its
|
|
35
|
+
* own inverse. */
|
|
36
|
+
export function frameLocalToWorld(p: Point, frame: WorldFrame = DEFAULT_FRAME): Point {
|
|
37
|
+
return { x: p.x, y: p.y * frame.yDir };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* `next` with its zoom held to [`min`, `max`], about the screen point the step
|
|
42
|
+
* from `prev` was anchored on, so a zoom that hits a bound stays anchored where
|
|
43
|
+
* the pointer is. A pan (equal scales) passes through.
|
|
44
|
+
*/
|
|
45
|
+
export function clampZoomAbout(prev: View, next: View, min: number, max: number): View {
|
|
46
|
+
const s0 = prev.scale.x;
|
|
47
|
+
const s1 = next.scale.x;
|
|
48
|
+
const clamped = Math.min(max, Math.max(min, s1));
|
|
49
|
+
if (clamped === s1) return next;
|
|
50
|
+
if (s0 === s1) return { ...next, scale: { x: clamped, y: clamped } };
|
|
51
|
+
// The screen point both views agree on: p / s0 + x0 = p / s1 + x1.
|
|
52
|
+
const inv = 1 / s0 - 1 / s1;
|
|
53
|
+
const px = (next.x - prev.x) / inv;
|
|
54
|
+
const py = (next.y - prev.y) / inv;
|
|
55
|
+
// The same point under the clamped scale.
|
|
56
|
+
return {
|
|
57
|
+
x: prev.x + px / s0 - px / clamped,
|
|
58
|
+
y: prev.y + py / s0 - py / clamped,
|
|
59
|
+
scale: { x: clamped, y: clamped },
|
|
60
|
+
};
|
|
61
|
+
}
|
package/src/canvas/index.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
export type { CameraContextValue, CameraInputProps, CameraViewOptions } from './CameraInput';
|
|
2
|
+
export {
|
|
3
|
+
CameraContext,
|
|
4
|
+
CameraInput,
|
|
5
|
+
CameraScope,
|
|
6
|
+
CameraScopeContext,
|
|
7
|
+
STAGE_VIEW_ID,
|
|
8
|
+
useCameraView,
|
|
9
|
+
} from './CameraInput';
|
|
1
10
|
export type { CameraWheelSlot } from './CameraWheelContext';
|
|
2
11
|
export { CameraWheelContext } from './CameraWheelContext';
|
|
3
12
|
export type { CanvasStackProps } from './CanvasStack';
|
|
@@ -6,11 +15,11 @@ export type { CanvasStackContextValue, CanvasStackSurface } from './CanvasStackC
|
|
|
6
15
|
export { CanvasStackContext } from './CanvasStackContext';
|
|
7
16
|
export type { ZoomAtOptions } from './camera';
|
|
8
17
|
export { centerOn, zoomAt } from './camera';
|
|
18
|
+
export { clampZoomAbout, frameLocalToWorld, fromCameraView, toCameraView } from './cameraView';
|
|
9
19
|
export { screenToWorld, worldToScreen } from './canvasCoords';
|
|
20
|
+
export { LinkedCursor } from './LinkedCursor';
|
|
10
21
|
export type { StageProps } from './Stage';
|
|
11
22
|
export { fitStage, Stage } from './Stage';
|
|
12
23
|
export type { CanvasLayerDescriptor } from './useLayerScheduler';
|
|
13
|
-
export type { PanZoomHandlers, UsePanZoomOptions } from './usePanZoom';
|
|
14
|
-
export { usePanZoom } from './usePanZoom';
|
|
15
24
|
export type { ViewportSize, WorldFrame, WorldSpec } from './worldSpec';
|
|
16
25
|
export { applyCamera, DEFAULT_FRAME, resolveFrame } from './worldSpec';
|
|
@@ -3,13 +3,20 @@ import { describe, expect, it } from 'vitest';
|
|
|
3
3
|
import type { RegionContribution, TrialChromeContext } from '../types';
|
|
4
4
|
import { SidebarRegion } from './SidebarRegion';
|
|
5
5
|
|
|
6
|
-
const ctx = {
|
|
6
|
+
const ctx = {
|
|
7
|
+
collapsedSections: {},
|
|
8
|
+
setSectionCollapsed: () => {},
|
|
9
|
+
} as unknown as TrialChromeContext;
|
|
7
10
|
|
|
8
11
|
describe('<SidebarRegion> stance and tone', () => {
|
|
9
12
|
it('puts a section’s stance and tone on the section', () => {
|
|
10
13
|
const contributions: RegionContribution<TrialChromeContext>[] = [
|
|
11
14
|
{ id: 'a', region: 'sidebar', item: { title: 'Plain', body: 'x' } },
|
|
12
|
-
{
|
|
15
|
+
{
|
|
16
|
+
id: 'b',
|
|
17
|
+
region: 'sidebar',
|
|
18
|
+
item: { title: 'Debug', body: 'y', stance: 'debug', tone: '#224a63' },
|
|
19
|
+
},
|
|
13
20
|
];
|
|
14
21
|
const { container } = render(<SidebarRegion contributions={contributions} ctx={ctx} />);
|
|
15
22
|
const [plain, debug] = [...container.querySelectorAll<HTMLElement>('.lk-sidebar-section')];
|
package/src/config/builder.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { PrefLeaf, PrefNumberFormat, PrefNumberUnit } from '@weasel-js/ui';
|
|
2
|
-
import { type InDialogOptions, inDialog } from '../controls/inDialog';
|
|
3
2
|
import { type Auto, isAuto } from './auto';
|
|
4
3
|
import type {
|
|
5
4
|
Annotations,
|
|
@@ -12,6 +11,7 @@ import type {
|
|
|
12
11
|
ConfigSchema,
|
|
13
12
|
ConfigShape,
|
|
14
13
|
ControlRenderer,
|
|
14
|
+
InDialogOptions,
|
|
15
15
|
InferConfig,
|
|
16
16
|
NodeOptions,
|
|
17
17
|
SectionOption,
|
|
@@ -89,13 +89,13 @@ export abstract class BaseNode<T> implements ConfigNode<T> {
|
|
|
89
89
|
|
|
90
90
|
/** Draw this one row yourself, keeping the kind, default and validation. */
|
|
91
91
|
render(renderer: ControlRenderer): this {
|
|
92
|
-
return this.opt({ render: renderer });
|
|
92
|
+
return this.opt({ render: renderer, dialog: undefined });
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/** Draw this row as a button that opens a dialog, with `body` as the
|
|
96
96
|
* dialog's content — for an editor too big for the row. */
|
|
97
97
|
dialog(body: ControlRenderer, opts?: InDialogOptions): this {
|
|
98
|
-
return this.
|
|
98
|
+
return this.opt({ dialog: { ...opts, body }, render: undefined });
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
package/src/config/entry.test.ts
CHANGED
|
@@ -39,7 +39,9 @@ describe('@weasel-js/labkit/config', () => {
|
|
|
39
39
|
// A forge frame loads this entry for every story, and ui's barrel is 167 modules on the dev server.
|
|
40
40
|
it('loads nothing from @weasel-js/ui at runtime', () => {
|
|
41
41
|
const importers = [...walk(resolve(__dirname, 'index.ts'))].filter((file) =>
|
|
42
|
-
[...readFileSync(file, 'utf8').matchAll(IMPORT)].some((m) =>
|
|
42
|
+
[...readFileSync(file, 'utf8').matchAll(IMPORT)].some((m) =>
|
|
43
|
+
(m[1] ?? m[2] ?? m[3])?.startsWith('@weasel-js/ui'),
|
|
44
|
+
),
|
|
43
45
|
);
|
|
44
46
|
expect(importers).toEqual([]);
|
|
45
47
|
});
|
package/src/config/index.ts
CHANGED
|
@@ -23,8 +23,8 @@ export {
|
|
|
23
23
|
withValueAtPath,
|
|
24
24
|
} from './path';
|
|
25
25
|
export { resolveConfigSchema } from './resolve';
|
|
26
|
-
export { type SectionTree, sectionTree } from './sectionTree';
|
|
27
26
|
export { applyRules, builtinRules, titleCase } from './rules';
|
|
27
|
+
export { type SectionTree, sectionTree } from './sectionTree';
|
|
28
28
|
export type {
|
|
29
29
|
Annotations,
|
|
30
30
|
BranchAnnotations,
|
|
@@ -40,12 +40,15 @@ export type {
|
|
|
40
40
|
ConfigSchema,
|
|
41
41
|
ConfigShape,
|
|
42
42
|
ControlRenderer,
|
|
43
|
+
DialogSpec,
|
|
43
44
|
EntryValue,
|
|
45
|
+
InDialogOptions,
|
|
44
46
|
InferConfig,
|
|
45
47
|
LeafPatch,
|
|
46
48
|
NodeOptions,
|
|
47
49
|
NodeValue,
|
|
48
50
|
ResolvedConfig,
|
|
51
|
+
SectionOption,
|
|
49
52
|
SectionSpec,
|
|
50
53
|
ValueAtPath,
|
|
51
54
|
} from './types';
|
package/src/config/resolve.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ConfigSchema,
|
|
8
8
|
ConfigShape,
|
|
9
9
|
ControlRenderer,
|
|
10
|
+
DialogSpec,
|
|
10
11
|
LeafPatch,
|
|
11
12
|
ResolvedConfig,
|
|
12
13
|
SectionSpec,
|
|
@@ -25,6 +26,7 @@ interface Sink {
|
|
|
25
26
|
sections: SectionSpec[];
|
|
26
27
|
showIf: Map<string, (config: Record<string, unknown>) => boolean>;
|
|
27
28
|
renderers: Record<string, ControlRenderer>;
|
|
29
|
+
dialogs: Record<string, DialogSpec>;
|
|
28
30
|
chain: readonly ConfigRule[];
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -45,10 +47,17 @@ export function resolveConfigSchema<TC>(
|
|
|
45
47
|
sections: [],
|
|
46
48
|
showIf: new Map(),
|
|
47
49
|
renderers: {},
|
|
50
|
+
dialogs: {},
|
|
48
51
|
chain: [...rules, ...builtinRules],
|
|
49
52
|
};
|
|
50
53
|
const group = resolveShape(schema.nodes, '', '', sink);
|
|
51
|
-
return {
|
|
54
|
+
return {
|
|
55
|
+
group,
|
|
56
|
+
sections: sink.sections,
|
|
57
|
+
showIf: sink.showIf,
|
|
58
|
+
renderers: sink.renderers,
|
|
59
|
+
dialogs: sink.dialogs,
|
|
60
|
+
};
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
/** A section while it is still being filled. */
|
|
@@ -81,6 +90,7 @@ function resolveShape(shape: ConfigShape, at: string, name: string, sink: Sink):
|
|
|
81
90
|
}
|
|
82
91
|
if (predicate) sink.showIf.set(path, predicate);
|
|
83
92
|
if (!isConfigBranch(entry) && entry.options.render) sink.renderers[path] = entry.options.render;
|
|
93
|
+
if (!isConfigBranch(entry) && entry.options.dialog) sink.dialogs[path] = entry.options.dialog;
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
return { name, children };
|
|
@@ -70,14 +70,19 @@ describe('sectionTree', () => {
|
|
|
70
70
|
'weight',
|
|
71
71
|
]);
|
|
72
72
|
const off = sectionTree(resolved, { lineNumbers: false, listing: { dim: 1, weight: 0 } });
|
|
73
|
-
expect(Object.keys(groupAt(groupAt(off.group, 'view'), 'listing').children)).toEqual([
|
|
73
|
+
expect(Object.keys(groupAt(groupAt(off.group, 'view'), 'listing').children)).toEqual([
|
|
74
|
+
'weight',
|
|
75
|
+
]);
|
|
74
76
|
});
|
|
75
77
|
|
|
76
78
|
it('drops a section nothing in it is showing, rather than opening it onto nothing', () => {
|
|
77
79
|
const resolved = resolveConfigSchema(
|
|
78
80
|
f.schema({
|
|
79
81
|
mode: f.enum<string>('a', ['a', 'b']).section('View'),
|
|
80
|
-
glow: f
|
|
82
|
+
glow: f
|
|
83
|
+
.number(1)
|
|
84
|
+
.section('Diff')
|
|
85
|
+
.showIf((c) => c.mode === 'b'),
|
|
81
86
|
}),
|
|
82
87
|
);
|
|
83
88
|
expect(Object.keys(sectionTree(resolved, { mode: 'b', glow: 1 }).group.children)).toEqual([
|
|
@@ -91,7 +96,10 @@ describe('sectionTree', () => {
|
|
|
91
96
|
|
|
92
97
|
it('keeps a hidden leaf out unless asked for it', () => {
|
|
93
98
|
const resolved = resolveConfigSchema(
|
|
94
|
-
f.schema({
|
|
99
|
+
f.schema({
|
|
100
|
+
seed: f.number(1).hidden().section('Debug'),
|
|
101
|
+
route: f.boolean(true).section('Debug'),
|
|
102
|
+
}),
|
|
95
103
|
);
|
|
96
104
|
expect(Object.keys(groupAt(sectionTree(resolved, {}).group, 'debug').children)).toEqual([
|
|
97
105
|
'route',
|
|
@@ -134,9 +142,7 @@ describe('sectionTree', () => {
|
|
|
134
142
|
it('ignores a section nested under a group, which buckets rows in a pane', () => {
|
|
135
143
|
const resolved = resolveConfigSchema(
|
|
136
144
|
f.schema({
|
|
137
|
-
cards: f
|
|
138
|
-
.group({ floor: f.number(1).section('Size') })
|
|
139
|
-
.section('View'),
|
|
145
|
+
cards: f.group({ floor: f.number(1).section('Size') }).section('View'),
|
|
140
146
|
}),
|
|
141
147
|
);
|
|
142
148
|
const { group } = sectionTree(resolved, { cards: { floor: 1 } });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { PrefGroup, PrefLeaf } from '@weasel-js/ui';
|
|
2
2
|
import { valueAtPath } from './path';
|
|
3
3
|
import type { ResolvedConfig } from './types';
|
|
4
4
|
import { isLeafVisible } from './visible';
|
|
@@ -22,10 +22,20 @@ export interface SectionTree {
|
|
|
22
22
|
pathAt: (railPath: string) => string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** Structural rather than `isPrefLeaf`, so the config entry stays free of a
|
|
26
|
+
* runtime import from ui. */
|
|
27
|
+
function isLeaf(node: PrefLeaf | PrefGroup): node is PrefLeaf {
|
|
28
|
+
return 'kind' in node;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
/** A section label as an object key: no dots, since a dotted path is how every
|
|
26
32
|
* caller addresses a node, and a label is free to hold one. */
|
|
27
33
|
function slugOf(label: string, taken: ReadonlySet<string>): string {
|
|
28
|
-
const base =
|
|
34
|
+
const base =
|
|
35
|
+
label
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
38
|
+
.replace(/^-|-$/g, '') || 'section';
|
|
29
39
|
if (!taken.has(base)) return base;
|
|
30
40
|
for (let n = 2; ; n += 1) if (!taken.has(`${base}-${n}`)) return `${base}-${n}`;
|
|
31
41
|
}
|
|
@@ -44,7 +54,7 @@ function prune(
|
|
|
44
54
|
for (const [key, child] of Object.entries(group.children)) {
|
|
45
55
|
const path = at === '' ? key : `${at}.${key}`;
|
|
46
56
|
if (!isLeafVisible(resolved, path, config, showHidden)) continue;
|
|
47
|
-
if (
|
|
57
|
+
if (isLeaf(child)) {
|
|
48
58
|
children[key] = child;
|
|
49
59
|
continue;
|
|
50
60
|
}
|
|
@@ -81,7 +91,7 @@ export function sectionTree(
|
|
|
81
91
|
claimed.add(path);
|
|
82
92
|
const child = resolved.group.children[path];
|
|
83
93
|
if (!child || !isLeafVisible(resolved, path, config, showHidden)) continue;
|
|
84
|
-
if (
|
|
94
|
+
if (isLeaf(child)) {
|
|
85
95
|
kept[path] = child;
|
|
86
96
|
} else {
|
|
87
97
|
const subtree = prune(resolved, child, path, config, showHidden);
|
|
@@ -101,7 +111,7 @@ export function sectionTree(
|
|
|
101
111
|
// gathers loose leaves into one item named for the root.
|
|
102
112
|
for (const [key, child] of Object.entries(resolved.group.children)) {
|
|
103
113
|
if (claimed.has(key) || !isLeafVisible(resolved, key, config, showHidden)) continue;
|
|
104
|
-
if (
|
|
114
|
+
if (isLeaf(child)) {
|
|
105
115
|
children[key] = child;
|
|
106
116
|
} else {
|
|
107
117
|
const subtree = prune(resolved, child, key, config, showHidden);
|
package/src/config/types.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
PrefNumberUnit,
|
|
6
6
|
PrefRenderer,
|
|
7
7
|
} from '@weasel-js/ui';
|
|
8
|
+
import type { ReactNode } from 'react';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Renders the control cell for one config leaf. Identical to weasel-ui's
|
|
@@ -104,6 +105,23 @@ export interface ResolvedConfig {
|
|
|
104
105
|
showIf: ReadonlyMap<string, (config: Record<string, unknown>) => boolean>;
|
|
105
106
|
/** Node-level `.render` overrides, keyed by path. */
|
|
106
107
|
renderers: Readonly<Record<string, ControlRenderer>>;
|
|
108
|
+
/** Node-level `.dialog` rows, keyed by path. */
|
|
109
|
+
dialogs: Readonly<Record<string, DialogSpec>>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Options for a row drawn as a button that opens a dialog. */
|
|
113
|
+
export interface InDialogOptions {
|
|
114
|
+
/** What the row's button shows of the value. Defaults to a list joined with
|
|
115
|
+
* commas, or the value itself. */
|
|
116
|
+
summary?: (value: unknown) => ReactNode;
|
|
117
|
+
/** The dialog's heading. Defaults to the row's label. */
|
|
118
|
+
title?: ReactNode;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A `.dialog` row: `body` is the dialog's content, handed the context a row
|
|
122
|
+
* renderer gets. */
|
|
123
|
+
export interface DialogSpec extends InDialogOptions {
|
|
124
|
+
body: ControlRenderer;
|
|
107
125
|
}
|
|
108
126
|
|
|
109
127
|
/** How a node names the section it belongs to: the heading, and how that
|
|
@@ -121,6 +139,9 @@ export interface SectionOption {
|
|
|
121
139
|
/** Per-node extras that do not belong on a `PrefLeaf`. */
|
|
122
140
|
export interface NodeOptions extends BranchOptions {
|
|
123
141
|
render?: ControlRenderer;
|
|
142
|
+
/** Draw the row as a button that opens a dialog. Set by `.dialog`; the
|
|
143
|
+
* config entry records it, and `ControlPanel` draws it. */
|
|
144
|
+
dialog?: DialogSpec;
|
|
124
145
|
/** Errors for this leaf under `config`, the instrument's whole config. */
|
|
125
146
|
validate?: (leaf: PrefLeaf, config: Record<string, unknown>) => string[];
|
|
126
147
|
/** Computes this leaf's value while it is auto. Attaching one is what gives
|
|
@@ -372,6 +372,7 @@ describe('<ControlPanel> built-in kind coverage', () => {
|
|
|
372
372
|
sections: [],
|
|
373
373
|
showIf: new Map(),
|
|
374
374
|
renderers: {},
|
|
375
|
+
dialogs: {},
|
|
375
376
|
});
|
|
376
377
|
|
|
377
378
|
it('declines paint and object with a named row rather than a blank one', () => {
|
|
@@ -1075,14 +1076,23 @@ describe('<ControlPanel> title, stance and tone', () => {
|
|
|
1075
1076
|
const fields: ConfigField[] = [{ key: 'on', label: 'On', type: 'checkbox', default: true }];
|
|
1076
1077
|
|
|
1077
1078
|
it('renders the bare list when given none of them', () => {
|
|
1078
|
-
const { container } = render(
|
|
1079
|
+
const { container } = render(
|
|
1080
|
+
<ControlPanel fields={fields} config={{ on: true }} setConfig={vi.fn()} />,
|
|
1081
|
+
);
|
|
1079
1082
|
expect(container.firstElementChild?.classList.contains('lk-control-panel')).toBe(true);
|
|
1080
1083
|
expect(container.querySelector('[data-stance], h2')).toBeNull();
|
|
1081
1084
|
});
|
|
1082
1085
|
|
|
1083
1086
|
it('wraps its rows in a panel carrying them when asked', () => {
|
|
1084
1087
|
const { container } = render(
|
|
1085
|
-
<ControlPanel
|
|
1088
|
+
<ControlPanel
|
|
1089
|
+
title="View"
|
|
1090
|
+
stance="scope"
|
|
1091
|
+
tone={1}
|
|
1092
|
+
fields={fields}
|
|
1093
|
+
config={{ on: true }}
|
|
1094
|
+
setConfig={vi.fn()}
|
|
1095
|
+
/>,
|
|
1086
1096
|
);
|
|
1087
1097
|
const panel = container.firstElementChild as HTMLElement;
|
|
1088
1098
|
expect(panel.dataset.stance).toBe('scope');
|
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
type PrefGroup,
|
|
7
7
|
type PrefLeaf,
|
|
8
8
|
type PropertyAlign,
|
|
9
|
-
type PropertyControlProps,
|
|
10
9
|
PropertyControl,
|
|
10
|
+
type PropertyControlProps,
|
|
11
11
|
type PropertyDensity,
|
|
12
12
|
PropertyField,
|
|
13
13
|
PropertyGroup,
|
|
@@ -26,7 +26,7 @@ import { fromConfigFields } from '../config/fromConfigField';
|
|
|
26
26
|
import { schemaNodeAtPath, valueAtPath } from '../config/path';
|
|
27
27
|
import type { ControlRenderer, ResolvedConfig, SectionSpec } from '../config/types';
|
|
28
28
|
import { isLeafVisible } from '../config/visible';
|
|
29
|
-
import { summarizeValue } from './inDialog';
|
|
29
|
+
import { inDialog, summarizeValue } from './inDialog';
|
|
30
30
|
import type { ConfigField } from './types';
|
|
31
31
|
|
|
32
32
|
/** How a panel packs its rows into the two-column property grid.
|
|
@@ -243,7 +243,13 @@ export function ControlPanel<TC extends Record<string, unknown>>({
|
|
|
243
243
|
if (pair === undefined) return undefined;
|
|
244
244
|
if (!isLeafVisible(resolved, path, config as Record<string, unknown>, showHidden))
|
|
245
245
|
return undefined;
|
|
246
|
-
if (
|
|
246
|
+
if (
|
|
247
|
+
renderers?.[path] ??
|
|
248
|
+
resolved.renderers[path] ??
|
|
249
|
+
resolved.dialogs[path] ??
|
|
250
|
+
renderers?.[found.kind]
|
|
251
|
+
)
|
|
252
|
+
return undefined;
|
|
247
253
|
if (!isBuiltinToolPref(found)) return undefined;
|
|
248
254
|
if (found.kind === 'paint' || found.kind === 'object' || isSliderLeaf(found)) return undefined;
|
|
249
255
|
return { leaf: found, pair };
|
|
@@ -405,8 +411,13 @@ function ControlRow<TC extends Record<string, unknown>>({
|
|
|
405
411
|
const autoProps = { auto: isAutoRow, onAutoChange };
|
|
406
412
|
|
|
407
413
|
// Most specific wins, and within a tier the lab's entry beats the
|
|
408
|
-
// instrument's: controls[path] -> node .render -> controls[kind] -> built-in.
|
|
409
|
-
const
|
|
414
|
+
// instrument's: controls[path] -> node .render/.dialog -> controls[kind] -> built-in.
|
|
415
|
+
const dialog = resolved.dialogs[path];
|
|
416
|
+
const custom =
|
|
417
|
+
renderers?.[path] ??
|
|
418
|
+
resolved.renderers[path] ??
|
|
419
|
+
(dialog ? inDialog(dialog.body, dialog) : undefined) ??
|
|
420
|
+
renderers?.[leaf.kind];
|
|
410
421
|
// A custom row places itself like any other; one that needs the full width
|
|
411
422
|
// says so with `<PropertyRow span>`, which is the same opt-out a built-in has.
|
|
412
423
|
if (custom) return custom({ path, pref: leaf, value, setValue: write, auto: isAutoRow, setAuto });
|
|
@@ -442,7 +453,8 @@ function ControlRow<TC extends Record<string, unknown>>({
|
|
|
442
453
|
}
|
|
443
454
|
|
|
444
455
|
const field = labField(leaf, value, write);
|
|
445
|
-
if (field === null)
|
|
456
|
+
if (field === null)
|
|
457
|
+
return <UnwiredRow label={label} kind={leaf.kind} description={description} />;
|
|
446
458
|
switch (field.kind) {
|
|
447
459
|
case 'string':
|
|
448
460
|
return <DebouncedTextRow leaf={leaf} field={field} span={wide} {...row} />;
|
|
@@ -479,7 +491,10 @@ function labField(
|
|
|
479
491
|
unit: extra<string>(leaf, 'suffix') ?? field.unit,
|
|
480
492
|
};
|
|
481
493
|
case 'boolean':
|
|
482
|
-
return {
|
|
494
|
+
return {
|
|
495
|
+
...field,
|
|
496
|
+
control: extra<string>(leaf, 'control') === 'switch' ? 'switch' : 'checkbox',
|
|
497
|
+
};
|
|
483
498
|
case 'enum':
|
|
484
499
|
return { ...field, control: extra<string>(leaf, 'control') === 'radio' ? 'radio' : 'select' };
|
|
485
500
|
case 'string':
|
|
@@ -605,7 +620,9 @@ function PairCell({
|
|
|
605
620
|
if (field === null) return null;
|
|
606
621
|
switch (field.kind) {
|
|
607
622
|
case 'string':
|
|
608
|
-
return
|
|
623
|
+
return (
|
|
624
|
+
<PropertyControl {...field} name={leaf.name} value={text.local} onChange={text.type} />
|
|
625
|
+
);
|
|
609
626
|
case 'number':
|
|
610
627
|
// The pair names the row, so a cell has no room for a unit.
|
|
611
628
|
return <PropertyControl {...field} name={leaf.name} control="input" unit={undefined} />;
|
|
@@ -618,7 +635,9 @@ function PairCell({
|
|
|
618
635
|
{...field}
|
|
619
636
|
name={leaf.name}
|
|
620
637
|
alpha={undefined}
|
|
621
|
-
onChange={(rgb: string) =>
|
|
638
|
+
onChange={(rgb: string) =>
|
|
639
|
+
write(field.alpha ? withAlpha01(rgb, getAlpha01(stored)) : rgb)
|
|
640
|
+
}
|
|
622
641
|
/>
|
|
623
642
|
);
|
|
624
643
|
}
|
|
@@ -69,6 +69,24 @@ describe('.dialog', () => {
|
|
|
69
69
|
render(<Panel schema={schema} />);
|
|
70
70
|
expect(screen.getByRole('button', { name: 'Edit Pts' }).textContent).toContain('3 points');
|
|
71
71
|
});
|
|
72
|
+
|
|
73
|
+
it('gives way to a later .render, and takes over from an earlier one', () => {
|
|
74
|
+
const schema = f.schema({
|
|
75
|
+
a: f
|
|
76
|
+
.string('x')
|
|
77
|
+
.dialog(() => null)
|
|
78
|
+
.render(() => <span>row a</span>),
|
|
79
|
+
b: f
|
|
80
|
+
.string('y')
|
|
81
|
+
.render(() => <span>row b</span>)
|
|
82
|
+
.dialog(() => null),
|
|
83
|
+
});
|
|
84
|
+
render(<Panel schema={schema} />);
|
|
85
|
+
expect(screen.getByText('row a')).toBeTruthy();
|
|
86
|
+
expect(screen.queryByRole('button', { name: 'Edit A' })).toBeNull();
|
|
87
|
+
expect(screen.queryByText('row b')).toBeNull();
|
|
88
|
+
expect(screen.getByRole('button', { name: 'Edit B' })).toBeTruthy();
|
|
89
|
+
});
|
|
72
90
|
});
|
|
73
91
|
|
|
74
92
|
describe('summarizeValue', () => {
|
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
import { DialogRow } from '@weasel-js/ui';
|
|
2
|
-
import type {
|
|
3
|
-
import type { ControlRenderer } from '../config/types';
|
|
2
|
+
import type { ControlRenderer, InDialogOptions } from '../config/types';
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
export interface InDialogOptions {
|
|
7
|
-
/** What the row's button shows of the value. Defaults to a list joined with
|
|
8
|
-
* commas, or the value itself. */
|
|
9
|
-
summary?: (value: unknown) => ReactNode;
|
|
10
|
-
/** The dialog's heading. Defaults to the row's label. */
|
|
11
|
-
title?: ReactNode;
|
|
12
|
-
}
|
|
4
|
+
export type { InDialogOptions };
|
|
13
5
|
|
|
14
6
|
/** The one-line summary a dialog row shows when it is given none. */
|
|
15
7
|
export function summarizeValue(value: unknown): string {
|
package/src/index.test.ts
CHANGED
|
@@ -72,7 +72,7 @@ describe('surface, job and orbit entry points', () => {
|
|
|
72
72
|
expect(typeof kit.annotationsFromJSON).toBe('function');
|
|
73
73
|
expect(typeof kit.seenFrom).toBe('function');
|
|
74
74
|
expect(typeof kit.fracToWorld).toBe('function');
|
|
75
|
-
expect(typeof kit.
|
|
75
|
+
expect(typeof kit.CameraInput).toBe('function');
|
|
76
76
|
expect(typeof kit.useJob).toBe('function');
|
|
77
77
|
expect(typeof kit.as2DView).toBe('function');
|
|
78
78
|
expect(kit.SurfaceContext).toBeDefined();
|
package/src/index.ts
CHANGED
|
@@ -79,6 +79,7 @@ export type {
|
|
|
79
79
|
AnnotationsCapability,
|
|
80
80
|
AnnotationTarget,
|
|
81
81
|
AnnotationTargetInfo,
|
|
82
|
+
AnnotationToolId,
|
|
82
83
|
CaptureDeps,
|
|
83
84
|
CaptureOptions,
|
|
84
85
|
CaptureResult,
|
|
@@ -129,13 +130,12 @@ export {
|
|
|
129
130
|
export type { ContentSize, PaneSize } from './annotations/view';
|
|
130
131
|
export { fitView, fromWeaselView, toWeaselView } from './annotations/view';
|
|
131
132
|
export type {
|
|
133
|
+
CameraContextValue,
|
|
132
134
|
CanvasLayerDescriptor,
|
|
133
135
|
CanvasStackContextValue,
|
|
134
136
|
CanvasStackProps,
|
|
135
137
|
CanvasStackSurface,
|
|
136
|
-
PanZoomHandlers,
|
|
137
138
|
StageProps,
|
|
138
|
-
UsePanZoomOptions,
|
|
139
139
|
ViewportSize,
|
|
140
140
|
WorldFrame,
|
|
141
141
|
WorldSpec,
|
|
@@ -143,6 +143,9 @@ export type {
|
|
|
143
143
|
} from './canvas';
|
|
144
144
|
export {
|
|
145
145
|
applyCamera,
|
|
146
|
+
CameraContext,
|
|
147
|
+
CameraInput,
|
|
148
|
+
CameraScope,
|
|
146
149
|
CanvasStack,
|
|
147
150
|
CanvasStackContext,
|
|
148
151
|
centerOn,
|
|
@@ -151,7 +154,6 @@ export {
|
|
|
151
154
|
resolveFrame,
|
|
152
155
|
Stage,
|
|
153
156
|
screenToWorld,
|
|
154
|
-
usePanZoom,
|
|
155
157
|
worldToScreen,
|
|
156
158
|
zoomAt,
|
|
157
159
|
} from './canvas';
|
|
@@ -173,7 +175,6 @@ export {
|
|
|
173
175
|
ValueNode,
|
|
174
176
|
} from './config/builder';
|
|
175
177
|
export { fromConfigFields } from './config/fromConfigField';
|
|
176
|
-
export { type SectionTree, sectionTree } from './config/sectionTree';
|
|
177
178
|
export {
|
|
178
179
|
fillConfigDefaults,
|
|
179
180
|
hasConfigPath,
|
|
@@ -183,6 +184,7 @@ export {
|
|
|
183
184
|
} from './config/path';
|
|
184
185
|
export { resolveConfigSchema } from './config/resolve';
|
|
185
186
|
export { applyRules, builtinRules, titleCase } from './config/rules';
|
|
187
|
+
export { type SectionTree, sectionTree } from './config/sectionTree';
|
|
186
188
|
export type {
|
|
187
189
|
Annotations,
|
|
188
190
|
BranchAnnotations,
|
|
@@ -198,12 +200,14 @@ export type {
|
|
|
198
200
|
ConfigSchema,
|
|
199
201
|
ConfigShape,
|
|
200
202
|
ControlRenderer,
|
|
203
|
+
DialogSpec,
|
|
201
204
|
EntryValue,
|
|
202
205
|
InferConfig,
|
|
203
206
|
LeafPatch,
|
|
204
207
|
NodeOptions,
|
|
205
208
|
NodeValue,
|
|
206
209
|
ResolvedConfig,
|
|
210
|
+
SectionOption,
|
|
207
211
|
SectionSpec,
|
|
208
212
|
ValueAtPath,
|
|
209
213
|
} from './config/types';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { render } from '@testing-library/react';
|
|
2
|
+
import { createPointerStore } from '@weasel-js/core';
|
|
2
3
|
import type { ReactElement } from 'react';
|
|
3
4
|
import { describe, expect, it, vi } from 'vitest';
|
|
4
5
|
import { DEFAULT_VIEW } from '../state/view';
|
|
@@ -94,6 +95,7 @@ describe('SineWave instrument smoke test', () => {
|
|
|
94
95
|
setZoom: vi.fn(),
|
|
95
96
|
activeToolId: null,
|
|
96
97
|
visibleLayers: [],
|
|
98
|
+
pointer: createPointerStore(),
|
|
97
99
|
},
|
|
98
100
|
emit: vi.fn(),
|
|
99
101
|
};
|
package/src/instrument/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PointerContextValue } from '@weasel-js/core';
|
|
1
2
|
import type { ReactNode } from 'react';
|
|
2
3
|
import type { AnnotationsCapability } from '../annotations/types';
|
|
3
4
|
import type { ViewportSize, WorldSpec } from '../canvas/worldSpec';
|
|
@@ -33,6 +34,14 @@ export interface RenderContext<TS = unknown, TC = unknown> {
|
|
|
33
34
|
* hidden layer's `draw`, so this is the only way to know what was
|
|
34
35
|
* painted — a legend listing rows the view did not draw needs it. */
|
|
35
36
|
visibleLayers: readonly string[];
|
|
37
|
+
/**
|
|
38
|
+
* Where the pointer is over this trial, in the instrument's world:
|
|
39
|
+
* `get()` answers `{ worldX, worldY, viewId }`, where `viewId` is
|
|
40
|
+
* `'stage'` over the trial's own camera and `'overview'` over its
|
|
41
|
+
* `<TrialOverview>`, or `null` when it is over neither. A key handler
|
|
42
|
+
* reading it works over either view.
|
|
43
|
+
*/
|
|
44
|
+
pointer: PointerContextValue;
|
|
36
45
|
};
|
|
37
46
|
emit: (event: string) => void;
|
|
38
47
|
/** Present only when the instrument declares a `job`. */
|
|
@@ -61,7 +70,7 @@ export interface CanvasCapability<TS = unknown, TC = unknown> {
|
|
|
61
70
|
* size, so an instrument can frame content it can only place in terms of
|
|
62
71
|
* the viewport; until then the trial's view is `null`. */
|
|
63
72
|
initialView?: ViewTransform | ((size: ViewportSize) => ViewTransform);
|
|
64
|
-
/** Widens
|
|
73
|
+
/** Widens the camera's default zoom clamp; the opening zoom stays reachable
|
|
65
74
|
* regardless of these. */
|
|
66
75
|
minZoom?: number;
|
|
67
76
|
maxZoom?: number;
|