@weasel-js/labkit 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_dts/{CanvasStackContext-DQgcH1ny.d.ts → CanvasStackContext-D2jnIwfk.d.ts} +1 -1
- package/dist/_dts/FloatingPanel-7j0IcXwz.d.ts +30 -0
- package/dist/_dts/{LayerList.d-2c9yup5Y.d.ts → LayerList.d-DiuEArKq.d.ts} +4 -4
- package/dist/_dts/PrefsRow.d-TOZ7BgD9.d.ts +79 -0
- package/dist/_dts/PropertyPanel.d-D-1paCbE.d.ts +141 -0
- package/dist/_dts/{Select.d-BchGMC2Y.d.ts → Select.d-Bensr_MT.d.ts} +15 -6
- package/dist/_dts/Stage-CCWfQQWM.d.ts +202 -0
- package/dist/_dts/index-B2Yj5aA0.d.ts +274 -0
- package/dist/_dts/{index-B0--8PEM.d.ts → index-CsYqUHD3.d.ts} +12 -13
- package/dist/_dts/{types-B7y8hX1D.d.ts → types-CRPSGFAp.d.ts} +33 -5
- package/dist/_dts/{frac-DTRq7Xpb.d.ts → types-CSm9jgol.d.ts} +700 -650
- package/dist/_dts/{useTrialState-CP4jfuKl.d.ts → useTrialState-CTZPgeZ7.d.ts} +2 -3
- package/dist/canvas/index.d.ts +40 -8
- package/dist/canvas/index.js +5 -3
- package/dist/chrome/index.d.ts +5 -6
- package/dist/chrome/index.js +9 -7
- package/dist/chunk-24W5VEJW.js +18 -0
- package/dist/chunk-24W5VEJW.js.map +1 -0
- package/dist/chunk-275JRRVD.js +728 -0
- package/dist/chunk-275JRRVD.js.map +1 -0
- package/dist/chunk-3RY6WL5T.js +38 -0
- package/dist/chunk-3RY6WL5T.js.map +1 -0
- package/dist/chunk-4LJSC7MB.js +143 -0
- package/dist/chunk-4LJSC7MB.js.map +1 -0
- package/dist/{chunk-3FI7Y4R4.js → chunk-7BO7HNGF.js} +5 -5
- package/dist/{chunk-3FI7Y4R4.js.map → chunk-7BO7HNGF.js.map} +1 -1
- package/dist/chunk-AWQG7FCC.js +19 -0
- package/dist/chunk-AWQG7FCC.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-ZORVNOKX.js → chunk-FMK7UQ6V.js} +24 -145
- package/dist/chunk-FMK7UQ6V.js.map +1 -0
- package/dist/{chunk-BVL7Y574.js → chunk-FZ7YFQN7.js} +26 -10
- package/dist/chunk-FZ7YFQN7.js.map +1 -0
- package/dist/{chunk-USO6TERP.js → chunk-GOZLNM4L.js} +11 -9
- package/dist/chunk-GOZLNM4L.js.map +1 -0
- package/dist/{chunk-H5VIV7MQ.js → chunk-I5DUQ5HM.js} +88 -82
- package/dist/chunk-I5DUQ5HM.js.map +1 -0
- package/dist/chunk-J7OWWTCW.js +143 -0
- package/dist/chunk-J7OWWTCW.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-VQ47KWD5.js → chunk-KMJ7RWD3.js} +18 -7
- package/dist/chunk-KMJ7RWD3.js.map +1 -0
- package/dist/{chunk-ERFMC6WU.js → chunk-M7HP3FTK.js} +20 -11
- package/dist/chunk-M7HP3FTK.js.map +1 -0
- package/dist/chunk-MIM5R4ZT.js +14972 -0
- package/dist/chunk-MIM5R4ZT.js.map +1 -0
- package/dist/{chunk-HFRE4QQV.js → chunk-T5IBSPAM.js} +96 -32
- package/dist/chunk-T5IBSPAM.js.map +1 -0
- package/dist/chunk-UNAZSLWE.js +294 -0
- package/dist/chunk-UNAZSLWE.js.map +1 -0
- package/dist/{chunk-RS3HRQWU.js → chunk-WPLAANH4.js} +4 -3
- package/dist/chunk-WPLAANH4.js.map +1 -0
- package/dist/{chunk-AWH3SRPW.js → chunk-Y23G2VZL.js} +5 -37
- package/dist/chunk-Y23G2VZL.js.map +1 -0
- package/dist/{chunk-MOLIUNVH.js → chunk-YT75VYRD.js} +4 -4
- package/dist/{chunk-MOLIUNVH.js.map → chunk-YT75VYRD.js.map} +1 -1
- package/dist/chunk-ZIFPHIWZ.js +302 -0
- package/dist/chunk-ZIFPHIWZ.js.map +1 -0
- package/dist/config/index.d.ts +48 -40
- 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 +9 -7
- package/dist/dragdrop/index.js +3 -2
- package/dist/index.d.ts +294 -82
- package/dist/index.js +425 -402
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +4 -5
- package/dist/layers/index.js +2 -1
- package/dist/loupe/index.d.ts +20 -17
- package/dist/loupe/index.js +5 -3
- package/dist/overview/index.d.ts +56 -0
- package/dist/overview/index.js +289 -0
- package/dist/overview/index.js.map +1 -0
- package/dist/passthrough/weasel-ui.d.ts +239 -69
- package/dist/passthrough/weasel-ui.js +3 -2
- package/dist/primitives/index.d.ts +5 -204
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +8 -8
- package/dist/state/index.js +4 -5
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +152 -82
- package/dist/undo/index.d.ts +4 -4
- package/package.json +17 -12
- package/src/annotations/AnnotationOverlay.tsx +2 -2
- package/src/annotations/Annotations.less +0 -10
- package/src/annotations/MarkList.tsx +4 -8
- package/src/annotations/index.ts +1 -0
- package/src/annotations/paint.test.ts +3 -14
- package/src/annotations/paint.ts +3 -19
- package/src/annotations/store.test.ts +26 -0
- package/src/annotations/store.ts +25 -6
- package/src/annotations/svgNodes.test.ts +6 -9
- package/src/annotations/svgNodes.ts +3 -3
- 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 +9 -10
- package/src/canvas/CameraInput.test.tsx +136 -0
- package/src/canvas/CameraInput.tsx +322 -0
- package/src/canvas/CanvasStack.tsx +47 -58
- package/src/canvas/LinkedCursor.less +16 -0
- package/src/canvas/LinkedCursor.tsx +54 -0
- package/src/canvas/Stage.test.tsx +33 -4
- package/src/canvas/Stage.tsx +47 -37
- package/src/canvas/cameraRegistry.test.ts +55 -0
- package/src/canvas/cameraRegistry.ts +86 -0
- package/src/canvas/cameraView.test.ts +60 -0
- package/src/canvas/cameraView.ts +61 -0
- package/src/canvas/cameraZoom.test.ts +46 -0
- package/src/canvas/cameraZoom.ts +24 -0
- package/src/canvas/index.ts +23 -2
- package/src/canvas/sharedScope.test.tsx +94 -0
- package/src/chrome/ChromeRegions.stories.tsx +1 -0
- package/src/chrome/regions/PaletteRegion.less +0 -7
- package/src/chrome/regions/SidebarRegion.less +12 -2
- package/src/chrome/regions/SidebarRegion.test.tsx +9 -2
- package/src/config/builder.test.ts +2 -1
- package/src/config/builder.ts +15 -8
- package/src/config/entry.test.ts +3 -1
- package/src/config/fromConfigField.test.ts +14 -0
- package/src/config/fromConfigField.ts +19 -4
- package/src/config/index.ts +4 -1
- package/src/config/path.ts +2 -5
- package/src/config/resolve.ts +15 -1
- package/src/config/sectionTree.test.ts +12 -6
- package/src/config/sectionTree.ts +15 -5
- package/src/config/types.ts +34 -9
- package/src/controls/ControlPanel.less +1 -1
- package/src/controls/ControlPanel.stories.tsx +3 -2
- package/src/controls/ControlPanel.test.tsx +76 -13
- package/src/controls/ControlPanel.tsx +80 -11
- 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 +23 -5
- 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 +87 -76
- 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 +8 -17
- package/src/lab/LabSwitcher.less +6 -6
- package/src/lab/LabZoom.test.tsx +223 -0
- package/src/lab/LabZoom.tsx +170 -0
- package/src/lab/index.ts +1 -0
- package/src/loupe/AGENTS.md +7 -6
- package/src/loupe/LoupeGestures.tsx +15 -10
- 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 +376 -0
- package/src/overview/index.ts +2 -0
- package/src/passthrough/weasel-ui.test.ts +2 -0
- package/src/passthrough/weasel-ui.ts +23 -4
- package/src/primitives/FloatingPanel.test.tsx +11 -0
- package/src/primitives/FloatingPanel.tsx +2 -5
- package/src/primitives/JobProgress.less +1 -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/index.ts +2 -0
- package/src/specimen/Specimen.stories.tsx +1 -0
- package/src/specimen/Specimen.tsx +21 -4
- package/src/state/Persistence.browser.test.tsx +47 -0
- package/src/state/labRecords.test.ts +4 -1
- package/src/state/store.test.ts +9 -5
- package/src/state/usePersistedState.test.tsx +1 -1
- package/src/state/useTrialState.test.tsx +1 -1
- package/src/styles.less +3 -0
- package/src/theme/Interstellar.stories.tsx +4 -6
- 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/tools/labTool.ts +6 -2
- package/src/trial/Trial.annotations.test.tsx +16 -0
- package/src/trial/Trial.less +0 -13
- package/src/trial/Trial.loupe.test.tsx +73 -1
- package/src/trial/Trial.stories.tsx +2 -0
- package/src/trial/Trial.tsx +23 -1
- package/src/trial/TrialChrome.tsx +21 -3
- package/src/trial/trialOps.test.ts +4 -4
- package/dist/_dts/PrefsRow.d-DFpPIeIC.d.ts +0 -39
- package/dist/_dts/inDialog-D5ZKGjiD.d.ts +0 -23
- package/dist/_dts/index-DINmCuLA.d.ts +0 -453
- package/dist/_dts/number.d-OeQYQoUO.d.ts +0 -28
- package/dist/_dts/usePanZoom-Bq73pwFe.d.ts +0 -117
- 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-SBXNQW4G.js +0 -3
- 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 +0 -14568
- 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
- package/src/state/Persistence.stories.tsx +0 -75
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<TrialOverview>` — the whole of a trial's content at a glance, in a panel
|
|
3
|
+
* that floats over the stage: the visible rect, a crosshair where the pointer
|
|
4
|
+
* is on the stage, and a press or drag that moves the stage's camera there.
|
|
5
|
+
*
|
|
6
|
+
* It is the labkit form of core's minimap contribution: the same
|
|
7
|
+
* `minimap.center` / `minimap.pan` actions, run on a dispatcher of its own with
|
|
8
|
+
* the trial's camera as `rootView`, and the same crosshair geometry.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
ActionsProvider,
|
|
12
|
+
ActiveToolContextProvider,
|
|
13
|
+
crosshairRects,
|
|
14
|
+
DepRegistryProvider,
|
|
15
|
+
MINIMAP_CENTER,
|
|
16
|
+
MINIMAP_PAN,
|
|
17
|
+
minimapCenterAction,
|
|
18
|
+
minimapPanAction,
|
|
19
|
+
type Tool,
|
|
20
|
+
useActionsRegistry,
|
|
21
|
+
useDepSource,
|
|
22
|
+
useGestureDispatcher,
|
|
23
|
+
usePointerPosition,
|
|
24
|
+
type View,
|
|
25
|
+
type ViewApi,
|
|
26
|
+
} from '@weasel-js/core';
|
|
27
|
+
import {
|
|
28
|
+
type CSSProperties,
|
|
29
|
+
type ReactNode,
|
|
30
|
+
type RefObject,
|
|
31
|
+
useContext,
|
|
32
|
+
useEffect,
|
|
33
|
+
useLayoutEffect,
|
|
34
|
+
useMemo,
|
|
35
|
+
useRef,
|
|
36
|
+
} from 'react';
|
|
37
|
+
import {
|
|
38
|
+
CameraContext,
|
|
39
|
+
type CameraContextValue,
|
|
40
|
+
pointerElsewhere,
|
|
41
|
+
usePublishPointer,
|
|
42
|
+
} from '../canvas/CameraInput';
|
|
43
|
+
import { CanvasStackContext } from '../canvas/CanvasStackContext';
|
|
44
|
+
import { fromCameraView } from '../canvas/cameraView';
|
|
45
|
+
import type { ViewportSize, WorldFrame } from '../canvas/worldSpec';
|
|
46
|
+
import { FloatingPanel, type FloatingPanelProps } from '../primitives/FloatingPanel';
|
|
47
|
+
import { OverviewMarks } from './OverviewMarks';
|
|
48
|
+
|
|
49
|
+
/** The view id the overview publishes its pointer under. */
|
|
50
|
+
export const OVERVIEW_VIEW_ID = 'overview';
|
|
51
|
+
|
|
52
|
+
/** A rect in the instrument's world. */
|
|
53
|
+
export interface OverviewBounds {
|
|
54
|
+
x: number;
|
|
55
|
+
y: number;
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** What an overview's `render` is handed. */
|
|
61
|
+
export interface OverviewRenderArgs {
|
|
62
|
+
/** The content's own size in CSS pixels, at zoom 1 — lay out at this size
|
|
63
|
+
* and the overview scales it to fit. */
|
|
64
|
+
size: ViewportSize;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Props for `<TrialOverview>`. */
|
|
68
|
+
export interface TrialOverviewProps {
|
|
69
|
+
/** The overview box, in CSS pixels. */
|
|
70
|
+
width: number;
|
|
71
|
+
height: number;
|
|
72
|
+
/**
|
|
73
|
+
* DOM content for the overview, laid out at the content's own size — for a
|
|
74
|
+
* `stage` instrument, typically a lighter copy of what its `render` draws.
|
|
75
|
+
* Never the instrument's own `render`: its effects would run twice. A canvas
|
|
76
|
+
* instrument omits it, and the overview redraws the canvas's layers.
|
|
77
|
+
*/
|
|
78
|
+
render?: (args: OverviewRenderArgs) => ReactNode;
|
|
79
|
+
/** What to frame, in the instrument's world. Defaults to a stage's content
|
|
80
|
+
* rect; a canvas instrument, whose world has no edge, names one. */
|
|
81
|
+
bounds?: OverviewBounds;
|
|
82
|
+
/** Panel title. */
|
|
83
|
+
title?: string;
|
|
84
|
+
anchor?: FloatingPanelProps['anchor'];
|
|
85
|
+
persist?: string;
|
|
86
|
+
className?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const PADDING = 6;
|
|
90
|
+
const OVERVIEW_INPUT: Tool = {
|
|
91
|
+
id: 'overview',
|
|
92
|
+
eligibility: { always: true },
|
|
93
|
+
bindings: [
|
|
94
|
+
{ spec: { kind: 'pointerDown' }, actionId: MINIMAP_CENTER },
|
|
95
|
+
{ spec: { kind: 'drag' }, actionId: MINIMAP_PAN },
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
const TOOLS_BY_ID: ReadonlyMap<string, Tool> = new Map([[OVERVIEW_INPUT.id, OVERVIEW_INPUT]]);
|
|
99
|
+
const CHANNELS = { wheel: false, pinch: false, contextMenu: false, ingest: false } as const;
|
|
100
|
+
|
|
101
|
+
/** A world rect as a frame-local one. */
|
|
102
|
+
function toLocalRect(b: OverviewBounds, frame: WorldFrame): OverviewBounds {
|
|
103
|
+
if (frame.yDir === 1) return b;
|
|
104
|
+
return { x: b.x, y: -(b.y + b.height), width: b.width, height: b.height };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The camera that fits `rect` into `size`, `PADDING` in from every edge. */
|
|
108
|
+
function fitRect(rect: OverviewBounds, size: ViewportSize): View {
|
|
109
|
+
const s = Math.min(
|
|
110
|
+
Math.max(1, size.width - 2 * PADDING) / Math.max(rect.width, 1e-9),
|
|
111
|
+
Math.max(1, size.height - 2 * PADDING) / Math.max(rect.height, 1e-9),
|
|
112
|
+
);
|
|
113
|
+
return {
|
|
114
|
+
x: rect.x + rect.width / 2 - size.width / (2 * s),
|
|
115
|
+
y: rect.y + rect.height / 2 - size.height / (2 * s),
|
|
116
|
+
scale: { x: s, y: s },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A canvas's 2D context sized to `size` at the device pixel ratio. */
|
|
121
|
+
function prepare(
|
|
122
|
+
canvas: HTMLCanvasElement | null,
|
|
123
|
+
size: ViewportSize,
|
|
124
|
+
): CanvasRenderingContext2D | null {
|
|
125
|
+
const ctx = canvas?.getContext('2d') ?? null;
|
|
126
|
+
if (!canvas || !ctx) return null;
|
|
127
|
+
const dpr = window.devicePixelRatio || 1;
|
|
128
|
+
const w = Math.round(size.width * dpr);
|
|
129
|
+
const h = Math.round(size.height * dpr);
|
|
130
|
+
if (canvas.width !== w) canvas.width = w;
|
|
131
|
+
if (canvas.height !== h) canvas.height = h;
|
|
132
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
133
|
+
ctx.clearRect(0, 0, size.width, size.height);
|
|
134
|
+
return ctx;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function cssVar(el: Element | null, name: string, fallback: string): string {
|
|
138
|
+
if (!el) return fallback;
|
|
139
|
+
return getComputedStyle(el).getPropertyValue(name).trim() || fallback;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A trial's overview. Mount it from an instrument's `render` (a canvas
|
|
144
|
+
* instrument) or `stage.overlay` (a DOM stage) — anywhere inside the trial's
|
|
145
|
+
* camera.
|
|
146
|
+
*/
|
|
147
|
+
export function TrialOverview(props: TrialOverviewProps) {
|
|
148
|
+
const camera = useContext(CameraContext);
|
|
149
|
+
if (!camera) return null;
|
|
150
|
+
return <Overview {...props} camera={camera} />;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function Overview({
|
|
154
|
+
width,
|
|
155
|
+
height,
|
|
156
|
+
render,
|
|
157
|
+
bounds,
|
|
158
|
+
title = 'Overview',
|
|
159
|
+
anchor = 'bottom-left',
|
|
160
|
+
persist,
|
|
161
|
+
className,
|
|
162
|
+
camera,
|
|
163
|
+
}: TrialOverviewProps & { camera: CameraContextValue }) {
|
|
164
|
+
const size = useMemo(() => ({ width, height }), [width, height]);
|
|
165
|
+
const content = bounds ? toLocalRect(bounds, camera.frame) : camera.content;
|
|
166
|
+
// `content` is rebuilt every render, so the fit keys on its numbers.
|
|
167
|
+
const framed = content !== null && content !== undefined;
|
|
168
|
+
const { x = 0, y = 0, width: w = 0, height: h = 0 } = content ?? {};
|
|
169
|
+
const fit = useMemo(
|
|
170
|
+
() =>
|
|
171
|
+
framed ? fitRect({ x, y, width: w, height: h }, size) : { x: 0, y: 0, scale: { x: 1, y: 1 } },
|
|
172
|
+
[framed, x, y, w, h, size],
|
|
173
|
+
);
|
|
174
|
+
const boxRef = useRef<HTMLDivElement | null>(null);
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<FloatingPanel
|
|
178
|
+
anchor={anchor}
|
|
179
|
+
className={className ? `lk-overview ${className}` : 'lk-overview'}
|
|
180
|
+
{...(persist ? { persist } : {})}
|
|
181
|
+
>
|
|
182
|
+
<div className="lk-overview__title">{title}</div>
|
|
183
|
+
<div ref={boxRef} className="lk-overview__box" data-no-drag style={boxVars(size)}>
|
|
184
|
+
{render && content ? (
|
|
185
|
+
<div className="lk-overview__content" style={contentVars(fit, content)}>
|
|
186
|
+
{render({ size: { width: content.width, height: content.height } })}
|
|
187
|
+
</div>
|
|
188
|
+
) : null}
|
|
189
|
+
{render ? null : <LayersCanvas fit={fit} size={size} frame={camera.frame} />}
|
|
190
|
+
<OverviewMarks fit={fit} camera={camera} />
|
|
191
|
+
<ChromeCanvas fit={fit} size={size} camera={camera} />
|
|
192
|
+
<OverviewInput boxRef={boxRef} fit={fit} camera={camera} />
|
|
193
|
+
</div>
|
|
194
|
+
</FloatingPanel>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// The box's size and the content's camera change with props, and a transform
|
|
199
|
+
// has nowhere to live but the element's own style: set as custom properties
|
|
200
|
+
// the stylesheet reads, as `<Stage>` does.
|
|
201
|
+
function boxVars(size: ViewportSize): CSSProperties {
|
|
202
|
+
return {
|
|
203
|
+
['--lk-overview-w' as string]: `${size.width}px`,
|
|
204
|
+
['--lk-overview-h' as string]: `${size.height}px`,
|
|
205
|
+
} as CSSProperties;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function contentVars(fit: View, content: OverviewBounds): CSSProperties {
|
|
209
|
+
const s = fit.scale.x;
|
|
210
|
+
return {
|
|
211
|
+
['--lk-overview-x' as string]: `${(content.x - fit.x) * s}px`,
|
|
212
|
+
['--lk-overview-y' as string]: `${(content.y - fit.y) * s}px`,
|
|
213
|
+
['--lk-overview-zoom' as string]: String(s),
|
|
214
|
+
['--lk-overview-cw' as string]: `${content.width}px`,
|
|
215
|
+
['--lk-overview-ch' as string]: `${content.height}px`,
|
|
216
|
+
} as CSSProperties;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** A canvas instrument's layers, redrawn through the fit camera. */
|
|
220
|
+
function LayersCanvas({ fit, size, frame }: { fit: View; size: ViewportSize; frame: WorldFrame }) {
|
|
221
|
+
const stack = useContext(CanvasStackContext);
|
|
222
|
+
const ref = useRef<HTMLCanvasElement | null>(null);
|
|
223
|
+
const layers = stack?.surface?.layers;
|
|
224
|
+
useLayoutEffect(() => {
|
|
225
|
+
const ctx = prepare(ref.current, size);
|
|
226
|
+
if (!ctx || !layers) return;
|
|
227
|
+
const local: WorldFrame = { originPx: { x: 0, y: 0 }, yDir: frame.yDir };
|
|
228
|
+
const view = fromCameraView(fit, local);
|
|
229
|
+
for (const layer of layers) {
|
|
230
|
+
if (!layer.visible || layer.id.startsWith('__lk_')) continue;
|
|
231
|
+
ctx.save();
|
|
232
|
+
layer.render(ctx, view, local);
|
|
233
|
+
ctx.restore();
|
|
234
|
+
}
|
|
235
|
+
}, [layers, fit, size, frame.yDir]);
|
|
236
|
+
return <canvas ref={ref} className="lk-overview__canvas" />;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The visible-rect indicator and the linked crosshair. */
|
|
240
|
+
function ChromeCanvas({
|
|
241
|
+
fit,
|
|
242
|
+
size,
|
|
243
|
+
camera,
|
|
244
|
+
}: {
|
|
245
|
+
fit: View;
|
|
246
|
+
size: ViewportSize;
|
|
247
|
+
camera: CameraContextValue;
|
|
248
|
+
}) {
|
|
249
|
+
const ref = useRef<HTMLCanvasElement | null>(null);
|
|
250
|
+
const pointer = usePointerPosition();
|
|
251
|
+
const root = camera.view.get();
|
|
252
|
+
const host = camera.view.hostSize?.() ?? null;
|
|
253
|
+
// No dependency list: this renders only when the pointer or the trial's
|
|
254
|
+
// camera moved, and either one moves the chrome.
|
|
255
|
+
useLayoutEffect(() => {
|
|
256
|
+
const ctx = prepare(ref.current, size);
|
|
257
|
+
if (!ctx) return;
|
|
258
|
+
const s = fit.scale.x;
|
|
259
|
+
const px = (x: number, y: number) => ({ x: (x - fit.x) * s, y: (y - fit.y) * s });
|
|
260
|
+
if (host) {
|
|
261
|
+
const a = px(root.x, root.y);
|
|
262
|
+
ctx.strokeStyle = cssVar(ref.current, '--wzl-fg', '#ffffff');
|
|
263
|
+
ctx.lineWidth = 1;
|
|
264
|
+
ctx.setLineDash([2, 3]);
|
|
265
|
+
ctx.strokeRect(
|
|
266
|
+
a.x + 0.5,
|
|
267
|
+
a.y + 0.5,
|
|
268
|
+
(host.width / root.scale.x) * s,
|
|
269
|
+
(host.height / root.scale.y) * s,
|
|
270
|
+
);
|
|
271
|
+
ctx.setLineDash([]);
|
|
272
|
+
}
|
|
273
|
+
const world = pointerElsewhere(pointer, OVERVIEW_VIEW_ID, camera.frame);
|
|
274
|
+
if (world) {
|
|
275
|
+
// Back to frame-local: the map is its own inverse.
|
|
276
|
+
const at = px(world.x, world.y * camera.frame.yDir);
|
|
277
|
+
const { halo, bars } = crosshairRects(at.x, at.y, 1);
|
|
278
|
+
ctx.fillStyle = 'rgb(0 0 0 / 60%)';
|
|
279
|
+
for (const r of halo) ctx.fillRect(r.x, r.y, r.w, r.h);
|
|
280
|
+
ctx.fillStyle = cssVar(ref.current, '--wzl-accent', '#4c8dff');
|
|
281
|
+
for (const r of bars) ctx.fillRect(r.x, r.y, r.w, r.h);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
return <canvas ref={ref} className="lk-overview__canvas lk-overview__chrome" />;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The overview's input, on a dispatcher of its own: its actions and deps are
|
|
289
|
+
* isolated from the trial's, its pointer store is shared with it.
|
|
290
|
+
*/
|
|
291
|
+
function OverviewInput(props: {
|
|
292
|
+
boxRef: RefObject<HTMLDivElement | null>;
|
|
293
|
+
fit: View;
|
|
294
|
+
camera: CameraContextValue;
|
|
295
|
+
}) {
|
|
296
|
+
return (
|
|
297
|
+
<DepRegistryProvider>
|
|
298
|
+
<ActionsProvider>
|
|
299
|
+
<ActiveToolContextProvider>
|
|
300
|
+
<OverviewDispatch {...props} />
|
|
301
|
+
</ActiveToolContextProvider>
|
|
302
|
+
</ActionsProvider>
|
|
303
|
+
</DepRegistryProvider>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function OverviewDispatch({
|
|
308
|
+
boxRef,
|
|
309
|
+
fit,
|
|
310
|
+
camera,
|
|
311
|
+
}: {
|
|
312
|
+
boxRef: RefObject<HTMLDivElement | null>;
|
|
313
|
+
fit: View;
|
|
314
|
+
camera: CameraContextValue;
|
|
315
|
+
}) {
|
|
316
|
+
const registry = useActionsRegistry();
|
|
317
|
+
const fitRef = useRef(fit);
|
|
318
|
+
fitRef.current = fit;
|
|
319
|
+
const root = camera.view;
|
|
320
|
+
useDepSource('rootView', () => root as ViewApi);
|
|
321
|
+
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
if (!registry) return;
|
|
324
|
+
const offs = [registry.register(minimapCenterAction()), registry.register(minimapPanAction())];
|
|
325
|
+
return () => {
|
|
326
|
+
for (const off of offs) off();
|
|
327
|
+
};
|
|
328
|
+
}, [registry]);
|
|
329
|
+
|
|
330
|
+
const clientToLocal = useMemo(
|
|
331
|
+
() => (cx: number, cy: number) => {
|
|
332
|
+
const r = boxRef.current?.getBoundingClientRect();
|
|
333
|
+
const f = fitRef.current;
|
|
334
|
+
return {
|
|
335
|
+
x: (cx - (r?.left ?? 0)) / f.scale.x + f.x,
|
|
336
|
+
y: (cy - (r?.top ?? 0)) / f.scale.y + f.y,
|
|
337
|
+
};
|
|
338
|
+
},
|
|
339
|
+
[boxRef],
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
useGestureDispatcher({
|
|
343
|
+
canvasRef: boxRef,
|
|
344
|
+
// Always present: `OverviewInput` mounts the provider directly above.
|
|
345
|
+
actions: registry as NonNullable<typeof registry>,
|
|
346
|
+
entriesById: TOOLS_BY_ID,
|
|
347
|
+
clientToWorld: clientToLocal,
|
|
348
|
+
keyboard: false,
|
|
349
|
+
channels: CHANNELS,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
usePublishPointer(boxRef, clientToLocal, camera.frame, OVERVIEW_VIEW_ID);
|
|
353
|
+
|
|
354
|
+
// The box sits inside the stage's element, whose own dispatcher, loupe and
|
|
355
|
+
// pointer publisher would otherwise take every event that bubbles out of it.
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
const el = boxRef.current;
|
|
358
|
+
if (!el) return;
|
|
359
|
+
const stop = (e: Event): void => e.stopPropagation();
|
|
360
|
+
const kinds = [
|
|
361
|
+
'pointerdown',
|
|
362
|
+
'pointermove',
|
|
363
|
+
'pointerup',
|
|
364
|
+
'click',
|
|
365
|
+
'wheel',
|
|
366
|
+
'dblclick',
|
|
367
|
+
'contextmenu',
|
|
368
|
+
];
|
|
369
|
+
for (const k of kinds) el.addEventListener(k, stop);
|
|
370
|
+
return () => {
|
|
371
|
+
for (const k of kinds) el.removeEventListener(k, stop);
|
|
372
|
+
};
|
|
373
|
+
}, [boxRef]);
|
|
374
|
+
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
paintGradientTrack,
|
|
21
21
|
RadioGroup,
|
|
22
22
|
RangeSlider,
|
|
23
|
+
sampleByInterpolation,
|
|
23
24
|
Select,
|
|
24
25
|
Sidebar,
|
|
25
26
|
Slider,
|
|
@@ -38,6 +39,7 @@ describe('weasel-ui passthrough', () => {
|
|
|
38
39
|
expect(typeof useReorderDragList).toBe('function');
|
|
39
40
|
expect(typeof paintGradientTrack).toBe('function');
|
|
40
41
|
expect(typeof oklchToHex).toBe('function');
|
|
42
|
+
expect(typeof sampleByInterpolation).toBe('function');
|
|
41
43
|
expect(typeof chromaAt).toBe('function');
|
|
42
44
|
});
|
|
43
45
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* future without consumer churn.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
export { formatNumber, MINUS_SIGN } from '@weasel-js/quantity';
|
|
8
9
|
export {
|
|
9
10
|
ActionBar,
|
|
10
11
|
type ActionBarProps,
|
|
@@ -21,6 +22,7 @@ export {
|
|
|
21
22
|
type BadgeVariant,
|
|
22
23
|
type BoundsCtx,
|
|
23
24
|
type BuiltInEdgeName,
|
|
25
|
+
type BuiltInProfileName,
|
|
24
26
|
type BuiltinPref,
|
|
25
27
|
Button,
|
|
26
28
|
ButtonBar,
|
|
@@ -38,6 +40,9 @@ export {
|
|
|
38
40
|
type CheckboxProps,
|
|
39
41
|
type ChromaCurve,
|
|
40
42
|
type ChromaCurvePoint,
|
|
43
|
+
CloseButton,
|
|
44
|
+
type CloseButtonProps,
|
|
45
|
+
COLOR_MODE_OPTIONS,
|
|
41
46
|
ColorModeControl,
|
|
42
47
|
type ColorModeControlProps,
|
|
43
48
|
ComboBox,
|
|
@@ -65,8 +70,8 @@ export {
|
|
|
65
70
|
type DisclosureRowProps,
|
|
66
71
|
DragGhost,
|
|
67
72
|
type DragGhostProps,
|
|
68
|
-
|
|
69
|
-
type
|
|
73
|
+
DragGrip,
|
|
74
|
+
type DragGripProps,
|
|
70
75
|
detectPlatform,
|
|
71
76
|
dlog,
|
|
72
77
|
EDGE_PROFILES,
|
|
@@ -78,11 +83,13 @@ export {
|
|
|
78
83
|
type FieldProps,
|
|
79
84
|
type FillSettings,
|
|
80
85
|
fieldClasses,
|
|
81
|
-
formatNumber,
|
|
82
86
|
formatShortcut,
|
|
83
87
|
formatShortcutParts,
|
|
88
|
+
GestureRoute,
|
|
89
|
+
type GestureRouteProps,
|
|
84
90
|
type GradientTrackOpts,
|
|
85
91
|
type GridSettings,
|
|
92
|
+
gestureRouteSegments,
|
|
86
93
|
ICON_PATHS,
|
|
87
94
|
Icon,
|
|
88
95
|
type IconName,
|
|
@@ -98,6 +105,7 @@ export {
|
|
|
98
105
|
type KeyCapVariant,
|
|
99
106
|
type KeycapKind,
|
|
100
107
|
KeySequence,
|
|
108
|
+
type KeySequenceJoins,
|
|
101
109
|
type KeySequenceProps,
|
|
102
110
|
type KeySpec,
|
|
103
111
|
keyGlyph,
|
|
@@ -115,7 +123,6 @@ export {
|
|
|
115
123
|
MenuButton,
|
|
116
124
|
type MenuButtonItem,
|
|
117
125
|
type MenuButtonProps,
|
|
118
|
-
MINUS_SIGN,
|
|
119
126
|
moveLayers,
|
|
120
127
|
NumberField,
|
|
121
128
|
type NumberFieldProps,
|
|
@@ -124,6 +131,7 @@ export {
|
|
|
124
131
|
type OptionsBarProps,
|
|
125
132
|
type OptionsBarSize,
|
|
126
133
|
type OptionsBarVariant,
|
|
134
|
+
type OverlayPortalProps,
|
|
127
135
|
oklchToHex,
|
|
128
136
|
type Platform,
|
|
129
137
|
Plot2D,
|
|
@@ -137,6 +145,7 @@ export {
|
|
|
137
145
|
type PowerlineSegment,
|
|
138
146
|
type PrefBoolean,
|
|
139
147
|
type PrefBooleanControl,
|
|
148
|
+
type PrefBooleanEncoding,
|
|
140
149
|
type PrefColor,
|
|
141
150
|
type PrefCustom,
|
|
142
151
|
type PrefEnum,
|
|
@@ -159,6 +168,9 @@ export {
|
|
|
159
168
|
PrefsForm,
|
|
160
169
|
type PrefsFormProps,
|
|
161
170
|
type PrefsLayout,
|
|
171
|
+
type PropertyAlign,
|
|
172
|
+
type PropertyDensity,
|
|
173
|
+
type PropertyMetricProps,
|
|
162
174
|
paintGradientTrack,
|
|
163
175
|
prefValueAtPath,
|
|
164
176
|
Radio,
|
|
@@ -172,6 +184,7 @@ export {
|
|
|
172
184
|
type ReorderGhost,
|
|
173
185
|
type RovingItem,
|
|
174
186
|
type RovingTabIndex,
|
|
187
|
+
type SegmentTooltipFields,
|
|
175
188
|
Select,
|
|
176
189
|
SelectItem,
|
|
177
190
|
type SelectItemProps,
|
|
@@ -183,8 +196,10 @@ export {
|
|
|
183
196
|
type SidebarProps,
|
|
184
197
|
Slider,
|
|
185
198
|
type SliderProps,
|
|
199
|
+
type StanceProps,
|
|
186
200
|
Switch,
|
|
187
201
|
type SwitchProps,
|
|
202
|
+
sampleByInterpolation,
|
|
188
203
|
Tab,
|
|
189
204
|
TabList,
|
|
190
205
|
type TabListProps,
|
|
@@ -193,6 +208,9 @@ export {
|
|
|
193
208
|
type TabProps,
|
|
194
209
|
Tabs,
|
|
195
210
|
type TabsProps,
|
|
211
|
+
ThemeSwitcher,
|
|
212
|
+
type ThemeSwitcherOption,
|
|
213
|
+
type ThemeSwitcherProps,
|
|
196
214
|
type Thumb,
|
|
197
215
|
type ThumbRenderCtx,
|
|
198
216
|
type ThumbShape,
|
|
@@ -215,4 +233,5 @@ export {
|
|
|
215
233
|
useReorderDragList,
|
|
216
234
|
useRovingTabIndex,
|
|
217
235
|
visiblePrefSubtree,
|
|
236
|
+
type WithoutPortalTarget,
|
|
218
237
|
} from '@weasel-js/ui';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { DRAG_THRESHOLD_PX } from '@weasel-js/core';
|
|
2
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
4
|
import { createMemoryAdapter } from '../state/adapters';
|
|
4
5
|
import { labPrefix } from '../state/labRecords';
|
|
@@ -130,6 +131,16 @@ describe('FloatingPanel dragging', () => {
|
|
|
130
131
|
expect(panel.dataset.dragging).toBeUndefined();
|
|
131
132
|
});
|
|
132
133
|
|
|
134
|
+
it('becomes a drag at the kit drag threshold, not before', () => {
|
|
135
|
+
const { panel } = renderPanel(<FloatingPanel>x</FloatingPanel>);
|
|
136
|
+
fireEvent.pointerDown(panel, { clientX: 100, clientY: 100 });
|
|
137
|
+
fireEvent.pointerMove(panel, { clientX: 100 + DRAG_THRESHOLD_PX - 0.5, clientY: 100 });
|
|
138
|
+
expect(panel.dataset.dragging).toBeUndefined();
|
|
139
|
+
fireEvent.pointerMove(panel, { clientX: 100 + DRAG_THRESHOLD_PX, clientY: 100 });
|
|
140
|
+
expect(panel.dataset.dragging).toBe('true');
|
|
141
|
+
fireEvent.pointerUp(panel);
|
|
142
|
+
});
|
|
143
|
+
|
|
133
144
|
it('a press that never moves is not a drag', () => {
|
|
134
145
|
const { panel } = renderPanel(<FloatingPanel anchor="top-left">x</FloatingPanel>);
|
|
135
146
|
const before = panel.style.left;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { openPointerSession, type PointerSession } from '@weasel-js/core';
|
|
1
|
+
import { openPointerSession, type PointerSession, pastDragThreshold } from '@weasel-js/core';
|
|
2
2
|
import type React from 'react';
|
|
3
3
|
import { type CSSProperties, type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import {
|
|
@@ -41,9 +41,6 @@ function borderBoxOf(entry: ResizeObserverEntry): { w: number; h: number } {
|
|
|
41
41
|
return { w: entry.contentRect.width, h: entry.contentRect.height };
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/** How far the pointer must travel before a press becomes a drag. */
|
|
45
|
-
const DRAG_THRESHOLD = 3;
|
|
46
|
-
|
|
47
44
|
/** A pointerdown on one of these is the child's, not a drag. */
|
|
48
45
|
function isInteractive(target: EventTarget | null): boolean {
|
|
49
46
|
if (!(target instanceof Element)) return false;
|
|
@@ -158,7 +155,7 @@ export function FloatingPanel({
|
|
|
158
155
|
{
|
|
159
156
|
onMove: (ev) => {
|
|
160
157
|
if (!dragging.current) {
|
|
161
|
-
if (
|
|
158
|
+
if (!pastDragThreshold({ clientX: start.x, clientY: start.y }, ev)) return;
|
|
162
159
|
dragging.current = { ...start };
|
|
163
160
|
setDragging(true);
|
|
164
161
|
origin.setPointerCapture?.(pointerId);
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
height: 100%;
|
|
19
19
|
background: var(--wzl-accent);
|
|
20
20
|
transform-origin: left center;
|
|
21
|
-
transition: transform
|
|
21
|
+
transition: transform var(--wzl-motion-fast) linear;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// A job that never reports a total sweeps instead of filling, so the bar
|