@jax-data-science/sci-image-visualizer 0.3.3 → 0.4.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 +208 -4
- package/esm2022/index.mjs +20 -1
- package/esm2022/lib/contracts/display-types.mjs +20 -2
- package/esm2022/lib/contracts/image.contract.mjs +1 -1
- package/esm2022/lib/contracts/plot-type.mjs +36 -1
- package/esm2022/lib/contracts/ports/spatial-data.port.mjs +3 -0
- package/esm2022/lib/contracts/spatial-dataset.contract.mjs +45 -0
- package/esm2022/lib/contracts/visualizer.contract.mjs +1 -1
- package/esm2022/lib/implementations/napari-js/napari-axes-labels.mjs +10 -19
- package/esm2022/lib/implementations/napari-js/napari-region-overlay.mjs +71 -7
- package/esm2022/lib/implementations/napari-js/napari-scale-bar.mjs +1 -1
- package/esm2022/lib/implementations/napari-js/napari-spatial-tooltip.mjs +96 -0
- package/esm2022/lib/implementations/napari-js/napari-visualizer.service.mjs +1934 -55
- package/esm2022/lib/implementations/napari-js/napari-zoom.mjs +18 -0
- package/esm2022/lib/implementations/osd/openseadragon-visualizer.service.mjs +5 -3
- package/esm2022/lib/implementations/osd/osd-region-overlay.mjs +3 -4
- package/esm2022/lib/implementations/osd/osd-zoom.mjs +20 -0
- package/esm2022/lib/implementations/plotly/omics-trace-builders.mjs +555 -0
- package/esm2022/lib/implementations/plotly/plotly.service.mjs +13 -7
- package/esm2022/lib/implementations/spatial-data-http/spatial-data-http.service.mjs +304 -0
- package/esm2022/lib/implementations/spatial-data-http/spatial-wire.mjs +179 -0
- package/esm2022/lib/routing-visualizer.service.mjs +223 -12
- package/esm2022/lib/spatial/embedding-compute.mjs +142 -0
- package/esm2022/lib/spatial/gene-search.mjs +77 -0
- package/esm2022/lib/spatial/spatial-density.mjs +224 -0
- package/esm2022/lib/spatial/spatial-encoding.mjs +232 -0
- package/esm2022/lib/spatial/spatial-expression.mjs +257 -0
- package/esm2022/lib/spatial/spatial-framing.mjs +60 -0
- package/esm2022/lib/spatial/spatial-heatmap.mjs +185 -0
- package/esm2022/lib/spatial/spatial-hover.mjs +79 -0
- package/esm2022/lib/spatial/spatial-sections.mjs +96 -0
- package/esm2022/lib/spatial/spatial-selection.mjs +264 -0
- package/esm2022/lib/spatial/spatial-volume-image.mjs +163 -0
- package/esm2022/lib/spatial/tsne-worker.mjs +10 -0
- package/esm2022/lib/spatial/world-grid.mjs +77 -0
- package/esm2022/lib/spatial-controls/spatial-charts/spatial-charts.component.mjs +1278 -0
- package/esm2022/lib/spatial-controls/spatial-controls.component.mjs +621 -0
- package/esm2022/lib/store/spatial-selection.service.mjs +47 -0
- package/esm2022/lib/store/visualizer-store.service.mjs +23 -1
- package/esm2022/lib/toolbar/toolbar.component.mjs +30 -7
- package/esm2022/lib/visualization.module.mjs +23 -5
- package/esm2022/lib/visualizer.component.mjs +283 -22
- package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-ci1q0xnJ.mjs → jax-data-science-sci-image-visualizer-onnx-sam-session-BwP-A11m.mjs} +6 -4
- package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-ci1q0xnJ.mjs.map → jax-data-science-sci-image-visualizer-onnx-sam-session-BwP-A11m.mjs.map} +1 -1
- package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs +12 -0
- package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs.map +1 -0
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs +15769 -8316
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs.map +1 -1
- package/fesm2022/tsne.worker.js +19091 -0
- package/index.d.ts +15 -0
- package/lib/contracts/display-types.d.ts +131 -0
- package/lib/contracts/image.contract.d.ts +7 -0
- package/lib/contracts/plot-type.d.ts +42 -2
- package/lib/contracts/ports/spatial-data.port.d.ts +90 -0
- package/lib/contracts/spatial-dataset.contract.d.ts +281 -0
- package/lib/contracts/visualizer.contract.d.ts +99 -1
- package/lib/implementations/napari-js/napari-axes-labels.d.ts +9 -7
- package/lib/implementations/napari-js/napari-region-overlay.d.ts +38 -4
- package/lib/implementations/napari-js/napari-scale-bar.d.ts +7 -3
- package/lib/implementations/napari-js/napari-spatial-tooltip.d.ts +32 -0
- package/lib/implementations/napari-js/napari-visualizer.service.d.ts +497 -16
- package/lib/implementations/napari-js/napari-zoom.d.ts +15 -0
- package/lib/implementations/osd/osd-zoom.d.ts +19 -0
- package/lib/implementations/plotly/omics-trace-builders.d.ts +193 -0
- package/lib/implementations/plotly/plotly.service.d.ts +9 -0
- package/lib/implementations/spatial-data-http/spatial-data-http.service.d.ts +123 -0
- package/lib/implementations/spatial-data-http/spatial-wire.d.ts +115 -0
- package/lib/routing-visualizer.service.d.ts +45 -6
- package/lib/spatial/embedding-compute.d.ts +73 -0
- package/lib/spatial/gene-search.d.ts +48 -0
- package/lib/spatial/spatial-density.d.ts +73 -0
- package/lib/spatial/spatial-encoding.d.ts +155 -0
- package/lib/spatial/spatial-expression.d.ts +144 -0
- package/lib/spatial/spatial-framing.d.ts +37 -0
- package/lib/spatial/spatial-heatmap.d.ts +111 -0
- package/lib/spatial/spatial-hover.d.ts +49 -0
- package/lib/spatial/spatial-sections.d.ts +59 -0
- package/lib/spatial/spatial-selection.d.ts +113 -0
- package/lib/spatial/spatial-volume-image.d.ts +67 -0
- package/lib/spatial/tsne-worker.d.ts +7 -0
- package/lib/spatial/world-grid.d.ts +58 -0
- package/lib/spatial-controls/spatial-charts/spatial-charts.component.d.ts +439 -0
- package/lib/spatial-controls/spatial-controls.component.d.ts +257 -0
- package/lib/store/spatial-selection.service.d.ts +27 -0
- package/lib/store/visualizer-store.service.d.ts +13 -0
- package/lib/toolbar/toolbar.component.d.ts +19 -3
- package/lib/visualization.module.d.ts +28 -24
- package/lib/visualizer.component.d.ts +73 -2
- package/package.json +6 -2
- package/src/lib/assets/spatial-omics-3d.svg +31 -0
- package/src/lib/assets/spatial-omics.svg +27 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Observable } from 'rxjs';
|
|
2
2
|
import { Image } from 'image-js';
|
|
3
3
|
import { IImageInfo } from '../../contracts/image.contract';
|
|
4
|
+
import { SpatialDataPort } from '../../contracts/ports/spatial-data.port';
|
|
5
|
+
import { SpatialObservations } from '../../contracts/spatial-dataset.contract';
|
|
6
|
+
import { SpatialSelectionStore } from '../../store/spatial-selection.service';
|
|
4
7
|
import { PlotType, PlotTypeDescriptor } from '../../contracts/plot-type';
|
|
5
8
|
import { IVisualizer, PixelData, IntensityProfile, IIsosurfaceControls, IIntensityControls, ISurface3dControls } from '../../contracts/visualizer.contract';
|
|
6
9
|
import { ViewerCapabilities } from '../../contracts/capabilities.contract';
|
|
@@ -30,20 +33,6 @@ interface NapariLoaded {
|
|
|
30
33
|
* handle's `filename` doesn't match the requested image (guards against stale clicks). */
|
|
31
34
|
filename: string;
|
|
32
35
|
}
|
|
33
|
-
/**
|
|
34
|
-
* A WebGPU image backend built on the published `napari-js` library — the POC engine for
|
|
35
|
-
* jit-ui#102 (a browser-based napari shipped as a JS library, swapping image plotting with
|
|
36
|
-
* OpenSeadragon and 3D slicing/isosurfaces with Plotly).
|
|
37
|
-
*
|
|
38
|
-
* Render strategy: rather than re-implement the server's tile/pyramid protocol (which OSD
|
|
39
|
-
* already handles), this backend renders the **complete per-slice image URLs the app already
|
|
40
|
-
* produces** (`IImageInfo.urls`) — `urls[z]` for the 2D image, and the full `urls` stack
|
|
41
|
-
* assembled into a downsampled volume for the 3D types. Region state and display options
|
|
42
|
-
* delegate to the shared {@link RegionStore} / {@link VisualizerStore}, exactly as OSD does.
|
|
43
|
-
*
|
|
44
|
-
* Follow-ups (jit-ui#102): native-resolution pyramidal tiling, on-canvas tools, region
|
|
45
|
-
* overlay rendering, per-channel histograms, TIFF export.
|
|
46
|
-
*/
|
|
47
36
|
export declare class NapariVisualizerService extends BaseStoreVisualizer implements IVisualizer {
|
|
48
37
|
private readonly tiles;
|
|
49
38
|
private readonly wandTool;
|
|
@@ -55,6 +44,8 @@ export declare class NapariVisualizerService extends BaseStoreVisualizer impleme
|
|
|
55
44
|
private readonly cellSegmentTool;
|
|
56
45
|
private readonly cellSegmenter;
|
|
57
46
|
private readonly simpleStack;
|
|
47
|
+
private readonly spatialData;
|
|
48
|
+
private readonly selectionStore;
|
|
58
49
|
readonly capabilities: ViewerCapabilities;
|
|
59
50
|
private readonly api;
|
|
60
51
|
private viewer;
|
|
@@ -90,6 +81,97 @@ export declare class NapariVisualizerService extends BaseStoreVisualizer impleme
|
|
|
90
81
|
private scatterRegionSub;
|
|
91
82
|
/** napari-js 3D scatter (voxel point cloud). */
|
|
92
83
|
private scatter3dLayer;
|
|
84
|
+
/** Spatial-omics observation markers + the dataset/view subscription driving them. */
|
|
85
|
+
private spatialPoints;
|
|
86
|
+
/** The 3D point cloud. One layer: selection is a per-point alpha on it, not a second. */
|
|
87
|
+
private spatialPoints3d;
|
|
88
|
+
private spatialLayerKey3d;
|
|
89
|
+
/** Interleaved x,y,z, cached so a colour change does not re-walk 3.7M observations. */
|
|
90
|
+
private spatialPositions3d;
|
|
91
|
+
/** Identity of the scalars currently uploaded — see {@link rebuildSpatialPoints3d}. */
|
|
92
|
+
private spatialScalarKey3d;
|
|
93
|
+
/** The anatomical volume the cloud sits inside, when the dataset has one. */
|
|
94
|
+
private spatialVolume;
|
|
95
|
+
private spatialVolumeKey;
|
|
96
|
+
/** Per-cluster density volumes drawn alongside the cloud, and what they were
|
|
97
|
+
* built from — rasterising is seconds of work, so it must not repeat for a
|
|
98
|
+
* change that cannot affect the field. */
|
|
99
|
+
private densityLayers;
|
|
100
|
+
private densityKey;
|
|
101
|
+
/** Selection identity, as a number the density key can carry. */
|
|
102
|
+
private lastSelectionSeen;
|
|
103
|
+
private selectionRevision;
|
|
104
|
+
/** The gene map: its layer, the field it was estimated from, and the inputs each
|
|
105
|
+
* was built for — the field is the expensive half and survives a recolour. */
|
|
106
|
+
private geneMapLayer;
|
|
107
|
+
private geneMapKey;
|
|
108
|
+
private geneMapField;
|
|
109
|
+
private geneMapFieldKey;
|
|
110
|
+
/** The 3D gene map: its volume layer, the field behind it, and the inputs each
|
|
111
|
+
* was built for — same two clocks as the 2D map. */
|
|
112
|
+
private geneMapVolumeLayer;
|
|
113
|
+
private geneMapVolumeKey;
|
|
114
|
+
private geneMapVolumeField;
|
|
115
|
+
private geneMapVolumeFieldKey;
|
|
116
|
+
/** Offset applied to observation coordinates to sit them in the volume's box. */
|
|
117
|
+
private spatialOrigin3d;
|
|
118
|
+
/** Cursor tooltip for the spatial views, and everything it needs: what the
|
|
119
|
+
* cloud is coloured by, and where each drawn observation is. */
|
|
120
|
+
private spatialTooltip;
|
|
121
|
+
private hoverSource;
|
|
122
|
+
private hoverSourceKey;
|
|
123
|
+
/** Sequences the async resolutions. The KEY is only committed once one lands,
|
|
124
|
+
* so a superseded fetch cannot leave the cache claiming to hold a source it
|
|
125
|
+
* never stored. */
|
|
126
|
+
private hoverSourceToken;
|
|
127
|
+
/** Drawn observations' positions for hit-testing, indexed BY OBSERVATION with
|
|
128
|
+
* NaN for anything not drawn. Screen pixels in 3D (the camera moves them, so
|
|
129
|
+
* they are rebuilt when it does) and WORLD units in 2D (where the camera only
|
|
130
|
+
* scales, so the pointer is converted instead of 374k points). */
|
|
131
|
+
private hoverPositions;
|
|
132
|
+
private hoverPositionsRev;
|
|
133
|
+
/**
|
|
134
|
+
* Screen-space bucket index over the 3D projection, built on the first hover after the
|
|
135
|
+
* scene moved rather than when it moves.
|
|
136
|
+
*
|
|
137
|
+
* Lazily, and that is the whole design: an orbit drag changes the camera every frame, so
|
|
138
|
+
* building eagerly would spend tens of milliseconds a frame indexing for picks nobody is
|
|
139
|
+
* making. Deferred, it is built once when the drag stops and the pointer next moves —
|
|
140
|
+
* measured upstream at 3.7M points, that turns a 12.6 ms scan per pointermove into
|
|
141
|
+
* 0.066 ms.
|
|
142
|
+
*/
|
|
143
|
+
private hoverIndex;
|
|
144
|
+
/** Bumped whenever the cached positions go stale: a marker rebuild, or — in 3D
|
|
145
|
+
* only, where the projection depends on it — a camera move. */
|
|
146
|
+
private spatialSceneRev;
|
|
147
|
+
private hoverPointer;
|
|
148
|
+
private hoverFrame;
|
|
149
|
+
private hoverOff;
|
|
150
|
+
/** Observation indices the cached 3D positions belong to, in the same order;
|
|
151
|
+
* null when every observation is drawn. Without it a projection built from the
|
|
152
|
+
* positions is indexed by DRAWN order and silently attributes each point to
|
|
153
|
+
* the wrong observation as soon as a section is isolated. */
|
|
154
|
+
private spatialDrawn3d;
|
|
155
|
+
/** Reused projection buffers — see {@link getSpatialScreenProjection}. */
|
|
156
|
+
private spatialProjection3d;
|
|
157
|
+
/** Per-observation depth from the last projection, for the depth-aware hover pick. */
|
|
158
|
+
private spatialDepths3d;
|
|
159
|
+
/** The (viewer, dataset) whose 3D scene has already had its opening framing. */
|
|
160
|
+
/** Whose 2D observations the camera has been framed on — see {@link frameSpatialPointsOnce}. */
|
|
161
|
+
private spatial2dFramed;
|
|
162
|
+
/** Dataset the 3D scale bar was built for. */
|
|
163
|
+
private spatialScaleBarKey;
|
|
164
|
+
private spatialSub;
|
|
165
|
+
/** Latest (dataset, view, selection) the spatial subscription saw, so a slice
|
|
166
|
+
* change can rebuild the markers for the new plane. */
|
|
167
|
+
private spatialLatest;
|
|
168
|
+
/** Which dataset the current marker layer was built for, so a display-only
|
|
169
|
+
* change (size, colour, opacity, selection) can update it in place. */
|
|
170
|
+
private spatialLayerKey;
|
|
171
|
+
/** Monotonic guard for the async colour rebuild: fetching a gene vector is a
|
|
172
|
+
* round-trip, so a fast sequence of colour-by changes can resolve out of
|
|
173
|
+
* order. Only the newest rebuild is allowed to touch the layer. */
|
|
174
|
+
private spatialRebuildToken;
|
|
93
175
|
/** Monotonic load generation. Bumped by {@link reset} and {@link cancelLoading}; the frame-loading
|
|
94
176
|
* loops (volume assembly, surface preload) capture it and bail when it changes, so a Cancel (or a
|
|
95
177
|
* new plot) actually stops fetching frames instead of running to completion in the background. */
|
|
@@ -168,7 +250,7 @@ export declare class NapariVisualizerService extends BaseStoreVisualizer impleme
|
|
|
168
250
|
private readonly autoscaleEvent$;
|
|
169
251
|
private readonly intensityProfile$;
|
|
170
252
|
private readonly viewportChange$;
|
|
171
|
-
constructor(tiles: TileAccessPort, store: VisualizerStore, regionStore: RegionStore, wandTool: WandToolService, brushTool: BrushToolService, eraserTool: VertexEraserToolService, zoomToBoxTool: ZoomToBoxToolService, samTool: SamToolService, samPointTool: SamPointToolService, cellSegmentTool: CellSegmentToolService, cellSegmenter: ICellSegmenter | null, simpleStack: SimpleSliceAccessService, config: VizConfig);
|
|
253
|
+
constructor(tiles: TileAccessPort, store: VisualizerStore, regionStore: RegionStore, wandTool: WandToolService, brushTool: BrushToolService, eraserTool: VertexEraserToolService, zoomToBoxTool: ZoomToBoxToolService, samTool: SamToolService, samPointTool: SamPointToolService, cellSegmentTool: CellSegmentToolService, cellSegmenter: ICellSegmenter | null, simpleStack: SimpleSliceAccessService, config: VizConfig, spatialData?: SpatialDataPort | null, selectionStore?: SpatialSelectionStore | null);
|
|
172
254
|
/**
|
|
173
255
|
* Fetch + cache the server pyramid descriptor (`GET /tiles/info`): the REAL per-level tile grid,
|
|
174
256
|
* tile size, channel metadata and physical pixel size. The backend returns 202 while the source
|
|
@@ -269,6 +351,15 @@ export declare class NapariVisualizerService extends BaseStoreVisualizer impleme
|
|
|
269
351
|
* world box (matching the AxesLayer geometry); the scale text reflects the FULL image extent —
|
|
270
352
|
* physical µm when µm/pixel is known, else pixel (X/Y) / slice (Z) counts. */
|
|
271
353
|
private buildAxesLabels;
|
|
354
|
+
/**
|
|
355
|
+
* The image's declared extent in pixels/slices, and its physical spacing per
|
|
356
|
+
* axis in µm (0 = undeclared, and then that axis reads in pixels).
|
|
357
|
+
*
|
|
358
|
+
* One place, because the volume world box and the axis labels have to agree
|
|
359
|
+
* about what the image's real dimensions are — the sampled grid is decimated and
|
|
360
|
+
* says nothing about either.
|
|
361
|
+
*/
|
|
362
|
+
private imageExtent;
|
|
272
363
|
/**
|
|
273
364
|
* Assemble a downsampled uint8 volume (luminance) from the per-slice tile endpoint. Slices are
|
|
274
365
|
* fetched with bounded concurrency (keeps the connection pool full without flooding it on a deep
|
|
@@ -286,6 +377,396 @@ export declare class NapariVisualizerService extends BaseStoreVisualizer impleme
|
|
|
286
377
|
private rebuildScatterPoints;
|
|
287
378
|
/** Region centroids as flat `[x, y, …]` data coords (rectangle / polygon / multipolygon). */
|
|
288
379
|
private regionCentroids;
|
|
380
|
+
/**
|
|
381
|
+
* The interaction stack every 2D mode needs: the region overlay (draw/select/edit), the pixel
|
|
382
|
+
* tools that read back displayed pixels (wand, brush, vertex eraser, zoom-to-box, SAM/cellpose),
|
|
383
|
+
* and a camera hook that keeps that readback current as the view pans/zooms and tiled levels
|
|
384
|
+
* load.
|
|
385
|
+
*
|
|
386
|
+
* Extracted because forgetting it is invisible: the toolbar gates its region buttons on 2D-vs-3D
|
|
387
|
+
* rather than on plot type, so a 2D mode that skips this shows every tool and silently does
|
|
388
|
+
* nothing. That is exactly what happened to the spatial and region-centroid scatter modes.
|
|
389
|
+
*/
|
|
390
|
+
private install2dInteraction;
|
|
391
|
+
/** Marker radius (image px) for a dataset that declares none — segmented cells often don't. */
|
|
392
|
+
private static readonly SPATIAL_FALLBACK_RADIUS;
|
|
393
|
+
/** Smallest marker DIAMETER, in slice pixels, over a volume-backed dataset. A cell's real
|
|
394
|
+
* size is honoured wherever it survives the grid: the ABC atlas serves 5 µm radii on a
|
|
395
|
+
* 40 µm/px template, so drawing them to scale would put every cell a fifth of a pixel wide
|
|
396
|
+
* and the section would come up empty. The point-size control scales up from this floor. */
|
|
397
|
+
private static readonly SPATIAL_SLICE_MIN_DIAMETER_PX;
|
|
398
|
+
/** Longest side of the gene-map raster, in field pixels. A gene map is a smooth
|
|
399
|
+
* field read as territory, so it gains nothing from matching a 2 Gpx slide's
|
|
400
|
+
* resolution — and the estimate costs one pass over this many pixels. */
|
|
401
|
+
private static readonly GENE_MAP_MAX_SIDE;
|
|
402
|
+
/** Kernel σ in field pixels at smoothing 1: wide enough to read between cells,
|
|
403
|
+
* tight enough to keep a nucleus-scale structure distinct. */
|
|
404
|
+
private static readonly GENE_MAP_SIGMA;
|
|
405
|
+
/** Clusters drawn as density volumes at once. Past a handful, additive translucent
|
|
406
|
+
* clouds stop being separable by eye — and each one is a full rasterisation. */
|
|
407
|
+
private static readonly DENSITY_MAX_CLUSTERS;
|
|
408
|
+
/** Colour for observations when nothing is selected to colour by: visible, neutral, and
|
|
409
|
+
* obviously not encoding anything. */
|
|
410
|
+
/** How much larger a selected marker is drawn, so a small selection is findable inside a
|
|
411
|
+
* 3.7M-point cloud rather than merely brighter. */
|
|
412
|
+
private static readonly SPATIAL_SELECTED_SIZE_SCALE;
|
|
413
|
+
private static readonly SPATIAL_NEUTRAL_COLOR;
|
|
414
|
+
/**
|
|
415
|
+
* Mount the SPATIAL_OMICS view: the tissue image with one marker per observation, coloured by
|
|
416
|
+
* an annotation column or a gene. The markers rebuild whenever the dataset or the view state
|
|
417
|
+
* changes, so switching the colour-by column does not remount the scene.
|
|
418
|
+
*
|
|
419
|
+
* Sets up the full 2D interaction stack — region overlay, pixel-tool hosts, readback currency —
|
|
420
|
+
* exactly as the plain image view does. That is NOT optional here: this mode's selection is
|
|
421
|
+
* driven by drawn ROIs, so without the overlay there is no way to make a selection at all.
|
|
422
|
+
*/
|
|
423
|
+
private mountSpatialOmics;
|
|
424
|
+
/**
|
|
425
|
+
* Region drawing for the 3D cloud.
|
|
426
|
+
*
|
|
427
|
+
* Reuses {@link NapariRegionOverlay} verbatim by handing it a SCREEN-SPACE viewer: the drawn
|
|
428
|
+
* shape is a lasso in canvas pixels, so "world" is the canvas and both transforms are identity
|
|
429
|
+
* (bar the client→canvas offset). Every existing tool — rectangle, polygon, freehand — therefore
|
|
430
|
+
* works in 3D with no new drawing code.
|
|
431
|
+
*
|
|
432
|
+
* A screen-space shape stops meaning anything the moment the camera moves, so an orbit clears
|
|
433
|
+
* the drawn regions. The SELECTION it produced is kept: that is the durable artefact, and the
|
|
434
|
+
* highlighted points stay highlighted from every angle.
|
|
435
|
+
*/
|
|
436
|
+
private install3dInteraction;
|
|
437
|
+
/** Movement, in screen pixels, under which a press-release is a CLICK and not a
|
|
438
|
+
* drag. An orbit or a pan starts the same way, so the two have to be told
|
|
439
|
+
* apart by how far the pointer travelled. */
|
|
440
|
+
private static readonly CLICK_SLOP_PX;
|
|
441
|
+
/** Longest press-release still treated as a click. A long press with the mouse
|
|
442
|
+
* held still is more likely an interrupted drag than a selection. */
|
|
443
|
+
private static readonly CLICK_MAX_MS;
|
|
444
|
+
/** Pointer distance, in screen pixels, that still counts as "on" a marker.
|
|
445
|
+
* Generous relative to a 1.5px disc: the cursor is a blunt instrument, and a
|
|
446
|
+
* tooltip you have to hunt for is worse than none. */
|
|
447
|
+
private static readonly HOVER_RADIUS_PX;
|
|
448
|
+
/**
|
|
449
|
+
* Cursor tooltip for the spatial views: hover a marker, read its class.
|
|
450
|
+
*
|
|
451
|
+
* A 34-entry legend cannot be read back from a dot — several classes get
|
|
452
|
+
* similar colours, and matching one to a swatch by eye is exactly the task this
|
|
453
|
+
* removes. It reports whatever the cloud is CURRENTLY coloured by, so it and
|
|
454
|
+
* the legend can never say different things.
|
|
455
|
+
*
|
|
456
|
+
* Listens on the HOST rather than the canvas so it keeps working over the region
|
|
457
|
+
* overlay (an SVG covering the canvas, which would otherwise swallow every
|
|
458
|
+
* move), and throttles to one hit-test per animation frame: a pointermove can
|
|
459
|
+
* fire far more often than that, and each test is a pass over the cloud.
|
|
460
|
+
*/
|
|
461
|
+
private installSpatialHover;
|
|
462
|
+
private removeSpatialHover;
|
|
463
|
+
/** Hit-test the last pointer position and show or hide the tooltip. */
|
|
464
|
+
private updateHover;
|
|
465
|
+
/**
|
|
466
|
+
* Select the class of the marker at a client position — the canvas equivalent of
|
|
467
|
+
* clicking that class in the panel's legend, and the same selection object, so
|
|
468
|
+
* the two controls cannot produce different results.
|
|
469
|
+
*
|
|
470
|
+
* Clicking a class that is already the whole selection CLEARS it, which is what
|
|
471
|
+
* the legend does. Compared against the selection itself rather than against a
|
|
472
|
+
* remembered click, so selecting from the legend and then clicking the same
|
|
473
|
+
* class on the canvas still toggles.
|
|
474
|
+
*/
|
|
475
|
+
private selectClassAt;
|
|
476
|
+
/**
|
|
477
|
+
* Which observation is under the cursor.
|
|
478
|
+
*
|
|
479
|
+
* In 3D this defers to napari-js's {@link nearestProjectedIndex} WITH the depths the
|
|
480
|
+
* projection produced, so the front-most candidate wins. That matters more than it
|
|
481
|
+
* sounds: the renderer depth-tests the billboards, and in a 3.7M-point cloud the cursor
|
|
482
|
+
* covers many of them — picking the one nearest the cursor's centre regularly names a
|
|
483
|
+
* cell that something else is drawn over, which reads as a wrong tooltip rather than as
|
|
484
|
+
* a subtlety of picking.
|
|
485
|
+
*
|
|
486
|
+
* In 2D the positions are WORLD coordinates on one plane, so there is no depth to break
|
|
487
|
+
* ties with and the existing nearest-marker rule is the right one.
|
|
488
|
+
*/
|
|
489
|
+
private pickObservation;
|
|
490
|
+
/**
|
|
491
|
+
* Positions to hit-test against, rebuilt only when the scene or camera moved.
|
|
492
|
+
*
|
|
493
|
+
* A pass over 3.7M observations is not something to do per pointermove, and the
|
|
494
|
+
* cloud does not move between frames unless something says it did.
|
|
495
|
+
*/
|
|
496
|
+
private hoverPositionsFor;
|
|
497
|
+
/**
|
|
498
|
+
* The 2D markers' WORLD positions, indexed by observation, NaN for any not on
|
|
499
|
+
* the displayed plane — the same affine and the same subset the marker layer was
|
|
500
|
+
* built from, so the tooltip cannot point at a cell that is not drawn.
|
|
501
|
+
*/
|
|
502
|
+
private hoverWorldPositions;
|
|
503
|
+
/**
|
|
504
|
+
* What the tooltip says, resolved once per colour source rather than per hover.
|
|
505
|
+
*
|
|
506
|
+
* Fetched separately from the colouring on purpose: the reference port caches
|
|
507
|
+
* columns, but a host's need not, and re-fetching a 3.7M-element vector because
|
|
508
|
+
* the pointer moved would be indefensible either way.
|
|
509
|
+
*/
|
|
510
|
+
private resolveHoverSource;
|
|
511
|
+
/**
|
|
512
|
+
* Project every observation to canvas pixels under the current 3D camera.
|
|
513
|
+
*
|
|
514
|
+
* The projection itself is the renderer's: `viewer.projectPoints` owns the camera, the
|
|
515
|
+
* viewport in CSS pixels, the perspective divide and the y-flip. This used to be fifteen
|
|
516
|
+
* lines of column-major matrix arithmetic here, duplicating a second copy in
|
|
517
|
+
* `napari-axes-labels` — and the two had drifted on what to do with a NaN w.
|
|
518
|
+
*
|
|
519
|
+
* What is left is the part that is genuinely this adapter's: SCATTERING the drawn subset
|
|
520
|
+
* back into observation order. The cloud holds only the points of the displayed section,
|
|
521
|
+
* in its own packing; every consumer indexes by observation. NaN stands for "not on
|
|
522
|
+
* screen", which is also the right answer for an observation whose section is hidden.
|
|
523
|
+
*
|
|
524
|
+
* Null when the 3D cloud is not mounted, so the caller falls back to the 2D affine path.
|
|
525
|
+
*/
|
|
526
|
+
getSpatialScreenProjection(obs: SpatialObservations): Float32Array | null;
|
|
527
|
+
/** Rebuild the markers on any dataset or view-state change. */
|
|
528
|
+
private subscribeSpatial;
|
|
529
|
+
/** (Re)build the observation marker layer for the current dataset + view state. */
|
|
530
|
+
private rebuildSpatialPoints;
|
|
531
|
+
/**
|
|
532
|
+
* Tell the region overlay how finely a drawn vertex may be placed.
|
|
533
|
+
*
|
|
534
|
+
* Region geometry is stored in whole world units, which is right when the world IS
|
|
535
|
+
* pixels — a region should align to them. It is wrong for a dataset that registers onto
|
|
536
|
+
* no image: seqFISH's observations span about 5 x 7 units in total, so whole-unit
|
|
537
|
+
* vertices leave roughly six by eight placeable positions across the entire sample and
|
|
538
|
+
* an ROI cannot be drawn at any zoom. Nothing errors; the tool just cannot express the
|
|
539
|
+
* shape.
|
|
540
|
+
*
|
|
541
|
+
* Keyed on whether the dataset brings PIXELS rather than on the extent, because the
|
|
542
|
+
* extent cannot tell the two apart — 2,000 units is a small slide or a large section
|
|
543
|
+
* depending only on what the units are, and only the dataset knows.
|
|
544
|
+
*/
|
|
545
|
+
private setRegionGridFor;
|
|
546
|
+
/**
|
|
547
|
+
* Frame the 2D camera on the observations, for a dataset that brings no image.
|
|
548
|
+
*
|
|
549
|
+
* The 2D camera is normally fitted to the IMAGE, because the observations are in that
|
|
550
|
+
* image's pixel space and framing the image frames them too. A dataset with no
|
|
551
|
+
* reference image has coordinates in its own units instead — seqFISH's span about
|
|
552
|
+
* 5x7 — so the image framing left over from whatever was on screen before puts the
|
|
553
|
+
* whole cloud offscreen: measured at 9.7 x 13.4 PIXELS, centred 256px from where the
|
|
554
|
+
* camera was looking. The points are all there and drawn; they are a speck. Which
|
|
555
|
+
* looks exactly like the dataset having failed to load.
|
|
556
|
+
*
|
|
557
|
+
* Once per dataset, as napari-js's `fit3d: 'once'` does for the cloud: re-colouring,
|
|
558
|
+
* slicing or picking a gene re-adds this layer, and re-framing on those would move
|
|
559
|
+
* the camera under the user — the camera tools and the canvas drag are meant to be
|
|
560
|
+
* the only things that do.
|
|
561
|
+
*/
|
|
562
|
+
/**
|
|
563
|
+
* Hide the image layer for a dataset that brings no image of its own.
|
|
564
|
+
*
|
|
565
|
+
* The host's viewer keeps whatever image was last loaded, and for a dataset that
|
|
566
|
+
* registers onto none that picture belongs to something else entirely — the example's
|
|
567
|
+
* default slide, say. It is not merely irrelevant: the two live in different coordinate
|
|
568
|
+
* spaces (image pixels against the embedding's own units), so the leftover is magnified
|
|
569
|
+
* roughly a hundredfold. At the fitted zoom the camera sits inside its corner, where it
|
|
570
|
+
* reads as blank background, and it appears only once you zoom out far enough to find
|
|
571
|
+
* it — which is exactly how it gets noticed.
|
|
572
|
+
*
|
|
573
|
+
* Hidden rather than removed, and hidden HERE rather than by clearing the host's image
|
|
574
|
+
* state: that state drives the whole render pipeline, including a Plotly backend whose
|
|
575
|
+
* fields are declared with definite-assignment assertions, so emptying it throws from
|
|
576
|
+
* whichever field the next path happens to read. This touches one layer's visibility
|
|
577
|
+
* and nothing else.
|
|
578
|
+
*/
|
|
579
|
+
private hideForeignImage;
|
|
580
|
+
private frameSpatialPointsOnce;
|
|
581
|
+
/**
|
|
582
|
+
* The **gene map**: the active gene's expression as a continuous field drawn under
|
|
583
|
+
* the cells.
|
|
584
|
+
*
|
|
585
|
+
* A scatter coloured by a gene says which cells express it; it cannot say where,
|
|
586
|
+
* because the eye will not integrate thousands of dots into a territory. The field
|
|
587
|
+
* is a kernel-weighted MEAN per cell (see `spatial-expression.ts`), so a dense
|
|
588
|
+
* region does not glow merely for being dense, and it is transparent wherever no
|
|
589
|
+
* cell was measured — an unsampled gap must not read as "not expressed".
|
|
590
|
+
*
|
|
591
|
+
* It shares the points' LUT, percentile window and log flag, so the layer under
|
|
592
|
+
* the cells and the cells themselves cannot disagree about what a colour means.
|
|
593
|
+
*
|
|
594
|
+
* Estimated on the DISPLAYED image's pixel grid, coarsened so the long side is at
|
|
595
|
+
* most {@link GENE_MAP_MAX_SIDE}: a smooth field gains nothing from a slide's full
|
|
596
|
+
* resolution. For a volume-backed dataset that grid is the current slice, and only
|
|
597
|
+
* that plane's observations are included — the same rule the markers follow.
|
|
598
|
+
*/
|
|
599
|
+
private ensureGeneMap;
|
|
600
|
+
/**
|
|
601
|
+
* The plane a volume-backed dataset is currently showing: its pixel affine, the
|
|
602
|
+
* observations that fall in it, and the marker floor that grid needs.
|
|
603
|
+
*
|
|
604
|
+
* Null for a dataset with a real `imageRef` (its coordinates are already the
|
|
605
|
+
* image's pixels and every observation belongs to the one section) and for one
|
|
606
|
+
* with no volume at all — both of which draw exactly as before.
|
|
607
|
+
*/
|
|
608
|
+
private spatialSlab;
|
|
609
|
+
/** Per-point colours for the drawn subset. A broadcast tuple stays broadcast —
|
|
610
|
+
* it is one colour for every point either way. */
|
|
611
|
+
private gatherColors;
|
|
612
|
+
/**
|
|
613
|
+
* Redraw the observation markers over a freshly rendered image.
|
|
614
|
+
*
|
|
615
|
+
* Two reasons, both invisible from the spatial store — which is why a scrub or a
|
|
616
|
+
* re-render never reached the markers on its own:
|
|
617
|
+
* - the image render CLEARS the layer list, taking the markers with it;
|
|
618
|
+
* - over a volume-backed dataset the displayed plane decides which
|
|
619
|
+
* observations belong on screen at all, so the cells have to move with the
|
|
620
|
+
* section rather than hang over a different one.
|
|
621
|
+
*
|
|
622
|
+
* Uses the latest values the marker subscription saw; a no-op until it has seen
|
|
623
|
+
* any, in the 3D cloud (no image, no plane), and with no dataset to draw.
|
|
624
|
+
*/
|
|
625
|
+
private redrawSpatialMarkers;
|
|
626
|
+
/**
|
|
627
|
+
* Mount the SPATIAL_OMICS_3D view: observations as a 3D point cloud under the orbit camera.
|
|
628
|
+
*
|
|
629
|
+
* Deliberately much thinner than the 2D mount. There is no tissue image to render (a registered
|
|
630
|
+
* volume like the Allen CCF has no single reference plane), so no scale bar, no readback, and no
|
|
631
|
+
* 2D interaction stack — the region tools draw in screen space and have no meaning against an
|
|
632
|
+
* orbiting camera, which is why `Select from ROIs` is hidden in this mode. `addPoints3D` puts
|
|
633
|
+
* the viewer in 3D and frames the camera on the cloud itself.
|
|
634
|
+
*/
|
|
635
|
+
private mountSpatialOmics3d;
|
|
636
|
+
/**
|
|
637
|
+
* Scale bar for the 3D cloud, measured at the ORBIT PIVOT.
|
|
638
|
+
*
|
|
639
|
+
* A perspective camera has no single scale — things farther away are smaller — so a bar can
|
|
640
|
+
* only be true at one depth. The pivot is the honest choice: it is what the camera is framing,
|
|
641
|
+
* what a zoom moves towards, and where the eye is anyway. napari's own 3D scale bar works the
|
|
642
|
+
* same way.
|
|
643
|
+
*
|
|
644
|
+
* `NapariScaleBar` needs CSS px per world unit, which an orbit camera does not expose, but at
|
|
645
|
+
* the pivot it is exactly `viewportHeight / (2 * distance * tan(fov / 2))` — the same
|
|
646
|
+
* relationship `Camera3D.pan` uses to track the cursor. The bar then converts through
|
|
647
|
+
* `micronsPerUnit`, and draws nothing at all when the dataset does not declare one, because a
|
|
648
|
+
* bar labelled in microns over unknown units would read as a measurement.
|
|
649
|
+
*/
|
|
650
|
+
private installSpatial3dScaleBar;
|
|
651
|
+
/** Base marker diameter for the 3D cloud, in SCREEN pixels (the layer's unit). */
|
|
652
|
+
private static readonly SPATIAL_3D_BASE_SIZE;
|
|
653
|
+
/** (Re)build the 3D point cloud for the current dataset + view state. */
|
|
654
|
+
private rebuildSpatialPoints3d;
|
|
655
|
+
/**
|
|
656
|
+
* The **3D gene map**: the active gene's expression over the whole sectioned
|
|
657
|
+
* specimen, as one raymarched volume.
|
|
658
|
+
*
|
|
659
|
+
* Two things it can be, and the panel's `Volume rendering` toggle picks which:
|
|
660
|
+
*
|
|
661
|
+
* - **sheets** — exactly the planes that were imaged, each carrying that
|
|
662
|
+
* slide's own 2D gene map, with the gaps between sections empty. A stack of
|
|
663
|
+
* measured fields, at their true z.
|
|
664
|
+
* - **volume** — the same fields smoothed along z, so the planes between the
|
|
665
|
+
* sections carry an interpolated value. An estimate, and drawn as a
|
|
666
|
+
* translucent cloud for the same reason the density volumes are.
|
|
667
|
+
*
|
|
668
|
+
* One `VolumeLayer` rather than a textured quad per section: an `ImageLayer`
|
|
669
|
+
* renders only at `ndisplay === 2`, so 53 sheets in the orbit view would need a
|
|
670
|
+
* new layer type upstream — while a scalar volume whose z sampling already IS
|
|
671
|
+
* the section spacing expresses the sheets exactly, and the same lattice then
|
|
672
|
+
* gives the interpolated version for free.
|
|
673
|
+
*
|
|
674
|
+
* Estimated on the reference volume's own lattice (`densityGrid` at stride 1),
|
|
675
|
+
* so the field lands voxel-for-voxel on the anatomy and needs no offset — a
|
|
676
|
+
* `VolumeLayer` has no translate, and napari centres both boxes on the world
|
|
677
|
+
* origin.
|
|
678
|
+
*/
|
|
679
|
+
private ensureGeneMapVolume;
|
|
680
|
+
/**
|
|
681
|
+
* Cluster density volumes: each cluster rasterised into a smooth scalar field and
|
|
682
|
+
* raymarched alongside the cloud, tinted with the cluster's own legend colour.
|
|
683
|
+
*
|
|
684
|
+
* This is what makes a serially sectioned dataset readable as an anatomical
|
|
685
|
+
* distribution. The cloud shows measured cells and nothing else — but at 200 µm
|
|
686
|
+
* section spacing the eye cannot integrate a stack of discs into a shape, and
|
|
687
|
+
* every gap reads as absence. A density field is a different object from a cell:
|
|
688
|
+
* an estimate, defined between the imaged planes, drawn as a translucent cloud so
|
|
689
|
+
* it cannot be mistaken for measurement. Individual cells are never interpolated —
|
|
690
|
+
* consecutive sections sample different cells, so there is nothing to interpolate
|
|
691
|
+
* along.
|
|
692
|
+
*
|
|
693
|
+
* One volume per cluster rather than one for everything: additive blending is what
|
|
694
|
+
* makes two clusters' territories comparable, and a single blended field would
|
|
695
|
+
* answer no question anyone asks of a taxonomy. Capped at
|
|
696
|
+
* {@link DENSITY_MAX_CLUSTERS} by cell count.
|
|
697
|
+
*
|
|
698
|
+
* Keyed so it rebuilds only when the field would actually differ — the dataset,
|
|
699
|
+
* the colour column, the bandwidth, or the selection.
|
|
700
|
+
*/
|
|
701
|
+
private ensureDensityVolumes;
|
|
702
|
+
/**
|
|
703
|
+
* Identity of the colour scale a continuous spatial layer will draw with, for a
|
|
704
|
+
* cache key: the explicit choice, or else the display colormap and its reverse
|
|
705
|
+
* flag, since that is what {@link spatialContinuousLut} falls back to.
|
|
706
|
+
*
|
|
707
|
+
* The gene maps cache their coloured output, so without this in the key a change
|
|
708
|
+
* of colour scale left the field on screen in the previous colours — the markers
|
|
709
|
+
* recoloured and the map under them did not.
|
|
710
|
+
*/
|
|
711
|
+
private continuousColormapKey;
|
|
712
|
+
/**
|
|
713
|
+
* A revision number for a selection object.
|
|
714
|
+
*
|
|
715
|
+
* The store hands out a NEW mask object per change, so object identity is the
|
|
716
|
+
* cheap and exact way to tell two selections apart — a fingerprint over 3.7M
|
|
717
|
+
* mask bytes would be neither. Counting revisions keeps the cache key a short
|
|
718
|
+
* string.
|
|
719
|
+
*/
|
|
720
|
+
private selectionRev;
|
|
721
|
+
/**
|
|
722
|
+
* The clusters to rasterise: the categories of the active categorical colouring,
|
|
723
|
+
* biggest first and capped, each with its legend colour.
|
|
724
|
+
*
|
|
725
|
+
* Restricted to the current selection when there is one, so "select a region,
|
|
726
|
+
* check the box" answers which clusters live there. With no categorical colouring
|
|
727
|
+
* there is one group — total cell density, which is a real question on its own
|
|
728
|
+
* ("where is the tissue dense?") and the honest thing to show when the view is
|
|
729
|
+
* not encoding a taxonomy.
|
|
730
|
+
*/
|
|
731
|
+
private densityGroups;
|
|
732
|
+
/**
|
|
733
|
+
* Add (or keep) the dataset's reference volume, and derive the offset that sits the
|
|
734
|
+
* observations inside it.
|
|
735
|
+
*
|
|
736
|
+
* `VolumeLayer` has no translate: napari-js maps the volume's unit cube to a world box
|
|
737
|
+
* **centred on the origin**, sized `dims x voxelSize`. The observations, by contract, are in
|
|
738
|
+
* the volume's own frame with its near corner at the coordinate origin. So the two only line
|
|
739
|
+
* up if the POINTS move — by half the box — which is what {@link spatialOrigin3d} is.
|
|
740
|
+
*
|
|
741
|
+
* A failed or absent volume is not fatal: the cloud renders on its own, at its own
|
|
742
|
+
* coordinates, and the camera frames the points instead.
|
|
743
|
+
*/
|
|
744
|
+
private ensureSpatialVolume;
|
|
745
|
+
private removeSpatial3dLayers;
|
|
746
|
+
/** A one-colour colormap, for the "nothing to colour by" state. */
|
|
747
|
+
private spatialFlatColormap;
|
|
748
|
+
/**
|
|
749
|
+
* The per-point scalar + colormap + window that colour the 3D cloud.
|
|
750
|
+
*
|
|
751
|
+
* Continuous data is the natural fit: values go straight through the active colormap with the
|
|
752
|
+
* same percentile window the 2D path uses. Categorical data has to be smuggled through the same
|
|
753
|
+
* scalar channel — see {@link SPATIAL_3D_MAX_CATEGORIES}. Codes map to LUT blocks, and
|
|
754
|
+
* `contrastLimits` of `[-0.5, K - 0.5]` puts code `i` at the centre of block `i`, which is what
|
|
755
|
+
* makes the round-trip exact instead of approximately right.
|
|
756
|
+
*/
|
|
757
|
+
private spatialScalar3d;
|
|
758
|
+
/** Category codes → a stepped LUT, exact for up to {@link SPATIAL_3D_MAX_CATEGORIES}. */
|
|
759
|
+
private encodeSpatial3dCategorical;
|
|
760
|
+
/** Continuous values → the active colormap over a percentile-clipped window. */
|
|
761
|
+
private encodeSpatial3dContinuous;
|
|
762
|
+
/**
|
|
763
|
+
* Per-observation colours for the current view state, or a single flat colour when nothing is
|
|
764
|
+
* selected to colour by. Categorical columns use the column's own palette; continuous columns
|
|
765
|
+
* and gene vectors go through the active colormap with a percentile-clipped window.
|
|
766
|
+
*/
|
|
767
|
+
private spatialFaceColors;
|
|
768
|
+
/** Continuous values → RGBA through the active colormap and a clipped window. */
|
|
769
|
+
private encodeSpatialContinuous;
|
|
289
770
|
/**
|
|
290
771
|
* Mount the NAPARI_SCATTER3D 3D scatter: the downsampled voxel grid as a 3D point cloud colored
|
|
291
772
|
* by intensity (napari-js analog of Plotly's voxel scatter3d). Assembles a coarse volume, then
|
|
@@ -451,7 +932,7 @@ export declare class NapariVisualizerService extends BaseStoreVisualizer impleme
|
|
|
451
932
|
* only (omitted when all are visible → server default). Mirrors the OSD backend. */
|
|
452
933
|
exportData(): Promise<void>;
|
|
453
934
|
unsubscribe(): void;
|
|
454
|
-
static ɵfac: i0.ɵɵFactoryDeclaration<NapariVisualizerService, [null, null, null, null, null, null, null, null, null, null, { optional: true; }, null, null]>;
|
|
935
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<NapariVisualizerService, [null, null, null, null, null, null, null, null, null, null, { optional: true; }, null, null, { optional: true; }, null]>;
|
|
455
936
|
static ɵprov: i0.ɵɵInjectableDeclaration<NapariVisualizerService>;
|
|
456
937
|
}
|
|
457
938
|
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wheel-zoom sensitivity for the napari-js backend, as `exp(-delta * speed)` per normalized,
|
|
3
|
+
* clamped wheel-delta unit.
|
|
4
|
+
*
|
|
5
|
+
* Derived from {@link OSD_ZOOM_PER_SCROLL} rather than written as its own number: the two backends
|
|
6
|
+
* draw the same images, and a wheel that zooms further under one renderer than the other is a
|
|
7
|
+
* difference the user has no way to explain.
|
|
8
|
+
*
|
|
9
|
+
* napari-js clamps a single event's delta before applying the speed — mouse notches and trackpad
|
|
10
|
+
* momentum ticks both exceed the clamp, so the clamped step IS the per-notch step — which makes
|
|
11
|
+
* matching the two a matter of solving `exp(WHEEL_DELTA_CLAMP * speed) = OSD_ZOOM_PER_SCROLL`.
|
|
12
|
+
* The clamp is imported rather than copied: it belongs to napari-js, and a local literal would go
|
|
13
|
+
* silently stale the day that library retuned it.
|
|
14
|
+
*/
|
|
15
|
+
export declare const NAPARI_WHEEL_ZOOM_SPEED: number;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wheel-zoom step for the OpenSeadragon backend.
|
|
3
|
+
*
|
|
4
|
+
* Applied per scroll EVENT, not per unit of delta — which is what OpenSeadragon's own
|
|
5
|
+
* `zoomPerScroll` does, and what the region overlay does when an active tool has taken the wheel
|
|
6
|
+
* over. That makes the step compound with the number of events a device sends: a mouse notch is
|
|
7
|
+
* one event, but a trackpad swipe is a burst of them, so a step that feels right for a mouse runs
|
|
8
|
+
* away under a trackpad.
|
|
9
|
+
*
|
|
10
|
+
* At the previous 1.1 (OpenSeadragon's default is 1.2) an image doubled in 7 notches, against the
|
|
11
|
+
* ~24 the napari-js backend took for the same picture — the same gesture zoomed several times
|
|
12
|
+
* further depending only on which renderer was active. 1.03 puts the two within a whisker of each
|
|
13
|
+
* other, so the wheel feels the same whichever backend is drawing.
|
|
14
|
+
*
|
|
15
|
+
* Shared because the viewer option and the overlay's handler MUST agree: whether the wheel is
|
|
16
|
+
* handled by OpenSeadragon or by the overlay depends on whether a region tool is active, and the
|
|
17
|
+
* zoom visibly changing pace when a tool is picked up is the bug this constant prevents.
|
|
18
|
+
*/
|
|
19
|
+
export declare const OSD_ZOOM_PER_SCROLL = 1.03;
|