@weasel-js/labkit 1.4.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/dist/_dts/{CanvasStackContext-Dde8_1my.d.ts → CanvasStackContext-CqkxkFJW.d.ts} +1 -1
- package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +116 -0
- package/dist/_dts/{frac-B5mB8LsJ.d.ts → frac-8R6UmlvN.d.ts} +256 -33
- package/dist/_dts/index-B8uu2ba7.d.ts +237 -0
- package/dist/_dts/{index-DelFfeV0.d.ts → index-retnAut7.d.ts} +45 -12
- package/dist/_dts/{types-B9_zrHmb.d.ts → types-Ca2LCnPq.d.ts} +65 -2
- package/dist/_dts/usePanZoom-nS798rOF.d.ts +160 -0
- package/dist/_dts/useTrialState-jcx_TdU6.d.ts +212 -0
- package/dist/canvas/index.d.ts +17 -128
- package/dist/canvas/index.js +4 -2
- package/dist/chrome/index.d.ts +4 -92
- package/dist/chrome/index.js +9 -5
- package/dist/chunk-67SJLMC7.js +704 -0
- package/dist/chunk-67SJLMC7.js.map +1 -0
- package/dist/chunk-6ZDGOZQV.js +310 -0
- package/dist/chunk-6ZDGOZQV.js.map +1 -0
- package/dist/{chunk-QAGAHJKZ.js → chunk-EKIICY6X.js} +111 -25
- package/dist/chunk-EKIICY6X.js.map +1 -0
- package/dist/{chunk-CTRKTLYZ.js → chunk-H6ZAOWNE.js} +11 -11
- package/dist/chunk-H6ZAOWNE.js.map +1 -0
- package/dist/{chunk-2RQPGOQF.js → chunk-KJALCDWE.js} +9 -7
- package/dist/chunk-KJALCDWE.js.map +1 -0
- package/dist/{chunk-ISCOFAUO.js → chunk-MUKOW3TC.js} +3 -3
- package/dist/{chunk-ISCOFAUO.js.map → chunk-MUKOW3TC.js.map} +1 -1
- package/dist/chunk-NDRYLVOW.js +15 -0
- package/dist/chunk-NDRYLVOW.js.map +1 -0
- package/dist/{chunk-C42T6DJT.js → chunk-NS64DXMN.js} +226 -159
- package/dist/chunk-NS64DXMN.js.map +1 -0
- package/dist/{chunk-I6JCVE24.js → chunk-PGETSEDK.js} +135 -13
- package/dist/chunk-PGETSEDK.js.map +1 -0
- package/dist/{chunk-FQMJUVHJ.js → chunk-QLSV2N3G.js} +32 -37
- package/dist/chunk-QLSV2N3G.js.map +1 -0
- package/dist/chunk-RWNDDKOH.js +682 -0
- package/dist/chunk-RWNDDKOH.js.map +1 -0
- package/dist/chunk-S2SHZRD7.js +7681 -0
- package/dist/chunk-S2SHZRD7.js.map +1 -0
- package/dist/{chunk-ASWKLRKJ.js → chunk-TQBBYTAF.js} +19 -82
- package/dist/chunk-TQBBYTAF.js.map +1 -0
- package/dist/chunk-UU3NJO6N.js +72 -0
- package/dist/chunk-UU3NJO6N.js.map +1 -0
- package/dist/chunk-WIG6XHJ7.js +35 -0
- package/dist/chunk-WIG6XHJ7.js.map +1 -0
- package/dist/{chunk-TJ7QY3OC.js → chunk-ZPH5WTWF.js} +21 -15
- package/dist/chunk-ZPH5WTWF.js.map +1 -0
- package/dist/config/index.d.ts +232 -0
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controls/index.d.ts +2 -3
- package/dist/controls/index.js +3 -2
- package/dist/dragdrop/index.d.ts +2 -4
- package/dist/index.d.ts +123 -348
- package/dist/index.js +794 -837
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +3 -5
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +6 -8
- package/dist/loupe/index.js +2 -2
- package/dist/passthrough/weasel-ui.d.ts +169 -117
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +53 -6
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +12 -7
- package/dist/state/index.js +16 -13
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +163 -62
- package/dist/surface/index.d.ts +44 -7
- package/dist/surface/index.js +2 -1
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +2 -4
- package/package.json +15 -8
- package/src/annotations/AnnotationOverlay.tsx +53 -5
- package/src/annotations/Annotations.overlay.test.tsx +85 -6
- package/src/annotations/drawOne.test.ts +1 -1
- package/src/annotations/drawOne.ts +2 -2
- package/src/annotations/frac.test.ts +4 -4
- package/src/annotations/frac.ts +3 -2
- package/src/annotations/index.ts +1 -1
- package/src/annotations/paint.test.ts +2 -2
- package/src/annotations/paint.ts +1 -1
- package/src/annotations/preload.ts +6 -0
- package/src/annotations/store.test.ts +18 -0
- package/src/annotations/store.ts +7 -6
- package/src/annotations/svgNodes.test.ts +2 -2
- package/src/annotations/svgNodes.ts +1 -2
- package/src/annotations/toolMap.ts +3 -3
- package/src/annotations/types.ts +6 -3
- package/src/canvas/CameraWheelContext.ts +12 -0
- package/src/canvas/CanvasStack.test.tsx +25 -0
- package/src/canvas/CanvasStack.tsx +31 -4
- package/src/canvas/Stage.less +18 -0
- package/src/canvas/Stage.test.tsx +186 -0
- package/src/canvas/Stage.tsx +134 -0
- package/src/canvas/camera.test.ts +17 -2
- package/src/canvas/camera.ts +9 -9
- package/src/canvas/index.ts +4 -0
- package/src/canvas/usePanZoom.ts +10 -5
- package/src/chrome/ChromeRegions.stories.tsx +0 -3
- package/src/chrome/LabChrome.tsx +114 -0
- package/src/chrome/builtins.tsx +20 -31
- package/src/chrome/index.ts +15 -0
- package/src/chrome/labRegions.test.tsx +208 -0
- package/src/chrome/labTypes.ts +50 -0
- package/src/chrome/merge.ts +7 -9
- package/src/chrome/regions/PaletteRegion.test.tsx +6 -4
- package/src/chrome/regions/PaletteRegion.tsx +21 -9
- package/src/chrome/regions/SidebarRegion.tsx +25 -13
- package/src/chrome/regions/StatusRegion.tsx +15 -8
- package/src/chrome/regions/ToolbarRegion.tsx +30 -16
- package/src/chrome/types.ts +47 -20
- package/src/config/builder.test.ts +4 -0
- package/src/config/builder.ts +16 -10
- package/src/config/declarationEmit.fixture.ts +11 -0
- package/src/config/declarationEmit.test.ts +51 -0
- package/src/config/entry.test.ts +46 -0
- package/src/config/index.ts +13 -1
- package/src/config/nodeClasses.test.ts +18 -0
- package/src/config/types.ts +3 -1
- package/src/controls/ControlPanel.test.tsx +16 -0
- package/src/controls/ControlPanel.tsx +5 -2
- package/src/fake-indexeddb-auto.d.ts +5 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +39 -6
- package/src/instrument/serializers.test.ts +22 -0
- package/src/instrument/serializers.ts +11 -0
- package/src/instrument/types.ts +25 -1
- package/src/lab/Lab.less +4 -13
- package/src/lab/Lab.persist.test.tsx +147 -0
- package/src/lab/Lab.stories.tsx +0 -2
- package/src/lab/Lab.surface.test.tsx +66 -3
- package/src/lab/Lab.test.tsx +91 -17
- package/src/lab/Lab.tsx +337 -124
- package/src/lab/LabFit.stories.less +18 -0
- package/src/lab/LabFit.stories.tsx +219 -0
- package/src/lab/LabFullChrome.stories.tsx +0 -3
- package/src/lab/LabHeader.test.tsx +40 -9
- package/src/lab/LabHeader.tsx +7 -13
- package/src/lab/LabPalette.tsx +13 -26
- package/src/lab/LabShell.less +49 -12
- package/src/lab/LabShell.tsx +14 -2
- package/src/lab/LabSwitcher.less +92 -0
- package/src/lab/LabSwitcher.test.tsx +96 -0
- package/src/lab/LabSwitcher.tsx +112 -0
- package/src/lab/Workspace.surface.test.tsx +2 -1
- package/src/lab/fitCheck.test.ts +57 -0
- package/src/lab/fitCheck.ts +125 -0
- package/src/lab/index.ts +2 -0
- package/src/loupe/AGENTS.md +1 -1
- package/src/loupe/Loupe.less +1 -1
- package/src/loupe/canvasLens.ts +1 -1
- package/src/loupe/types.ts +1 -1
- package/src/loupe/useLoupe.test.tsx +1 -1
- package/src/loupe/useLoupe.ts +1 -1
- package/src/passthrough/weasel-ui.ts +9 -0
- package/src/primitives/FloatingPanel.stories.tsx +13 -1
- package/src/primitives/FloatingPanel.test.tsx +47 -16
- package/src/primitives/FloatingPanel.tsx +11 -25
- package/src/primitives/Split.less +14 -0
- package/src/primitives/Split.test.tsx +108 -0
- package/src/primitives/Split.tsx +159 -0
- package/src/primitives/ZoomControl.tsx +5 -2
- package/src/primitives/index.ts +2 -0
- package/src/state/Persistence.stories.tsx +75 -0
- package/src/state/Persistence.tsx +33 -0
- package/src/state/SingletonExperiment.test.tsx +47 -53
- package/src/state/SingletonExperiment.tsx +24 -13
- package/src/state/adapterContract.ts +116 -0
- package/src/state/adapters.test.ts +113 -61
- package/src/state/adapters.ts +328 -69
- package/src/state/document.test.ts +71 -96
- package/src/state/document.ts +66 -52
- package/src/state/helpers.test.ts +20 -0
- package/src/state/helpers.ts +16 -0
- package/src/state/index.ts +8 -0
- package/src/state/labRecords.test.ts +105 -0
- package/src/state/labRecords.ts +160 -0
- package/src/state/openLabStore.test.ts +658 -0
- package/src/state/openLabStore.ts +403 -0
- package/src/state/records.test.ts +234 -0
- package/src/state/records.ts +219 -0
- package/src/state/store.test.ts +104 -635
- package/src/state/store.ts +117 -158
- package/src/state/toolSlot.test.ts +1 -1
- package/src/state/types.ts +39 -15
- package/src/state/useOpenOnce.ts +68 -0
- package/src/state/usePersistedState.test.tsx +125 -0
- package/src/state/usePersistedState.ts +66 -0
- package/src/state/useTrialState.test.tsx +2 -3
- package/src/state/view.test.ts +49 -15
- package/src/state/view.ts +18 -1
- package/src/styles.less +3 -0
- package/src/surface/AGENTS.md +39 -7
- package/src/surface/SurfaceContext.ts +26 -3
- package/src/surface/index.ts +2 -0
- package/src/surface/useSurfaceTile.test.tsx +2 -1
- package/src/surface/useSurfaceTile.ts +6 -5
- package/src/surface/useTiledSurface.test.tsx +175 -0
- package/src/surface/useTiledSurface.ts +54 -5
- package/src/theme/Interstellar.stories.tsx +7 -7
- package/src/theme/base.less +4 -4
- package/src/theme/interstellar.test.ts +1 -1
- package/src/theme/interstellar.tokens.json +2 -2
- package/src/tools/labTool.ts +11 -0
- package/src/trial/Trial.annotations.persist.test.tsx +100 -58
- package/src/trial/Trial.annotations.test.tsx +47 -4
- package/src/trial/Trial.job.test.tsx +3 -3
- package/src/trial/Trial.less +1 -26
- package/src/trial/Trial.stories.tsx +1 -2
- package/src/trial/Trial.test.tsx +28 -4
- package/src/trial/Trial.trialId.test.tsx +23 -1
- package/src/trial/Trial.tsx +118 -38
- package/src/trial/TrialBody.tsx +17 -141
- package/src/trial/TrialChrome.tsx +7 -4
- package/src/trial/trialOps.ts +7 -4
- package/dist/_dts/types-BP2OCcpg.d.ts +0 -155
- package/dist/_dts/types-x92Kfeme.d.ts +0 -62
- package/dist/_dts/useTrialState-D6Vb3T-g.d.ts +0 -99
- package/dist/chunk-2RQPGOQF.js.map +0 -1
- package/dist/chunk-5TROQVQ2.js +0 -547
- package/dist/chunk-5TROQVQ2.js.map +0 -1
- package/dist/chunk-ASWKLRKJ.js.map +0 -1
- package/dist/chunk-C42T6DJT.js.map +0 -1
- package/dist/chunk-CTRKTLYZ.js.map +0 -1
- package/dist/chunk-FQMJUVHJ.js.map +0 -1
- package/dist/chunk-I6JCVE24.js.map +0 -1
- package/dist/chunk-MHAGC6VD.js +0 -7320
- package/dist/chunk-MHAGC6VD.js.map +0 -1
- package/dist/chunk-QAGAHJKZ.js.map +0 -1
- package/dist/chunk-TJ7QY3OC.js.map +0 -1
package/dist/surface/index.d.ts
CHANGED
|
@@ -42,6 +42,11 @@ interface SurfaceFrame {
|
|
|
42
42
|
width: number;
|
|
43
43
|
height: number;
|
|
44
44
|
};
|
|
45
|
+
/** The tile geometry changed this frame — a tile moved, resized, appeared or
|
|
46
|
+
* went away, or the dpr did. Every tile is dirty on such a frame, but the
|
|
47
|
+
* pixels a tile vacated are in no tile's scissor now, so the owner clears
|
|
48
|
+
* the whole buffer. `<Lab>` does; a host owning its own surface must too. */
|
|
49
|
+
retiled: boolean;
|
|
45
50
|
}
|
|
46
51
|
/** The invalidators and the two ref callbacks that publish geometry. */
|
|
47
52
|
interface SurfaceHandle {
|
|
@@ -53,6 +58,12 @@ interface SurfaceHandle {
|
|
|
53
58
|
* moved something a ResizeObserver cannot see. */
|
|
54
59
|
invalidateRects: () => void;
|
|
55
60
|
registerTile: (id: string, el: HTMLElement | null) => void;
|
|
61
|
+
/** Register a tenant's whole-buffer clear. Every one registered runs, before
|
|
62
|
+
* any painter, on a frame where the tile geometry changed — the pixels a
|
|
63
|
+
* tile vacated are in no scissor now, and only the context that drew them
|
|
64
|
+
* can erase them. Backend-agnostic by necessity: labkit owns the canvas,
|
|
65
|
+
* never the context. */
|
|
66
|
+
registerClear: (id: string, clear: SurfaceClear) => () => void;
|
|
56
67
|
/** Subscribe a tile to the frames it is dirty on. A tile paints on its own
|
|
57
68
|
* loop, so this is how the surface wakes one: a resize of the shared buffer
|
|
58
69
|
* clears every tile, not only the one that moved. */
|
|
@@ -65,6 +76,12 @@ interface SurfaceHandle {
|
|
|
65
76
|
/** What a tile's painter is handed: where it sits on the surface now, and the
|
|
66
77
|
* frame that dirtied it. */
|
|
67
78
|
type TilePainter = (rect: Rect, frame: SurfaceFrame) => void;
|
|
79
|
+
/** Erase the whole shared buffer. Device pixels, because that is what a GL
|
|
80
|
+
* viewport takes. */
|
|
81
|
+
type SurfaceClear = (size: {
|
|
82
|
+
width: number;
|
|
83
|
+
height: number;
|
|
84
|
+
}, dpr: number) => void;
|
|
68
85
|
interface UseTiledSurfaceOptions {
|
|
69
86
|
onFrame: (frame: SurfaceFrame) => void;
|
|
70
87
|
}
|
|
@@ -72,13 +89,33 @@ declare function useTiledSurface({ onFrame }: UseTiledSurfaceOptions): SurfaceHa
|
|
|
72
89
|
|
|
73
90
|
/** Null when no surface owner is above — a lab with no shared surface at all. */
|
|
74
91
|
declare const SurfaceContext: react.Context<SurfaceHandle | null>;
|
|
75
|
-
/**
|
|
76
|
-
*
|
|
77
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Which of the two stacked buffers a tenant paints into.
|
|
94
|
+
*
|
|
95
|
+
* `'over'` sits above the trial DOM: right for marks that annotate an
|
|
96
|
+
* instrument, which is what the surface was built for and so the default.
|
|
97
|
+
* `'under'` sits below it, which is what an opaque renderer needs — a tile that
|
|
98
|
+
* fills its pane hides anything the pane's own DOM draws, so a 3D viewport on
|
|
99
|
+
* the over-buffer has to paint its own chrome into the backend rather than
|
|
100
|
+
* putting a label or a button in the pane.
|
|
101
|
+
*
|
|
102
|
+
* Both exist for the lifetime of the surface and share one set of tile rects,
|
|
103
|
+
* so a lab can hold an annotated 2D trial and an opaque 3D one at once.
|
|
104
|
+
*/
|
|
105
|
+
type SurfaceLayer = 'over' | 'under';
|
|
106
|
+
/** The two buffers a surface owner publishes. A member is null until the
|
|
107
|
+
* owner's canvas mounts, and for an owner that keeps its surface unpainted. */
|
|
108
|
+
interface SurfaceCanvases {
|
|
109
|
+
over: HTMLCanvasElement | null;
|
|
110
|
+
under: HTMLCanvasElement | null;
|
|
111
|
+
}
|
|
112
|
+
/** The buffers the surface's tiles paint into. */
|
|
113
|
+
declare const SurfaceCanvasContext: react.Context<SurfaceCanvases>;
|
|
78
114
|
|
|
79
|
-
/**
|
|
80
|
-
* hands to `paintInto`.
|
|
81
|
-
|
|
115
|
+
/** One of the surface's two shared buffers, or null where nothing paints one.
|
|
116
|
+
* What a tile hands to `paintInto`. See {@link SurfaceLayer} for which to ask
|
|
117
|
+
* for; the default is the one over the trial DOM. */
|
|
118
|
+
declare function useSurfaceCanvas(layer?: SurfaceLayer): HTMLCanvasElement | null;
|
|
82
119
|
/** The surface above, or null. Use this where a surface is genuinely optional. */
|
|
83
120
|
declare function useSurfaceOptional(): SurfaceHandle | null;
|
|
84
121
|
/** The surface above. Throws where a caller cannot work without one. */
|
|
@@ -105,4 +142,4 @@ declare function useTileId(id: string): string;
|
|
|
105
142
|
declare function useSurfaceTile(id: string): (el: HTMLElement | null) => void;
|
|
106
143
|
|
|
107
144
|
export { SurfaceCanvasContext, SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTileId, useTiledSurface };
|
|
108
|
-
export type { Box, Rect, SurfaceFrame, SurfaceHandle, TilePainter, UseTiledSurfaceOptions };
|
|
145
|
+
export type { Box, Rect, SurfaceCanvases, SurfaceClear, SurfaceFrame, SurfaceHandle, SurfaceLayer, TilePainter, UseTiledSurfaceOptions };
|
package/dist/surface/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { composeRects, rectsEqual, toDeviceRect, useTiledSurface } from '../chunk-QLSV2N3G.js';
|
|
2
|
+
export { SurfaceCanvasContext, SurfaceContext, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTileId } from '../chunk-WIG6XHJ7.js';
|
|
2
3
|
import '../chunk-2YILQ7Y5.js';
|
|
3
4
|
//# sourceMappingURL=index.js.map
|
|
4
5
|
//# sourceMappingURL=index.js.map
|
package/dist/ui/layers/index.js
CHANGED
package/dist/undo/index.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { as as UndoCapability } from '../_dts/frac-8R6UmlvN.js';
|
|
2
2
|
import 'react';
|
|
3
|
-
import '../_dts/types-
|
|
3
|
+
import '../_dts/types-Ca2LCnPq.js';
|
|
4
4
|
import '@weasel-js/core';
|
|
5
5
|
import '../_dts/PrefsForm.d-DDHFANkc.js';
|
|
6
|
-
import '../_dts/types-x92Kfeme.js';
|
|
7
|
-
import '../_dts/types-BP2OCcpg.js';
|
|
8
6
|
import '../_dts/types-DJ79Tg5J.js';
|
|
9
7
|
|
|
10
8
|
/** 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.5.0",
|
|
4
4
|
"description": "React widgets for building self-contained interactive lab pages",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "orochi235",
|
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
"types": "./dist/controls/index.d.ts",
|
|
43
43
|
"import": "./dist/controls/index.js"
|
|
44
44
|
},
|
|
45
|
+
"./config": {
|
|
46
|
+
"types": "./dist/config/index.d.ts",
|
|
47
|
+
"import": "./dist/config/index.js"
|
|
48
|
+
},
|
|
45
49
|
"./canvas": {
|
|
46
50
|
"types": "./dist/canvas/index.d.ts",
|
|
47
51
|
"import": "./dist/canvas/index.js"
|
|
@@ -107,18 +111,21 @@
|
|
|
107
111
|
"docs:build": "vitepress build docs",
|
|
108
112
|
"docs:preview": "vitepress preview docs",
|
|
109
113
|
"dev:schema": "LABKIT_EXAMPLE=schema-lab vite",
|
|
110
|
-
"dev:annotate": "LABKIT_EXAMPLE=annotate-lab vite"
|
|
114
|
+
"dev:annotate": "LABKIT_EXAMPLE=annotate-lab vite",
|
|
115
|
+
"dev:3d": "LABKIT_EXAMPLE=3d-lab vite"
|
|
111
116
|
},
|
|
112
117
|
"peerDependencies": {
|
|
113
|
-
"@weasel-js/core": "1.
|
|
118
|
+
"@weasel-js/core": "1.5.0",
|
|
114
119
|
"react": "^19.0.0",
|
|
115
120
|
"react-dom": "^19.0.0"
|
|
116
121
|
},
|
|
117
122
|
"dependencies": {
|
|
118
|
-
"@weasel-js/
|
|
119
|
-
"@weasel-js/
|
|
120
|
-
"@weasel-js/
|
|
121
|
-
"@weasel-js/
|
|
123
|
+
"@weasel-js/geom": "1.5.0",
|
|
124
|
+
"@weasel-js/kernel3d": "1.5.0",
|
|
125
|
+
"@weasel-js/loupe": "1.5.0",
|
|
126
|
+
"@weasel-js/svg": "1.5.0",
|
|
127
|
+
"@weasel-js/theme": "1.5.0",
|
|
128
|
+
"@weasel-js/ui": "1.5.0",
|
|
122
129
|
"earcut": "2.2.4",
|
|
123
130
|
"polygon-clipping": "^0.15.7",
|
|
124
131
|
"react-aria-components": "^1.5.0",
|
|
@@ -129,7 +136,7 @@
|
|
|
129
136
|
"@biomejs/biome": "^2.4.12",
|
|
130
137
|
"@rollup/plugin-alias": "^6.0.0",
|
|
131
138
|
"@testing-library/jest-dom": "^6.6.0",
|
|
132
|
-
"@weasel-js/modes": "1.
|
|
139
|
+
"@weasel-js/modes": "1.5.0",
|
|
133
140
|
"less": "^4.2.0",
|
|
134
141
|
"rollup-plugin-dts": "^6.4.1",
|
|
135
142
|
"tsx": "^4.19.0",
|
|
@@ -6,8 +6,9 @@ import {
|
|
|
6
6
|
type View,
|
|
7
7
|
WeaselProvider,
|
|
8
8
|
} from '@weasel-js/core';
|
|
9
|
-
import { type CSSProperties, useEffect, useRef, useState } from 'react';
|
|
10
|
-
import { createPortal } from 'react-dom';
|
|
9
|
+
import { type CSSProperties, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
10
|
+
import { createPortal, flushSync } from 'react-dom';
|
|
11
|
+
import { CameraWheelContext } from '../canvas/CameraWheelContext';
|
|
11
12
|
import type { Rect } from '../surface/rect';
|
|
12
13
|
import { useSurfaceCanvas, useSurfaceOptional, useTileId } from '../surface/useSurfaceTile';
|
|
13
14
|
import { createMarkDrawOne } from './drawOne';
|
|
@@ -34,7 +35,7 @@ export interface AnnotationOverlayProps {
|
|
|
34
35
|
config: unknown;
|
|
35
36
|
/** The trial's resolved tool slot. */
|
|
36
37
|
activeToolId: string | null;
|
|
37
|
-
/** The instrument's vocabulary, for a status's
|
|
38
|
+
/** The instrument's vocabulary, for a status's color. */
|
|
38
39
|
meaning?: AnnotationMeaning;
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -123,10 +124,57 @@ export function AnnotationOverlay({
|
|
|
123
124
|
[surface, tileId],
|
|
124
125
|
);
|
|
125
126
|
|
|
127
|
+
// Erases the whole shared buffer when the tiles move: a tile that moved took
|
|
128
|
+
// its scissor with it, and what it painted last frame stays where it was.
|
|
129
|
+
// Each renderer on the context re-applies its viewport and scissor per frame,
|
|
130
|
+
// so resetting them here is safe.
|
|
131
|
+
// Only once this pane has painted: a bare `getContext` on a buffer no
|
|
132
|
+
// renderer has opened creates the context without the stencil buffer marks
|
|
133
|
+
// need, and every later request gets that context back.
|
|
134
|
+
const painted = useRef(false);
|
|
135
|
+
const canvasRef = useRef(canvas);
|
|
136
|
+
if (canvasRef.current !== canvas) painted.current = false;
|
|
137
|
+
canvasRef.current = canvas;
|
|
138
|
+
const unsubscribeFrame = useRef<(() => void) | null>(null);
|
|
139
|
+
const attachSceneCanvas = useCallback((api: SceneCanvasApi | null) => {
|
|
140
|
+
sceneCanvas.current = api;
|
|
141
|
+
unsubscribeFrame.current?.();
|
|
142
|
+
unsubscribeFrame.current = api
|
|
143
|
+
? api.subscribeFrame(() => {
|
|
144
|
+
painted.current = true;
|
|
145
|
+
})
|
|
146
|
+
: null;
|
|
147
|
+
}, []);
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (!surface) return;
|
|
150
|
+
return surface.registerClear(tileId, (size, dpr) => {
|
|
151
|
+
if (!painted.current) return;
|
|
152
|
+
const gl = canvasRef.current?.getContext('webgl2');
|
|
153
|
+
if (!gl) return;
|
|
154
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
155
|
+
gl.viewport(0, 0, Math.round(size.width * dpr), Math.round(size.height * dpr));
|
|
156
|
+
gl.clearColor(0, 0, 0, 0);
|
|
157
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
|
|
158
|
+
});
|
|
159
|
+
}, [surface, tileId]);
|
|
160
|
+
|
|
161
|
+
// The box is portalled out of whatever the picture sits in, so a wheel over
|
|
162
|
+
// a mark would never reach the trial's camera without being handed to it.
|
|
163
|
+
const wheelSlot = useContext(CameraWheelContext);
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (!input || !wheelSlot) return;
|
|
166
|
+
const wheel = (e: WheelEvent): void => wheelSlot.current?.(e);
|
|
167
|
+
input.addEventListener('wheel', wheel, { passive: false });
|
|
168
|
+
return () => input.removeEventListener('wheel', wheel);
|
|
169
|
+
}, [input, wheelSlot]);
|
|
170
|
+
|
|
126
171
|
useEffect(
|
|
127
172
|
() =>
|
|
128
173
|
surface?.registerPainter(tileId, (next) => {
|
|
129
|
-
|
|
174
|
+
// Committed before the redraw is asked for: the renderer reads its
|
|
175
|
+
// position from the last render, so a redraw landing first repaints the
|
|
176
|
+
// tile where it was — after this frame's clear, which leaves a ghost.
|
|
177
|
+
flushSync(() => setRect((prev) => (sameRect(prev, next) ? prev : next)));
|
|
130
178
|
// The shared buffer was cleared if it resized this frame; this pane's
|
|
131
179
|
// own loop has no way to know that.
|
|
132
180
|
sceneCanvas.current?.requestRedraw();
|
|
@@ -190,7 +238,7 @@ export function AnnotationOverlay({
|
|
|
190
238
|
<WeaselProvider isolate>
|
|
191
239
|
<ToolBridge toolId={annotationToolInfo(activeToolId)?.weaselTool ?? 'select'} />
|
|
192
240
|
<SceneCanvas<AnnotationData, 'marks', WorldRect>
|
|
193
|
-
ref={
|
|
241
|
+
ref={attachSceneCanvas}
|
|
194
242
|
scene={scene}
|
|
195
243
|
width={rect.w}
|
|
196
244
|
height={rect.h}
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
* marks themselves are proved by `paint.test.ts` (pure) and by a screenshot.
|
|
9
9
|
*/
|
|
10
10
|
import { act, render } from '@testing-library/react';
|
|
11
|
-
import { StrictMode, useRef } from 'react';
|
|
11
|
+
import { StrictMode, useMemo, useRef } from 'react';
|
|
12
12
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
|
+
import { CameraWheelContext, type CameraWheelSlot } from '../canvas/CameraWheelContext';
|
|
13
14
|
import { TrialIdProvider } from '../state/context';
|
|
14
15
|
import { SurfaceCanvasContext, SurfaceContext } from '../surface/SurfaceContext';
|
|
15
|
-
import type { SurfaceHandle } from '../surface/useTiledSurface';
|
|
16
|
+
import type { SurfaceClear, SurfaceHandle } from '../surface/useTiledSurface';
|
|
16
17
|
import { useTiledSurface } from '../surface/useTiledSurface';
|
|
17
18
|
import { AnnotationTargets } from './AnnotationTargets';
|
|
18
19
|
import { createAnnotationStore } from './store';
|
|
@@ -38,10 +39,33 @@ beforeAll(() => {
|
|
|
38
39
|
) as unknown as HTMLCanvasElement['getContext'];
|
|
39
40
|
});
|
|
40
41
|
|
|
41
|
-
function Harness({
|
|
42
|
+
function Harness({
|
|
43
|
+
toolId = 'rect',
|
|
44
|
+
onRegisterClear,
|
|
45
|
+
canvas = null,
|
|
46
|
+
}: {
|
|
47
|
+
toolId?: string;
|
|
48
|
+
/** Told each tile a clear is registered for, and handed the clear. */
|
|
49
|
+
onRegisterClear?: (id: string, clear: SurfaceClear) => void;
|
|
50
|
+
/** The shared buffer the overlays paint into. */
|
|
51
|
+
canvas?: HTMLCanvasElement | null;
|
|
52
|
+
}) {
|
|
42
53
|
const a = useRef<HTMLDivElement | null>(null);
|
|
43
54
|
const b = useRef<HTMLDivElement | null>(null);
|
|
44
|
-
const
|
|
55
|
+
const tiled = useTiledSurface({ onFrame: () => {} });
|
|
56
|
+
const surface = useMemo<SurfaceHandle>(
|
|
57
|
+
() =>
|
|
58
|
+
onRegisterClear
|
|
59
|
+
? {
|
|
60
|
+
...tiled,
|
|
61
|
+
registerClear: (id, clear) => {
|
|
62
|
+
onRegisterClear(id, clear);
|
|
63
|
+
return tiled.registerClear(id, clear);
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
: tiled,
|
|
67
|
+
[tiled, onRegisterClear],
|
|
68
|
+
);
|
|
45
69
|
const annotations = useRef(createAnnotationStore({ targets: () => [] })).current;
|
|
46
70
|
|
|
47
71
|
const capability: AnnotationsCapability = {
|
|
@@ -53,7 +77,7 @@ function Harness({ toolId = 'rect' }: { toolId?: string }) {
|
|
|
53
77
|
|
|
54
78
|
return (
|
|
55
79
|
<SurfaceContext.Provider value={surface}>
|
|
56
|
-
<SurfaceCanvasContext.Provider value={null}>
|
|
80
|
+
<SurfaceCanvasContext.Provider value={{ over: canvas, under: null }}>
|
|
57
81
|
<div
|
|
58
82
|
data-testid="stage"
|
|
59
83
|
ref={(el) => {
|
|
@@ -133,6 +157,61 @@ describe('<AnnotationTargets>', () => {
|
|
|
133
157
|
}
|
|
134
158
|
});
|
|
135
159
|
|
|
160
|
+
it('hands a wheel over a target to the trial camera', () => {
|
|
161
|
+
// The input box is portalled into the surface container, so the wheel
|
|
162
|
+
// would otherwise bubble past the stage the picture sits on.
|
|
163
|
+
const wheel = vi.fn();
|
|
164
|
+
const slot: CameraWheelSlot = { current: wheel };
|
|
165
|
+
const { container } = render(
|
|
166
|
+
<CameraWheelContext.Provider value={slot}>
|
|
167
|
+
<Harness />
|
|
168
|
+
</CameraWheelContext.Provider>,
|
|
169
|
+
{ wrapper: StrictMode },
|
|
170
|
+
);
|
|
171
|
+
act(() => {
|
|
172
|
+
vi.advanceTimersByTime(64);
|
|
173
|
+
});
|
|
174
|
+
const box = container.querySelector('.lk-annotate__input');
|
|
175
|
+
box?.dispatchEvent(new WheelEvent('wheel', { bubbles: true, cancelable: true }));
|
|
176
|
+
expect(wheel).toHaveBeenCalledTimes(1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('registers a clear for each tile, so a moved mark leaves nothing behind', () => {
|
|
180
|
+
// A proxy: jsdom has no WebGL2, so the clear itself cannot run here. Without
|
|
181
|
+
// one registered, a tile that moves leaves its last frame's marks painted
|
|
182
|
+
// where it used to be — which the surface only erases through a clear.
|
|
183
|
+
const registered = vi.fn();
|
|
184
|
+
render(<Harness onRegisterClear={registered} />, { wrapper: StrictMode });
|
|
185
|
+
act(() => {
|
|
186
|
+
vi.advanceTimersByTime(64);
|
|
187
|
+
});
|
|
188
|
+
expect(new Set(registered.mock.calls.map((call) => call[0])).size).toBe(2);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('never opens the shared context itself when it clears', () => {
|
|
192
|
+
// A bare `getContext('webgl2')` on a buffer nobody has opened yet creates
|
|
193
|
+
// the context with default attributes, and the renderer's later request for
|
|
194
|
+
// a stencil buffer then gets that one back: every mark paints nothing, with
|
|
195
|
+
// no error anywhere. jsdom opens no context, so the call is the proxy.
|
|
196
|
+
const clears: SurfaceClear[] = [];
|
|
197
|
+
const canvas = document.createElement('canvas');
|
|
198
|
+
const { unmount } = render(
|
|
199
|
+
<Harness canvas={canvas} onRegisterClear={(_id, clear) => clears.push(clear)} />,
|
|
200
|
+
{ wrapper: StrictMode },
|
|
201
|
+
);
|
|
202
|
+
act(() => {
|
|
203
|
+
vi.advanceTimersByTime(64);
|
|
204
|
+
});
|
|
205
|
+
// The prototype is already a mock file-wide, so the spy arrives holding
|
|
206
|
+
// the renderer's own attempts to open it.
|
|
207
|
+
const open = vi.spyOn(canvas, 'getContext');
|
|
208
|
+
open.mockClear();
|
|
209
|
+
for (const clear of clears) clear({ width: 800, height: 600 }, 1);
|
|
210
|
+
expect(open).not.toHaveBeenCalled();
|
|
211
|
+
// Before the frame stubs come off: the canvas's own loop cancels through them.
|
|
212
|
+
unmount();
|
|
213
|
+
});
|
|
214
|
+
|
|
136
215
|
it('takes its tiles back out on unmount', () => {
|
|
137
216
|
const { container, unmount } = render(<Harness />, { wrapper: StrictMode });
|
|
138
217
|
act(() => {
|
|
@@ -206,7 +285,7 @@ describe('two trials of one instrument', () => {
|
|
|
206
285
|
handle = surface;
|
|
207
286
|
return (
|
|
208
287
|
<SurfaceContext.Provider value={surface}>
|
|
209
|
-
<SurfaceCanvasContext.Provider value={null}>
|
|
288
|
+
<SurfaceCanvasContext.Provider value={{ over: null, under: null }}>
|
|
210
289
|
<div
|
|
211
290
|
ref={(el) => {
|
|
212
291
|
if (!el) return;
|
|
@@ -23,7 +23,7 @@ const strokeOf = (cmds: ReturnType<ReturnType<typeof createMarkDrawOne>>) => {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
describe('the draw callback marks share between the screen and an export', () => {
|
|
26
|
-
it("paints a mark in its status's
|
|
26
|
+
it("paints a mark in its status's color", () => {
|
|
27
27
|
const drawOne = createMarkDrawOne({
|
|
28
28
|
content: CONTENT,
|
|
29
29
|
config: {},
|
|
@@ -12,7 +12,7 @@ export interface MarkDrawOptions {
|
|
|
12
12
|
positionDependsOn?: readonly string[];
|
|
13
13
|
/** The trial's config at the time of the draw. */
|
|
14
14
|
config: unknown;
|
|
15
|
-
/** The instrument's vocabulary, for a status's
|
|
15
|
+
/** The instrument's vocabulary, for a status's color. */
|
|
16
16
|
meaning?: AnnotationMeaning;
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -20,7 +20,7 @@ export interface MarkDrawOptions {
|
|
|
20
20
|
* How one mark is drawn — shared by the pane on screen and by an export.
|
|
21
21
|
*
|
|
22
22
|
* One function rather than two call sites building their own, because an
|
|
23
|
-
* export that resolves a status
|
|
23
|
+
* export that resolves a status color or a stale dash differently from the
|
|
24
24
|
* screen produces a picture nobody was looking at.
|
|
25
25
|
*/
|
|
26
26
|
export function createMarkDrawOne(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { fracContains,
|
|
2
|
+
import { fracContains, fracEncloses, fracToWorld, roundFrac, worldToFrac } from './frac';
|
|
3
3
|
import type { FracRect } from './types';
|
|
4
4
|
|
|
5
5
|
const CONTENT = { w: 256, h: 170 };
|
|
@@ -51,9 +51,9 @@ 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('
|
|
55
|
-
expect(
|
|
54
|
+
it('encloses only a box that lies wholly inside', () => {
|
|
55
|
+
expect(fracEncloses({ x: 0, y: 0, w: 1, h: 1 }, box)).toBe(true);
|
|
56
56
|
// Overlapping is not containing: a marquee takes what it encloses.
|
|
57
|
-
expect(
|
|
57
|
+
expect(fracEncloses({ x: 0.4, y: 0.4, w: 0.4, h: 0.4 }, box)).toBe(false);
|
|
58
58
|
});
|
|
59
59
|
});
|
package/src/annotations/frac.ts
CHANGED
|
@@ -55,8 +55,9 @@ export function fracContains(box: FracRect, pt: FracPoint, tol = 0): boolean {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/** Whether `outer` wholly encloses `inner`. A marquee takes what it encloses,
|
|
58
|
-
* not what it grazes — brushing selection is a different gesture
|
|
59
|
-
|
|
58
|
+
* not what it grazes — brushing selection is a different gesture, and this
|
|
59
|
+
* answers false for two rects that merely overlap. */
|
|
60
|
+
export function fracEncloses(outer: FracRect, inner: FracRect): boolean {
|
|
60
61
|
return (
|
|
61
62
|
inner.x >= outer.x &&
|
|
62
63
|
inner.y >= outer.y &&
|
package/src/annotations/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ export type { MarkDrawOptions } from './drawOne';
|
|
|
4
4
|
export { createMarkDrawOne, resolveMarkStyle } from './drawOne';
|
|
5
5
|
export { ExportMenu } from './ExportMenu';
|
|
6
6
|
export type { WorldRect } from './frac';
|
|
7
|
-
export { fracContains,
|
|
7
|
+
export { fracContains, fracEncloses, fracToWorld, roundFrac, worldToFrac } from './frac';
|
|
8
8
|
export type { HistoryScene } from './history';
|
|
9
9
|
export { MarkHistory } from './history';
|
|
10
10
|
export type { MarkListProps } from './MarkList';
|
|
@@ -102,7 +102,7 @@ describe('markCommands', () => {
|
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
describe('markCommands styling', () => {
|
|
105
|
-
it('takes a status
|
|
105
|
+
it('takes a status color over the default', () => {
|
|
106
106
|
const [cmd] = markCommands(mark('rect'), CONTENT, { color: '#30a46c' });
|
|
107
107
|
expect((cmd as { stroke?: { paint?: { color?: string } } }).stroke?.paint?.color).toBe(
|
|
108
108
|
'#30a46c',
|
|
@@ -117,7 +117,7 @@ describe('markCommands styling', () => {
|
|
|
117
117
|
expect((stale as { stroke?: { dash?: number[] } }).stroke?.dash).toEqual([6, 4]);
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
-
it("
|
|
120
|
+
it("colors a text mark's glyphs too, not only the outlines", () => {
|
|
121
121
|
const [cmd] = markCommands(mark('text', { title: 'x' }), CONTENT, { color: '#30a46c' });
|
|
122
122
|
const runs = (cmd as { runs: { fill?: { color?: string } }[] }).runs;
|
|
123
123
|
expect(runs[0]?.fill?.color).toBe('#30a46c');
|
package/src/annotations/paint.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface PaintableMark {
|
|
|
27
27
|
/** How a mark is drawn, as opposed to where. Resolved by the overlay from the
|
|
28
28
|
* instrument's vocabulary and the mark's own staleness. */
|
|
29
29
|
export interface MarkStyle {
|
|
30
|
-
/** The status's
|
|
30
|
+
/** The status's color, or the default. */
|
|
31
31
|
color?: string;
|
|
32
32
|
/** A mark whose stored position no longer describes the picture. Drawn
|
|
33
33
|
* dashed rather than hidden: it still describes *something*, and dropping
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
|
|
3
|
+
/** Marks an instrument keeps in its own `AnnotationStorage`, already loaded by
|
|
4
|
+
* `<Lab>` for the trials present when it opened, keyed by trial id. A trial
|
|
5
|
+
* missing from it loads its own. */
|
|
6
|
+
export const AnnotationPreloadContext = createContext<ReadonlyMap<string, unknown> | null>(null);
|
|
@@ -244,4 +244,22 @@ describe('the annotation store', () => {
|
|
|
244
244
|
expect(revived.get(id)).toEqual(store.get(id));
|
|
245
245
|
expect(revived.query()).toHaveLength(1);
|
|
246
246
|
});
|
|
247
|
+
|
|
248
|
+
it('reads a meaning passed as a thunk at each capture', async () => {
|
|
249
|
+
let color = '#111111';
|
|
250
|
+
const store = annotationsFromJSON(null, () => TARGETS, {
|
|
251
|
+
meaning: () => ({ statuses: [{ id: 'open', label: 'Open', color }] }),
|
|
252
|
+
});
|
|
253
|
+
store.add(RING, { angle: 'iso', shading: 'outline' });
|
|
254
|
+
color = '#222222';
|
|
255
|
+
const { blob } = await store.capture('naive', { format: 'svg' });
|
|
256
|
+
// jsdom's Blob has no text().
|
|
257
|
+
const svg = await new Promise<string>((resolve) => {
|
|
258
|
+
const reader = new FileReader();
|
|
259
|
+
reader.onload = () => resolve(String(reader.result));
|
|
260
|
+
reader.readAsText(blob);
|
|
261
|
+
});
|
|
262
|
+
expect(svg).not.toContain('#111111');
|
|
263
|
+
expect(svg).toContain('#222222');
|
|
264
|
+
});
|
|
247
265
|
});
|
package/src/annotations/store.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from '@weasel-js/core';
|
|
8
8
|
import { captureTarget } from './capture';
|
|
9
9
|
import type { WorldRect } from './frac';
|
|
10
|
-
import { fracContains,
|
|
10
|
+
import { fracContains, fracEncloses, fracToWorld, roundFrac, worldToFrac } from './frac';
|
|
11
11
|
import { MarkHistory } from './history';
|
|
12
12
|
import { isStale as isStaleAgainst, seenFrom } from './staleness';
|
|
13
13
|
import type {
|
|
@@ -43,9 +43,10 @@ export interface AnnotationStoreOptions {
|
|
|
43
43
|
targets: () => readonly AnnotationTargetInfo[];
|
|
44
44
|
/** Serialized scenes from a previous `toJSON`, keyed by target. */
|
|
45
45
|
restore?: Readonly<Record<string, unknown>>;
|
|
46
|
-
/** The instrument's vocabulary, so an export draws a mark in the
|
|
47
|
-
* status gives it.
|
|
48
|
-
|
|
46
|
+
/** The instrument's vocabulary, so an export draws a mark in the color its
|
|
47
|
+
* status gives it. A thunk is read at each capture, for a vocabulary that
|
|
48
|
+
* changes under a store built once. */
|
|
49
|
+
meaning?: AnnotationMeaning | (() => AnnotationMeaning | undefined);
|
|
49
50
|
/** The trial's live config. A getter for the same reason `targets` is: the
|
|
50
51
|
* store is built once and the config changes under it. */
|
|
51
52
|
config?: () => unknown;
|
|
@@ -197,7 +198,7 @@ export function createAnnotationStore(opts: AnnotationStoreOptions): Annotations
|
|
|
197
198
|
},
|
|
198
199
|
|
|
199
200
|
within(target, box: FracRect) {
|
|
200
|
-
return marksOn(target).filter((a) =>
|
|
201
|
+
return marksOn(target).filter((a) => fracEncloses(box, a.frac));
|
|
201
202
|
},
|
|
202
203
|
|
|
203
204
|
isStale(a, config) {
|
|
@@ -314,7 +315,7 @@ export function createAnnotationStore(opts: AnnotationStoreOptions): Annotations
|
|
|
314
315
|
content: info.content,
|
|
315
316
|
positionDependsOn: info.positionDependsOn,
|
|
316
317
|
config: opts.config?.(),
|
|
317
|
-
meaning: opts.meaning,
|
|
318
|
+
meaning: typeof opts.meaning === 'function' ? opts.meaning() : opts.meaning,
|
|
318
319
|
},
|
|
319
320
|
base: info.base,
|
|
320
321
|
onWarn: (message) => console.warn(`[labkit] capture: ${message}`),
|
|
@@ -39,7 +39,7 @@ describe('a mark as vector', () => {
|
|
|
39
39
|
expect(node.stroke?.cap).toBe(cmd.stroke?.cap);
|
|
40
40
|
expect(node.stroke?.join).toBe(cmd.stroke?.join);
|
|
41
41
|
// An arrow's head arrives filled and unstroked; the shaft is the other
|
|
42
|
-
// way round. Whichever paint the command carries is the mark
|
|
42
|
+
// way round. Whichever paint the command carries is the mark color.
|
|
43
43
|
const paint = node.stroke?.paint ?? node.fill;
|
|
44
44
|
expect(paint).toEqual({ kind: 'solid', color: '#e5484d' });
|
|
45
45
|
}
|
|
@@ -65,7 +65,7 @@ describe('a mark as vector', () => {
|
|
|
65
65
|
);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
it('emits text as text, at the pose, in the mark
|
|
68
|
+
it('emits text as text, at the pose, in the mark color', () => {
|
|
69
69
|
const m = mark('text', { title: 'missing edge' });
|
|
70
70
|
const [node] = markSvgNodes(m, CONTENT);
|
|
71
71
|
expect(node).toMatchObject({ kind: 'text', text: 'missing edge', x: 10, y: 12 });
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { DrawCommand, FillStyle, Stroke } from '@weasel-js/core';
|
|
2
|
-
import { resolveStrokeWidth } from '@weasel-js/core';
|
|
3
2
|
import type { SvgNode, SvgPaint, SvgStroke } from '@weasel-js/svg';
|
|
4
3
|
import { type MarkStyle, markCommands, type PaintableMark } from './paint';
|
|
5
4
|
|
|
@@ -23,7 +22,7 @@ function toSvgPaint(paint: FillStyle | undefined): SvgPaint {
|
|
|
23
22
|
function toSvgStroke(stroke: Stroke): SvgStroke {
|
|
24
23
|
return {
|
|
25
24
|
paint: toSvgPaint(stroke.paint),
|
|
26
|
-
width:
|
|
25
|
+
width: stroke.width ?? 1,
|
|
27
26
|
...(stroke.cap ? { cap: stroke.cap } : {}),
|
|
28
27
|
...(stroke.join ? { join: stroke.join } : {}),
|
|
29
28
|
...(stroke.dash ? { dash: [...stroke.dash] } : {}),
|
|
@@ -30,9 +30,9 @@ const TOOLS: Record<string, AnnotationToolInfo> = {
|
|
|
30
30
|
text: { weaselTool: 'text', kind: 'text' },
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
/** The
|
|
34
|
-
*
|
|
35
|
-
* holds whichever is active. */
|
|
33
|
+
/** The tools a lab's rail gets when any instrument declares `annotations`.
|
|
34
|
+
* Ids share the lab's contribution namespace with its own `tools`, and the
|
|
35
|
+
* lab's tool slot holds whichever is active. */
|
|
36
36
|
export const ANNOTATION_TOOLS: readonly TrialTool[] = [
|
|
37
37
|
{ id: 'select', label: 'Select', icon: SelectIcon, group: 'annotate' },
|
|
38
38
|
{ id: 'stroke', label: 'Freehand', icon: PencilIcon, group: 'annotate' },
|
package/src/annotations/types.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface AnnotationStatus {
|
|
|
29
29
|
id: string;
|
|
30
30
|
label: string;
|
|
31
31
|
/** What a mark in this status is drawn in. Omitted, it takes the default
|
|
32
|
-
* mark
|
|
32
|
+
* mark color — a status is allowed to be a label and nothing more. */
|
|
33
33
|
color?: string;
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -150,9 +150,10 @@ export interface AnnotationTarget extends AnnotationTargetInfo {
|
|
|
150
150
|
/** Where an instrument keeps its own marks. Declaring this means labkit never
|
|
151
151
|
* writes its trial slot — for an instrument whose marks belong in a format it
|
|
152
152
|
* already owns. Both halves are called outside React; `save` is already
|
|
153
|
-
* debounced by the time it arrives.
|
|
153
|
+
* debounced by the time it arrives. A trial shows an empty body until `load`
|
|
154
|
+
* settles. */
|
|
154
155
|
export interface AnnotationStorage {
|
|
155
|
-
load: () => SerializedAnnotations | null | undefined
|
|
156
|
+
load: () => Promise<SerializedAnnotations | null | undefined>;
|
|
156
157
|
save: (doc: SerializedAnnotations) => void;
|
|
157
158
|
}
|
|
158
159
|
|
|
@@ -163,6 +164,8 @@ export interface AnnotationsCapability<TS = unknown, TC = unknown> {
|
|
|
163
164
|
* is called once per trial, and its targets are that trial's own. */
|
|
164
165
|
targets: (state: TS, config: TC, trial: TrialInfo) => readonly AnnotationTarget[];
|
|
165
166
|
meaning?: AnnotationMeaning;
|
|
167
|
+
/** An instrument replacing this one under a live trial must pass the same
|
|
168
|
+
* object: the trial keeps the marks it loaded from the first. */
|
|
166
169
|
storage?: AnnotationStorage;
|
|
167
170
|
/** Fires after every finished export, labkit's own chrome included. A
|
|
168
171
|
* notification, not an interception: a host wanting its own flow calls
|