@biongff/vizarr 1.0.1 → 1.2.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/dist/biongff-vizarr.cjs.js +1 -0
- package/dist/biongff-vizarr.es.js +7 -0
- package/dist/blosc-CeItQ6qj.cjs +17 -0
- package/dist/blosc-DaK8KnI4.js +719 -0
- package/dist/chunk-INHXZS53-DiyuLb3Z.js +14 -0
- package/dist/chunk-INHXZS53-z3BpFH8p.cjs +1 -0
- package/dist/deflate-BeJu5ryY.js +10 -0
- package/dist/deflate-Ctyqttht.cjs +1 -0
- package/dist/index-BR2zkK5Y.cjs +3811 -0
- package/dist/index-CU_d8468.js +49053 -0
- package/dist/index.d.ts +67 -0
- package/dist/jpeg-BWDIgcDK.js +533 -0
- package/dist/jpeg-Mp_UXZAJ.cjs +1 -0
- package/dist/jpeg2k-1tnzEScI.js +24 -0
- package/dist/jpeg2k-CcF6vCxI.cjs +1 -0
- package/dist/lerc-BJaDi7aO.js +1039 -0
- package/dist/lerc-D-6RihsZ.cjs +1 -0
- package/dist/lz4-CssV0LoA.js +643 -0
- package/dist/lz4-PFaIsPAh.cjs +15 -0
- package/dist/lzw-DBp1lk8U.js +84 -0
- package/dist/lzw-De76dcEC.cjs +1 -0
- package/dist/packbits-B6vt-6GN.cjs +1 -0
- package/dist/packbits-MT4dFhEA.js +24 -0
- package/dist/pako.esm-Bx5X36Wo.js +1074 -0
- package/dist/pako.esm-DZC2QrbJ.cjs +1 -0
- package/dist/raw-BF-NUDI1.js +9 -0
- package/dist/raw-vwCA5qWn.cjs +1 -0
- package/dist/ref-DAXKePD4.cjs +1 -0
- package/dist/ref-HqVJFnCG.js +196 -0
- package/dist/webimage-BQlxeHEQ.cjs +1 -0
- package/dist/webimage-DTyyK3Ev.js +19 -0
- package/dist/zstd-Cqadn9HA.js +610 -0
- package/dist/zstd-_xUhkGOV.cjs +15 -0
- package/package.json +25 -20
- package/.gitattributes +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -32
- package/.github/ISSUE_TEMPLATE/documentation_update.md +0 -16
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- package/.github/ISSUE_TEMPLATE/maintenance.md +0 -19
- package/.github/ISSUE_TEMPLATE/optimisation_request.md +0 -23
- package/.github/ISSUE_TEMPLATE/support_request.md +0 -25
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -21
- package/.github/dependabot.yml +0 -13
- package/.github/workflows/deploy.yml +0 -56
- package/.github/workflows/npm-publish-dev.yml +0 -49
- package/.github/workflows/npm-publish.yml +0 -42
- package/.github/workflows/release.yml +0 -37
- package/.github/workflows/test.yml +0 -21
- package/.releaserc.json +0 -29
- package/CHANGELOG.md +0 -56
- package/CITATION.cff +0 -56
- package/CONTRIBUTING.md +0 -82
- package/README.md +0 -85
- package/assets/favicon.ico +0 -0
- package/assets/favicon.png +0 -0
- package/assets/logo-wide.png +0 -0
- package/assets/logo-wide.svg +0 -60
- package/assets/logo.png +0 -0
- package/assets/logo.svg +0 -36
- package/assets/robots.txt +0 -3
- package/assets/screenshot.png +0 -0
- package/biome.json +0 -34
- package/index.html +0 -24
- package/main.ts +0 -52
- package/patches/@vivjs__loaders@0.17.2.patch +0 -13
- package/python/.python-version +0 -1
- package/python/README.md +0 -14
- package/python/Untitled.ipynb +0 -40
- package/python/deno.json +0 -22
- package/python/notebooks/IDR_example.ipynb +0 -86
- package/python/notebooks/README.md +0 -35
- package/python/notebooks/create_fixture.py +0 -72
- package/python/notebooks/getting_started.ipynb +0 -217
- package/python/notebooks/mandelbrot.ipynb +0 -211
- package/python/notebooks/spatial_transformations.ipynb +0 -139
- package/python/pyproject.toml +0 -18
- package/python/src/vizarr/__init__.py +0 -10
- package/python/src/vizarr/_widget.js +0 -113
- package/python/src/vizarr/_widget.py +0 -67
- package/python/uv.lock +0 -2495
- package/src/ZarrPixelSource.ts +0 -182
- package/src/codecs/jpeg2k.ts +0 -22
- package/src/codecs/register.ts +0 -2
- package/src/components/LayerController/AcquisitionController.tsx +0 -46
- package/src/components/LayerController/AddChannelButton.tsx +0 -122
- package/src/components/LayerController/AxisOptions.tsx +0 -93
- package/src/components/LayerController/AxisSlider.tsx +0 -86
- package/src/components/LayerController/AxisSliders.tsx +0 -29
- package/src/components/LayerController/ChannelController.tsx +0 -84
- package/src/components/LayerController/ChannelOptions.tsx +0 -183
- package/src/components/LayerController/ColorPalette.tsx +0 -40
- package/src/components/LayerController/Content.tsx +0 -71
- package/src/components/LayerController/Header.tsx +0 -48
- package/src/components/LayerController/Labels.tsx +0 -73
- package/src/components/LayerController/LayerFitToViewportButton.tsx +0 -43
- package/src/components/LayerController/LayerVisibilityButton.tsx +0 -32
- package/src/components/LayerController/OpacitySlider.tsx +0 -25
- package/src/components/LayerController/index.tsx +0 -42
- package/src/components/Menu.tsx +0 -54
- package/src/components/Viewer.tsx +0 -136
- package/src/hooks.ts +0 -29
- package/src/index.tsx +0 -138
- package/src/io.ts +0 -276
- package/src/layers/grid-layer.ts +0 -214
- package/src/layers/label-layer.ts +0 -446
- package/src/layers/viv-layers.ts +0 -55
- package/src/lru-store.ts +0 -57
- package/src/ome.ts +0 -388
- package/src/state.ts +0 -202
- package/src/theme.ts +0 -80
- package/src/types.ts +0 -112
- package/src/utils.ts +0 -655
- package/tsconfig.json +0 -14
- package/vite.config.js +0 -47
package/src/layers/grid-layer.ts
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import { CompositeLayer, SolidPolygonLayer, TextLayer } from "deck.gl";
|
|
2
|
-
import pMap from "p-map";
|
|
3
|
-
|
|
4
|
-
import { ColorPaletteExtension, XRLayer } from "@hms-dbmi/viv";
|
|
5
|
-
import type { SupportedTypedArray } from "@vivjs/types";
|
|
6
|
-
import type { CompositeLayerProps, PickingInfo, SolidPolygonLayerProps, TextLayerProps } from "deck.gl";
|
|
7
|
-
import type { ZarrPixelSource } from "../ZarrPixelSource";
|
|
8
|
-
import { assert } from "../utils";
|
|
9
|
-
import type { BaseLayerProps } from "./viv-layers";
|
|
10
|
-
|
|
11
|
-
export interface GridLoader {
|
|
12
|
-
loader: ZarrPixelSource;
|
|
13
|
-
row: number;
|
|
14
|
-
col: number;
|
|
15
|
-
name: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type Polygon = Array<[number, number]>;
|
|
19
|
-
|
|
20
|
-
export interface GridLayerProps
|
|
21
|
-
extends Omit<CompositeLayerProps, "loaders" | "modelMatrix" | "opacity" | "onClick" | "id">,
|
|
22
|
-
BaseLayerProps {
|
|
23
|
-
loaders: GridLoader[];
|
|
24
|
-
rows: number;
|
|
25
|
-
columns: number;
|
|
26
|
-
spacer?: number;
|
|
27
|
-
text?: boolean;
|
|
28
|
-
concurrency?: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function scaleBounds(width: number, height: number, translate = [0, 0], scale = 1) {
|
|
32
|
-
const [left, top] = translate;
|
|
33
|
-
const right = width * scale + left;
|
|
34
|
-
const bottom = height * scale + top;
|
|
35
|
-
return [left, bottom, right, top];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function validateWidthHeight(d: { data: { width: number; height: number } }[]) {
|
|
39
|
-
const [first] = d;
|
|
40
|
-
// Return early if no grid data. Maybe throw an error?
|
|
41
|
-
const { width, height } = first.data;
|
|
42
|
-
// Verify that all grid data is same shape (ignoring undefined)
|
|
43
|
-
for (const { data } of d) {
|
|
44
|
-
if (!data) continue;
|
|
45
|
-
assert(data.width === width && data.height === height, "Grid data is not same shape.");
|
|
46
|
-
}
|
|
47
|
-
return { width, height };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function refreshGridData(props: GridLayerProps) {
|
|
51
|
-
const { loaders, selections = [] } = props;
|
|
52
|
-
let { concurrency } = props;
|
|
53
|
-
if (concurrency && selections.length > 0) {
|
|
54
|
-
// There are `loaderSelection.length` requests per loader. This block scales
|
|
55
|
-
// the provided concurrency to map to the number of actual requests.
|
|
56
|
-
concurrency = Math.ceil(concurrency / selections.length);
|
|
57
|
-
}
|
|
58
|
-
const mapper = async (d: GridLoader) => {
|
|
59
|
-
const promises = selections.map((selection) => d.loader.getRaster({ selection }));
|
|
60
|
-
const tiles = await Promise.all(promises);
|
|
61
|
-
return {
|
|
62
|
-
...d,
|
|
63
|
-
data: {
|
|
64
|
-
data: tiles.map((d) => d.data),
|
|
65
|
-
width: tiles[0].width,
|
|
66
|
-
height: tiles[0].height,
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
};
|
|
70
|
-
return pMap(loaders, mapper, { concurrency });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
type SharedLayerState = {
|
|
74
|
-
gridData: Awaited<ReturnType<typeof refreshGridData>>;
|
|
75
|
-
width: number;
|
|
76
|
-
height: number;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
class GridLayer extends CompositeLayer<CompositeLayerProps & GridLayerProps> {
|
|
80
|
-
static layerName = "VizarrGridLayer";
|
|
81
|
-
static defaultProps = {
|
|
82
|
-
// @ts-expect-error - XRLayer props are not typed
|
|
83
|
-
...XRLayer.defaultProps,
|
|
84
|
-
// Special grid props
|
|
85
|
-
loaders: { type: "array", value: [], compare: true },
|
|
86
|
-
spacer: { type: "number", value: 5, compare: true },
|
|
87
|
-
rows: { type: "number", value: 0, compare: true },
|
|
88
|
-
columns: { type: "number", value: 0, compare: true },
|
|
89
|
-
concurrency: { type: "number", value: 10, compare: false }, // set concurrency for queue
|
|
90
|
-
text: { type: "boolean", value: false, compare: true },
|
|
91
|
-
// Deck.gl
|
|
92
|
-
onClick: { type: "function", value: null, compare: true },
|
|
93
|
-
onHover: { type: "function", value: null, compare: true },
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
get #state(): SharedLayerState {
|
|
97
|
-
// @ts-expect-error - typed as any by deck
|
|
98
|
-
return this.state;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
set #state(state: SharedLayerState) {
|
|
102
|
-
this.state = state;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
initializeState() {
|
|
106
|
-
this.#state = { gridData: [], width: 0, height: 0 };
|
|
107
|
-
refreshGridData(this.props).then((gridData) => {
|
|
108
|
-
const { width, height } = validateWidthHeight(gridData);
|
|
109
|
-
this.setState({ gridData, width, height });
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
updateState({
|
|
114
|
-
props,
|
|
115
|
-
oldProps,
|
|
116
|
-
changeFlags,
|
|
117
|
-
}: {
|
|
118
|
-
props: GridLayerProps;
|
|
119
|
-
oldProps: GridLayerProps;
|
|
120
|
-
changeFlags: {
|
|
121
|
-
propsChanged: string | boolean | null;
|
|
122
|
-
};
|
|
123
|
-
}) {
|
|
124
|
-
const { propsChanged } = changeFlags;
|
|
125
|
-
const loaderChanged = typeof propsChanged === "string" && propsChanged.includes("props.loaders");
|
|
126
|
-
const loaderSelectionChanged = props.selections !== oldProps.selections;
|
|
127
|
-
if (loaderChanged || loaderSelectionChanged) {
|
|
128
|
-
// Only fetch new data to render if loader has changed
|
|
129
|
-
refreshGridData(this.props).then((gridData) => {
|
|
130
|
-
this.setState({ gridData });
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
getPickingInfo({ info }: { info: PickingInfo }) {
|
|
136
|
-
// provide Grid row and column info for mouse events (hover & click)
|
|
137
|
-
if (!info.coordinate) {
|
|
138
|
-
return info;
|
|
139
|
-
}
|
|
140
|
-
const spacer = this.props.spacer || 0;
|
|
141
|
-
const { width, height } = this.#state;
|
|
142
|
-
const [x, y] = info.coordinate;
|
|
143
|
-
const row = Math.floor(y / (height + spacer));
|
|
144
|
-
const column = Math.floor(x / (width + spacer));
|
|
145
|
-
return {
|
|
146
|
-
...info,
|
|
147
|
-
gridCoord: { row, column },
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
renderLayers() {
|
|
152
|
-
const { gridData, width, height } = this.#state;
|
|
153
|
-
if (width === 0 || height === 0) return null; // early return if no data
|
|
154
|
-
|
|
155
|
-
const { rows, columns, spacer = 0, id = "" } = this.props;
|
|
156
|
-
type Data = { row: number; col: number; loader: Pick<ZarrPixelSource, "dtype">; data: Array<SupportedTypedArray> };
|
|
157
|
-
const layers = gridData.map((d) => {
|
|
158
|
-
const y = d.row * (height + spacer);
|
|
159
|
-
const x = d.col * (width + spacer);
|
|
160
|
-
const layerProps = {
|
|
161
|
-
channelData: d.data, // coerce to null if no data
|
|
162
|
-
bounds: scaleBounds(width, height, [x, y]),
|
|
163
|
-
id: `${id}-GridLayer-${d.row}-${d.col}`,
|
|
164
|
-
dtype: d.loader.dtype || "Uint16", // fallback if missing,
|
|
165
|
-
pickable: false,
|
|
166
|
-
extensions: [new ColorPaletteExtension()],
|
|
167
|
-
};
|
|
168
|
-
// @ts-expect-error - XRLayer props are not well typed
|
|
169
|
-
return new XRLayer({ ...this.props, ...layerProps });
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
if (this.props.pickable) {
|
|
173
|
-
type Data = { polygon: Polygon };
|
|
174
|
-
const bottom = rows * (height + spacer);
|
|
175
|
-
const right = columns * (width + spacer);
|
|
176
|
-
const polygon = [
|
|
177
|
-
[0, 0],
|
|
178
|
-
[right, 0],
|
|
179
|
-
[right, bottom],
|
|
180
|
-
[0, bottom],
|
|
181
|
-
] satisfies Polygon;
|
|
182
|
-
const layerProps = {
|
|
183
|
-
data: [{ polygon }],
|
|
184
|
-
getPolygon: (d) => d.polygon,
|
|
185
|
-
getFillColor: [0, 0, 0, 0], // transparent
|
|
186
|
-
getLineColor: [0, 0, 0, 0],
|
|
187
|
-
pickable: true, // enable picking
|
|
188
|
-
id: `${id}-GridLayer-picking`,
|
|
189
|
-
} satisfies SolidPolygonLayerProps<Data>;
|
|
190
|
-
const layer = new SolidPolygonLayer<Data, SolidPolygonLayerProps<Data>>({ ...this.props, ...layerProps });
|
|
191
|
-
layers.push(layer);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (this.props.text) {
|
|
195
|
-
type Data = { col: number; row: number; name: string };
|
|
196
|
-
const layer = new TextLayer<Data, TextLayerProps<Data>>({
|
|
197
|
-
id: `${id}-GridLayer-text`,
|
|
198
|
-
data: gridData,
|
|
199
|
-
getPosition: (d) => [d.col * (width + spacer), d.row * (height + spacer)],
|
|
200
|
-
getText: (d) => d.name,
|
|
201
|
-
getColor: [255, 255, 255, 255],
|
|
202
|
-
getSize: 16,
|
|
203
|
-
getAngle: 0,
|
|
204
|
-
getTextAnchor: "start",
|
|
205
|
-
getAlignmentBaseline: "top",
|
|
206
|
-
});
|
|
207
|
-
layers.push(layer);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return layers;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export { GridLayer };
|
|
@@ -1,446 +0,0 @@
|
|
|
1
|
-
import { BitmapLayer, TileLayer, _Tileset2D as Tileset2D } from "deck.gl";
|
|
2
|
-
import * as utils from "../utils";
|
|
3
|
-
|
|
4
|
-
import type { FilterContext } from "@deck.gl/core";
|
|
5
|
-
import type { _Tile2DHeader as Tile2DHeader } from "@deck.gl/geo-layers";
|
|
6
|
-
import type { BitmapLayerPickingInfo } from "@deck.gl/layers";
|
|
7
|
-
import type { GetPickingInfoParams, Layer, UpdateParameters } from "deck.gl";
|
|
8
|
-
import { Matrix4, clamp } from "math.gl";
|
|
9
|
-
import type * as zarr from "zarrita";
|
|
10
|
-
import type { ZarrPixelSource } from "../ZarrPixelSource";
|
|
11
|
-
import { transformBox } from "../utils";
|
|
12
|
-
|
|
13
|
-
type Texture = ReturnType<BitmapLayer["context"]["device"]["createTexture"]>;
|
|
14
|
-
|
|
15
|
-
export const DEFAULT_LABEL_OPACITY = 0.5;
|
|
16
|
-
|
|
17
|
-
export type OmeColor = Readonly<{
|
|
18
|
-
labelValue: number;
|
|
19
|
-
rgba: readonly [r: number, g: number, b: number, a: number];
|
|
20
|
-
value?: string | number | null;
|
|
21
|
-
}>;
|
|
22
|
-
|
|
23
|
-
export interface LabelLayerProps {
|
|
24
|
-
id: string;
|
|
25
|
-
loader: Array<ZarrPixelSource>;
|
|
26
|
-
selection: Array<number>;
|
|
27
|
-
opacity: number;
|
|
28
|
-
modelMatrix: Matrix4;
|
|
29
|
-
colors?: ReadonlyArray<OmeColor>;
|
|
30
|
-
pickable?: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface GrayscaleBitmapLayerPickingInfo extends BitmapLayerPickingInfo {
|
|
34
|
-
label?: number;
|
|
35
|
-
value?: string | number | null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* @see {@href https://ngff.openmicroscopy.org/0.5/index.html#labels-md}
|
|
40
|
-
*
|
|
41
|
-
* The pixels of the label images MUST be integer data types, i.e. one of [uint8, int8, uint16, int16, uint32, int32, uint64, int64].
|
|
42
|
-
*/
|
|
43
|
-
type LabelDataType = zarr.Uint8 | zarr.Int8 | zarr.Uint16 | zarr.Int16 | zarr.Uint32 | zarr.Int32;
|
|
44
|
-
// TODO: bigint data types are supported by the spec but not by Viv's data loader.
|
|
45
|
-
// | zarr.Uint64
|
|
46
|
-
// | zarr.Int64;
|
|
47
|
-
|
|
48
|
-
/** The decoded tile data from a OME-NGFF label source */
|
|
49
|
-
type LabelPixelData = {
|
|
50
|
-
data: zarr.TypedArray<LabelDataType>;
|
|
51
|
-
width: number;
|
|
52
|
-
height: number;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
// @TODO: update deck.gl, this bug is fixed in version 9.2.0
|
|
56
|
-
// Extend Tileset2D to use modelMatrix in isTileVisible
|
|
57
|
-
// so picking works in the transformed tiles
|
|
58
|
-
class LabelTileset2D extends Tileset2D {
|
|
59
|
-
isTileVisible(
|
|
60
|
-
tile: Tile2DHeader,
|
|
61
|
-
cullRect?: { x: number; y: number; width: number; height: number },
|
|
62
|
-
modelMatrix?: Matrix4 | null,
|
|
63
|
-
) {
|
|
64
|
-
if (!tile.isVisible) {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// @ts-ignore
|
|
69
|
-
if (cullRect && this._viewport) {
|
|
70
|
-
// @ts-ignore
|
|
71
|
-
const boundsArr = this._getCullBounds({
|
|
72
|
-
// @ts-ignore
|
|
73
|
-
viewport: this._viewport,
|
|
74
|
-
// @ts-ignore
|
|
75
|
-
z: this._zRange,
|
|
76
|
-
cullRect,
|
|
77
|
-
});
|
|
78
|
-
let { bbox } = tile;
|
|
79
|
-
for (const [minX, minY, maxX, maxY] of boundsArr) {
|
|
80
|
-
let overlaps: boolean;
|
|
81
|
-
if ("west" in bbox) {
|
|
82
|
-
overlaps = bbox.west < maxX && bbox.east > minX && bbox.south < maxY && bbox.north > minY;
|
|
83
|
-
} else {
|
|
84
|
-
if (modelMatrix && !Matrix4.IDENTITY.equals(modelMatrix)) {
|
|
85
|
-
const [left, top, right, bottom] = transformBox(
|
|
86
|
-
[bbox.left, bbox.top, bbox.right, bbox.bottom],
|
|
87
|
-
modelMatrix,
|
|
88
|
-
);
|
|
89
|
-
bbox = { left, top, right, bottom };
|
|
90
|
-
}
|
|
91
|
-
// top/bottom could be swapped depending on the indexing system
|
|
92
|
-
const y0 = Math.min(bbox.top, bbox.bottom);
|
|
93
|
-
const y1 = Math.max(bbox.top, bbox.bottom);
|
|
94
|
-
overlaps = bbox.left < maxX && bbox.right > minX && y0 < maxY && y1 > minY;
|
|
95
|
-
}
|
|
96
|
-
if (overlaps) {
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
return true;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export class LabelLayer extends TileLayer<LabelPixelData, LabelLayerProps> {
|
|
107
|
-
static layerName = "VizarrLabelLayer";
|
|
108
|
-
// @ts-expect-error - only way to extend the base state type
|
|
109
|
-
state!: { colorTexture: Texture; valueMap?: Map<number, string | number | null> } & TileLayer["state"];
|
|
110
|
-
|
|
111
|
-
constructor(props: LabelLayerProps) {
|
|
112
|
-
const { id, loader, selection, opacity, modelMatrix, colors, ...restTileLayerProps } = props;
|
|
113
|
-
|
|
114
|
-
const resolutions = loader;
|
|
115
|
-
const dimensions = {
|
|
116
|
-
height: resolutions[0].shape.at(-2),
|
|
117
|
-
width: resolutions[0].shape.at(-1),
|
|
118
|
-
};
|
|
119
|
-
utils.assert(dimensions.width && dimensions.height);
|
|
120
|
-
const tileSize = getTileSizeForResolutions(resolutions);
|
|
121
|
-
super({
|
|
122
|
-
TilesetClass: LabelTileset2D,
|
|
123
|
-
id: `labels-${id}`,
|
|
124
|
-
extent: [0, 0, dimensions.width, dimensions.height],
|
|
125
|
-
tileSize: tileSize,
|
|
126
|
-
minZoom: Math.round(-(resolutions.length - 1)),
|
|
127
|
-
opacity: opacity,
|
|
128
|
-
maxZoom: 0,
|
|
129
|
-
modelMatrix: modelMatrix,
|
|
130
|
-
colors: colors,
|
|
131
|
-
zoomOffset: Math.round(Math.log2(modelMatrix ? modelMatrix.getScale()[0] : 1)),
|
|
132
|
-
updateTriggers: {
|
|
133
|
-
getTileData: [loader, selection],
|
|
134
|
-
},
|
|
135
|
-
async getTileData({ index, signal }) {
|
|
136
|
-
const { x, y, z } = index;
|
|
137
|
-
const resolution = resolutions[Math.round(-z)];
|
|
138
|
-
const request = { x, y, signal, selection: selection };
|
|
139
|
-
let { data, width, height } = await resolution.getTile(request);
|
|
140
|
-
utils.assert(
|
|
141
|
-
!(data instanceof Float32Array) && !(data instanceof Float64Array),
|
|
142
|
-
`The pixels of labels MUST be integer data types, got ${JSON.stringify(resolution.dtype)}`,
|
|
143
|
-
);
|
|
144
|
-
return { data, width, height };
|
|
145
|
-
},
|
|
146
|
-
...restTileLayerProps,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
renderSubLayers(
|
|
151
|
-
params: TileLayer["props"] & {
|
|
152
|
-
data: LabelPixelData;
|
|
153
|
-
tile: {
|
|
154
|
-
index: { x: number; y: number; z: number };
|
|
155
|
-
boundingBox: [min: Array<number>, max: Array<number>];
|
|
156
|
-
};
|
|
157
|
-
},
|
|
158
|
-
): Layer {
|
|
159
|
-
const { tile, data, ...props } = params;
|
|
160
|
-
const [[left, bottom], [right, top]] = tile.boundingBox;
|
|
161
|
-
utils.assert(props.extent, "missing extent");
|
|
162
|
-
const [_x0, _y0, width, height] = props.extent;
|
|
163
|
-
return new GrayscaleBitmapLayer({
|
|
164
|
-
id: `tile-${tile.index.x}.${tile.index.y}.${tile.index.z}-${props.id}`,
|
|
165
|
-
pixelData: data,
|
|
166
|
-
opacity: props.opacity,
|
|
167
|
-
modelMatrix: props.modelMatrix,
|
|
168
|
-
colorTexture: this.state.colorTexture,
|
|
169
|
-
valueMap: this.state.valueMap,
|
|
170
|
-
bounds: [clamp(left, 0, width), clamp(top, 0, height), clamp(right, 0, width), clamp(bottom, 0, height)],
|
|
171
|
-
// For underlying class
|
|
172
|
-
image: new ImageData(data.width, data.height),
|
|
173
|
-
pickable: props.pickable ?? false,
|
|
174
|
-
modelMatrixInverse: props.modelMatrix ? new Matrix4(props.modelMatrix).invert() : undefined,
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
filterSubLayer({ layer, cullRect }: FilterContext) {
|
|
179
|
-
const { tile } = (layer as Layer<{ tile: Tile2DHeader }>).props;
|
|
180
|
-
const { modelMatrix } = this.props;
|
|
181
|
-
const tileset = this.state.tileset as unknown as LabelTileset2D;
|
|
182
|
-
return tileset.isTileVisible(tile, cullRect, modelMatrix ? new Matrix4(modelMatrix) : null);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
updateState({ props, oldProps, changeFlags, ...rest }: UpdateParameters<this>): void {
|
|
186
|
-
super.updateState({ props, oldProps, changeFlags, ...rest });
|
|
187
|
-
// we make the colorTexture on this layer so we can share it amoung all the sublayers
|
|
188
|
-
if (props.colors !== oldProps.colors || !this.state.colorTexture) {
|
|
189
|
-
this.state.colorTexture?.destroy();
|
|
190
|
-
const colorTexture = createColorTexture({
|
|
191
|
-
source: props.colors,
|
|
192
|
-
maxTextureDimension2D: this.context.device.limits.maxTextureDimension2D,
|
|
193
|
-
});
|
|
194
|
-
this.setState({
|
|
195
|
-
colorTexture: this.context.device.createTexture({
|
|
196
|
-
width: colorTexture.width,
|
|
197
|
-
height: colorTexture.height,
|
|
198
|
-
data: colorTexture.data,
|
|
199
|
-
dimension: "2d",
|
|
200
|
-
mipmaps: false,
|
|
201
|
-
sampler: {
|
|
202
|
-
minFilter: "nearest",
|
|
203
|
-
magFilter: "nearest",
|
|
204
|
-
addressModeU: "clamp-to-edge",
|
|
205
|
-
addressModeV: "clamp-to-edge",
|
|
206
|
-
},
|
|
207
|
-
format: "rgba8unorm",
|
|
208
|
-
}),
|
|
209
|
-
valueMap: props.colors ? new Map(props.colors.map((c) => [c.labelValue, c.value])) : null,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export class GrayscaleBitmapLayer extends BitmapLayer<{
|
|
216
|
-
pixelData: LabelPixelData;
|
|
217
|
-
colorTexture: Texture;
|
|
218
|
-
valueMap?: Map<number, string | number | null>;
|
|
219
|
-
modelMatrixInverse?: Matrix4;
|
|
220
|
-
}> {
|
|
221
|
-
static layerName = "VizarrGrayscaleBitmapLayer";
|
|
222
|
-
// @ts-expect-error - only way to extend the base state type
|
|
223
|
-
state!: { texture: Texture; valueMap?: Map<number, string | number | null> } & BitmapLayer["state"];
|
|
224
|
-
|
|
225
|
-
getPickingInfo(params: GetPickingInfoParams): GrayscaleBitmapLayerPickingInfo {
|
|
226
|
-
const info = super.getPickingInfo(params) as GrayscaleBitmapLayerPickingInfo;
|
|
227
|
-
|
|
228
|
-
// Get label value
|
|
229
|
-
if (!info.coordinate) {
|
|
230
|
-
return info;
|
|
231
|
-
}
|
|
232
|
-
const { pixelData, bounds, modelMatrixInverse, valueMap } = this.props;
|
|
233
|
-
const { data, width, height } = pixelData;
|
|
234
|
-
let [x, y] = info.coordinate;
|
|
235
|
-
if (modelMatrixInverse && !Matrix4.IDENTITY.equals(modelMatrixInverse)) {
|
|
236
|
-
[x, y] = modelMatrixInverse.transformAsPoint([x, y]);
|
|
237
|
-
}
|
|
238
|
-
const [left, bottom, right, top] = bounds as number[];
|
|
239
|
-
|
|
240
|
-
if (right - left === 0 || top - bottom === 0) {
|
|
241
|
-
console.log("Picking info has zero-sized bounds");
|
|
242
|
-
return info;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const normX = (x - left) / (right - left);
|
|
246
|
-
const normY = (y - bottom) / (top - bottom);
|
|
247
|
-
const pixelX = Math.floor(normX * width);
|
|
248
|
-
const pixelY = Math.floor((1 - normY) * height);
|
|
249
|
-
const clampedX = clamp(pixelX, 0, width);
|
|
250
|
-
const clampedY = clamp(pixelY, 0, height);
|
|
251
|
-
|
|
252
|
-
const index = clampedY * width + clampedX;
|
|
253
|
-
|
|
254
|
-
if (index < 0 || index >= data.length) {
|
|
255
|
-
return info;
|
|
256
|
-
}
|
|
257
|
-
const label = data[index];
|
|
258
|
-
const value = valueMap ? valueMap.get(label) : null;
|
|
259
|
-
info.label = label;
|
|
260
|
-
info.value = value;
|
|
261
|
-
return info;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Temporary workaround for ANGLE bug https://issues.angleproject.org/issues/401546698
|
|
265
|
-
// "Error during validation: Two textures of different types use the same sampler location."
|
|
266
|
-
// Force grayscaleTexture to float32 to use sampler2D
|
|
267
|
-
getShaders() {
|
|
268
|
-
// const sampler = (
|
|
269
|
-
// {
|
|
270
|
-
// Uint8Array: "usampler2D",
|
|
271
|
-
// Uint16Array: "usampler2D",
|
|
272
|
-
// Uint32Array: "usampler2D",
|
|
273
|
-
// Int8Array: "isampler2D",
|
|
274
|
-
// Int16Array: "isampler2D",
|
|
275
|
-
// Int32Array: "isampler2D",
|
|
276
|
-
// } as const
|
|
277
|
-
// )[typedArrayConstructorName(this.props.pixelData.data)];
|
|
278
|
-
const sampler = "sampler2D"; // Use sampler2D for ANGLE bug workaround
|
|
279
|
-
// replace the builtin fragment shader with our own
|
|
280
|
-
return {
|
|
281
|
-
...super.getShaders(),
|
|
282
|
-
fs: `\
|
|
283
|
-
#version 300 es
|
|
284
|
-
#define SHADER_NAME grayscale-bitmap-layer-fragment-shader
|
|
285
|
-
|
|
286
|
-
precision highp float;
|
|
287
|
-
precision highp int;
|
|
288
|
-
precision highp ${sampler};
|
|
289
|
-
|
|
290
|
-
uniform ${sampler} grayscaleTexture;
|
|
291
|
-
uniform sampler2D colorTexture;
|
|
292
|
-
uniform float colorTextureWidth;
|
|
293
|
-
uniform float colorTextureHeight;
|
|
294
|
-
uniform float opacity;
|
|
295
|
-
|
|
296
|
-
in vec2 vTexCoord;
|
|
297
|
-
out vec4 fragColor;
|
|
298
|
-
|
|
299
|
-
void main() {
|
|
300
|
-
int index = int(texture(grayscaleTexture, vTexCoord).r);
|
|
301
|
-
float x = (mod(float(index), colorTextureWidth) + 0.5) / colorTextureWidth;
|
|
302
|
-
float y = (floor(float(index) / colorTextureWidth) + 0.5) / colorTextureHeight;
|
|
303
|
-
vec2 uv = vec2(x, y);
|
|
304
|
-
vec3 color = texture(colorTexture, uv).rgb;
|
|
305
|
-
fragColor = vec4(color, ((index > 0) ? 1.0 : 0.0) * opacity);
|
|
306
|
-
}
|
|
307
|
-
`,
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
updateState({ props, oldProps, changeFlags, ...rest }: UpdateParameters<this>): void {
|
|
312
|
-
super.updateState({ props, oldProps, changeFlags, ...rest });
|
|
313
|
-
if (props.pixelData !== oldProps.pixelData) {
|
|
314
|
-
this.state.texture?.destroy();
|
|
315
|
-
this.setState({
|
|
316
|
-
texture: this.context.device.createTexture({
|
|
317
|
-
width: props.pixelData.width,
|
|
318
|
-
height: props.pixelData.height,
|
|
319
|
-
data: new Float32Array(props.pixelData.data), // Force float32 for ANGLE bug workaround
|
|
320
|
-
dimension: "2d",
|
|
321
|
-
mipmaps: false,
|
|
322
|
-
sampler: {
|
|
323
|
-
minFilter: "nearest",
|
|
324
|
-
magFilter: "nearest",
|
|
325
|
-
addressModeU: "clamp-to-edge",
|
|
326
|
-
addressModeV: "clamp-to-edge",
|
|
327
|
-
},
|
|
328
|
-
format: "r32float", // ANGLE bug workaround
|
|
329
|
-
// format: (
|
|
330
|
-
// {
|
|
331
|
-
// Uint8Array: "r8uint",
|
|
332
|
-
// Uint16Array: "r16uint",
|
|
333
|
-
// Uint32Array: "r32uint",
|
|
334
|
-
// Int8Array: "r8sint",
|
|
335
|
-
// Int16Array: "r16sint",
|
|
336
|
-
// Int32Array: "r32sint",
|
|
337
|
-
// } as const
|
|
338
|
-
// )[typedArrayConstructorName(props.pixelData.data)],
|
|
339
|
-
}),
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
draw(opts: unknown) {
|
|
345
|
-
const { model, texture } = this.state;
|
|
346
|
-
const { colorTexture } = this.props;
|
|
347
|
-
|
|
348
|
-
if (model && texture && colorTexture) {
|
|
349
|
-
model.setUniforms({
|
|
350
|
-
colorTextureWidth: colorTexture.width,
|
|
351
|
-
colorTextureHeight: colorTexture.height,
|
|
352
|
-
});
|
|
353
|
-
model.setBindings({
|
|
354
|
-
grayscaleTexture: texture,
|
|
355
|
-
colorTexture: colorTexture,
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
super.draw(opts);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function getTileSizeForResolutions(resolutions: Array<ZarrPixelSource>): number {
|
|
363
|
-
const tileSize = resolutions[0].tileSize;
|
|
364
|
-
utils.assert(
|
|
365
|
-
resolutions.every((resolution) => resolution.tileSize === tileSize),
|
|
366
|
-
"resolutions must all have the same tile size",
|
|
367
|
-
);
|
|
368
|
-
return tileSize;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const SEEN_LUTS = new WeakSet<ReadonlyArray<OmeColor>>();
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Creates a color lookup table (LUT) as a 2D texture.
|
|
375
|
-
*
|
|
376
|
-
* @param options.source - The source lookup table.
|
|
377
|
-
* @param options.maxTextureDimension2D - The maximum texture dimension size.
|
|
378
|
-
*/
|
|
379
|
-
function createColorTexture(options: {
|
|
380
|
-
source?: ReadonlyArray<OmeColor>;
|
|
381
|
-
maxTextureDimension2D: number;
|
|
382
|
-
}) {
|
|
383
|
-
const { source, maxTextureDimension2D } = options;
|
|
384
|
-
const fallback = {
|
|
385
|
-
data: DEFAULT_COLOR_TEXTURE,
|
|
386
|
-
width: DEFAULT_COLOR_TEXTURE.length / 4,
|
|
387
|
-
height: 1,
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
if (!source) {
|
|
391
|
-
return fallback;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// pack the colors into a 2D texture
|
|
395
|
-
const size = Math.max(...source.map((e) => e.labelValue)) + 1;
|
|
396
|
-
const width = Math.min(size, maxTextureDimension2D);
|
|
397
|
-
const height = Math.ceil(size / width);
|
|
398
|
-
|
|
399
|
-
if (width > maxTextureDimension2D || height > maxTextureDimension2D) {
|
|
400
|
-
if (!SEEN_LUTS.has(source)) {
|
|
401
|
-
console.warn("[vizarr] Skipping color palette from OME-NGFF `image-label` source: max texture dimension limit.");
|
|
402
|
-
SEEN_LUTS.add(source);
|
|
403
|
-
}
|
|
404
|
-
return fallback;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const data = new Uint8Array(width * height * 4);
|
|
408
|
-
for (const { labelValue, rgba } of source) {
|
|
409
|
-
const x = labelValue % width;
|
|
410
|
-
const y = Math.floor(labelValue / width);
|
|
411
|
-
const texIndex = (y * width + x) * 4;
|
|
412
|
-
data[texIndex] = rgba[0];
|
|
413
|
-
data[texIndex + 1] = rgba[1];
|
|
414
|
-
data[texIndex + 2] = rgba[2];
|
|
415
|
-
data[texIndex + 3] = rgba[3];
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
return { data, width, height };
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// From Vitessce https://github.com/vitessce/vitessce/blob/03c6d5d843640982e984a0e309f1ba1807085128/packages/utils/other-utils/src/components.ts#L50-L67
|
|
422
|
-
const DEFAULT_COLOR_TEXTURE = Uint8Array.from(
|
|
423
|
-
[
|
|
424
|
-
[0, 73, 73],
|
|
425
|
-
[0, 146, 146],
|
|
426
|
-
[255, 109, 182],
|
|
427
|
-
[255, 182, 219],
|
|
428
|
-
[73, 0, 146],
|
|
429
|
-
[0, 109, 219],
|
|
430
|
-
[182, 109, 255],
|
|
431
|
-
[109, 182, 255],
|
|
432
|
-
[182, 219, 255],
|
|
433
|
-
[146, 0, 0],
|
|
434
|
-
[146, 72, 0],
|
|
435
|
-
[219, 109, 0],
|
|
436
|
-
[36, 255, 36],
|
|
437
|
-
[255, 255, 109],
|
|
438
|
-
[255, 255, 255],
|
|
439
|
-
].flatMap((color) => [...color, 255]),
|
|
440
|
-
);
|
|
441
|
-
|
|
442
|
-
function typedArrayConstructorName(arr: zarr.TypedArray<LabelDataType>) {
|
|
443
|
-
const ArrayType = arr.constructor as zarr.TypedArrayConstructor<LabelDataType>;
|
|
444
|
-
const name = ArrayType.name as `${Capitalize<LabelDataType>}Array`;
|
|
445
|
-
return name;
|
|
446
|
-
}
|