@weasel-js/labkit 1.7.0 → 1.7.2
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-D2jnIwfk.d.ts → CanvasStackContext-Cjy6I-Iv.d.ts} +4 -1
- package/dist/_dts/{Stage-CCWfQQWM.d.ts → Stage-D6t7o0pF.d.ts} +3 -2
- package/dist/_dts/{index-CsYqUHD3.d.ts → index-BTmaRy6a.d.ts} +10 -6
- package/dist/_dts/{types-CSm9jgol.d.ts → index-DCMA4Fy1.d.ts} +59 -113
- package/dist/_dts/{types-CRPSGFAp.d.ts → types-BUBSIZuV.d.ts} +8 -7
- package/dist/_dts/{useTrialState-CTZPgeZ7.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 +7 -5
- package/dist/chrome/index.js +10 -9
- 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-275JRRVD.js → chunk-E23Q6A3Y.js} +232 -37
- package/dist/chunk-E23Q6A3Y.js.map +1 -0
- package/dist/{chunk-GOZLNM4L.js → chunk-E6NARGU7.js} +5 -5
- package/dist/{chunk-GOZLNM4L.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-AWQG7FCC.js → chunk-FG3EXZDV.js} +3 -3
- package/dist/{chunk-AWQG7FCC.js.map → chunk-FG3EXZDV.js.map} +1 -1
- package/dist/{chunk-YT75VYRD.js → chunk-IBVRMHW5.js} +5 -5
- package/dist/{chunk-YT75VYRD.js.map → chunk-IBVRMHW5.js.map} +1 -1
- package/dist/{chunk-RWNDDKOH.js → chunk-PMLZP7RT.js} +18 -15
- package/dist/chunk-PMLZP7RT.js.map +1 -0
- package/dist/{chunk-Y23G2VZL.js → chunk-READTDQX.js} +2 -2
- package/dist/chunk-READTDQX.js.map +1 -0
- package/dist/{chunk-I5DUQ5HM.js → chunk-RODIY7HI.js} +75 -72
- package/dist/chunk-RODIY7HI.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-ZIFPHIWZ.js → chunk-T6DLGEJB.js} +12 -14
- package/dist/chunk-T6DLGEJB.js.map +1 -0
- package/dist/chunk-U6UQVGFP.js +15583 -0
- package/dist/chunk-U6UQVGFP.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-UNAZSLWE.js → chunk-VYVGVBGY.js} +17 -20
- package/dist/chunk-VYVGVBGY.js.map +1 -0
- package/dist/chunk-WFALF2X2.js +302 -0
- package/dist/chunk-WFALF2X2.js.map +1 -0
- package/dist/{chunk-KMJ7RWD3.js → chunk-WLMDTI2Q.js} +3 -3
- package/dist/{chunk-KMJ7RWD3.js.map → chunk-WLMDTI2Q.js.map} +1 -1
- package/dist/{chunk-T5IBSPAM.js → chunk-WWJDPNZZ.js} +43 -33
- package/dist/chunk-WWJDPNZZ.js.map +1 -0
- package/dist/{chunk-J7OWWTCW.js → chunk-XE5HJOPL.js} +5 -6
- package/dist/chunk-XE5HJOPL.js.map +1 -0
- package/dist/{chunk-7BO7HNGF.js → chunk-XRX5S5NT.js} +8 -61
- package/dist/chunk-XRX5S5NT.js.map +1 -0
- package/dist/chunk-XVUKDSYV.js +38 -0
- package/dist/chunk-XVUKDSYV.js.map +1 -0
- package/dist/{chunk-FMK7UQ6V.js → chunk-XZDCEMEL.js} +64 -47
- package/dist/chunk-XZDCEMEL.js.map +1 -0
- package/dist/{chunk-FZ7YFQN7.js → chunk-YCFKW5RX.js} +3 -3
- 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 +18 -10
- package/dist/config/index.js +4 -4
- package/dist/controls/index.d.ts +58 -9
- package/dist/controls/index.js +7 -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 +61 -715
- package/dist/index.js +456 -432
- 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 +15 -14
- 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 -2871
- package/dist/passthrough/weasel-ui.js +3 -2
- package/dist/primitives/index.d.ts +213 -3
- package/dist/primitives/index.js +8 -7
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +6 -6
- package/dist/styles.css +284 -51
- 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 +11 -11
- package/src/annotations/AnnotationOverlay.abandon.test.tsx +143 -0
- package/src/annotations/AnnotationOverlay.tsx +50 -31
- package/src/annotations/Annotations.overlay.test.tsx +31 -0
- package/src/annotations/capture.browser.test.ts +47 -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 +74 -33
- package/src/annotations/paint.ts +70 -32
- package/src/annotations/store.test.ts +212 -2
- package/src/annotations/store.ts +130 -27
- package/src/annotations/svgNodes.test.ts +22 -10
- package/src/annotations/svgNodes.ts +3 -3
- package/src/annotations/types.ts +29 -10
- package/src/canvas/CameraInput.test.tsx +30 -0
- package/src/canvas/CameraInput.tsx +16 -15
- package/src/canvas/CanvasStack.tsx +5 -5
- package/src/canvas/CanvasStackContext.ts +4 -1
- package/src/canvas/Stage.test.tsx +34 -25
- package/src/canvas/Stage.tsx +3 -3
- package/src/canvas/cameraRegistry.ts +1 -0
- package/src/canvas/worldSpec.ts +3 -1
- 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/labRegions.test.tsx +37 -26
- package/src/chrome/merge.test.ts +5 -0
- package/src/chrome/merge.ts +4 -2
- package/src/chrome/regions/PaletteRegion.test.tsx +4 -5
- package/src/chrome/regions/PaletteRegion.tsx +3 -4
- 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.ts +6 -0
- package/src/config/types.ts +2 -1
- package/src/controls/ControlMatrix.less +245 -0
- package/src/controls/ControlMatrix.stories.less +13 -0
- package/src/controls/ControlMatrix.stories.tsx +135 -0
- package/src/controls/ControlMatrix.test.tsx +206 -0
- package/src/controls/ControlMatrix.tsx +446 -0
- package/src/controls/ControlPanel.test.tsx +37 -3
- package/src/controls/ControlPanel.tsx +16 -5
- package/src/controls/index.ts +6 -0
- package/src/dragdrop/DragDropRuntime.tsx +22 -13
- package/src/dragdrop/DragGhost.tsx +3 -0
- package/src/dragdrop/Palette.tsx +2 -0
- package/src/dragdrop/dragDrop.test.tsx +34 -33
- package/src/index.test.ts +5 -0
- package/src/index.ts +7 -1
- package/src/instrument/types.ts +0 -6
- package/src/job/useJob.ts +14 -9
- package/src/lab/Lab.chrome.test.tsx +15 -10
- package/src/lab/Lab.nebula.test.tsx +5 -5
- package/src/lab/Lab.surface.test.tsx +30 -21
- package/src/lab/Lab.test.tsx +75 -74
- package/src/lab/Lab.tsx +5 -1
- package/src/lab/Lab.undock.test.tsx +15 -10
- package/src/lab/LabHeader.test.tsx +15 -14
- package/src/lab/LabPalette.test.tsx +12 -7
- package/src/lab/LabShell.less +1 -1
- package/src/lab/LabSwitcher.browser.test.tsx +27 -0
- package/src/lab/LabSwitcher.less +2 -0
- package/src/lab/LabZoom.test.tsx +55 -44
- package/src/lab/LabZoom.tsx +4 -4
- package/src/lab/Workspace.surface.test.tsx +16 -13
- package/src/lab/Workspace.test.tsx +32 -27
- package/src/lab/Workspace.tsx +48 -52
- 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/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.test.tsx +9 -8
- package/src/overview/OverviewMarks.tsx +12 -4
- package/src/overview/TrialOverview.test.tsx +18 -17
- package/src/overview/TrialOverview.tsx +3 -3
- package/src/passthrough/weasel-ui.test.ts +1 -1
- package/src/passthrough/weasel-ui.ts +1 -1
- package/src/primitives/FloatingPanel.tsx +7 -3
- package/src/primitives/FpsMeter.less +1 -2
- package/src/primitives/JobProgress.less +1 -1
- package/src/primitives/Readout.test.tsx +2 -2
- package/src/primitives/ScaleIndicator.less +1 -1
- package/src/primitives/Split.tsx +69 -41
- package/src/primitives/StatusBar.less +1 -1
- package/src/primitives/Toolbar.tsx +3 -3
- package/src/primitives/ZoomControl.less +1 -2
- package/src/specimen/Specimen.less +1 -0
- package/src/specimen/Specimen.tsx +0 -1
- package/src/state/undock.ts +3 -0
- package/src/state/useOpenOnce.ts +20 -16
- package/src/state/view.ts +1 -0
- package/src/styles.less +3 -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/trial/Trial.annotations.persist.test.tsx +20 -9
- package/src/trial/Trial.annotations.test.tsx +105 -27
- package/src/trial/Trial.canvas.test.tsx +21 -17
- package/src/trial/Trial.config.test.tsx +34 -27
- package/src/trial/Trial.less +0 -16
- package/src/trial/Trial.loupe.test.tsx +51 -25
- package/src/trial/Trial.targets.test.tsx +9 -8
- package/src/trial/Trial.test.tsx +51 -46
- package/src/trial/Trial.trialId.test.tsx +7 -6
- package/src/trial/Trial.tsx +74 -62
- package/src/trial/TrialChrome.tsx +6 -3
- package/src/trial/index.ts +2 -0
- package/src/trial/loupeSwitch.ts +15 -0
- package/dist/_dts/LayerList.d-DiuEArKq.d.ts +0 -83
- package/dist/_dts/PrefsRow.d-TOZ7BgD9.d.ts +0 -79
- package/dist/_dts/PropertyPanel.d-D-1paCbE.d.ts +0 -141
- package/dist/_dts/Select.d-Bensr_MT.d.ts +0 -579
- package/dist/_dts/index-B2Yj5aA0.d.ts +0 -274
- package/dist/chunk-275JRRVD.js.map +0 -1
- package/dist/chunk-4LJSC7MB.js +0 -143
- package/dist/chunk-4LJSC7MB.js.map +0 -1
- package/dist/chunk-7BO7HNGF.js.map +0 -1
- package/dist/chunk-CQLQPQ4P.js.map +0 -1
- package/dist/chunk-FMK7UQ6V.js.map +0 -1
- package/dist/chunk-FZ7YFQN7.js.map +0 -1
- package/dist/chunk-HXXTULDN.js.map +0 -1
- package/dist/chunk-I5DUQ5HM.js.map +0 -1
- package/dist/chunk-IJJUK4FU.js.map +0 -1
- package/dist/chunk-J7OWWTCW.js.map +0 -1
- package/dist/chunk-M7HP3FTK.js +0 -549
- package/dist/chunk-M7HP3FTK.js.map +0 -1
- package/dist/chunk-MIM5R4ZT.js +0 -14972
- package/dist/chunk-MIM5R4ZT.js.map +0 -1
- package/dist/chunk-RWNDDKOH.js.map +0 -1
- package/dist/chunk-T5IBSPAM.js.map +0 -1
- package/dist/chunk-UNAZSLWE.js.map +0 -1
- package/dist/chunk-WPLAANH4.js.map +0 -1
- package/dist/chunk-Y23G2VZL.js.map +0 -1
- package/dist/chunk-ZIFPHIWZ.js.map +0 -1
- package/src/primitives/useRovingTabIndex.test.ts +0 -48
- package/src/primitives/useRovingTabIndex.ts +0 -88
package/src/lab/LabZoom.test.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { act, fireEvent,
|
|
1
|
+
import { act, fireEvent, screen, within } from '@testing-library/react';
|
|
2
2
|
import {
|
|
3
3
|
ActionsProvider,
|
|
4
4
|
type ActionsRegistry,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useActionsRegistry,
|
|
9
9
|
useGestureDispatcher,
|
|
10
10
|
} from '@weasel-js/core';
|
|
11
|
+
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
11
12
|
import { type ReactNode, useEffect } from 'react';
|
|
12
13
|
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
13
14
|
import { defineInstrument } from '../instrument/defineInstrument';
|
|
@@ -54,16 +55,16 @@ function Capture({ children }: { children?: ReactNode }) {
|
|
|
54
55
|
const zoomBar = () => within(screen.getByRole('toolbar', { name: 'Zoom' }));
|
|
55
56
|
const readout = () => zoomBar().getByRole('button', { name: /^Zoom \d+%|^Zoom unavailable/ });
|
|
56
57
|
const press = (name: string) =>
|
|
57
|
-
act(() => {
|
|
58
|
+
act(async () => {
|
|
58
59
|
fireEvent.click(zoomBar().getByRole('button', { name }));
|
|
59
60
|
});
|
|
60
61
|
|
|
61
62
|
const MOD = /mac/i.test(navigator.platform || navigator.userAgent)
|
|
62
63
|
? { metaKey: true }
|
|
63
64
|
: { ctrlKey: true };
|
|
64
|
-
function key(k: string): boolean {
|
|
65
|
+
async function key(k: string): Promise<boolean> {
|
|
65
66
|
let prevented = false;
|
|
66
|
-
act(() => {
|
|
67
|
+
await act(async () => {
|
|
67
68
|
prevented = !fireEvent.keyDown(window, { key: k, ...MOD });
|
|
68
69
|
});
|
|
69
70
|
return prevented;
|
|
@@ -111,113 +112,123 @@ function stageZoom(container: HTMLElement, index = 0): number {
|
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
describe("the lab header's zoom controls", () => {
|
|
114
|
-
it('step the focused trial through its camera and read its zoom back', () => {
|
|
115
|
-
const { container } =
|
|
115
|
+
it('step the focused trial through its camera and read its zoom back', async () => {
|
|
116
|
+
const { container } = await renderSettled(
|
|
117
|
+
<Lab instruments={[staged]} defaultInstrument="Staged" />,
|
|
118
|
+
);
|
|
116
119
|
expect(readout()).toHaveTextContent('100%');
|
|
117
|
-
press('Zoom in');
|
|
120
|
+
await press('Zoom in');
|
|
118
121
|
expect(stageZoom(container)).toBeCloseTo(1.25);
|
|
119
122
|
expect(readout()).toHaveTextContent('125%');
|
|
120
|
-
press('Zoom out');
|
|
123
|
+
await press('Zoom out');
|
|
121
124
|
expect(stageZoom(container)).toBeCloseTo(1);
|
|
122
|
-
press('Zoom out');
|
|
125
|
+
await press('Zoom out');
|
|
123
126
|
expect(readout()).toHaveTextContent('80%');
|
|
124
127
|
});
|
|
125
128
|
|
|
126
|
-
it('reset to actual size from the readout', () => {
|
|
127
|
-
const { container } =
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
it('reset to actual size from the readout', async () => {
|
|
130
|
+
const { container } = await renderSettled(
|
|
131
|
+
<Lab instruments={[staged]} defaultInstrument="Staged" />,
|
|
132
|
+
);
|
|
133
|
+
await press('Zoom out');
|
|
134
|
+
await press('Zoom out');
|
|
135
|
+
await act(async () => {
|
|
131
136
|
fireEvent.click(readout());
|
|
132
137
|
});
|
|
133
138
|
expect(stageZoom(container)).toBe(1);
|
|
134
139
|
expect(readout()).toHaveTextContent('100%');
|
|
135
140
|
});
|
|
136
141
|
|
|
137
|
-
it("stop at the camera's own limit, and say so", () => {
|
|
138
|
-
const { container } =
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
it("stop at the camera's own limit, and say so", async () => {
|
|
143
|
+
const { container } = await renderSettled(
|
|
144
|
+
<Lab instruments={[staged]} defaultInstrument="Staged" />,
|
|
145
|
+
);
|
|
146
|
+
await press('Zoom in');
|
|
147
|
+
await press('Zoom in');
|
|
141
148
|
expect(stageZoom(container)).toBe(1.5);
|
|
142
149
|
expect(zoomBar().getByRole('button', { name: 'Zoom in' })).toBeDisabled();
|
|
143
150
|
expect(zoomBar().getByRole('button', { name: 'Zoom out' })).toBeEnabled();
|
|
144
151
|
});
|
|
145
152
|
|
|
146
|
-
it('answer Mod+=, Mod+- and Mod+0, claiming the key from the browser', () => {
|
|
147
|
-
const { container } =
|
|
148
|
-
|
|
153
|
+
it('answer Mod+=, Mod+- and Mod+0, claiming the key from the browser', async () => {
|
|
154
|
+
const { container } = await renderSettled(
|
|
155
|
+
<Lab instruments={[staged]} defaultInstrument="Staged" />,
|
|
156
|
+
);
|
|
157
|
+
expect(await key('=')).toBe(true);
|
|
149
158
|
expect(stageZoom(container)).toBeCloseTo(1.25);
|
|
150
|
-
key('-');
|
|
151
|
-
key('-');
|
|
159
|
+
await key('-');
|
|
160
|
+
await key('-');
|
|
152
161
|
expect(stageZoom(container)).toBeCloseTo(0.8);
|
|
153
|
-
key('0');
|
|
162
|
+
await key('0');
|
|
154
163
|
expect(stageZoom(container)).toBe(1);
|
|
155
164
|
});
|
|
156
165
|
|
|
157
|
-
it('go inert over a trial with no camera, and leave its keys to the browser', () => {
|
|
158
|
-
|
|
166
|
+
it('go inert over a trial with no camera, and leave its keys to the browser', async () => {
|
|
167
|
+
await renderSettled(<Lab instruments={[plain]} defaultInstrument="Plain" />);
|
|
159
168
|
for (const name of ['Zoom out', 'Zoom in']) {
|
|
160
169
|
expect(zoomBar().getByRole('button', { name })).toBeDisabled();
|
|
161
170
|
}
|
|
162
171
|
expect(readout()).toBeDisabled();
|
|
163
172
|
expect(readout()).toHaveTextContent('–');
|
|
164
|
-
expect(key('=')).toBe(false);
|
|
173
|
+
expect(await key('=')).toBe(false);
|
|
165
174
|
});
|
|
166
175
|
|
|
167
|
-
it("take the key ahead of a story's own binding in a trial with a camera", () => {
|
|
176
|
+
it("take the key ahead of a story's own binding in a trial with a camera", async () => {
|
|
168
177
|
const story = vi.fn();
|
|
169
178
|
const zooming = defineInstrument<Record<string, never>, Record<string, never>>({
|
|
170
179
|
...staged,
|
|
171
180
|
name: 'Zooming',
|
|
172
181
|
render: () => <StoryZoomKey run={story} />,
|
|
173
182
|
});
|
|
174
|
-
const { container } =
|
|
175
|
-
|
|
183
|
+
const { container } = await renderSettled(
|
|
184
|
+
<Lab instruments={[zooming]} defaultInstrument="Zooming" />,
|
|
185
|
+
);
|
|
186
|
+
await key('=');
|
|
176
187
|
expect(stageZoom(container)).toBeCloseTo(1.25);
|
|
177
188
|
expect(story).not.toHaveBeenCalled();
|
|
178
189
|
});
|
|
179
190
|
|
|
180
|
-
it("leave the key to a story's own binding in a trial with no camera", () => {
|
|
191
|
+
it("leave the key to a story's own binding in a trial with no camera", async () => {
|
|
181
192
|
const story = vi.fn();
|
|
182
193
|
const zooming = defineInstrument<Record<string, never>, Record<string, never>>({
|
|
183
194
|
...plain,
|
|
184
195
|
name: 'ZoomingPlain',
|
|
185
196
|
render: () => <StoryZoomKey run={story} />,
|
|
186
197
|
});
|
|
187
|
-
|
|
188
|
-
key('=');
|
|
198
|
+
await renderSettled(<Lab instruments={[zooming]} defaultInstrument="ZoomingPlain" />);
|
|
199
|
+
await key('=');
|
|
189
200
|
expect(story).toHaveBeenCalledTimes(1);
|
|
190
201
|
});
|
|
191
202
|
|
|
192
|
-
it('follow the focus from trial to trial', () => {
|
|
193
|
-
const { container } =
|
|
203
|
+
it('follow the focus from trial to trial', async () => {
|
|
204
|
+
const { container } = await renderSettled(
|
|
194
205
|
<Lab instruments={[staged, plain]} defaultInstrument="Staged">
|
|
195
206
|
<Capture />
|
|
196
207
|
</Lab>,
|
|
197
208
|
);
|
|
198
|
-
act(() => lab?.addTrial('Staged'));
|
|
199
|
-
act(() => lab?.addTrial('Plain'));
|
|
209
|
+
await act(async () => lab?.addTrial('Staged'));
|
|
210
|
+
await act(async () => lab?.addTrial('Plain'));
|
|
200
211
|
const [first, second, third] = lab?.trials.map((t) => t.id) ?? [];
|
|
201
212
|
expect(zoomBar().getByRole('button', { name: 'Zoom in' })).toBeDisabled();
|
|
202
213
|
|
|
203
|
-
act(() => lab?.focusTrial(second ?? ''));
|
|
204
|
-
press('Zoom out');
|
|
214
|
+
await act(async () => lab?.focusTrial(second ?? ''));
|
|
215
|
+
await press('Zoom out');
|
|
205
216
|
expect(stageZoom(container, 1)).toBeCloseTo(0.8);
|
|
206
217
|
expect(stageZoom(container, 0)).toBe(1);
|
|
207
218
|
expect(readout()).toHaveTextContent('80%');
|
|
208
219
|
|
|
209
|
-
act(() => lab?.focusTrial(first ?? ''));
|
|
220
|
+
await act(async () => lab?.focusTrial(first ?? ''));
|
|
210
221
|
expect(readout()).toHaveTextContent('100%');
|
|
211
|
-
key('=');
|
|
222
|
+
await key('=');
|
|
212
223
|
expect(stageZoom(container, 0)).toBeCloseTo(1.25);
|
|
213
224
|
expect(stageZoom(container, 1)).toBeCloseTo(0.8);
|
|
214
225
|
|
|
215
|
-
act(() => lab?.focusTrial(third ?? ''));
|
|
226
|
+
await act(async () => lab?.focusTrial(third ?? ''));
|
|
216
227
|
expect(readout()).toBeDisabled();
|
|
217
228
|
});
|
|
218
229
|
|
|
219
|
-
it('are left out when the lab says so', () => {
|
|
220
|
-
|
|
230
|
+
it('are left out when the lab says so', async () => {
|
|
231
|
+
await renderSettled(<Lab instruments={[staged]} defaultInstrument="Staged" zoom={false} />);
|
|
221
232
|
expect(screen.queryByRole('toolbar', { name: 'Zoom' })).toBeNull();
|
|
222
233
|
});
|
|
223
234
|
});
|
package/src/lab/LabZoom.tsx
CHANGED
|
@@ -10,10 +10,11 @@ import {
|
|
|
10
10
|
useActionsRegistry,
|
|
11
11
|
useDepSource,
|
|
12
12
|
useGestureDispatcher,
|
|
13
|
+
useLatest,
|
|
13
14
|
type ViewApi,
|
|
14
15
|
} from '@weasel-js/core';
|
|
15
16
|
import { ZoomInIcon, ZoomOutIcon } from '@weasel-js/ui';
|
|
16
|
-
import { useContext, useEffect, useMemo,
|
|
17
|
+
import { useContext, useEffect, useMemo, useSyncExternalStore } from 'react';
|
|
17
18
|
import { useStore } from 'zustand/react';
|
|
18
19
|
import type { CameraView } from '../canvas/CameraInput';
|
|
19
20
|
import { CameraRegistryContext } from '../canvas/cameraRegistry';
|
|
@@ -96,8 +97,7 @@ function LabZoomControls() {
|
|
|
96
97
|
|
|
97
98
|
const live = zoom !== null && camera !== null;
|
|
98
99
|
const view = useMemo(() => (camera ? forZoom(camera) : null), [camera]);
|
|
99
|
-
const viewRef =
|
|
100
|
-
viewRef.current = live ? view : null;
|
|
100
|
+
const viewRef = useLatest(live ? view : null);
|
|
101
101
|
|
|
102
102
|
useDepSource('view', () => viewRef.current as ViewApi);
|
|
103
103
|
|
|
@@ -108,7 +108,7 @@ function LabZoomControls() {
|
|
|
108
108
|
enabled: () => (viewRef.current ? true : ActionDisabledReason.NotApplicable),
|
|
109
109
|
};
|
|
110
110
|
return registry.register(action);
|
|
111
|
-
}, [registry]);
|
|
111
|
+
}, [registry, viewRef]);
|
|
112
112
|
|
|
113
113
|
// First, so a story's own zoom keys do not also fire over a trial with a
|
|
114
114
|
// camera; over one without, the action is disabled and the keys pass on.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { act } from '@testing-library/react';
|
|
2
|
+
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
2
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
4
|
import { SurfaceContext } from '../surface/SurfaceContext';
|
|
4
5
|
import type { SurfaceHandle } from '../surface/useTiledSurface';
|
|
@@ -18,9 +19,9 @@ function fakeHandle(): SurfaceHandle {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
describe('Workspace with a surface above it', () => {
|
|
21
|
-
it('invalidates rects when the tile set changes, because a re-tile moves tiles', () => {
|
|
22
|
+
it('invalidates rects when the tile set changes, because a re-tile moves tiles', async () => {
|
|
22
23
|
const handle = fakeHandle();
|
|
23
|
-
const { rerender } =
|
|
24
|
+
const { rerender } = await renderSettled(
|
|
24
25
|
<SurfaceContext.Provider value={handle}>
|
|
25
26
|
<Workspace ids={['a']} viewport={{ w: 800, h: 600 }}>
|
|
26
27
|
<div>a</div>
|
|
@@ -29,20 +30,22 @@ describe('Workspace with a surface above it', () => {
|
|
|
29
30
|
);
|
|
30
31
|
(handle.invalidateRects as ReturnType<typeof vi.fn>).mockClear();
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
await act(async () => {
|
|
34
|
+
rerender(
|
|
35
|
+
<SurfaceContext.Provider value={handle}>
|
|
36
|
+
<Workspace ids={['a', 'b']} viewport={{ w: 800, h: 600 }}>
|
|
37
|
+
<div>a</div>
|
|
38
|
+
<div>b</div>
|
|
39
|
+
</Workspace>
|
|
40
|
+
</SurfaceContext.Provider>,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
40
43
|
|
|
41
44
|
expect(handle.invalidateRects).toHaveBeenCalled();
|
|
42
45
|
});
|
|
43
46
|
|
|
44
|
-
it('renders unchanged with no surface above it', () => {
|
|
45
|
-
const { getByText } =
|
|
47
|
+
it('renders unchanged with no surface above it', async () => {
|
|
48
|
+
const { getByText } = await renderSettled(
|
|
46
49
|
<Workspace ids={['a']} viewport={{ w: 800, h: 600 }}>
|
|
47
50
|
<div>a</div>
|
|
48
51
|
</Workspace>,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { act, screen } from '@testing-library/react';
|
|
2
|
+
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
2
3
|
import { describe, expect, test, vi } from 'vitest';
|
|
3
4
|
import { useTrialDrag } from '../trial/TrialDragContext';
|
|
4
5
|
import { Workspace } from './Workspace';
|
|
@@ -13,8 +14,8 @@ function DragProbe({ label }: { label: string }) {
|
|
|
13
14
|
const VIEWPORT = { w: 800, h: 600 };
|
|
14
15
|
|
|
15
16
|
describe('Workspace', () => {
|
|
16
|
-
test('renders all children', () => {
|
|
17
|
-
|
|
17
|
+
test('renders all children', async () => {
|
|
18
|
+
await renderSettled(
|
|
18
19
|
<Workspace viewport={VIEWPORT}>
|
|
19
20
|
<div>one</div>
|
|
20
21
|
<div>two</div>
|
|
@@ -26,9 +27,9 @@ describe('Workspace', () => {
|
|
|
26
27
|
expect(screen.getByText('three')).toBeInTheDocument();
|
|
27
28
|
});
|
|
28
29
|
|
|
29
|
-
test('keeps a child with the tile its id names when an earlier one closes', () => {
|
|
30
|
+
test('keeps a child with the tile its id names when an earlier one closes', async () => {
|
|
30
31
|
const ids = ['a', 'b', 'c'];
|
|
31
|
-
const { rerender, container } =
|
|
32
|
+
const { rerender, container } = await renderSettled(
|
|
32
33
|
<Workspace ids={ids} viewport={VIEWPORT}>
|
|
33
34
|
<div>a</div>
|
|
34
35
|
<div>b</div>
|
|
@@ -37,18 +38,20 @@ describe('Workspace', () => {
|
|
|
37
38
|
);
|
|
38
39
|
expect(container.querySelector('[data-node="c"]')).toHaveTextContent('c');
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
await act(async () => {
|
|
42
|
+
rerender(
|
|
43
|
+
<Workspace ids={['b', 'c']} viewport={VIEWPORT}>
|
|
44
|
+
<div>b</div>
|
|
45
|
+
<div>c</div>
|
|
46
|
+
</Workspace>,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
46
49
|
expect(container.querySelector('[data-node="c"]')).toHaveTextContent('c');
|
|
47
50
|
expect(container.querySelector('[data-node="a"]')).toBeNull();
|
|
48
51
|
});
|
|
49
52
|
|
|
50
|
-
test('uses lk-workspace class', () => {
|
|
51
|
-
const { container } =
|
|
53
|
+
test('uses lk-workspace class', async () => {
|
|
54
|
+
const { container } = await renderSettled(
|
|
52
55
|
<Workspace viewport={VIEWPORT}>
|
|
53
56
|
<div />
|
|
54
57
|
</Workspace>,
|
|
@@ -56,8 +59,8 @@ describe('Workspace', () => {
|
|
|
56
59
|
expect((container.firstChild as HTMLElement).className).toContain('lk-workspace');
|
|
57
60
|
});
|
|
58
61
|
|
|
59
|
-
test('renders resize affordances only when resizable', () => {
|
|
60
|
-
const { container, rerender } =
|
|
62
|
+
test('renders resize affordances only when resizable', async () => {
|
|
63
|
+
const { container, rerender } = await renderSettled(
|
|
61
64
|
<Workspace ids={['a', 'b']} viewport={VIEWPORT}>
|
|
62
65
|
<div>a</div>
|
|
63
66
|
<div>b</div>
|
|
@@ -65,17 +68,19 @@ describe('Workspace', () => {
|
|
|
65
68
|
);
|
|
66
69
|
expect(container.querySelectorAll('[role="separator"]')).toHaveLength(0);
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
await act(async () => {
|
|
72
|
+
rerender(
|
|
73
|
+
<Workspace ids={['a', 'b']} resizable viewport={VIEWPORT}>
|
|
74
|
+
<div>a</div>
|
|
75
|
+
<div>b</div>
|
|
76
|
+
</Workspace>,
|
|
77
|
+
);
|
|
78
|
+
});
|
|
74
79
|
expect(container.querySelectorAll('[role="separator"]').length).toBeGreaterThan(0);
|
|
75
80
|
});
|
|
76
81
|
|
|
77
|
-
test('applies a saved extent to a tile as it registers', () => {
|
|
78
|
-
const { container } =
|
|
82
|
+
test('applies a saved extent to a tile as it registers', async () => {
|
|
83
|
+
const { container } = await renderSettled(
|
|
79
84
|
<Workspace
|
|
80
85
|
ids={['a', 'b']}
|
|
81
86
|
resizable
|
|
@@ -92,9 +97,9 @@ describe('Workspace', () => {
|
|
|
92
97
|
expect(Number.parseFloat(a.style.width)).toBeGreaterThan(Number.parseFloat(b.style.width));
|
|
93
98
|
});
|
|
94
99
|
|
|
95
|
-
test('reports the order a drop would produce instead of applying it', () => {
|
|
100
|
+
test('reports the order a drop would produce instead of applying it', async () => {
|
|
96
101
|
const onReorder = vi.fn();
|
|
97
|
-
const { container } =
|
|
102
|
+
const { container } = await renderSettled(
|
|
98
103
|
<Workspace ids={['a', 'b']} reorderable onReorder={onReorder} viewport={VIEWPORT}>
|
|
99
104
|
<DragProbe label="a" />
|
|
100
105
|
<DragProbe label="b" />
|
|
@@ -111,8 +116,8 @@ describe('Workspace', () => {
|
|
|
111
116
|
expect(nodes).toEqual(['a', 'b']);
|
|
112
117
|
});
|
|
113
118
|
|
|
114
|
-
test('offers no drag source unless reorderable', () => {
|
|
115
|
-
|
|
119
|
+
test('offers no drag source unless reorderable', async () => {
|
|
120
|
+
await renderSettled(
|
|
116
121
|
<Workspace ids={['a', 'b']} viewport={VIEWPORT}>
|
|
117
122
|
<DragProbe label="a" />
|
|
118
123
|
<DragProbe label="b" />
|
package/src/lab/Workspace.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useLatest } from '@weasel-js/core';
|
|
1
2
|
import {
|
|
2
3
|
Children,
|
|
3
4
|
type ReactNode,
|
|
@@ -33,6 +34,7 @@ export type TrialLayout = Record<
|
|
|
33
34
|
{ size?: { w?: number; h?: number }; span?: { cols?: number; rows?: number } }
|
|
34
35
|
>;
|
|
35
36
|
|
|
37
|
+
/** Props for `Workspace`. */
|
|
36
38
|
export interface WorkspaceProps {
|
|
37
39
|
children: ReactNode;
|
|
38
40
|
/**
|
|
@@ -61,7 +63,7 @@ export interface WorkspaceProps {
|
|
|
61
63
|
padding?: number;
|
|
62
64
|
/** Undocked sidebar panels to render alongside the trials. A `'tile'` panel
|
|
63
65
|
* joins the grid as a peer of the trials; a `'floating'` one goes into the
|
|
64
|
-
* floating zone above it. The body is
|
|
66
|
+
* floating zone above it. The body is portaled in by the trial that owns
|
|
65
67
|
* it, so all this renders is the frame and the host. */
|
|
66
68
|
panels?: readonly PanelDescriptor[];
|
|
67
69
|
/**
|
|
@@ -72,6 +74,26 @@ export interface WorkspaceProps {
|
|
|
72
74
|
viewport?: { w: number; h: number };
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
/** The layout store a workspace keeps for its lifetime: the tile grid, and the zone floating panels live in. */
|
|
78
|
+
function createWorkspaceStore(grid: { resizable: boolean; gap: number; padding: number }): Store {
|
|
79
|
+
const store = new Store();
|
|
80
|
+
store.registerNode(
|
|
81
|
+
createNode({
|
|
82
|
+
kind: 'zone',
|
|
83
|
+
id: ZONE_ID,
|
|
84
|
+
container: { strategyId: 'grid', config: grid },
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
store.registerNode(
|
|
88
|
+
createNode({
|
|
89
|
+
kind: 'zone',
|
|
90
|
+
id: FLOAT_ZONE_ID,
|
|
91
|
+
container: { strategyId: 'floating', config: {} },
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
return store;
|
|
95
|
+
}
|
|
96
|
+
|
|
75
97
|
function extentOf(store: Store, id: NodeId): TrialLayout[string] | null {
|
|
76
98
|
const p = store.getNode(id)?.membership?.placement as TrialLayout[string] | undefined;
|
|
77
99
|
if (!p) return null;
|
|
@@ -81,21 +103,6 @@ function extentOf(store: Store, id: NodeId): TrialLayout[string] | null {
|
|
|
81
103
|
return Object.keys(out).length > 0 ? out : null;
|
|
82
104
|
}
|
|
83
105
|
|
|
84
|
-
/**
|
|
85
|
-
* Auto-balanced tiling of trials, `ceil(sqrt(n))` columns wide.
|
|
86
|
-
*
|
|
87
|
-
* Tiles are absolutely positioned at the rects `gridStrategy` computes, not
|
|
88
|
-
* laid out by CSS — `windease/styles.css` (folded into
|
|
89
|
-
* `@weasel-js/labkit/styles.css`) carries the rules that positioning depends on.
|
|
90
|
-
*
|
|
91
|
-
* `panels` are undocked sidebar sections. A `'tile'` panel is registered as a
|
|
92
|
-
* peer of the trials under `PANEL_KIND`, so the grid places and resizes it like
|
|
93
|
-
* one; a `'floating'` panel goes in the overlay above. Either way this renders
|
|
94
|
-
* only the frame and an empty host: the section's content is portalled in by
|
|
95
|
-
* the trial that owns it, which is what keeps a torn-out section inside its
|
|
96
|
-
* trial's React tree instead of rebuilding it as a sibling.
|
|
97
|
-
*/
|
|
98
|
-
|
|
99
106
|
/** The frame an undocked panel gets: a box, and the host element its trial
|
|
100
107
|
* portals into. The title and the dock control come through the portal with
|
|
101
108
|
* the body — the workspace knows a panel's key, not what is in it. */
|
|
@@ -116,6 +123,20 @@ export interface PanelDescriptor {
|
|
|
116
123
|
as: 'tile' | 'floating';
|
|
117
124
|
}
|
|
118
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Auto-balanced tiling of trials, `ceil(sqrt(n))` columns wide.
|
|
128
|
+
*
|
|
129
|
+
* Tiles are absolutely positioned at the rects `gridStrategy` computes, not
|
|
130
|
+
* laid out by CSS — `windease/styles.css` (folded into
|
|
131
|
+
* `@weasel-js/labkit/styles.css`) carries the rules that positioning depends on.
|
|
132
|
+
*
|
|
133
|
+
* `panels` are undocked sidebar sections. A `'tile'` panel is registered as a
|
|
134
|
+
* peer of the trials under `PANEL_KIND`, so the grid places and resizes it like
|
|
135
|
+
* one; a `'floating'` panel goes in the overlay above. Either way this renders
|
|
136
|
+
* only the frame and an empty host: the section's content is portaled in by
|
|
137
|
+
* the trial that owns it, which is what keeps a torn-out section inside its
|
|
138
|
+
* trial's React tree instead of rebuilding it as a sibling.
|
|
139
|
+
*/
|
|
119
140
|
export function Workspace({
|
|
120
141
|
children,
|
|
121
142
|
ids,
|
|
@@ -134,50 +155,25 @@ export function Workspace({
|
|
|
134
155
|
const floatPanels = useMemo(() => (panels ?? []).filter((p) => p.as === 'floating'), [panels]);
|
|
135
156
|
// Identity has to be stable while the id *contents* are unchanged: the sync
|
|
136
157
|
// effect below keys off it, and a fresh array every render would re-register
|
|
137
|
-
// every tile forever.
|
|
138
|
-
//
|
|
139
|
-
const
|
|
158
|
+
// every tile forever. So the ids are memoized on a serialized key, and read
|
|
159
|
+
// back from it.
|
|
160
|
+
const idKey = JSON.stringify([
|
|
140
161
|
...items.map((_, i) => ids?.[i] ?? `lk-ws-${i}`),
|
|
141
162
|
...tilePanels.map((p) => `lk-panel-${p.key}`),
|
|
142
|
-
];
|
|
143
|
-
const
|
|
144
|
-
const idKeyRef = useRef<string | null>(null);
|
|
145
|
-
const nodeIdsRef = useRef<NodeId[]>([]);
|
|
146
|
-
if (idKeyRef.current !== idKey) {
|
|
147
|
-
idKeyRef.current = idKey;
|
|
148
|
-
nodeIdsRef.current = wantedIds.map(asNodeId);
|
|
149
|
-
}
|
|
150
|
-
const nodeIds = nodeIdsRef.current;
|
|
163
|
+
]);
|
|
164
|
+
const nodeIds = useMemo(() => (JSON.parse(idKey) as string[]).map(asNodeId), [idKey]);
|
|
151
165
|
|
|
152
166
|
// Held in refs rather than depended on: a fresh object each render would
|
|
153
167
|
// re-run the sync effect, and only a newly registered tile reads `layout`.
|
|
154
|
-
const layoutRef =
|
|
155
|
-
|
|
156
|
-
const onLayoutChangeRef = useRef(onLayoutChange);
|
|
157
|
-
onLayoutChangeRef.current = onLayoutChange;
|
|
168
|
+
const layoutRef = useLatest(layout);
|
|
169
|
+
const onLayoutChangeRef = useLatest(onLayoutChange);
|
|
158
170
|
|
|
159
171
|
// One store for the component's lifetime: a tile's dragged extent lives in
|
|
160
172
|
// its node, so rebuilding the store on every add or close would silently
|
|
161
173
|
// reset every pane.
|
|
162
174
|
const storeRef = useRef<Store | null>(null);
|
|
163
|
-
if (storeRef.current === null)
|
|
164
|
-
|
|
165
|
-
store.registerNode(
|
|
166
|
-
createNode({
|
|
167
|
-
kind: 'zone',
|
|
168
|
-
id: ZONE_ID,
|
|
169
|
-
container: { strategyId: 'grid', config: { resizable, gap, padding } },
|
|
170
|
-
}),
|
|
171
|
-
);
|
|
172
|
-
store.registerNode(
|
|
173
|
-
createNode({
|
|
174
|
-
kind: 'zone',
|
|
175
|
-
id: FLOAT_ZONE_ID,
|
|
176
|
-
container: { strategyId: 'floating', config: {} },
|
|
177
|
-
}),
|
|
178
|
-
);
|
|
179
|
-
storeRef.current = store;
|
|
180
|
-
}
|
|
175
|
+
if (storeRef.current === null)
|
|
176
|
+
storeRef.current = createWorkspaceStore({ resizable, gap, padding });
|
|
181
177
|
const store = storeRef.current;
|
|
182
178
|
|
|
183
179
|
// A tile that only moves reports nothing to a ResizeObserver, and only this
|
|
@@ -203,7 +199,7 @@ export function Workspace({
|
|
|
203
199
|
if (saved) store.patchPlacement(id, saved);
|
|
204
200
|
}
|
|
205
201
|
store.setChildOrder(ZONE_ID, [...nodeIds]);
|
|
206
|
-
}, [store, nodeIds]);
|
|
202
|
+
}, [store, nodeIds, layoutRef]);
|
|
207
203
|
|
|
208
204
|
// biome-ignore lint/correctness/useExhaustiveDependencies: nodeIds is the signal that the tile set changed — a re-tile moves tiles without resizing any — not a value this reads
|
|
209
205
|
useEffect(() => {
|
|
@@ -222,7 +218,7 @@ export function Workspace({
|
|
|
222
218
|
}
|
|
223
219
|
onLayoutChangeRef.current?.(next);
|
|
224
220
|
});
|
|
225
|
-
}, [store, onLayoutChange]);
|
|
221
|
+
}, [store, onLayoutChange, onLayoutChangeRef]);
|
|
226
222
|
|
|
227
223
|
const commitOrder = useCallback(
|
|
228
224
|
(nextIds: NodeId[]) => onReorder?.(nextIds.map(String)),
|
package/src/loupe/AGENTS.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# Loupe — Agent Guide
|
|
2
2
|
|
|
3
|
-
`src/loupe/` is
|
|
4
|
-
|
|
3
|
+
`src/loupe/` is `@weasel-js/labkit/loupe`: `<TrialLoupe>`, a magnifier the
|
|
4
|
+
instrument mounts in its own content, painted by whichever painter suits it.
|
|
5
|
+
Its own bundle entry; nothing in the main bundle imports it, so a lab that
|
|
6
|
+
mounts no lens never loads it.
|
|
5
7
|
|
|
6
8
|
The magnifier itself is not here. `@weasel-js/loupe` holds the model — aim,
|
|
7
9
|
factor, mode, color, picking — over a `LoupeSurface` it asks five questions.
|
|
@@ -11,29 +13,40 @@ This directory binds that model to a labkit trial and draws it.
|
|
|
11
13
|
|
|
12
14
|
| File | Role |
|
|
13
15
|
|---|---|
|
|
14
|
-
| `types.ts` | `
|
|
16
|
+
| `types.ts` | `LoupeOptions`, and `resolveLoupe` filling in every default |
|
|
15
17
|
| `useLoupe.ts` | The model over a host element, and pointer aiming |
|
|
16
18
|
| `loupeActions.ts` | `loupe.peek` and `loupe.magnify`, as `Action` descriptors |
|
|
17
19
|
| `LoupeGestures.tsx` | Registers those, and mounts a dispatcher on the host when no camera has one there |
|
|
18
|
-
| `TrialLoupe.tsx` |
|
|
20
|
+
| `TrialLoupe.tsx` | Finds its host and the trial's switch, picks the painter, mounts the lens |
|
|
19
21
|
| `LoupeBubble.tsx` | The circular clip, positioned on the aim |
|
|
20
22
|
| `CanvasLoupe.tsx` | Painter for a `<CanvasStack>` |
|
|
21
23
|
| `canvasLens.ts` | That painter's geometry and drawing, with no React in it |
|
|
22
24
|
| `DomLoupe.tsx` | Painter for DOM content |
|
|
23
25
|
| `useHostSize.ts` | The host's measured box, for the DOM stage |
|
|
24
26
|
|
|
25
|
-
##
|
|
27
|
+
## How it reads the trial
|
|
28
|
+
|
|
29
|
+
Only through context, the way `<TrialOverview>` does. `CanvasStackContext`'s
|
|
30
|
+
`surface` gives the canvas painter the stack's layers, pixels and `worldSpec`;
|
|
31
|
+
`CameraContext` gives a lens in a `<Stage>` its element and camera. Outside
|
|
32
|
+
both, it tracks `hostRef`, or failing that wraps its `children` in a
|
|
33
|
+
`.lk-loupe-host` box of its own.
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
`
|
|
35
|
+
The toolbar toggle is `LoupeSwitchContext` (`src/trial/loupeSwitch.ts`, main
|
|
36
|
+
bundle). A lens with no `enabled` prop calls `mount` and follows `on`; the
|
|
37
|
+
trial offers its Loupe toggle while any lens is mounted. `enabled` opts a lens
|
|
38
|
+
out of the switch entirely. Because the lens mounts after the trial's first
|
|
39
|
+
render, `loupe` is in `TRANSIENT_BUILTINS`, so suppressing it before then is
|
|
40
|
+
not a typo.
|
|
41
|
+
|
|
42
|
+
## Which painter
|
|
32
43
|
|
|
33
|
-
|
|
34
|
-
`
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
`render` decides. Absent, the lens re-runs the stack's canvas layers through
|
|
45
|
+
`lensCamera` — sharp at any factor, and `mode: 'pixel'` enlarges the presented
|
|
46
|
+
pixels with smoothing off instead. Present, it is handed a camera and draws the
|
|
47
|
+
content again; a DOM loupe is always `vector`, since DOM has no framebuffer to
|
|
48
|
+
enlarge. The canvas painter needs the stack's own pixels, which is why a
|
|
49
|
+
drawing instrument mounts the lens in its `render`, inside `<CanvasStack>`.
|
|
37
50
|
|
|
38
51
|
## Traps
|
|
39
52
|
|
|
@@ -51,7 +64,7 @@ In a trial the loupe's actions join the camera's dispatcher (`CameraInput`).
|
|
|
51
64
|
`loupe.magnify` sits in the hotkey tier, so while the lens is up it outranks
|
|
52
65
|
the camera's wheel zoom; while it is down its `enabled` returns a disabled
|
|
53
66
|
reason and the dispatcher falls through to the zoom. Outside a camera — the DOM
|
|
54
|
-
`.lk-
|
|
67
|
+
`.lk-loupe-host` — `<LoupeGestures>` mounts a dispatcher of its own.
|
|
55
68
|
|
|
56
69
|
**Aiming is a plain listener because a hover is not a gesture.**
|
|
57
70
|
`GESTURE_DESCRIPTORS` names no continuous-motion gesture, so `pointermove` /
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useVisibleRaf } from '@weasel-js/core';
|
|
1
|
+
import { useLatest, useVisibleRaf } from '@weasel-js/core';
|
|
2
2
|
import type { LoupeMode, LoupePoint } from '@weasel-js/loupe';
|
|
3
3
|
import { useEffect, useRef } from 'react';
|
|
4
4
|
import type { CanvasStackSurface } from '../canvas/CanvasStackContext';
|
|
@@ -40,8 +40,7 @@ export function CanvasLoupe({
|
|
|
40
40
|
// Whatever the stack redraws every frame, the lens has to redraw too — an
|
|
41
41
|
// instrument that animates from its own loop never re-renders this component.
|
|
42
42
|
const args = { aim, factor, mode, diameter, dpr, view, frame, worldSpec, surface };
|
|
43
|
-
const argsRef =
|
|
44
|
-
argsRef.current = args;
|
|
43
|
+
const argsRef = useLatest(args);
|
|
45
44
|
|
|
46
45
|
const loop = useVisibleRaf(
|
|
47
46
|
() => {
|