@weasel-js/labkit 1.4.4 → 1.5.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 +54 -6
- package/dist/_dts/{CanvasStackContext-kTEZvEgE.d.ts → CanvasStackContext-CJNMS6mz.d.ts} +1 -1
- package/dist/_dts/{PrefsForm.d-DDHFANkc.d.ts → PrefsForm.d-DPy9I8It.d.ts} +4 -0
- package/dist/_dts/Select.d-Gwr8fLpK.d.ts +453 -0
- package/dist/_dts/{frac-z7ker2Vx.d.ts → frac-B4okC2lX.d.ts} +291 -31
- package/dist/_dts/{index-Blv9uQzu.d.ts → index-BNdz-tNd.d.ts} +105 -17
- package/dist/_dts/index-CSJoj4LC.d.ts +248 -0
- package/dist/_dts/{types-B9_zrHmb.d.ts → types-DzoNY1pO.d.ts} +91 -4
- package/dist/_dts/usePanZoom-Pzf9f2jh.d.ts +160 -0
- package/dist/_dts/useTrialState-uLlmaccn.d.ts +212 -0
- package/dist/canvas/index.d.ts +18 -129
- package/dist/canvas/index.js +4 -2
- package/dist/chrome/index.d.ts +5 -93
- package/dist/chrome/index.js +10 -6
- package/dist/chunk-7PFH3CNM.js +150 -0
- package/dist/chunk-7PFH3CNM.js.map +1 -0
- package/dist/{chunk-I6JCVE24.js → chunk-CEIUBPPW.js} +143 -17
- package/dist/chunk-CEIUBPPW.js.map +1 -0
- package/dist/chunk-EH7UEQV7.js +590 -0
- package/dist/chunk-EH7UEQV7.js.map +1 -0
- package/dist/{chunk-MOM3GOVY.js → chunk-EHGVROVV.js} +125 -27
- package/dist/chunk-EHGVROVV.js.map +1 -0
- package/dist/{chunk-TJ7QY3OC.js → chunk-ERFMC6WU.js} +160 -78
- package/dist/chunk-ERFMC6WU.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-64ZCN3DA.js → chunk-IHUVQ3TX.js} +250 -160
- package/dist/chunk-IHUVQ3TX.js.map +1 -0
- package/dist/{chunk-FQMJUVHJ.js → chunk-IJJUK4FU.js} +53 -46
- package/dist/chunk-IJJUK4FU.js.map +1 -0
- package/dist/chunk-IXTQI7JX.js +737 -0
- package/dist/chunk-IXTQI7JX.js.map +1 -0
- package/dist/{chunk-BKFVHKJH.js → chunk-MDVTK5U3.js} +4 -4
- package/dist/{chunk-BKFVHKJH.js.map → chunk-MDVTK5U3.js.map} +1 -1
- package/dist/chunk-PHK4W562.js +72 -0
- package/dist/chunk-PHK4W562.js.map +1 -0
- package/dist/chunk-PJ7WEQBJ.js +18 -0
- package/dist/chunk-PJ7WEQBJ.js.map +1 -0
- package/dist/chunk-RWNDDKOH.js +682 -0
- package/dist/chunk-RWNDDKOH.js.map +1 -0
- package/dist/chunk-T3EC2DXD.js +12628 -0
- package/dist/chunk-T3EC2DXD.js.map +1 -0
- package/dist/{chunk-SMHP6XZ4.js → chunk-UN4U4P63.js} +10 -8
- package/dist/chunk-UN4U4P63.js.map +1 -0
- package/dist/chunk-WIG6XHJ7.js +35 -0
- package/dist/chunk-WIG6XHJ7.js.map +1 -0
- package/dist/chunk-Z76KEZAX.js +15 -0
- package/dist/chunk-Z76KEZAX.js.map +1 -0
- package/dist/chunk-ZDHY47EV.js +363 -0
- package/dist/chunk-ZDHY47EV.js.map +1 -0
- package/dist/config/index.d.ts +276 -0
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controls/index.d.ts +3 -4
- package/dist/controls/index.js +4 -3
- package/dist/dragdrop/index.d.ts +3 -5
- package/dist/index.d.ts +266 -413
- package/dist/index.js +1775 -2251
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +4 -6
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +69 -20
- package/dist/loupe/index.js +2 -2
- package/dist/passthrough/weasel-ui.d.ts +175 -309
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +55 -6
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +13 -8
- package/dist/state/index.js +18 -13
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +223 -76
- 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 +3 -5
- package/package.json +15 -8
- package/src/annotations/AnnotationOverlay.tsx +56 -5
- package/src/annotations/Annotations.less +4 -0
- package/src/annotations/Annotations.overlay.test.tsx +105 -6
- package/src/annotations/drawOne.test.ts +1 -1
- package/src/annotations/drawOne.ts +2 -2
- 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 +5 -4
- package/src/annotations/svgNodes.test.ts +2 -2
- package/src/annotations/svgNodes.ts +1 -0
- package/src/annotations/toolMap.test.ts +9 -5
- package/src/annotations/toolMap.ts +6 -3
- package/src/annotations/types.ts +6 -3
- package/src/canvas/CameraWheelContext.ts +12 -0
- package/src/canvas/CanvasStack.stories.tsx +1 -1
- 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.test.ts +28 -0
- package/src/canvas/usePanZoom.ts +21 -9
- package/src/chrome/ChromeRegions.stories.tsx +1 -4
- package/src/chrome/LabChrome.tsx +130 -0
- package/src/chrome/builtins.test.ts +1 -0
- package/src/chrome/builtins.tsx +21 -31
- package/src/chrome/index.ts +16 -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 +16 -9
- package/src/chrome/regions/SidebarRegion.stories.tsx +44 -0
- package/src/chrome/regions/SidebarRegion.tsx +25 -13
- package/src/chrome/regions/StatusRegion.tsx +15 -8
- package/src/chrome/regions/ToolbarRegion.tsx +34 -16
- package/src/chrome/types.ts +50 -20
- package/src/config/auto.test.ts +12 -0
- package/src/config/auto.ts +15 -0
- package/src/config/autoConfig.test.ts +107 -0
- package/src/config/autoConfig.ts +153 -0
- package/src/config/builder.test.ts +42 -0
- package/src/config/builder.ts +68 -17
- 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 +16 -1
- package/src/config/nodeClasses.test.ts +18 -0
- package/src/config/path.ts +7 -2
- package/src/config/resolve.test.ts +25 -0
- package/src/config/resolve.ts +17 -1
- package/src/config/types.ts +34 -2
- package/src/config/useResolvedConfig.ts +26 -0
- package/src/controls/AutoControls.stories.tsx +88 -0
- package/src/controls/ControlPanel.stories.tsx +42 -1
- package/src/controls/ControlPanel.test.tsx +530 -4
- package/src/controls/ControlPanel.tsx +562 -82
- package/src/fake-indexeddb-auto.d.ts +5 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +66 -5
- package/src/instrument/serializers.test.ts +22 -0
- package/src/instrument/serializers.ts +40 -1
- package/src/instrument/types.ts +25 -1
- package/src/lab/Lab.chrome.test.tsx +16 -0
- package/src/lab/Lab.less +4 -13
- package/src/lab/Lab.persist.test.tsx +147 -0
- package/src/lab/Lab.stories.tsx +1 -3
- package/src/lab/Lab.surface.test.tsx +71 -3
- package/src/lab/Lab.test.tsx +168 -16
- package/src/lab/Lab.tsx +434 -129
- package/src/lab/LabContext.ts +8 -0
- package/src/lab/LabFit.stories.less +18 -0
- package/src/lab/LabFit.stories.tsx +219 -0
- package/src/lab/LabFullChrome.stories.tsx +1 -4
- package/src/lab/LabHeader.test.tsx +40 -9
- package/src/lab/LabHeader.tsx +7 -14
- package/src/lab/LabPalette.tsx +13 -26
- package/src/lab/LabRoot.test.tsx +56 -0
- package/src/lab/LabRoot.tsx +47 -0
- package/src/lab/LabShell.less +73 -15
- package/src/lab/LabShell.stories.tsx +50 -1
- package/src/lab/LabShell.tsx +18 -27
- package/src/lab/LabSwitcher.less +18 -11
- package/src/lab/LabSwitcher.test.tsx +14 -9
- package/src/lab/LabSwitcher.tsx +8 -4
- package/src/lab/Workspace.less +1 -1
- package/src/lab/Workspace.stories.tsx +1 -1
- package/src/lab/Workspace.surface.test.tsx +1 -0
- package/src/lab/fitCheck.test.ts +57 -0
- package/src/lab/fitCheck.ts +125 -0
- package/src/lab/index.ts +2 -0
- package/src/layers/LayerList.stories.tsx +1 -1
- package/src/loupe/AGENTS.md +17 -5
- package/src/loupe/Loupe.less +1 -1
- package/src/loupe/LoupeGestures.tsx +67 -0
- package/src/loupe/TrialLoupe.tsx +43 -28
- package/src/loupe/canvasLens.ts +1 -1
- package/src/loupe/index.ts +4 -0
- package/src/loupe/loupeActions.ts +98 -0
- package/src/loupe/types.ts +1 -1
- package/src/loupe/useLoupe.test.tsx +60 -3
- package/src/loupe/useLoupe.ts +29 -49
- package/src/passthrough/weasel-ui.ts +9 -0
- package/src/primitives/FloatingPanel.stories.tsx +14 -2
- package/src/primitives/FloatingPanel.test.tsx +47 -16
- package/src/primitives/FloatingPanel.tsx +11 -25
- package/src/primitives/FpsMeter.stories.tsx +1 -1
- package/src/primitives/JobProgress.stories.tsx +1 -1
- package/src/primitives/Legend.stories.tsx +1 -1
- package/src/primitives/ScaleIndicator.stories.tsx +1 -1
- package/src/primitives/Sidebar.less +7 -0
- package/src/primitives/Sidebar.stories.tsx +1 -1
- package/src/primitives/Split.less +14 -0
- package/src/primitives/Split.test.tsx +133 -0
- package/src/primitives/Split.tsx +175 -0
- package/src/primitives/StatusBar.stories.tsx +1 -1
- package/src/primitives/Toolbar.stories.tsx +1 -1
- package/src/primitives/ZoomControl.tsx +5 -2
- package/src/primitives/index.ts +2 -0
- package/src/specimen/Specimen.less +95 -0
- package/src/specimen/Specimen.stories.tsx +13 -0
- package/src/specimen/Specimen.test.tsx +13 -0
- package/src/specimen/Specimen.tsx +888 -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 +1 -1
- 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 +169 -635
- package/src/state/store.ts +146 -163
- package/src/state/toolSlot.test.ts +1 -1
- package/src/state/types.ts +65 -17
- 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 +87 -3
- package/src/state/useTrialState.ts +9 -1
- 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 +1 -0
- package/src/surface/useSurfaceTile.ts +6 -5
- package/src/surface/useTiledSurface.test.tsx +231 -1
- package/src/surface/useTiledSurface.ts +76 -15
- package/src/theme/Interstellar.stories.tsx +9 -9
- package/src/theme/interstellar.test.ts +17 -6
- package/src/theme/interstellar.theme.json +180 -0
- package/src/theme/interstellar.ts +8 -5
- package/src/tools/labTool.ts +11 -0
- package/src/trial/Trial.annotations.persist.test.tsx +100 -58
- package/src/trial/Trial.annotations.test.tsx +59 -7
- package/src/trial/Trial.canvas.test.tsx +32 -0
- package/src/trial/Trial.config.test.tsx +75 -2
- package/src/trial/Trial.job.test.tsx +3 -3
- package/src/trial/Trial.less +5 -26
- package/src/trial/Trial.stories.tsx +5 -3
- package/src/trial/Trial.test.tsx +31 -4
- package/src/trial/Trial.trialId.test.tsx +23 -1
- package/src/trial/Trial.tsx +139 -52
- package/src/trial/TrialBody.tsx +17 -141
- package/src/trial/TrialChrome.tsx +24 -7
- package/src/trial/trialOps.test.ts +101 -1
- package/src/trial/trialOps.ts +67 -7
- 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-2P6PP5N4.js +0 -553
- package/dist/chunk-2P6PP5N4.js.map +0 -1
- package/dist/chunk-64ZCN3DA.js.map +0 -1
- package/dist/chunk-CTRKTLYZ.js.map +0 -1
- package/dist/chunk-E44SU6XS.js +0 -50
- package/dist/chunk-E44SU6XS.js.map +0 -1
- package/dist/chunk-FQMJUVHJ.js.map +0 -1
- package/dist/chunk-I6JCVE24.js.map +0 -1
- package/dist/chunk-MOM3GOVY.js.map +0 -1
- package/dist/chunk-SMHP6XZ4.js.map +0 -1
- package/dist/chunk-TJ7QY3OC.js.map +0 -1
- package/dist/chunk-U3IYHIAE.js +0 -343
- package/dist/chunk-U3IYHIAE.js.map +0 -1
- package/dist/chunk-W3ECWC2K.js +0 -7335
- package/dist/chunk-W3ECWC2K.js.map +0 -1
- package/src/theme/interstellar.tokens.json +0 -61
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-IJJUK4FU.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 { at as UndoCapability } from '../_dts/frac-B4okC2lX.js';
|
|
2
2
|
import 'react';
|
|
3
|
-
import '../_dts/types-
|
|
3
|
+
import '../_dts/types-DzoNY1pO.js';
|
|
4
4
|
import '@weasel-js/core';
|
|
5
|
-
import '../_dts/PrefsForm.d-
|
|
6
|
-
import '../_dts/types-x92Kfeme.js';
|
|
7
|
-
import '../_dts/types-BP2OCcpg.js';
|
|
5
|
+
import '../_dts/PrefsForm.d-DPy9I8It.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.1",
|
|
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.1",
|
|
119
|
+
"@weasel-js/theme": "1.5.1",
|
|
114
120
|
"react": "^19.0.0",
|
|
115
121
|
"react-dom": "^19.0.0"
|
|
116
122
|
},
|
|
117
123
|
"dependencies": {
|
|
118
|
-
"@weasel-js/
|
|
119
|
-
"@weasel-js/
|
|
120
|
-
"@weasel-js/
|
|
121
|
-
"@weasel-js/
|
|
124
|
+
"@weasel-js/geom": "1.5.1",
|
|
125
|
+
"@weasel-js/kernel3d": "1.5.1",
|
|
126
|
+
"@weasel-js/loupe": "1.5.1",
|
|
127
|
+
"@weasel-js/svg": "1.5.1",
|
|
128
|
+
"@weasel-js/ui": "1.5.1",
|
|
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.1",
|
|
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();
|
|
@@ -178,6 +226,9 @@ export function AnnotationOverlay({
|
|
|
178
226
|
ref={setInput}
|
|
179
227
|
className="lk-annotate__input"
|
|
180
228
|
data-annotation-target={id}
|
|
229
|
+
// Pass-through while the rail holds a tool that makes no marks, so the
|
|
230
|
+
// instrument under the overlay stays clickable.
|
|
231
|
+
data-idle={annotationToolInfo(activeToolId) ? undefined : ''}
|
|
181
232
|
role="application"
|
|
182
233
|
aria-label={`Annotations on ${id}`}
|
|
183
234
|
// biome-ignore lint/a11y/noNoninteractiveTabindex: role="application" IS the interactive case — a region with its own keymap. The rule's allowed-role list stops at the native widget roles and does not include it, and the gesture dispatcher listens on this element, so without focus no tool's keyboard binding ever fires.
|
|
@@ -190,7 +241,7 @@ export function AnnotationOverlay({
|
|
|
190
241
|
<WeaselProvider isolate>
|
|
191
242
|
<ToolBridge toolId={annotationToolInfo(activeToolId)?.weaselTool ?? 'select'} />
|
|
192
243
|
<SceneCanvas<AnnotationData, 'marks', WorldRect>
|
|
193
|
-
ref={
|
|
244
|
+
ref={attachSceneCanvas}
|
|
194
245
|
scene={scene}
|
|
195
246
|
width={rect.w}
|
|
196
247
|
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) => {
|
|
@@ -122,6 +146,26 @@ describe('<AnnotationTargets>', () => {
|
|
|
122
146
|
expect(boxes[1]?.style.getPropertyValue('--lk-anno-h')).toBe('100px');
|
|
123
147
|
});
|
|
124
148
|
|
|
149
|
+
it('takes input while a mark tool is held, and passes it through otherwise', () => {
|
|
150
|
+
const drawing = render(<Harness toolId="rect" />, { wrapper: StrictMode });
|
|
151
|
+
act(() => {
|
|
152
|
+
vi.advanceTimersByTime(64);
|
|
153
|
+
});
|
|
154
|
+
expect(drawing.container.querySelector('.lk-annotate__input')?.hasAttribute('data-idle')).toBe(
|
|
155
|
+
false,
|
|
156
|
+
);
|
|
157
|
+
drawing.unmount();
|
|
158
|
+
|
|
159
|
+
// `pointer` is in the rail but not in the tool map: the instrument under the overlay stays clickable.
|
|
160
|
+
const idle = render(<Harness toolId="pointer" />, { wrapper: StrictMode });
|
|
161
|
+
act(() => {
|
|
162
|
+
vi.advanceTimersByTime(64);
|
|
163
|
+
});
|
|
164
|
+
expect(idle.container.querySelector('.lk-annotate__input')?.hasAttribute('data-idle')).toBe(
|
|
165
|
+
true,
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
125
169
|
it('lands the boxes in the surface container, not among the instrument DOM', () => {
|
|
126
170
|
const { container } = render(<Harness />, { wrapper: StrictMode });
|
|
127
171
|
act(() => {
|
|
@@ -133,6 +177,61 @@ describe('<AnnotationTargets>', () => {
|
|
|
133
177
|
}
|
|
134
178
|
});
|
|
135
179
|
|
|
180
|
+
it('hands a wheel over a target to the trial camera', () => {
|
|
181
|
+
// The input box is portalled into the surface container, so the wheel
|
|
182
|
+
// would otherwise bubble past the stage the picture sits on.
|
|
183
|
+
const wheel = vi.fn();
|
|
184
|
+
const slot: CameraWheelSlot = { current: wheel };
|
|
185
|
+
const { container } = render(
|
|
186
|
+
<CameraWheelContext.Provider value={slot}>
|
|
187
|
+
<Harness />
|
|
188
|
+
</CameraWheelContext.Provider>,
|
|
189
|
+
{ wrapper: StrictMode },
|
|
190
|
+
);
|
|
191
|
+
act(() => {
|
|
192
|
+
vi.advanceTimersByTime(64);
|
|
193
|
+
});
|
|
194
|
+
const box = container.querySelector('.lk-annotate__input');
|
|
195
|
+
box?.dispatchEvent(new WheelEvent('wheel', { bubbles: true, cancelable: true }));
|
|
196
|
+
expect(wheel).toHaveBeenCalledTimes(1);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('registers a clear for each tile, so a moved mark leaves nothing behind', () => {
|
|
200
|
+
// A proxy: jsdom has no WebGL2, so the clear itself cannot run here. Without
|
|
201
|
+
// one registered, a tile that moves leaves its last frame's marks painted
|
|
202
|
+
// where it used to be — which the surface only erases through a clear.
|
|
203
|
+
const registered = vi.fn();
|
|
204
|
+
render(<Harness onRegisterClear={registered} />, { wrapper: StrictMode });
|
|
205
|
+
act(() => {
|
|
206
|
+
vi.advanceTimersByTime(64);
|
|
207
|
+
});
|
|
208
|
+
expect(new Set(registered.mock.calls.map((call) => call[0])).size).toBe(2);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('never opens the shared context itself when it clears', () => {
|
|
212
|
+
// A bare `getContext('webgl2')` on a buffer nobody has opened yet creates
|
|
213
|
+
// the context with default attributes, and the renderer's later request for
|
|
214
|
+
// a stencil buffer then gets that one back: every mark paints nothing, with
|
|
215
|
+
// no error anywhere. jsdom opens no context, so the call is the proxy.
|
|
216
|
+
const clears: SurfaceClear[] = [];
|
|
217
|
+
const canvas = document.createElement('canvas');
|
|
218
|
+
const { unmount } = render(
|
|
219
|
+
<Harness canvas={canvas} onRegisterClear={(_id, clear) => clears.push(clear)} />,
|
|
220
|
+
{ wrapper: StrictMode },
|
|
221
|
+
);
|
|
222
|
+
act(() => {
|
|
223
|
+
vi.advanceTimersByTime(64);
|
|
224
|
+
});
|
|
225
|
+
// The prototype is already a mock file-wide, so the spy arrives holding
|
|
226
|
+
// the renderer's own attempts to open it.
|
|
227
|
+
const open = vi.spyOn(canvas, 'getContext');
|
|
228
|
+
open.mockClear();
|
|
229
|
+
for (const clear of clears) clear({ width: 800, height: 600 }, 1);
|
|
230
|
+
expect(open).not.toHaveBeenCalled();
|
|
231
|
+
// Before the frame stubs come off: the canvas's own loop cancels through them.
|
|
232
|
+
unmount();
|
|
233
|
+
});
|
|
234
|
+
|
|
136
235
|
it('takes its tiles back out on unmount', () => {
|
|
137
236
|
const { container, unmount } = render(<Harness />, { wrapper: StrictMode });
|
|
138
237
|
act(() => {
|
|
@@ -206,7 +305,7 @@ describe('two trials of one instrument', () => {
|
|
|
206
305
|
handle = surface;
|
|
207
306
|
return (
|
|
208
307
|
<SurfaceContext.Provider value={surface}>
|
|
209
|
-
<SurfaceCanvasContext.Provider value={null}>
|
|
308
|
+
<SurfaceCanvasContext.Provider value={{ over: null, under: null }}>
|
|
210
309
|
<div
|
|
211
310
|
ref={(el) => {
|
|
212
311
|
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(
|
|
@@ -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
|
@@ -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;
|
|
@@ -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 });
|
|
@@ -27,6 +27,7 @@ function toSvgStroke(stroke: Stroke): SvgStroke {
|
|
|
27
27
|
...(stroke.join ? { join: stroke.join } : {}),
|
|
28
28
|
...(stroke.dash ? { dash: [...stroke.dash] } : {}),
|
|
29
29
|
...(stroke.miterLimit != null ? { miterLimit: stroke.miterLimit } : {}),
|
|
30
|
+
...(stroke.align ? { align: stroke.align } : {}),
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -6,8 +6,12 @@ import type { AnnotationKind } from './types';
|
|
|
6
6
|
const KINDS: AnnotationKind[] = ['stroke', 'line', 'arrow', 'rect', 'ellipse', 'text'];
|
|
7
7
|
|
|
8
8
|
describe('the annotation tool table', () => {
|
|
9
|
-
it('offers one tool per mark kind, plus select', () => {
|
|
10
|
-
expect(ANNOTATION_TOOLS.map((t) => t.id)).toEqual(['select', ...KINDS]);
|
|
9
|
+
it('offers one tool per mark kind, plus pointer and select', () => {
|
|
10
|
+
expect(ANNOTATION_TOOLS.map((t) => t.id)).toEqual(['pointer', 'select', ...KINDS]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('gives pointer no entry at all, which is what leaves the overlay idle', () => {
|
|
14
|
+
expect(annotationToolInfo('pointer')).toBeUndefined();
|
|
11
15
|
});
|
|
12
16
|
|
|
13
17
|
it('only names weasel tools SceneCanvas will mount', () => {
|
|
@@ -15,9 +19,9 @@ describe('the annotation tool table', () => {
|
|
|
15
19
|
// being built in should fail here rather than in a silent no-op palette.
|
|
16
20
|
const mountable = new Set<string>([...KIT_SHAPE_KINDS, 'select']);
|
|
17
21
|
for (const t of ANNOTATION_TOOLS) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
);
|
|
22
|
+
const info = annotationToolInfo(t.id);
|
|
23
|
+
if (!info) continue;
|
|
24
|
+
expect(mountable, `${t.id} maps to a tool the kit does not mount`).toContain(info.weaselTool);
|
|
21
25
|
}
|
|
22
26
|
});
|
|
23
27
|
|
|
@@ -2,6 +2,7 @@ import type { BuiltinToolId } from '@weasel-js/core';
|
|
|
2
2
|
import {
|
|
3
3
|
ArrowIcon,
|
|
4
4
|
EllipseIcon,
|
|
5
|
+
HandIcon,
|
|
5
6
|
LineIcon,
|
|
6
7
|
PencilIcon,
|
|
7
8
|
RectIcon,
|
|
@@ -30,10 +31,12 @@ const TOOLS: Record<string, AnnotationToolInfo> = {
|
|
|
30
31
|
text: { weaselTool: 'text', kind: 'text' },
|
|
31
32
|
};
|
|
32
33
|
|
|
33
|
-
/** The
|
|
34
|
-
*
|
|
35
|
-
* holds whichever is active. */
|
|
34
|
+
/** The tools a lab's rail gets when any instrument declares `annotations`.
|
|
35
|
+
* Ids share the lab's contribution namespace with its own `tools`, and the
|
|
36
|
+
* lab's tool slot holds whichever is active. */
|
|
36
37
|
export const ANNOTATION_TOOLS: readonly TrialTool[] = [
|
|
38
|
+
// Not in `TOOLS`: an id the map does not carry is what leaves the overlay idle.
|
|
39
|
+
{ id: 'pointer', label: 'Interact', icon: HandIcon, group: 'annotate' },
|
|
37
40
|
{ id: 'select', label: 'Select', icon: SelectIcon, group: 'annotate' },
|
|
38
41
|
{ id: 'stroke', label: 'Freehand', icon: PencilIcon, group: 'annotate' },
|
|
39
42
|
{ id: 'line', label: 'Line', icon: LineIcon, group: 'annotate' },
|