@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/loupe/DomLoupe.tsx
CHANGED
|
@@ -16,13 +16,11 @@ export interface DomLoupeProps {
|
|
|
16
16
|
/** The trial's own camera. */
|
|
17
17
|
view: ViewTransform;
|
|
18
18
|
frame?: WorldFrame;
|
|
19
|
-
state: unknown;
|
|
20
|
-
config: unknown;
|
|
21
19
|
render: (args: LoupeRenderArgs) => React.ReactNode;
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
/**
|
|
25
|
-
* Paints a lens over DOM content by asking
|
|
23
|
+
* Paints a lens over DOM content by asking its `render` to draw it again
|
|
26
24
|
* at a magnified camera.
|
|
27
25
|
*
|
|
28
26
|
* The stage is a full copy of the host, so the instrument's own layout still
|
|
@@ -38,8 +36,6 @@ export function DomLoupe({
|
|
|
38
36
|
size,
|
|
39
37
|
view,
|
|
40
38
|
frame,
|
|
41
|
-
state,
|
|
42
|
-
config,
|
|
43
39
|
render,
|
|
44
40
|
}: DomLoupeProps) {
|
|
45
41
|
const style: CSSProperties = {
|
|
@@ -50,8 +46,6 @@ export function DomLoupe({
|
|
|
50
46
|
return (
|
|
51
47
|
<div className="lk-loupe__stage" style={style}>
|
|
52
48
|
{render({
|
|
53
|
-
state,
|
|
54
|
-
config,
|
|
55
49
|
view: zoomAt(view, factor, aim, { frame }),
|
|
56
50
|
factor,
|
|
57
51
|
mode,
|
package/src/loupe/Loupe.less
CHANGED
|
@@ -36,3 +36,19 @@
|
|
|
36
36
|
left: 0;
|
|
37
37
|
transform-origin: 0 0;
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
// The box a lens wraps around DOM content when nothing around it has one to
|
|
41
|
+
// track. Reproduces the content well's column so the content still stretches
|
|
42
|
+
// to it rather than collapsing to its intrinsic height.
|
|
43
|
+
.lk-loupe-host {
|
|
44
|
+
position: relative;
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
min-height: 0;
|
|
48
|
+
height: 100%;
|
|
49
|
+
|
|
50
|
+
> * {
|
|
51
|
+
flex: 1 1 auto;
|
|
52
|
+
min-height: 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/loupe/TrialLoupe.tsx
CHANGED
|
@@ -1,53 +1,97 @@
|
|
|
1
|
-
import { type RefObject, useContext, useMemo } from 'react';
|
|
2
|
-
import { CameraScope } from '../canvas/CameraInput';
|
|
1
|
+
import { type ReactNode, type RefObject, useContext, useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { CameraContext, CameraScope } from '../canvas/CameraInput';
|
|
3
3
|
import { CanvasStackContext } from '../canvas/CanvasStackContext';
|
|
4
|
-
import
|
|
4
|
+
import { fromCameraView } from '../canvas/cameraView';
|
|
5
5
|
import type { ViewTransform } from '../instrument/types';
|
|
6
|
+
import { LoupeSwitchContext } from '../trial/loupeSwitch';
|
|
6
7
|
import { CanvasLoupe } from './CanvasLoupe';
|
|
7
8
|
import { sampleStack } from './canvasLens';
|
|
8
9
|
import { DomLoupe } from './DomLoupe';
|
|
9
10
|
import { LoupeBubble } from './LoupeBubble';
|
|
10
11
|
import { LoupeGestures } from './LoupeGestures';
|
|
11
|
-
import type
|
|
12
|
+
import { type LoupeOptions, resolveLoupe } from './types';
|
|
12
13
|
import { useHostSize } from './useHostSize';
|
|
13
14
|
import { useLoupe } from './useLoupe';
|
|
14
15
|
|
|
15
16
|
/** Props for `<TrialLoupe>`. */
|
|
16
|
-
export interface TrialLoupeProps {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
view
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* takes the stack's own element instead, and this goes unused. */
|
|
17
|
+
export interface TrialLoupeProps extends LoupeOptions {
|
|
18
|
+
/** Whether the lens is turned on. Omitted, it follows the trial's toolbar
|
|
19
|
+
* toggle, which the trial offers while a lens like this is mounted — and is
|
|
20
|
+
* off outside a trial. Hold-to-peek shows it either way. */
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
/** The camera a `render` lens composes its magnification onto. Defaults to
|
|
23
|
+
* the camera around it, and to the identity outside one. */
|
|
24
|
+
view?: ViewTransform;
|
|
25
|
+
/** The element the lens tracks. Defaults to the canvas stack or stage around
|
|
26
|
+
* it, and outside both to a box wrapped around `children`. */
|
|
27
27
|
hostRef?: RefObject<HTMLElement | null>;
|
|
28
|
+
/** Content for the lens to sit over. Wrapped in a box of the lens's own only
|
|
29
|
+
* when nothing around it gives the lens an element to track. */
|
|
30
|
+
children?: ReactNode;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
* listeners on every render. */
|
|
32
|
-
const NO_HOST: RefObject<HTMLElement | null> = { current: null };
|
|
33
|
+
const IDENTITY: ViewTransform = { zoom: 1, pan: { x: 0, y: 0 } };
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
|
-
* A trial's
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* A trial's magnifier, mounted by the instrument wherever its content is: in
|
|
37
|
+
* a canvas instrument's `render`, a stage's `overlay`, or around DOM content.
|
|
38
|
+
*
|
|
39
|
+
* With no `render` inside a `<CanvasStack>`, it re-draws the stack's own
|
|
40
|
+
* layers through a zoomed camera. With `render`, it asks for the content again
|
|
41
|
+
* at a magnified camera.
|
|
38
42
|
*/
|
|
39
43
|
export function TrialLoupe({
|
|
40
|
-
capability,
|
|
41
44
|
enabled,
|
|
42
|
-
state,
|
|
43
|
-
config,
|
|
44
45
|
view,
|
|
45
|
-
worldSpec,
|
|
46
46
|
hostRef,
|
|
47
|
+
children,
|
|
48
|
+
render,
|
|
49
|
+
factor,
|
|
50
|
+
minFactor,
|
|
51
|
+
maxFactor,
|
|
52
|
+
mode,
|
|
53
|
+
diameter,
|
|
54
|
+
peekKey,
|
|
55
|
+
onColorChange,
|
|
47
56
|
}: TrialLoupeProps) {
|
|
57
|
+
const options = useMemo(
|
|
58
|
+
() =>
|
|
59
|
+
resolveLoupe({
|
|
60
|
+
render,
|
|
61
|
+
factor,
|
|
62
|
+
minFactor,
|
|
63
|
+
maxFactor,
|
|
64
|
+
mode,
|
|
65
|
+
diameter,
|
|
66
|
+
peekKey,
|
|
67
|
+
onColorChange,
|
|
68
|
+
}),
|
|
69
|
+
[render, factor, minFactor, maxFactor, mode, diameter, peekKey, onColorChange],
|
|
70
|
+
);
|
|
71
|
+
|
|
48
72
|
const stack = useContext(CanvasStackContext);
|
|
49
|
-
const
|
|
50
|
-
const
|
|
73
|
+
const camera = useContext(CameraContext);
|
|
74
|
+
const loupeSwitch = useContext(LoupeSwitchContext);
|
|
75
|
+
const surface = options.render ? undefined : stack?.surface;
|
|
76
|
+
|
|
77
|
+
const ownHost = useRef<HTMLDivElement | null>(null);
|
|
78
|
+
const cameraHost = useMemo<RefObject<HTMLElement | null> | null>(
|
|
79
|
+
() =>
|
|
80
|
+
camera
|
|
81
|
+
? {
|
|
82
|
+
get current() {
|
|
83
|
+
return camera.element();
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
: null,
|
|
87
|
+
[camera],
|
|
88
|
+
);
|
|
89
|
+
const host = surface?.element ?? hostRef ?? cameraHost ?? ownHost;
|
|
90
|
+
const wraps = host === ownHost;
|
|
91
|
+
|
|
92
|
+
// A lens told whether it is on has no use for the trial's toggle.
|
|
93
|
+
const mount = enabled === undefined ? loupeSwitch?.mount : undefined;
|
|
94
|
+
useEffect(() => mount?.(), [mount]);
|
|
51
95
|
|
|
52
96
|
const sample = useMemo(() => {
|
|
53
97
|
if (!surface) return undefined;
|
|
@@ -57,7 +101,12 @@ export function TrialLoupe({
|
|
|
57
101
|
};
|
|
58
102
|
}, [surface]);
|
|
59
103
|
|
|
60
|
-
const loupe = useLoupe({
|
|
104
|
+
const loupe = useLoupe({
|
|
105
|
+
options,
|
|
106
|
+
hostRef: host,
|
|
107
|
+
enabled: enabled ?? loupeSwitch?.on ?? false,
|
|
108
|
+
sample,
|
|
109
|
+
});
|
|
61
110
|
const measured = useHostSize(host);
|
|
62
111
|
const size = surface?.size ?? measured;
|
|
63
112
|
|
|
@@ -67,42 +116,56 @@ export function TrialLoupe({
|
|
|
67
116
|
// dispatcher; outside one, `<CameraScope>` gives them an isolated scope.
|
|
68
117
|
const gestures = (
|
|
69
118
|
<CameraScope>
|
|
70
|
-
<LoupeGestures hostRef={host} input={loupe.input} peekKey={
|
|
119
|
+
<LoupeGestures hostRef={host} input={loupe.input} peekKey={options.peekKey ?? null} />
|
|
71
120
|
</CameraScope>
|
|
72
121
|
);
|
|
73
122
|
|
|
74
|
-
|
|
123
|
+
const lens = loupe.visible ? (
|
|
124
|
+
<LoupeBubble aim={loupe.aim} diameter={options.diameter}>
|
|
125
|
+
{options.render ? (
|
|
126
|
+
<DomLoupe
|
|
127
|
+
aim={loupe.aim}
|
|
128
|
+
factor={loupe.factor}
|
|
129
|
+
mode={loupe.mode}
|
|
130
|
+
diameter={options.diameter}
|
|
131
|
+
size={size}
|
|
132
|
+
view={
|
|
133
|
+
view ??
|
|
134
|
+
stack?.view ??
|
|
135
|
+
(camera ? fromCameraView(camera.view.get(), camera.frame) : IDENTITY)
|
|
136
|
+
}
|
|
137
|
+
frame={stack?.frame ?? camera?.frame}
|
|
138
|
+
render={options.render}
|
|
139
|
+
/>
|
|
140
|
+
) : surface && stack ? (
|
|
141
|
+
<CanvasLoupe
|
|
142
|
+
aim={loupe.aim}
|
|
143
|
+
factor={loupe.factor}
|
|
144
|
+
mode={loupe.mode}
|
|
145
|
+
diameter={options.diameter}
|
|
146
|
+
surface={surface}
|
|
147
|
+
view={stack.view}
|
|
148
|
+
frame={stack.frame}
|
|
149
|
+
worldSpec={surface.worldSpec}
|
|
150
|
+
/>
|
|
151
|
+
) : null}
|
|
152
|
+
</LoupeBubble>
|
|
153
|
+
) : null;
|
|
75
154
|
|
|
155
|
+
if (wraps) {
|
|
156
|
+
return (
|
|
157
|
+
<div ref={ownHost} className="lk-loupe-host">
|
|
158
|
+
{children}
|
|
159
|
+
{gestures}
|
|
160
|
+
{lens}
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
76
164
|
return (
|
|
77
165
|
<>
|
|
166
|
+
{children}
|
|
78
167
|
{gestures}
|
|
79
|
-
|
|
80
|
-
{capability.render ? (
|
|
81
|
-
<DomLoupe
|
|
82
|
-
aim={loupe.aim}
|
|
83
|
-
factor={loupe.factor}
|
|
84
|
-
mode={loupe.mode}
|
|
85
|
-
diameter={capability.diameter}
|
|
86
|
-
size={size}
|
|
87
|
-
view={view}
|
|
88
|
-
frame={stack?.frame}
|
|
89
|
-
state={state}
|
|
90
|
-
config={config}
|
|
91
|
-
render={capability.render}
|
|
92
|
-
/>
|
|
93
|
-
) : surface && stack ? (
|
|
94
|
-
<CanvasLoupe
|
|
95
|
-
aim={loupe.aim}
|
|
96
|
-
factor={loupe.factor}
|
|
97
|
-
mode={loupe.mode}
|
|
98
|
-
diameter={capability.diameter}
|
|
99
|
-
surface={surface}
|
|
100
|
-
view={stack.view}
|
|
101
|
-
frame={stack.frame}
|
|
102
|
-
worldSpec={worldSpec}
|
|
103
|
-
/>
|
|
104
|
-
) : null}
|
|
105
|
-
</LoupeBubble>
|
|
168
|
+
{lens}
|
|
106
169
|
</>
|
|
107
170
|
);
|
|
108
171
|
}
|
package/src/loupe/index.ts
CHANGED
|
@@ -13,12 +13,7 @@ export type { LoupeInputApi } from './loupeActions';
|
|
|
13
13
|
export { createLoupeActions, LOUPE_MAGNIFY_ID, LOUPE_PEEK_ID } from './loupeActions';
|
|
14
14
|
export type { TrialLoupeProps } from './TrialLoupe';
|
|
15
15
|
export { TrialLoupe } from './TrialLoupe';
|
|
16
|
-
export type {
|
|
17
|
-
LoupeCapability,
|
|
18
|
-
LoupeDeclaration,
|
|
19
|
-
LoupeRenderArgs,
|
|
20
|
-
ResolvedLoupe,
|
|
21
|
-
} from './types';
|
|
16
|
+
export type { LoupeOptions, LoupeRenderArgs, ResolvedLoupe } from './types';
|
|
22
17
|
export { LOUPE_DEFAULTS, resolveLoupe } from './types';
|
|
23
18
|
export type { LoupeState, UseLoupeOptions } from './useLoupe';
|
|
24
19
|
export { useLoupe } from './useLoupe';
|
package/src/loupe/types.test.ts
CHANGED
|
@@ -2,11 +2,11 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import { LOUPE_DEFAULTS, resolveLoupe } from './types';
|
|
3
3
|
|
|
4
4
|
describe('resolveLoupe', () => {
|
|
5
|
-
it('takes every default
|
|
6
|
-
expect(resolveLoupe(
|
|
5
|
+
it('takes every default when given nothing', () => {
|
|
6
|
+
expect(resolveLoupe()).toMatchObject(LOUPE_DEFAULTS);
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
-
it('keeps what
|
|
9
|
+
it('keeps what it was given', () => {
|
|
10
10
|
const cap = resolveLoupe({ factor: 12, diameter: 320, minFactor: 1, maxFactor: 40 });
|
|
11
11
|
expect(cap).toMatchObject({ factor: 12, diameter: 320, minFactor: 1, maxFactor: 40 });
|
|
12
12
|
});
|
package/src/loupe/types.ts
CHANGED
|
@@ -3,11 +3,9 @@ import type { ReactNode } from 'react';
|
|
|
3
3
|
import type { ViewportSize } from '../canvas/worldSpec';
|
|
4
4
|
import type { ViewTransform } from '../instrument/types';
|
|
5
5
|
|
|
6
|
-
/** What a DOM
|
|
7
|
-
*
|
|
8
|
-
export interface LoupeRenderArgs
|
|
9
|
-
state: TS;
|
|
10
|
-
config: TC;
|
|
6
|
+
/** What a DOM loupe's `render` is handed: the camera to draw the content
|
|
7
|
+
* through again. */
|
|
8
|
+
export interface LoupeRenderArgs {
|
|
11
9
|
/** The trial's own camera composed with the magnification, about the aimed
|
|
12
10
|
* point — so drawing the same content through it magnifies in place. */
|
|
13
11
|
view: ViewTransform;
|
|
@@ -21,14 +19,14 @@ export interface LoupeRenderArgs<TS = unknown, TC = unknown> {
|
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
/**
|
|
24
|
-
*
|
|
22
|
+
* How a lens magnifies.
|
|
25
23
|
*
|
|
26
|
-
* With no `render`, the
|
|
27
|
-
* zoomed camera, so it stays sharp at any factor.
|
|
28
|
-
*
|
|
24
|
+
* With no `render`, the lens re-draws the canvas stack's layers through a
|
|
25
|
+
* zoomed camera, so it stays sharp at any factor. Over DOM content, `render`
|
|
26
|
+
* draws it instead: given a camera, draw me again.
|
|
29
27
|
*/
|
|
30
|
-
export interface
|
|
31
|
-
render?: (args: LoupeRenderArgs
|
|
28
|
+
export interface LoupeOptions {
|
|
29
|
+
render?: (args: LoupeRenderArgs) => ReactNode;
|
|
32
30
|
/** Opening magnification, clamped to the bounds below. Default 6. */
|
|
33
31
|
factor?: number;
|
|
34
32
|
/** What the wheel clamps to. Defaults 2 and 32. */
|
|
@@ -48,12 +46,9 @@ export interface LoupeCapability<TS = unknown, TC = unknown> {
|
|
|
48
46
|
onColorChange?: (hex: string) => void;
|
|
49
47
|
}
|
|
50
48
|
|
|
51
|
-
/**
|
|
52
|
-
export
|
|
53
|
-
|
|
54
|
-
/** A {@link LoupeCapability} with every default filled in. */
|
|
55
|
-
export interface ResolvedLoupe<TS = unknown, TC = unknown> {
|
|
56
|
-
render?: (args: LoupeRenderArgs<TS, TC>) => ReactNode;
|
|
49
|
+
/** {@link LoupeOptions} with every default filled in. */
|
|
50
|
+
export interface ResolvedLoupe {
|
|
51
|
+
render?: (args: LoupeRenderArgs) => ReactNode;
|
|
57
52
|
onColorChange?: (hex: string) => void;
|
|
58
53
|
factor: number;
|
|
59
54
|
minFactor: number;
|
|
@@ -63,6 +58,7 @@ export interface ResolvedLoupe<TS = unknown, TC = unknown> {
|
|
|
63
58
|
peekKey: string | null;
|
|
64
59
|
}
|
|
65
60
|
|
|
61
|
+
/** What `resolveLoupe` fills in for anything a declaration leaves unset. */
|
|
66
62
|
export const LOUPE_DEFAULTS = {
|
|
67
63
|
factor: 6,
|
|
68
64
|
minFactor: 2,
|
|
@@ -72,19 +68,20 @@ export const LOUPE_DEFAULTS = {
|
|
|
72
68
|
peekKey: 'Alt',
|
|
73
69
|
} as const satisfies Omit<ResolvedLoupe, 'render' | 'onColorChange'>;
|
|
74
70
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
71
|
+
/** `options` with every default filled in and `factor` clamped to
|
|
72
|
+
* `[minFactor, maxFactor]`. A loupe with its own `render` is always `'vector'`. */
|
|
73
|
+
export function resolveLoupe(options: LoupeOptions = {}): ResolvedLoupe {
|
|
74
|
+
const minFactor = options.minFactor ?? LOUPE_DEFAULTS.minFactor;
|
|
75
|
+
const maxFactor = options.maxFactor ?? LOUPE_DEFAULTS.maxFactor;
|
|
79
76
|
return {
|
|
80
|
-
render:
|
|
81
|
-
onColorChange:
|
|
77
|
+
render: options.render,
|
|
78
|
+
onColorChange: options.onColorChange,
|
|
82
79
|
minFactor,
|
|
83
80
|
maxFactor,
|
|
84
|
-
factor: Math.min(maxFactor, Math.max(minFactor,
|
|
85
|
-
diameter:
|
|
86
|
-
peekKey:
|
|
81
|
+
factor: Math.min(maxFactor, Math.max(minFactor, options.factor ?? LOUPE_DEFAULTS.factor)),
|
|
82
|
+
diameter: options.diameter ?? LOUPE_DEFAULTS.diameter,
|
|
83
|
+
peekKey: options.peekKey === undefined ? LOUPE_DEFAULTS.peekKey : options.peekKey,
|
|
87
84
|
// A DOM loupe has no framebuffer, so `pixel` would have nothing to enlarge.
|
|
88
|
-
mode:
|
|
85
|
+
mode: options.render ? 'vector' : (options.mode ?? LOUPE_DEFAULTS.mode),
|
|
89
86
|
};
|
|
90
87
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
2
2
|
import { WeaselProvider } from '@weasel-js/core';
|
|
3
|
+
import { renderThenAbandon } from '@weasel-js/react/testing/abandonRender';
|
|
3
4
|
import { type RefObject, StrictMode, useRef } from 'react';
|
|
4
5
|
import { describe, expect, it, vi } from 'vitest';
|
|
5
6
|
import { LoupeGestures } from './LoupeGestures';
|
|
@@ -23,12 +24,12 @@ interface HarnessProps {
|
|
|
23
24
|
* cases below exercise the real dispatcher route rather than a stand-in. */
|
|
24
25
|
function Harness({ enabled = true, seen, sample, ...rest }: HarnessProps) {
|
|
25
26
|
const hostRef = useRef<HTMLDivElement | null>(null);
|
|
26
|
-
const
|
|
27
|
+
const options = resolveLoupe(rest);
|
|
27
28
|
const loupe = useLoupe({
|
|
28
29
|
hostRef: hostRef as RefObject<HTMLElement | null>,
|
|
29
30
|
enabled,
|
|
30
31
|
sample,
|
|
31
|
-
|
|
32
|
+
options,
|
|
32
33
|
});
|
|
33
34
|
seen?.(loupe);
|
|
34
35
|
return (
|
|
@@ -37,7 +38,7 @@ function Harness({ enabled = true, seen, sample, ...rest }: HarnessProps) {
|
|
|
37
38
|
<LoupeGestures
|
|
38
39
|
hostRef={hostRef as RefObject<HTMLElement | null>}
|
|
39
40
|
input={loupe.input}
|
|
40
|
-
peekKey={
|
|
41
|
+
peekKey={options.peekKey ?? null}
|
|
41
42
|
/>
|
|
42
43
|
</WeaselProvider>
|
|
43
44
|
<span data-testid="visible">{String(loupe.visible)}</span>
|
|
@@ -92,7 +93,7 @@ describe('useLoupe', () => {
|
|
|
92
93
|
expect(read('visible')).toBe('false');
|
|
93
94
|
});
|
|
94
95
|
|
|
95
|
-
it('does not peek when
|
|
96
|
+
it('does not peek when its options turned the key off', () => {
|
|
96
97
|
render(<Harness enabled={false} peekKey={null} />);
|
|
97
98
|
move(40, 25);
|
|
98
99
|
act(() => {
|
|
@@ -251,4 +252,28 @@ describe('useLoupe', () => {
|
|
|
251
252
|
});
|
|
252
253
|
expect(read('mode')).toBe('pixel');
|
|
253
254
|
});
|
|
255
|
+
|
|
256
|
+
it('aims as the committed `enabled` allows, not an abandoned render', () => {
|
|
257
|
+
const seen: LoupeState[] = [];
|
|
258
|
+
// Reads the aim back through the model: the DOM is not re-rendered while
|
|
259
|
+
// the abandoned transition is still pending.
|
|
260
|
+
const aimOf = (p: { x: number; y: number }) => `${p.x},${p.y}`;
|
|
261
|
+
renderThenAbandon({ enabled: false }, { enabled: true }, (p) => (
|
|
262
|
+
<Harness enabled={p.enabled} sample={aimOf} seen={(l) => seen.push(l)} />
|
|
263
|
+
));
|
|
264
|
+
const before = seen[0]?.pick();
|
|
265
|
+
move(40, 25);
|
|
266
|
+
expect(seen[0]?.pick()).toBe(before);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('samples through the committed `sample`, not an abandoned render', () => {
|
|
270
|
+
const seen: LoupeState[] = [];
|
|
271
|
+
const committed = vi.fn(() => '#111111');
|
|
272
|
+
const abandoned = vi.fn(() => '#222222');
|
|
273
|
+
renderThenAbandon({ sample: committed }, { sample: abandoned }, (p) => (
|
|
274
|
+
<Harness sample={p.sample} seen={(l) => seen.push(l)} />
|
|
275
|
+
));
|
|
276
|
+
expect(seen[0]?.pick()).toBe('#111111');
|
|
277
|
+
expect(abandoned).not.toHaveBeenCalled();
|
|
278
|
+
});
|
|
254
279
|
});
|
package/src/loupe/useLoupe.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useLatest } from '@weasel-js/core';
|
|
1
2
|
import {
|
|
2
3
|
createLoupeModel,
|
|
3
4
|
type LoupeMode,
|
|
@@ -11,7 +12,7 @@ import type { ResolvedLoupe } from './types';
|
|
|
11
12
|
|
|
12
13
|
/** Options for {@link useLoupe}. */
|
|
13
14
|
export interface UseLoupeOptions {
|
|
14
|
-
|
|
15
|
+
options: ResolvedLoupe;
|
|
15
16
|
/** The element the lens tracks the pointer across. */
|
|
16
17
|
hostRef: RefObject<HTMLElement | null>;
|
|
17
18
|
/** Whether the loupe is turned on. Hold-to-peek shows it regardless. */
|
|
@@ -39,6 +40,11 @@ export interface LoupeState {
|
|
|
39
40
|
input: LoupeInputApi;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/** Whether the lens is up: the pointer is over the host, and the loupe is on or peeked. */
|
|
44
|
+
function lensShown(over: boolean, enabled: boolean, peeking: boolean): boolean {
|
|
45
|
+
return over && (enabled || peeking);
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
/**
|
|
43
49
|
* Binds `@weasel-js/loupe`'s model to a host element and tracks the pointer
|
|
44
50
|
* across it.
|
|
@@ -50,31 +56,28 @@ export interface LoupeState {
|
|
|
50
56
|
* has no continuous-motion entry, and inventing one to carry the lens' aim
|
|
51
57
|
* would be an input taxonomy change, not a loupe change.
|
|
52
58
|
*/
|
|
53
|
-
export function useLoupe({
|
|
59
|
+
export function useLoupe({ options, hostRef, enabled, sample }: UseLoupeOptions): LoupeState {
|
|
54
60
|
const [, bump] = useReducer((n: number) => n + 1, 0);
|
|
55
61
|
|
|
56
62
|
const overRef = useRef(false);
|
|
57
63
|
const peekingRef = useRef(false);
|
|
58
|
-
const enabledRef =
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
sampleRef.current = sample;
|
|
62
|
-
const onColorChangeRef = useRef(capability.onColorChange);
|
|
63
|
-
onColorChangeRef.current = capability.onColorChange;
|
|
64
|
+
const enabledRef = useLatest(enabled);
|
|
65
|
+
const sampleRef = useLatest(sample);
|
|
66
|
+
const onColorChangeRef = useLatest(options.onColorChange);
|
|
64
67
|
|
|
65
68
|
// Reads nothing but refs, so it is stable and an effect may depend on it.
|
|
66
69
|
const shown = useCallback(
|
|
67
|
-
(): boolean => overRef.current
|
|
68
|
-
[],
|
|
70
|
+
(): boolean => lensShown(overRef.current, enabledRef.current, peekingRef.current),
|
|
71
|
+
[enabledRef],
|
|
69
72
|
);
|
|
70
73
|
|
|
71
74
|
const modelRef = useRef<LoupeModel | null>(null);
|
|
72
75
|
if (modelRef.current === null) {
|
|
73
76
|
modelRef.current = createLoupeModel({
|
|
74
|
-
mode:
|
|
75
|
-
factor:
|
|
76
|
-
minFactor:
|
|
77
|
-
maxFactor:
|
|
77
|
+
mode: options.mode,
|
|
78
|
+
factor: options.factor,
|
|
79
|
+
minFactor: options.minFactor,
|
|
80
|
+
maxFactor: options.maxFactor,
|
|
78
81
|
onColorChange: (hex) => onColorChangeRef.current?.(hex),
|
|
79
82
|
surface: {
|
|
80
83
|
lens: () => {
|
|
@@ -94,8 +97,7 @@ export function useLoupe({ capability, hostRef, enabled, sample }: UseLoupeOptio
|
|
|
94
97
|
}
|
|
95
98
|
const model = modelRef.current;
|
|
96
99
|
|
|
97
|
-
const diameterRef =
|
|
98
|
-
diameterRef.current = capability.diameter;
|
|
100
|
+
const diameterRef = useLatest(options.diameter);
|
|
99
101
|
const goneRef = useRef(false);
|
|
100
102
|
|
|
101
103
|
// The model holds no resources, and `dispose` is one-way — so unmounting only
|
|
@@ -156,7 +158,8 @@ export function useLoupe({ capability, hostRef, enabled, sample }: UseLoupeOptio
|
|
|
156
158
|
}
|
|
157
159
|
|
|
158
160
|
return {
|
|
159
|
-
|
|
161
|
+
// This render's `enabled`: `enabledRef` holds the last committed one.
|
|
162
|
+
visible: lensShown(overRef.current, enabled, peekingRef.current),
|
|
160
163
|
aim: model.aim,
|
|
161
164
|
factor: model.factor,
|
|
162
165
|
mode: model.mode,
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* and redraws when one is added. jsdom lays nothing out, so the target's rect
|
|
4
4
|
* and the stage's are mocked; the stage's camera is the identity there.
|
|
5
5
|
*/
|
|
6
|
-
import { act
|
|
6
|
+
import { act } from '@testing-library/react';
|
|
7
|
+
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
7
8
|
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
8
9
|
import { useAnnotations } from '../annotations/AnnotationsContext';
|
|
9
10
|
import type { AnnotationsApi } from '../annotations/types';
|
|
@@ -33,7 +34,7 @@ let restore: (() => void) | null = null;
|
|
|
33
34
|
afterEach(() => restore?.());
|
|
34
35
|
|
|
35
36
|
/** Stage 400×300, the pane at stage (50, 20) and 200×100. */
|
|
36
|
-
function mount() {
|
|
37
|
+
async function mount() {
|
|
37
38
|
const pane = { current: null as HTMLDivElement | null };
|
|
38
39
|
let api: AnnotationsApi | null = null;
|
|
39
40
|
function Body() {
|
|
@@ -68,21 +69,21 @@ function mount() {
|
|
|
68
69
|
restore = () => {
|
|
69
70
|
HTMLElement.prototype.getBoundingClientRect = original;
|
|
70
71
|
};
|
|
71
|
-
const r =
|
|
72
|
+
const r = await renderSettled(<Lab instruments={[marked]} defaultInstrument="Marked" />);
|
|
72
73
|
return { ...r, api: () => api as AnnotationsApi };
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
const px = (el: HTMLElement, name: string) => Number.parseFloat(el.style.getPropertyValue(name));
|
|
76
77
|
|
|
77
78
|
describe('marks on the overview', () => {
|
|
78
|
-
it('draws nothing for a target with no marks', () => {
|
|
79
|
-
const h = mount();
|
|
79
|
+
it('draws nothing for a target with no marks', async () => {
|
|
80
|
+
const h = await mount();
|
|
80
81
|
expect(h.container.querySelector('.lk-overview__marks')).toBeNull();
|
|
81
82
|
});
|
|
82
83
|
|
|
83
|
-
it("places a target's marks at the target's rect, through the fit", () => {
|
|
84
|
-
const h = mount();
|
|
85
|
-
act(() => {
|
|
84
|
+
it("places a target's marks at the target's rect, through the fit", async () => {
|
|
85
|
+
const h = await mount();
|
|
86
|
+
await act(async () => {
|
|
86
87
|
h.api().add({ target: 'pane', kind: 'rect', frac: { x: 0.1, y: 0.1, w: 0.2, h: 0.2 } });
|
|
87
88
|
});
|
|
88
89
|
const marks = h.container.querySelector<HTMLElement>('.lk-overview__marks[data-target="pane"]');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Every annotation target's marks, drawn read-only on the overview through the
|
|
3
|
-
* export's own vector path: `markSvgNodes`, then `serializeSvg
|
|
3
|
+
* export's own vector path: `markSvgNodes`, then `serializeSvg` — synchronous
|
|
4
|
+
* with no `warmSvg`, since marks export nothing that loads on demand.
|
|
4
5
|
*/
|
|
5
6
|
import type { View } from '@weasel-js/core';
|
|
6
7
|
import { serializeSvg } from '@weasel-js/svg';
|
|
@@ -40,10 +41,17 @@ function elementOf(target: object): HTMLElement | null {
|
|
|
40
41
|
return (target as { ref?: RefObject<HTMLElement | null> }).ref?.current ?? null;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
function markupOf(
|
|
44
|
+
function markupOf(
|
|
45
|
+
api: AnnotationsApi,
|
|
46
|
+
target: string,
|
|
47
|
+
content: { w: number; h: number },
|
|
48
|
+
shown: { w: number; h: number },
|
|
49
|
+
): string {
|
|
44
50
|
const painted = api.paintedMarks(target);
|
|
45
51
|
if (painted.length === 0) return '';
|
|
46
|
-
|
|
52
|
+
// The document is shown at `shown` px, fitted like any SVG viewBox.
|
|
53
|
+
const k = Math.min(shown.w / content.w, shown.h / content.h) || 1;
|
|
54
|
+
const nodes = painted.flatMap(({ mark, style }) => markSvgNodes(mark, style, { x: k, y: k }));
|
|
47
55
|
return serializeSvg(nodes, { viewBox: { x: 0, y: 0, width: content.w, height: content.h } });
|
|
48
56
|
}
|
|
49
57
|
|
|
@@ -69,7 +77,7 @@ export function OverviewMarks({ fit, camera }: OverviewMarksProps) {
|
|
|
69
77
|
{api.targets().map((t) => {
|
|
70
78
|
const el = elementOf(t);
|
|
71
79
|
const box = el ? localRectOf(el, camera) : null;
|
|
72
|
-
const markup = box ? markupOf(api, t.id, t.content) : '';
|
|
80
|
+
const markup = box ? markupOf(api, t.id, t.content, { w: box.w * s, h: box.h * s }) : '';
|
|
73
81
|
if (!box || !markup) return null;
|
|
74
82
|
const place = {
|
|
75
83
|
['--lk-overview-mx' as string]: `${(box.x - fit.x) * s}px`,
|