@weasel-js/labkit 1.5.2 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -7
- package/dist/_dts/{CanvasStackContext-CMMo180l.d.ts → CanvasStackContext-Wo6OwIVM.d.ts} +1 -1
- package/dist/_dts/FloatingPanel-7j0IcXwz.d.ts +30 -0
- package/dist/_dts/LayerList.d-BoELAMsM.d.ts +83 -0
- package/dist/_dts/{PrefsForm.d-DPy9I8It.d.ts → PrefsRow.d-DFpPIeIC.d.ts} +16 -1
- package/dist/_dts/PropertyField.d-D0ndfIIt.d.ts +404 -0
- package/dist/_dts/{Select.d-ZTiwzYyT.d.ts → Select.d-Cr8-zoup.d.ts} +105 -15
- package/dist/_dts/{usePanZoom-SSTZe410.d.ts → Stage-DWBBi8IU.d.ts} +80 -35
- package/dist/_dts/{index-DlKolEIZ.d.ts → index-CKyX0ME_.d.ts} +12 -13
- package/dist/_dts/{frac-B4hFJMiN.d.ts → types-B7yBTTF9.d.ts} +709 -651
- package/dist/_dts/{types-Db-3NNk1.d.ts → types-DTuViBvc.d.ts} +21 -2
- package/dist/_dts/{useTrialState-pnAE-UBD.d.ts → useTrialState-CMaV6UD4.d.ts} +2 -3
- package/dist/canvas/index.d.ts +41 -8
- package/dist/canvas/index.js +5 -3
- package/dist/chrome/index.d.ts +6 -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-2RVCB2PT.js +3 -0
- package/dist/chunk-2RVCB2PT.js.map +1 -0
- package/dist/chunk-3RY6WL5T.js +38 -0
- package/dist/chunk-3RY6WL5T.js.map +1 -0
- package/dist/{chunk-CKUKF72R.js → chunk-4TT3PNPR.js} +68 -55
- package/dist/chunk-4TT3PNPR.js.map +1 -0
- package/dist/{chunk-KJCUERQF.js → chunk-6FLXL3I7.js} +9 -164
- package/dist/chunk-6FLXL3I7.js.map +1 -0
- package/dist/chunk-ASOK76ZH.js +431 -0
- package/dist/chunk-ASOK76ZH.js.map +1 -0
- package/dist/chunk-AWQG7FCC.js +19 -0
- package/dist/chunk-AWQG7FCC.js.map +1 -0
- package/dist/chunk-CGHF6RZV.js +300 -0
- package/dist/chunk-CGHF6RZV.js.map +1 -0
- package/dist/chunk-DWMSB4CA.js +228 -0
- package/dist/chunk-DWMSB4CA.js.map +1 -0
- package/dist/chunk-GHGO52SO.js +92 -0
- package/dist/chunk-GHGO52SO.js.map +1 -0
- package/dist/{chunk-LRI3MBGR.js → chunk-HKV7LRHY.js} +4 -4
- package/dist/{chunk-LRI3MBGR.js.map → chunk-HKV7LRHY.js.map} +1 -1
- package/dist/{chunk-EMXAIRUJ.js → chunk-IRTGUQ5O.js} +5 -4
- package/dist/chunk-IRTGUQ5O.js.map +1 -0
- package/dist/{chunk-H6ZAOWNE.js → chunk-JVBYV6TL.js} +3 -3
- package/dist/{chunk-H6ZAOWNE.js.map → chunk-JVBYV6TL.js.map} +1 -1
- package/dist/chunk-KHK5FGER.js +145 -0
- package/dist/chunk-KHK5FGER.js.map +1 -0
- package/dist/chunk-NEXR37YJ.js +144 -0
- package/dist/chunk-NEXR37YJ.js.map +1 -0
- package/dist/{chunk-A2ACAGW6.js → chunk-O6FFFOHS.js} +32 -7
- package/dist/chunk-O6FFFOHS.js.map +1 -0
- package/dist/chunk-QGMOA2YE.js +731 -0
- package/dist/chunk-QGMOA2YE.js.map +1 -0
- package/dist/{chunk-ERFMC6WU.js → chunk-S4NCI32V.js} +19 -10
- package/dist/chunk-S4NCI32V.js.map +1 -0
- package/dist/{chunk-753Y6HE6.js → chunk-SLAQPUI4.js} +4 -4
- package/dist/chunk-SLAQPUI4.js.map +1 -0
- package/dist/{chunk-REOBGYMC.js → chunk-TJ77A4A3.js} +3 -5
- package/dist/chunk-TJ77A4A3.js.map +1 -0
- package/dist/chunk-TYN6MLXX.js +13864 -0
- package/dist/chunk-TYN6MLXX.js.map +1 -0
- package/dist/{chunk-RS3HRQWU.js → chunk-WPLAANH4.js} +4 -3
- package/dist/chunk-WPLAANH4.js.map +1 -0
- package/dist/config/index.d.ts +51 -7
- package/dist/config/index.js +4 -5
- package/dist/controls/index.d.ts +92 -5
- package/dist/controls/index.js +5 -4
- package/dist/dragdrop/index.d.ts +9 -6
- package/dist/dragdrop/index.js +3 -2
- package/dist/index.d.ts +108 -154
- package/dist/index.js +456 -600
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +5 -35
- package/dist/layers/index.js +3 -3
- package/dist/loupe/index.d.ts +21 -17
- package/dist/loupe/index.js +5 -3
- package/dist/overview/index.d.ts +56 -0
- package/dist/overview/index.js +288 -0
- package/dist/overview/index.js.map +1 -0
- package/dist/passthrough/weasel-ui.d.ts +434 -231
- package/dist/passthrough/weasel-ui.js +2 -1
- package/dist/primitives/index.d.ts +19 -54
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +8 -7
- package/dist/state/index.js +4 -5
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +360 -174
- package/dist/undo/index.d.ts +5 -4
- package/package.json +13 -13
- package/src/annotations/frac.test.ts +7 -0
- package/src/annotations/index.ts +1 -0
- package/src/annotations/paint.test.ts +21 -0
- package/src/annotations/paint.ts +12 -0
- package/src/annotations/store.test.ts +57 -0
- package/src/annotations/store.ts +25 -6
- package/src/annotations/toolMap.test.ts +37 -2
- package/src/annotations/toolMap.ts +13 -0
- package/src/annotations/types.ts +30 -2
- package/src/canvas/AGENTS.md +8 -10
- package/src/canvas/CameraInput.test.tsx +136 -0
- package/src/canvas/CameraInput.tsx +311 -0
- package/src/canvas/CanvasStack.tsx +42 -58
- package/src/canvas/LinkedCursor.less +16 -0
- package/src/canvas/LinkedCursor.tsx +54 -0
- package/src/canvas/Stage.test.tsx +18 -0
- package/src/canvas/Stage.tsx +42 -37
- package/src/canvas/cameraView.test.ts +60 -0
- package/src/canvas/cameraView.ts +61 -0
- package/src/canvas/index.ts +11 -2
- package/src/chrome/regions/SidebarRegion.less +197 -0
- package/src/chrome/regions/SidebarRegion.test.tsx +27 -0
- package/src/chrome/regions/SidebarRegion.tsx +7 -2
- package/src/chrome/types.ts +5 -2
- package/src/config/autoConfig.ts +2 -2
- package/src/config/builder.ts +21 -2
- package/src/config/entry.test.ts +10 -0
- package/src/config/fromConfigField.ts +1 -0
- package/src/config/index.ts +5 -0
- package/src/config/resolve.ts +11 -1
- package/src/config/rules.ts +2 -0
- package/src/config/sectionTree.test.ts +152 -0
- package/src/config/sectionTree.ts +134 -0
- package/src/config/types.ts +21 -0
- package/src/controls/ControlPanel.less +4 -0
- package/src/controls/ControlPanel.stories.tsx +55 -2
- package/src/controls/ControlPanel.test.tsx +40 -3
- package/src/controls/ControlPanel.tsx +134 -336
- package/src/controls/inDialog.test.tsx +100 -0
- package/src/controls/inDialog.tsx +38 -0
- package/src/controls/index.ts +1 -0
- package/src/index.test.ts +1 -1
- package/src/index.ts +18 -29
- package/src/instrument/SineWave.smoke.test.tsx +2 -0
- package/src/instrument/types.ts +15 -3
- package/src/lab/Lab.nebula.test.tsx +40 -0
- package/src/lab/Lab.test.tsx +3 -1
- package/src/lab/Lab.tsx +42 -19
- package/src/lab/LabBoundary.test.tsx +66 -0
- package/src/lab/LabBoundary.tsx +56 -0
- package/src/lab/LabHeader.tsx +4 -23
- package/src/lab/LabRoot.tsx +2 -3
- package/src/lab/LabShell.less +0 -1
- package/src/lab/LabShell.stories.tsx +15 -0
- package/src/lab/LabShell.test.tsx +18 -0
- package/src/lab/LabShell.tsx +14 -1
- package/src/lab/LabSwitcher.less +1 -1
- package/src/lab/LabSwitcher.test.tsx +41 -0
- package/src/lab/LabSwitcher.tsx +30 -8
- package/src/lab/index.ts +1 -0
- package/src/layers/AGENTS.md +18 -74
- package/src/layers/index.ts +4 -2
- package/src/loupe/AGENTS.md +7 -6
- package/src/loupe/LoupeGestures.tsx +14 -9
- package/src/loupe/TrialLoupe.tsx +5 -5
- package/src/loupe/index.ts +1 -0
- package/src/loupe/loupeActions.ts +6 -2
- package/src/overview/AGENTS.md +38 -0
- package/src/overview/OverviewMarks.test.tsx +100 -0
- package/src/overview/OverviewMarks.tsx +93 -0
- package/src/overview/TrialOverview.less +53 -0
- package/src/overview/TrialOverview.stories.tsx +50 -0
- package/src/overview/TrialOverview.test.tsx +116 -0
- package/src/overview/TrialOverview.tsx +374 -0
- package/src/overview/index.ts +2 -0
- package/src/passthrough/weasel-ui.ts +32 -10
- package/src/primitives/index.ts +0 -2
- package/src/specimen/Specimen.tsx +59 -56
- package/src/state/labRecords.test.ts +4 -1
- package/src/state/store.test.ts +4 -4
- package/src/state/types.ts +2 -1
- package/src/state/useTrialState.test.tsx +1 -1
- package/src/styles.less +6 -2
- package/src/theme/Interstellar.stories.tsx +8 -13
- package/src/theme/base.less +3 -20
- package/src/theme/interstellar.test.ts +6 -3
- package/src/theme/interstellar.theme.json +0 -56
- package/src/theme/interstellar.ts +2 -2
- package/src/tools/labTool.ts +6 -2
- package/src/trial/Trial.annotations.test.tsx +16 -0
- package/src/trial/Trial.canvas.test.tsx +1 -1
- package/src/trial/Trial.loupe.test.tsx +73 -1
- package/src/trial/Trial.stories.tsx +2 -0
- package/src/trial/Trial.tsx +47 -9
- package/src/trial/trialOps.test.ts +4 -4
- package/dist/_dts/index-8LiKe3A-.d.ts +0 -410
- package/dist/chunk-2AYGEN57.js +0 -32
- package/dist/chunk-2AYGEN57.js.map +0 -1
- package/dist/chunk-6BUQ77ZV.js +0 -18
- package/dist/chunk-6BUQ77ZV.js.map +0 -1
- package/dist/chunk-753Y6HE6.js.map +0 -1
- package/dist/chunk-A2ACAGW6.js.map +0 -1
- package/dist/chunk-CKUKF72R.js.map +0 -1
- package/dist/chunk-D35PFTS7.js +0 -170
- package/dist/chunk-D35PFTS7.js.map +0 -1
- package/dist/chunk-EMXAIRUJ.js.map +0 -1
- package/dist/chunk-ERFMC6WU.js.map +0 -1
- package/dist/chunk-FKLT3ITC.js +0 -590
- package/dist/chunk-FKLT3ITC.js.map +0 -1
- package/dist/chunk-KJCUERQF.js.map +0 -1
- package/dist/chunk-LASCBWW7.js +0 -13456
- package/dist/chunk-LASCBWW7.js.map +0 -1
- package/dist/chunk-REOBGYMC.js.map +0 -1
- package/dist/chunk-RS3HRQWU.js.map +0 -1
- package/dist/chunk-TT5C5S6R.js +0 -368
- package/dist/chunk-TT5C5S6R.js.map +0 -1
- package/dist/ui/layers/index.d.ts +0 -66
- package/dist/ui/layers/index.js +0 -3
- package/dist/ui/layers/index.js.map +0 -1
- package/src/canvas/usePanZoom.test.ts +0 -244
- package/src/canvas/usePanZoom.ts +0 -147
- package/src/lab/useSystemMode.ts +0 -30
- package/src/layers/LayerList.less +0 -93
- package/src/layers/LayerList.stories.tsx +0 -66
- package/src/layers/LayerList.test.tsx +0 -232
- package/src/layers/LayerList.tsx +0 -268
- package/src/primitives/Sidebar.less +0 -112
- package/src/primitives/Sidebar.stories.tsx +0 -38
- package/src/primitives/Sidebar.test.tsx +0 -74
- package/src/primitives/Sidebar.tsx +0 -35
- package/src/ui/layers/index.ts +0 -5
package/dist/undo/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export {
|
|
2
|
-
import 'react';
|
|
3
|
-
import '../_dts/types-Db-3NNk1.js';
|
|
1
|
+
export { az as UndoCapability } from '../_dts/types-B7yBTTF9.js';
|
|
4
2
|
import '@weasel-js/core';
|
|
5
|
-
import '
|
|
3
|
+
import 'react';
|
|
4
|
+
import '../_dts/PrefsRow.d-DFpPIeIC.js';
|
|
5
|
+
import '@weasel-js/theme';
|
|
6
|
+
import '../_dts/types-DTuViBvc.js';
|
|
6
7
|
import '../_dts/types-DJ79Tg5J.js';
|
|
7
8
|
|
|
8
9
|
/** A subscriber to a named event. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/labkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "React widgets for building self-contained interactive lab pages",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "orochi235",
|
|
@@ -74,6 +74,10 @@
|
|
|
74
74
|
"types": "./dist/job/index.d.ts",
|
|
75
75
|
"import": "./dist/job/index.js"
|
|
76
76
|
},
|
|
77
|
+
"./overview": {
|
|
78
|
+
"types": "./dist/overview/index.d.ts",
|
|
79
|
+
"import": "./dist/overview/index.js"
|
|
80
|
+
},
|
|
77
81
|
"./weasel-ui": {
|
|
78
82
|
"types": "./dist/passthrough/weasel-ui.d.ts",
|
|
79
83
|
"import": "./dist/passthrough/weasel-ui.js"
|
|
@@ -82,10 +86,6 @@
|
|
|
82
86
|
"types": "./dist/passthrough/weasel-canvas.d.ts",
|
|
83
87
|
"import": "./dist/passthrough/weasel-canvas.js"
|
|
84
88
|
},
|
|
85
|
-
"./ui/layers": {
|
|
86
|
-
"types": "./dist/ui/layers/index.d.ts",
|
|
87
|
-
"import": "./dist/ui/layers/index.js"
|
|
88
|
-
},
|
|
89
89
|
"./styles.css": "./dist/styles.css",
|
|
90
90
|
"./loupe": {
|
|
91
91
|
"types": "./dist/loupe/index.d.ts",
|
|
@@ -115,17 +115,17 @@
|
|
|
115
115
|
"dev:3d": "LABKIT_EXAMPLE=3d-lab vite"
|
|
116
116
|
},
|
|
117
117
|
"peerDependencies": {
|
|
118
|
-
"@weasel-js/core": "1.
|
|
119
|
-
"@weasel-js/theme": "1.
|
|
118
|
+
"@weasel-js/core": "1.6.1",
|
|
119
|
+
"@weasel-js/theme": "1.6.1",
|
|
120
120
|
"react": "^19.0.0",
|
|
121
121
|
"react-dom": "^19.0.0"
|
|
122
122
|
},
|
|
123
123
|
"dependencies": {
|
|
124
|
-
"@weasel-js/geom": "1.
|
|
125
|
-
"@weasel-js/kernel3d": "1.
|
|
126
|
-
"@weasel-js/loupe": "1.
|
|
127
|
-
"@weasel-js/svg": "1.
|
|
128
|
-
"@weasel-js/ui": "1.
|
|
124
|
+
"@weasel-js/geom": "1.6.1",
|
|
125
|
+
"@weasel-js/kernel3d": "1.6.1",
|
|
126
|
+
"@weasel-js/loupe": "1.6.1",
|
|
127
|
+
"@weasel-js/svg": "1.6.1",
|
|
128
|
+
"@weasel-js/ui": "1.6.1",
|
|
129
129
|
"earcut": "2.2.4",
|
|
130
130
|
"polygon-clipping": "^0.15.7",
|
|
131
131
|
"react-aria-components": "^1.5.0",
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
"@biomejs/biome": "^2.4.12",
|
|
137
137
|
"@rollup/plugin-alias": "^6.0.0",
|
|
138
138
|
"@testing-library/jest-dom": "^6.6.0",
|
|
139
|
-
"@weasel-js/modes": "1.
|
|
139
|
+
"@weasel-js/modes": "1.6.1",
|
|
140
140
|
"less": "^4.2.0",
|
|
141
141
|
"rollup-plugin-dts": "^6.4.1",
|
|
142
142
|
"tsx": "^4.19.0",
|
|
@@ -51,6 +51,13 @@ describe('fraction hit geometry', () => {
|
|
|
51
51
|
expect(fracContains(box, { x: 0.18, y: 0.3 }, 0.05)).toBe(true);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
it('reaches a zero-size box through the tolerance alone', () => {
|
|
55
|
+
const dot: FracRect = { x: 0.25, y: 0.5, w: 0, h: 0 };
|
|
56
|
+
expect(fracContains(dot, { x: 0.25, y: 0.5 })).toBe(true);
|
|
57
|
+
expect(fracContains(dot, { x: 0.26, y: 0.51 })).toBe(false);
|
|
58
|
+
expect(fracContains(dot, { x: 0.26, y: 0.51 }, 0.02)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
54
61
|
it('encloses only a box that lies wholly inside', () => {
|
|
55
62
|
expect(fracEncloses({ x: 0, y: 0, w: 1, h: 1 }, box)).toBe(true);
|
|
56
63
|
// Overlapping is not containing: a marquee takes what it encloses.
|
package/src/annotations/index.ts
CHANGED
|
@@ -99,6 +99,20 @@ describe('markCommands', () => {
|
|
|
99
99
|
it('draws an untitled text mark as nothing rather than an empty run', () => {
|
|
100
100
|
expect(markCommands(mark('text'), CONTENT)).toEqual([]);
|
|
101
101
|
});
|
|
102
|
+
|
|
103
|
+
it('draws a point as a small unfilled ring centered on its one stored point', () => {
|
|
104
|
+
const cmds = markCommands(mark('point', { points: [{ x: 0.25, y: 0.5 }] }), CONTENT);
|
|
105
|
+
expect(cmds).toHaveLength(1);
|
|
106
|
+
expect((cmds[0] as { fill?: unknown }).fill).toBeUndefined();
|
|
107
|
+
const pts = anchors(cmds);
|
|
108
|
+
const xs = pts.filter((_, i) => i % 2 === 0);
|
|
109
|
+
const ys = pts.filter((_, i) => i % 2 === 1);
|
|
110
|
+
// Centered on (50, 50) in world, radius 4.
|
|
111
|
+
expect(Math.min(...xs)).toBeCloseTo(46, 4);
|
|
112
|
+
expect(Math.max(...xs)).toBeCloseTo(54, 4);
|
|
113
|
+
expect(Math.min(...ys)).toBeCloseTo(46, 4);
|
|
114
|
+
expect(Math.max(...ys)).toBeCloseTo(54, 4);
|
|
115
|
+
});
|
|
102
116
|
});
|
|
103
117
|
|
|
104
118
|
describe('markCommands styling', () => {
|
|
@@ -117,6 +131,13 @@ describe('markCommands styling', () => {
|
|
|
117
131
|
expect((stale as { stroke?: { dash?: number[] } }).stroke?.dash).toEqual([6, 4]);
|
|
118
132
|
});
|
|
119
133
|
|
|
134
|
+
it('dashes a stale point ring like any other mark', () => {
|
|
135
|
+
const [ring] = markCommands(mark('point', { points: [{ x: 0.25, y: 0.5 }] }), CONTENT, {
|
|
136
|
+
stale: true,
|
|
137
|
+
});
|
|
138
|
+
expect((ring as { stroke?: { dash?: number[] } }).stroke?.dash).toEqual([6, 4]);
|
|
139
|
+
});
|
|
140
|
+
|
|
120
141
|
it("colors a text mark's glyphs too, not only the outlines", () => {
|
|
121
142
|
const [cmd] = markCommands(mark('text', { title: 'x' }), CONTENT, { color: '#30a46c' });
|
|
122
143
|
const runs = (cmd as { runs: { fill?: { color?: string } }[] }).runs;
|
package/src/annotations/paint.ts
CHANGED
|
@@ -17,6 +17,7 @@ const MARK_COLOR = '#e5484d';
|
|
|
17
17
|
* marker geometry can be resolved here, which needs a resolved width. */
|
|
18
18
|
const MARK_WIDTH = 2;
|
|
19
19
|
const TEXT_SIZE = 14;
|
|
20
|
+
const POINT_RADIUS = 4;
|
|
20
21
|
|
|
21
22
|
/** The subset of a mark's scene node this needs: where it is, and what it is. */
|
|
22
23
|
export interface PaintableMark {
|
|
@@ -108,6 +109,17 @@ export function markCommands(
|
|
|
108
109
|
}
|
|
109
110
|
case 'stroke':
|
|
110
111
|
return [{ kind: 'path', path: polyline(vertices(m, content)), stroke }];
|
|
112
|
+
case 'point': {
|
|
113
|
+
const stored = m.data.points?.[0];
|
|
114
|
+
const c = stored ? toWorld(stored, content) : { x: m.pose.x, y: m.pose.y };
|
|
115
|
+
const ring = {
|
|
116
|
+
x: c.x - POINT_RADIUS,
|
|
117
|
+
y: c.y - POINT_RADIUS,
|
|
118
|
+
width: 2 * POINT_RADIUS,
|
|
119
|
+
height: 2 * POINT_RADIUS,
|
|
120
|
+
};
|
|
121
|
+
return [{ kind: 'path', path: ellipsePath(ring), stroke }];
|
|
122
|
+
}
|
|
111
123
|
case 'text': {
|
|
112
124
|
const text = m.data.title;
|
|
113
125
|
if (!text) return [];
|
|
@@ -245,6 +245,37 @@ describe('the annotation store', () => {
|
|
|
245
245
|
expect(revived.query()).toHaveLength(1);
|
|
246
246
|
});
|
|
247
247
|
|
|
248
|
+
it('round-trips a point mark with its one stored point and zero-size bounds', () => {
|
|
249
|
+
const store = makeStore();
|
|
250
|
+
const id = store.add({
|
|
251
|
+
target: 'naive',
|
|
252
|
+
kind: 'point',
|
|
253
|
+
frac: { x: 0.25, y: 0.5, w: 0, h: 0 },
|
|
254
|
+
points: [{ x: 0.25, y: 0.5 }],
|
|
255
|
+
});
|
|
256
|
+
expect(store.get(id)).toMatchObject({
|
|
257
|
+
kind: 'point',
|
|
258
|
+
frac: { x: 0.25, y: 0.5, w: 0, h: 0 },
|
|
259
|
+
points: [{ x: 0.25, y: 0.5 }],
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const revived = annotationsFromJSON(JSON.parse(JSON.stringify(store.toJSON())), () => TARGETS);
|
|
263
|
+
expect(revived.get(id)).toEqual(store.get(id));
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('hit-tests a point mark through the tolerance, since its bounds have no area', () => {
|
|
267
|
+
const store = makeStore();
|
|
268
|
+
const id = store.add({
|
|
269
|
+
target: 'naive',
|
|
270
|
+
kind: 'point',
|
|
271
|
+
frac: { x: 0.25, y: 0.5, w: 0, h: 0 },
|
|
272
|
+
points: [{ x: 0.25, y: 0.5 }],
|
|
273
|
+
});
|
|
274
|
+
expect(store.hitTest('naive', { x: 0.26, y: 0.51 })).toHaveLength(0);
|
|
275
|
+
expect(store.hitTest('naive', { x: 0.26, y: 0.51 }, 0.02).map((a) => a.id)).toEqual([id]);
|
|
276
|
+
expect(store.hitTest('naive', { x: 0.3, y: 0.51 }, 0.02)).toHaveLength(0);
|
|
277
|
+
});
|
|
278
|
+
|
|
248
279
|
it('reads a meaning passed as a thunk at each capture', async () => {
|
|
249
280
|
let color = '#111111';
|
|
250
281
|
const store = annotationsFromJSON(null, () => TARGETS, {
|
|
@@ -263,3 +294,29 @@ describe('the annotation store', () => {
|
|
|
263
294
|
expect(svg).toContain('#222222');
|
|
264
295
|
});
|
|
265
296
|
});
|
|
297
|
+
|
|
298
|
+
describe('paintedMarks', () => {
|
|
299
|
+
it("hands over a target's marks in world units with their resolved style", () => {
|
|
300
|
+
let config: Record<string, string> = { angle: 'iso', shading: 'outline' };
|
|
301
|
+
const store = createAnnotationStore({
|
|
302
|
+
targets: () => TARGETS,
|
|
303
|
+
meaning: { statuses: [{ id: 'open', label: 'Open', color: '#123456' }] },
|
|
304
|
+
config: () => config,
|
|
305
|
+
});
|
|
306
|
+
store.add(RING, config);
|
|
307
|
+
const [painted] = store.paintedMarks('naive');
|
|
308
|
+
expect(painted?.mark.data.kind).toBe('rect');
|
|
309
|
+
expect(painted?.mark.pose.x).toBeCloseTo(0.3322 * 256);
|
|
310
|
+
expect(painted?.style).toEqual({ color: '#123456', stale: false });
|
|
311
|
+
|
|
312
|
+
// A config the mark no longer describes draws it dashed, as the pane does.
|
|
313
|
+
config = { angle: 'top', shading: 'outline' };
|
|
314
|
+
expect(store.paintedMarks('naive')[0]?.style.stale).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('answers nothing for a target with no marks or no declaration', () => {
|
|
318
|
+
const store = makeStore();
|
|
319
|
+
expect(store.paintedMarks('occt')).toEqual([]);
|
|
320
|
+
expect(store.paintedMarks('nowhere')).toEqual([]);
|
|
321
|
+
});
|
|
322
|
+
});
|
package/src/annotations/store.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
sceneFromJSON,
|
|
7
7
|
} from '@weasel-js/core';
|
|
8
8
|
import { captureTarget } from './capture';
|
|
9
|
+
import { type MarkDrawOptions, resolveMarkStyle } from './drawOne';
|
|
9
10
|
import type { WorldRect } from './frac';
|
|
10
11
|
import { fracContains, fracEncloses, fracToWorld, roundFrac, worldToFrac } from './frac';
|
|
11
12
|
import { MarkHistory } from './history';
|
|
@@ -123,6 +124,14 @@ export function createAnnotationStore(opts: AnnotationStoreOptions): Annotations
|
|
|
123
124
|
targets().find((t) => t.id === id);
|
|
124
125
|
|
|
125
126
|
const contentOf = (id: string) => targetOf(id)?.content ?? NO_CONTENT;
|
|
127
|
+
/** How a target's marks are drawn right now — one answer for the pane, an
|
|
128
|
+
* export and an overview, so none can disagree on a color or a dash. */
|
|
129
|
+
const drawOptionsFor = (info: AnnotationTargetInfo): MarkDrawOptions => ({
|
|
130
|
+
content: info.content,
|
|
131
|
+
positionDependsOn: info.positionDependsOn,
|
|
132
|
+
config: opts.config?.(),
|
|
133
|
+
meaning: typeof opts.meaning === 'function' ? opts.meaning() : opts.meaning,
|
|
134
|
+
});
|
|
126
135
|
|
|
127
136
|
const project = (target: string, node: string): Annotation | undefined => {
|
|
128
137
|
const found = scenes.get(target)?.get(asNodeId(node));
|
|
@@ -304,6 +313,21 @@ export function createAnnotationStore(opts: AnnotationStoreOptions): Annotations
|
|
|
304
313
|
if (found) found.scene.remove(asNodeId(found.node.id));
|
|
305
314
|
},
|
|
306
315
|
|
|
316
|
+
paintedMarks(target) {
|
|
317
|
+
const info = targetOf(target);
|
|
318
|
+
const scene = scenes.get(target);
|
|
319
|
+
if (!info || !scene) return [];
|
|
320
|
+
const draw = drawOptionsFor(info);
|
|
321
|
+
return [...scene.renderOrder()].flatMap((id) => {
|
|
322
|
+
const node = scene.get(asNodeId(String(id)));
|
|
323
|
+
if (!node) return [];
|
|
324
|
+
const data = node.data as AnnotationData;
|
|
325
|
+
return [
|
|
326
|
+
{ mark: { pose: node.pose as MarkPose, data }, style: resolveMarkStyle(data, draw) },
|
|
327
|
+
];
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
|
|
307
331
|
async capture(target: string, captureOpts?: CaptureOptions) {
|
|
308
332
|
const info = targetOf(target);
|
|
309
333
|
if (!info) throw new Error(`[labkit] no annotation target called '${target}'`);
|
|
@@ -311,12 +335,7 @@ export function createAnnotationStore(opts: AnnotationStoreOptions): Annotations
|
|
|
311
335
|
{
|
|
312
336
|
target,
|
|
313
337
|
scene: sceneFor(target),
|
|
314
|
-
draw:
|
|
315
|
-
content: info.content,
|
|
316
|
-
positionDependsOn: info.positionDependsOn,
|
|
317
|
-
config: opts.config?.(),
|
|
318
|
-
meaning: typeof opts.meaning === 'function' ? opts.meaning() : opts.meaning,
|
|
319
|
-
},
|
|
338
|
+
draw: drawOptionsFor(info),
|
|
320
339
|
base: info.base,
|
|
321
340
|
onWarn: (message) => console.warn(`[labkit] capture: ${message}`),
|
|
322
341
|
},
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { KIT_SHAPE_KINDS } from '@weasel-js/core';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import type { InstrumentList } from '../instrument/types';
|
|
4
|
+
import { resolveLabTool } from '../tools/labTool';
|
|
5
|
+
import { ANNOTATION_TOOLS, annotationToolInfo, labAnnotationTools } from './toolMap';
|
|
6
|
+
import type { AnnotationKind, AnnotationToolId } from './types';
|
|
5
7
|
|
|
6
8
|
const KINDS: AnnotationKind[] = ['stroke', 'line', 'arrow', 'rect', 'ellipse', 'text'];
|
|
7
9
|
|
|
@@ -38,3 +40,36 @@ describe('the annotation tool table', () => {
|
|
|
38
40
|
expect(annotationToolInfo('hand')).toBeUndefined();
|
|
39
41
|
});
|
|
40
42
|
});
|
|
43
|
+
|
|
44
|
+
describe('the tools a lab rail carries', () => {
|
|
45
|
+
const ids = (instruments: InstrumentList) => labAnnotationTools(instruments).map((t) => t.id);
|
|
46
|
+
const inst = (annotations?: { tools?: AnnotationToolId[] }) =>
|
|
47
|
+
({
|
|
48
|
+
annotations: annotations && { targets: () => [], ...annotations },
|
|
49
|
+
}) as unknown as InstrumentList[number];
|
|
50
|
+
|
|
51
|
+
it('carries none when no instrument annotates', () => {
|
|
52
|
+
expect(ids([inst()])).toEqual([]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('carries every tool for an instrument that names none', () => {
|
|
56
|
+
expect(ids([inst({})])).toEqual(ANNOTATION_TOOLS.map((t) => t.id));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('carries only the named tools, in the kit order', () => {
|
|
60
|
+
expect(ids([inst({ tools: ['select', 'pointer'] })])).toEqual(['pointer', 'select']);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('carries the union across annotating instruments', () => {
|
|
64
|
+
expect(ids([inst({ tools: ['pointer'] }), inst({ tools: ['rect'] }), inst()])).toEqual([
|
|
65
|
+
'pointer',
|
|
66
|
+
'rect',
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('starts the lab in pointer when the rail has it, else in its first tool', () => {
|
|
71
|
+
expect(resolveLabTool(null, [inst({ tools: ['select', 'pointer'] })])).toBe('pointer');
|
|
72
|
+
expect(resolveLabTool(null, [inst({ tools: ['text', 'select'] })])).toBe('select');
|
|
73
|
+
expect(resolveLabTool(null, [inst()])).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
SelectIcon,
|
|
10
10
|
TextIcon,
|
|
11
11
|
} from '@weasel-js/ui';
|
|
12
|
+
import type { InstrumentList } from '../instrument/types';
|
|
12
13
|
import type { TrialTool } from '../tools/types';
|
|
13
14
|
import type { AnnotationKind } from './types';
|
|
14
15
|
|
|
@@ -46,6 +47,18 @@ export const ANNOTATION_TOOLS: readonly TrialTool[] = [
|
|
|
46
47
|
{ id: 'text', label: 'Text', icon: TextIcon, group: 'annotate' },
|
|
47
48
|
];
|
|
48
49
|
|
|
50
|
+
/** The annotation tools a lab's rail carries for these instruments: none
|
|
51
|
+
* without an annotating instrument, else the union of what each asks for. */
|
|
52
|
+
export function labAnnotationTools(instruments: InstrumentList): readonly TrialTool[] {
|
|
53
|
+
const wanted = new Set<string>();
|
|
54
|
+
for (const i of instruments) {
|
|
55
|
+
if (i.annotations == null) continue;
|
|
56
|
+
if (i.annotations.tools == null) return ANNOTATION_TOOLS;
|
|
57
|
+
for (const id of i.annotations.tools) wanted.add(id);
|
|
58
|
+
}
|
|
59
|
+
return ANNOTATION_TOOLS.filter((t) => wanted.has(t.id));
|
|
60
|
+
}
|
|
61
|
+
|
|
49
62
|
/** What `id` drives, or undefined for a tool that is not one of these. */
|
|
50
63
|
export function annotationToolInfo(id: string | null): AnnotationToolInfo | undefined {
|
|
51
64
|
return id === null ? undefined : TOOLS[id];
|
package/src/annotations/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RefObject } from 'react';
|
|
2
2
|
import type { ViewTransform } from '../instrument/types';
|
|
3
3
|
import type { TrialInfo } from '../state/types';
|
|
4
|
+
import type { MarkStyle, PaintableMark } from './paint';
|
|
4
5
|
import type { MarkScene } from './store';
|
|
5
6
|
|
|
6
7
|
/** A point in fractions of a target's content box. */
|
|
@@ -21,8 +22,9 @@ export interface FracRect {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/** What shape a mark is. The kinds map onto weasel's own tools; an arrow is a
|
|
24
|
-
* line carrying an end marker, not a separate geometry.
|
|
25
|
-
|
|
25
|
+
* line carrying an end marker, not a separate geometry. A point holds one
|
|
26
|
+
* entry in `points` and zero-size bounds at that position. */
|
|
27
|
+
export type AnnotationKind = 'stroke' | 'line' | 'arrow' | 'rect' | 'ellipse' | 'text' | 'point';
|
|
26
28
|
|
|
27
29
|
/** One selectable status in an instrument's meaning tier. */
|
|
28
30
|
export interface AnnotationStatus {
|
|
@@ -159,10 +161,25 @@ export interface AnnotationStorage {
|
|
|
159
161
|
|
|
160
162
|
/** Declares that an instrument accepts marks: which regions take them,
|
|
161
163
|
* optionally what a mark is allowed to mean, and optionally where they live. */
|
|
164
|
+
/** The annotation tools a lab's rail can carry. */
|
|
165
|
+
export type AnnotationToolId =
|
|
166
|
+
| 'pointer'
|
|
167
|
+
| 'select'
|
|
168
|
+
| 'stroke'
|
|
169
|
+
| 'line'
|
|
170
|
+
| 'arrow'
|
|
171
|
+
| 'rect'
|
|
172
|
+
| 'ellipse'
|
|
173
|
+
| 'text';
|
|
174
|
+
|
|
162
175
|
export interface AnnotationsCapability<TS = unknown, TC = unknown> {
|
|
163
176
|
/** `trial` is which trial is asking: a declaration made once per instrument
|
|
164
177
|
* is called once per trial, and its targets are that trial's own. */
|
|
165
178
|
targets: (state: TS, config: TC, trial: TrialInfo) => readonly AnnotationTarget[];
|
|
179
|
+
/** Which annotation tools the lab's rail offers. The rail is shared, so it
|
|
180
|
+
* carries every tool any annotating instrument asks for, in the kit's own
|
|
181
|
+
* order; an instrument that leaves this unset asks for all of them. */
|
|
182
|
+
tools?: readonly AnnotationToolId[];
|
|
166
183
|
meaning?: AnnotationMeaning;
|
|
167
184
|
/** An instrument replacing this one under a live trial must pass the same
|
|
168
185
|
* object: the trial keeps the marks it loaded from the first. */
|
|
@@ -182,6 +199,12 @@ export interface SerializedAnnotations {
|
|
|
182
199
|
scenes: Record<string, unknown>;
|
|
183
200
|
}
|
|
184
201
|
|
|
202
|
+
/** One mark, as `AnnotationsApi.paintedMarks` hands it over. */
|
|
203
|
+
export interface PaintedMark {
|
|
204
|
+
mark: PaintableMark;
|
|
205
|
+
style: MarkStyle;
|
|
206
|
+
}
|
|
207
|
+
|
|
185
208
|
/** Everything a host can ask or tell labkit about the marks on its targets. */
|
|
186
209
|
export interface AnnotationsApi {
|
|
187
210
|
/** The scene a target's marks live in, created on first ask. One per target
|
|
@@ -221,6 +244,11 @@ export interface AnnotationsApi {
|
|
|
221
244
|
undo(): boolean;
|
|
222
245
|
redo(): boolean;
|
|
223
246
|
|
|
247
|
+
/** A target's marks as its pane paints them, in scene order: geometry in
|
|
248
|
+
* the target's content box, each with the style its status and staleness
|
|
249
|
+
* resolve to. For a painter outside the pane — an overview. */
|
|
250
|
+
paintedMarks(target: string): PaintedMark[];
|
|
251
|
+
|
|
224
252
|
/** Export a target's picture with its marks on it. Rejects on an id the
|
|
225
253
|
* instrument does not declare. */
|
|
226
254
|
capture(target: string, opts?: CaptureOptions): Promise<CaptureResult>;
|
package/src/canvas/AGENTS.md
CHANGED
|
@@ -6,18 +6,16 @@ For a renderer labkit does not own — three.js, raw WebGL, anything with its ow
|
|
|
6
6
|
render loop — see `src/surface/AGENTS.md` instead. `CanvasStack` is 2D and
|
|
7
7
|
schedules its own layers; a foreign renderer wants rects and dirtiness only.
|
|
8
8
|
|
|
9
|
-
`useOrbit` also lives here, as the 3D peer of `usePanZoom`. It produces a trial
|
|
10
|
-
view (`{ yaw, pitch, distance, target }`) rather than a matrix — turning that
|
|
11
|
-
into a camera is the host's job.
|
|
12
|
-
|
|
13
9
|
## Files
|
|
14
10
|
|
|
15
11
|
| File | Role |
|
|
16
12
|
|---|---|
|
|
17
|
-
| `CanvasStack.tsx` | Layered `<canvas>` container; owns sizing
|
|
13
|
+
| `CanvasStack.tsx` | Layered `<canvas>` container; owns sizing and per-layer canvas refs |
|
|
18
14
|
| `CanvasStackContext.ts` | React context exposing the current `view` to descendants |
|
|
19
15
|
| `useLayerScheduler.ts` | DPR-aware rAF scheduler; redraws dirty layers on view/state changes |
|
|
20
|
-
| `
|
|
16
|
+
| `CameraInput.tsx` | Pan, zoom and tap routed through weasel's gesture dispatcher; `CameraContext`; publishes the pointer |
|
|
17
|
+
| `cameraView.ts` | The camera as a weasel `View` over frame-local coordinates, and the anchored zoom clamp |
|
|
18
|
+
| `LinkedCursor.tsx` | The crosshair on the camera's element while the pointer is over another view |
|
|
21
19
|
| `camera.ts` | `zoomAt` (fixed-point zoom) and `centerOn` (put a world point at a viewport's middle) |
|
|
22
20
|
| `canvasCoords.ts` | Pure `screenToWorld` / `worldToScreen` helpers |
|
|
23
21
|
| `worldSpec.ts` | The instrument's declared coordinate system, and the camera derived from it |
|
|
@@ -32,8 +30,8 @@ interface CanvasStackProps {
|
|
|
32
30
|
onViewChange: (v: ViewTransform) => void;
|
|
33
31
|
worldSpec?: WorldSpec; // origin + y direction; default top-left, y down
|
|
34
32
|
onResize?: (size: ViewportSize) => void;
|
|
35
|
-
minZoom?: number; // default 0.1
|
|
36
|
-
maxZoom?: number; // default 32
|
|
33
|
+
minZoom?: number; // default 0.1; the opening zoom stays reachable
|
|
34
|
+
maxZoom?: number; // default 32
|
|
37
35
|
width?: number | string; // default '100%'
|
|
38
36
|
height?: number | string; // default '100%'
|
|
39
37
|
className?: string;
|
|
@@ -93,11 +91,11 @@ convention: origin at the element's top-left, y downward.
|
|
|
93
91
|
`resolveFrame(spec, size)` turns a spec plus a measured viewport into a
|
|
94
92
|
`WorldFrame` — `{ originPx, yDir }` — which is what the rest of the directory
|
|
95
93
|
actually consumes. `CanvasStack` owns the measurement, so it owns the frame and
|
|
96
|
-
hands it to `
|
|
94
|
+
hands it to the camera (`useCameraView`), to the scheduler, and to every `render`.
|
|
97
95
|
|
|
98
96
|
**Every world↔screen path must go through the frame.** There are four, and each
|
|
99
97
|
one is a silent wrong answer if it is missed: `worldToScreen` / `screenToWorld`,
|
|
100
|
-
the camera (`applyCamera`), the
|
|
98
|
+
the camera (`applyCamera`), the camera as a weasel `View` (`cameraView.ts`), and
|
|
101
99
|
`DragDropRuntime`'s drop position. A miss produces geometry that is off by a
|
|
102
100
|
constant, or a wheel that drifts — never an error.
|
|
103
101
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A labkit camera on weasel's dispatcher: drag pans by the screen delta, the
|
|
3
|
+
* zoom range holds with the opening zoom kept reachable, and the pointer is
|
|
4
|
+
* published in the instrument's world.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { act, render } from '@testing-library/react';
|
|
8
|
+
import { createPointerStore, PointerContextProvider } from '@weasel-js/core';
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import type { ViewTransform } from '../instrument/types';
|
|
12
|
+
import { CanvasStack } from './CanvasStack';
|
|
13
|
+
import { Stage } from './Stage';
|
|
14
|
+
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
HTMLCanvasElement.prototype.getContext = vi.fn(
|
|
17
|
+
() => null,
|
|
18
|
+
) as unknown as HTMLCanvasElement['getContext'];
|
|
19
|
+
const proto = HTMLElement.prototype as unknown as Record<string, unknown>;
|
|
20
|
+
proto.setPointerCapture = vi.fn();
|
|
21
|
+
proto.releasePointerCapture = vi.fn();
|
|
22
|
+
proto.hasPointerCapture = vi.fn(() => true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function lastOf<T>(list: readonly T[]): T {
|
|
26
|
+
const item = list[list.length - 1];
|
|
27
|
+
if (item === undefined) throw new Error('nothing recorded');
|
|
28
|
+
return item;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function pointer(el: Element, type: string, x: number, y: number, buttons = 1) {
|
|
32
|
+
el.dispatchEvent(
|
|
33
|
+
new PointerEvent(type, {
|
|
34
|
+
bubbles: true,
|
|
35
|
+
cancelable: true,
|
|
36
|
+
pointerId: 1,
|
|
37
|
+
clientX: x,
|
|
38
|
+
clientY: y,
|
|
39
|
+
button: 0,
|
|
40
|
+
buttons,
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function mountStage(initial: ViewTransform, opts: { minZoom?: number; maxZoom?: number } = {}) {
|
|
46
|
+
const seen: ViewTransform[] = [];
|
|
47
|
+
function Host() {
|
|
48
|
+
const [view, setView] = useState(initial);
|
|
49
|
+
return (
|
|
50
|
+
<Stage
|
|
51
|
+
size={{ width: 100, height: 100 }}
|
|
52
|
+
view={view}
|
|
53
|
+
onViewChange={(v) => {
|
|
54
|
+
seen.push(v);
|
|
55
|
+
setView(v);
|
|
56
|
+
}}
|
|
57
|
+
{...opts}
|
|
58
|
+
>
|
|
59
|
+
<div />
|
|
60
|
+
</Stage>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const { container } = render(<Host />);
|
|
64
|
+
const host = container.querySelector('.lk-stage') as HTMLElement;
|
|
65
|
+
return { host, seen };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('camera input', () => {
|
|
69
|
+
it('pans by the screen delta of a drag', () => {
|
|
70
|
+
const { host, seen } = mountStage({ zoom: 2, pan: { x: 10, y: 20 } });
|
|
71
|
+
act(() => {
|
|
72
|
+
pointer(host, 'pointerdown', 50, 50);
|
|
73
|
+
pointer(host, 'pointermove', 60, 55);
|
|
74
|
+
pointer(host, 'pointermove', 80, 70);
|
|
75
|
+
pointer(host, 'pointerup', 80, 70, 0);
|
|
76
|
+
});
|
|
77
|
+
const last = lastOf(seen);
|
|
78
|
+
expect(last.zoom).toBeCloseTo(2);
|
|
79
|
+
expect(last.pan.x).toBeCloseTo(40);
|
|
80
|
+
expect(last.pan.y).toBeCloseTo(40);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('stops the zoom at the bound, anchored on the pointer', () => {
|
|
84
|
+
const { host, seen } = mountStage({ zoom: 1, pan: { x: 0, y: 0 } }, { maxZoom: 1.2 });
|
|
85
|
+
act(() => {
|
|
86
|
+
host.dispatchEvent(
|
|
87
|
+
new WheelEvent('wheel', {
|
|
88
|
+
bubbles: true,
|
|
89
|
+
cancelable: true,
|
|
90
|
+
deltaY: -2000,
|
|
91
|
+
clientX: 40,
|
|
92
|
+
clientY: 30,
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
const last = lastOf(seen);
|
|
97
|
+
expect(last.zoom).toBeCloseTo(1.2);
|
|
98
|
+
// The world point under (40, 30) stayed there.
|
|
99
|
+
expect((40 - last.pan.x) / last.zoom).toBeCloseTo(40);
|
|
100
|
+
expect((30 - last.pan.y) / last.zoom).toBeCloseTo(30);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('keeps the opening zoom reachable past a narrower range', () => {
|
|
104
|
+
const { host, seen } = mountStage({ zoom: 4, pan: { x: 0, y: 0 } }, { maxZoom: 2 });
|
|
105
|
+
act(() => {
|
|
106
|
+
host.dispatchEvent(
|
|
107
|
+
new WheelEvent('wheel', {
|
|
108
|
+
bubbles: true,
|
|
109
|
+
cancelable: true,
|
|
110
|
+
deltaY: 100,
|
|
111
|
+
clientX: 0,
|
|
112
|
+
clientY: 0,
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
// Zooming out from 4 lands between 2 and 4, not snapped to 2.
|
|
117
|
+
expect(lastOf(seen).zoom).toBeGreaterThan(2);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("publishes the pointer in the instrument's world as the stage", () => {
|
|
121
|
+
const store = createPointerStore();
|
|
122
|
+
const { container } = render(
|
|
123
|
+
<PointerContextProvider store={store}>
|
|
124
|
+
<CanvasStack
|
|
125
|
+
layers={[]}
|
|
126
|
+
view={{ zoom: 2, pan: { x: 0, y: 0 } }}
|
|
127
|
+
onViewChange={() => {}}
|
|
128
|
+
worldSpec={{ yAxis: 'up' }}
|
|
129
|
+
/>
|
|
130
|
+
</PointerContextProvider>,
|
|
131
|
+
);
|
|
132
|
+
const host = container.querySelector('.lk-canvas-stack') as HTMLElement;
|
|
133
|
+
act(() => pointer(host, 'pointermove', 40, 30, 0));
|
|
134
|
+
expect(store.get()).toEqual({ worldX: 20, worldY: -15, viewId: 'stage' });
|
|
135
|
+
});
|
|
136
|
+
});
|