@jax-data-science/sci-image-visualizer 0.11.0 → 0.12.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.
Files changed (32) hide show
  1. package/esm2022/index.mjs +4 -1
  2. package/esm2022/lib/contracts/image.contract.mjs +1 -1
  3. package/esm2022/lib/implementations/napari-js/napari-spatial-3d-scene.mjs +70 -2
  4. package/esm2022/lib/implementations/napari-js/napari-visualizer.service.mjs +2 -2
  5. package/esm2022/lib/implementations/napari-js/napari-volume-region-meshes.mjs +11 -1
  6. package/esm2022/lib/region-editor/region-editor.component.mjs +46 -41
  7. package/esm2022/lib/region-import/label-volume.mjs +35 -4
  8. package/esm2022/lib/region-import/ome-zarr-labels.mjs +253 -0
  9. package/esm2022/lib/region-import/open-annotations.mjs +289 -0
  10. package/esm2022/lib/region-import/zarr-codecs.mjs +167 -0
  11. package/esm2022/lib/region-import/zip.mjs +157 -0
  12. package/esm2022/lib/routing-visualizer.service.mjs +20 -7
  13. package/esm2022/lib/visualizer/region-load.mjs +16 -1
  14. package/fesm2022/{jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-BELcx0jv.mjs → jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-a68Hw5rw.mjs} +1050 -50
  15. package/fesm2022/jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-a68Hw5rw.mjs.map +1 -0
  16. package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-DEDSm_NL.mjs → jax-data-science-sci-image-visualizer-onnx-sam-session-CvadfzjS.mjs} +4 -2
  17. package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-DEDSm_NL.mjs.map → jax-data-science-sci-image-visualizer-onnx-sam-session-CvadfzjS.mjs.map} +1 -1
  18. package/fesm2022/jax-data-science-sci-image-visualizer.mjs +3 -1
  19. package/fesm2022/jax-data-science-sci-image-visualizer.mjs.map +1 -1
  20. package/index.d.ts +6 -0
  21. package/lib/contracts/image.contract.d.ts +7 -0
  22. package/lib/implementations/napari-js/napari-spatial-3d-scene.d.ts +40 -1
  23. package/lib/implementations/napari-js/napari-volume-region-meshes.d.ts +5 -0
  24. package/lib/region-editor/region-editor.component.d.ts +5 -4
  25. package/lib/region-import/label-volume.d.ts +4 -0
  26. package/lib/region-import/ome-zarr-labels.d.ts +36 -0
  27. package/lib/region-import/open-annotations.d.ts +73 -0
  28. package/lib/region-import/zarr-codecs.d.ts +14 -0
  29. package/lib/region-import/zip.d.ts +31 -0
  30. package/lib/routing-visualizer.service.d.ts +9 -1
  31. package/package.json +2 -1
  32. package/fesm2022/jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-BELcx0jv.mjs.map +0 -1
@@ -7,6 +7,8 @@ import { BehaviorSubject, firstValueFrom, Subject, Subscription, of, Observable,
7
7
  import { timeout, debounceTime, takeUntil, switchMap, catchError, startWith, map } from 'rxjs/operators';
8
8
  import * as ort from 'onnxruntime-web';
9
9
  import { smoothMesh, surfaceNets, pointInRing as pointInRing$2, ringArea as ringArea$1, traceContours, rasterizePolygon as rasterizePolygon$1, floodFill } from 'napari-js/geometry';
10
+ import { encode } from 'fast-png';
11
+ import { decompress } from 'fzstd';
10
12
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
11
13
  import * as i1$1 from '@angular/common';
12
14
  import { CommonModule } from '@angular/common';
@@ -5925,7 +5927,31 @@ async function readNrrd(bytes, gunzip) {
5925
5927
  // A 3D volume: sections along axis 0, each axis 2 wide and axis 1 high (jit-service's cut).
5926
5928
  const [n0, n1, n2] = sizes.length === 3 ? sizes : [sizes[0], sizes[1], 1];
5927
5929
  const axes = sizes.length === 3 ? { z: 0, x: 2, y: 1 } : { z: 2, x: 0, y: 1 };
5928
- return volume('nrrd', [n0, n1, n2], axes, data, SAMPLES[type], little);
5930
+ return volume('nrrd', [n0, n1, n2], axes, data, SAMPLES[type], little, nrrdSpacing(field, sizes.length));
5931
+ }
5932
+ /** Per file axis, the voxel size in micrometres (see {@link LabelVolume.spacing}), or undefined. */
5933
+ function nrrdSpacing(field, dims) {
5934
+ let sizes;
5935
+ const directions = field['space directions'];
5936
+ if (directions) {
5937
+ const vectors = directions.match(/\(([^)]*)\)|none/g) ?? [];
5938
+ sizes = vectors.filter((v) => v !== 'none').map((v) => Math.hypot(...v.slice(1, -1).split(',').map(Number)));
5939
+ }
5940
+ else if (field['spacings']) {
5941
+ sizes = field['spacings'].split(/\s+/).map(Number);
5942
+ }
5943
+ if (!sizes || sizes.length !== dims || sizes.some((n) => !(n > 0)))
5944
+ return undefined;
5945
+ // One unit per axis (`space units: "mm" "um" "um"`), each axis scaled by its own.
5946
+ const units = (field['space units'] ?? '').match(/"[^"]*"|\S+/g)?.map((u) => u.replace(/"/g, '').toLowerCase()) ?? [];
5947
+ const factor = (unit) => unit === 'mm' || unit === 'millimeter' || unit === 'millimeters'
5948
+ ? 1e3
5949
+ : unit === 'm' || unit === 'meter' || unit === 'meters'
5950
+ ? 1e6
5951
+ : unit === 'nm' || unit === 'nanometer' || unit === 'nanometers'
5952
+ ? 1e-3
5953
+ : 1; // micrometres, or none said
5954
+ return [...sizes.map((n, i) => n * factor(units[i])), ...(dims === 2 ? [1] : [])];
5929
5955
  }
5930
5956
  function readNifti(bytes) {
5931
5957
  if (bytes.length < 352)
@@ -5952,9 +5978,13 @@ function readNifti(bytes) {
5952
5978
  const offset = Math.max(352, Math.round(view.getFloat32(108, little)));
5953
5979
  const [n0, n1, n2] = [dims[0], dims[1], ndim >= 3 ? dims[2] : 1];
5954
5980
  // Slices along axis 2, each axis 0 wide and axis 1 high (Bio-Formats' planes).
5955
- return volume('nifti', [n0, n1, n2], { z: 2, x: 0, y: 1 }, bytes.subarray(offset), SAMPLES[type], little);
5981
+ const pixdim = [1, 2, 3].map((i) => Math.abs(view.getFloat32(76 + 4 * i, little)));
5982
+ const units = bytes[123] & 7;
5983
+ const factor = units === 1 ? 1e6 : units === 3 ? 1 : 1e3; // metres, micrometres, else millimetres
5984
+ const spacing = pixdim.every((n) => n > 0) ? pixdim.map((n) => n * factor) : undefined;
5985
+ return volume('nifti', [n0, n1, n2], { z: 2, x: 0, y: 1 }, bytes.subarray(offset), SAMPLES[type], little, spacing);
5956
5986
  }
5957
- function volume(format, n, axes, data, sample, little) {
5987
+ function volume(format, n, axes, data, sample, little, spacing) {
5958
5988
  const count = n[0] * n[1] * n[2];
5959
5989
  if (!(count > 0))
5960
5990
  throw new Error(`${format}: empty volume`);
@@ -5970,6 +6000,9 @@ function volume(format, n, axes, data, sample, little) {
5970
6000
  width,
5971
6001
  height,
5972
6002
  slices: n[axes.z],
6003
+ ...(spacing
6004
+ ? { spacing: [spacing[axes.x], spacing[axes.y], spacing[axes.z]] }
6005
+ : {}),
5973
6006
  sliceLabels(z) {
5974
6007
  const out = new Uint32Array(width * height);
5975
6008
  const base = z * stride[axes.z];
@@ -6226,6 +6259,862 @@ async function folderSliceGeoJson(file, regionSet) {
6226
6259
  return JSON.stringify(doc);
6227
6260
  }
6228
6261
 
6262
+ /**
6263
+ * The chunk codecs of the zarr stores label images come in (OME-Zarr `labels/`): Blosc framing
6264
+ * with its lz4, zstd or zlib inner codecs (zarr v2's default, as ome-zarr-py writes), zstd (zarr
6265
+ * v3's default), gzip and zlib. Anything else throws rather than be decoded wrongly. Pure: usable
6266
+ * from workers.
6267
+ *
6268
+ * The Blosc framing follows jit-bioformats' `Blosc` (the decoder JIT validated on SpatialData
6269
+ * stores): `u8 version, u8 versionlz, u8 flags, u8 typesize, u32 nbytes, u32 blocksize,
6270
+ * u32 cbytes`, then `u32 bstarts[nblocks]`, then blocks of `nstreams` sub-streams, each
6271
+ * `i32 csize` + bytes (a stream whose csize equals its decoded size stored raw, a negative one a
6272
+ * run of one byte). Flags: 0x1 byte-shuffle, 0x2 memcpyed, 0x4 bit-shuffle, 0x10 no-split, bits
6273
+ * 5-7 the inner codec.
6274
+ */
6275
+ const BLOSC_CODECS = ['blosclz', 'lz4', 'snappy', 'zlib', 'zstd'];
6276
+ /** Decode a Blosc frame. */
6277
+ async function bloscDecompress(frame, inflate = platformInflate) {
6278
+ const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
6279
+ if (frame.length < 16)
6280
+ throw new Error('blosc: frame too short');
6281
+ const flags = frame[2];
6282
+ const typesize = frame[3];
6283
+ const nbytes = view.getUint32(4, true);
6284
+ const blocksize = view.getUint32(8, true);
6285
+ const out = new Uint8Array(nbytes);
6286
+ if (nbytes === 0)
6287
+ return out;
6288
+ if (flags & 0x2) {
6289
+ out.set(frame.subarray(16, 16 + nbytes));
6290
+ return out;
6291
+ }
6292
+ if (flags & 0x4)
6293
+ throw new Error('blosc: bit-shuffle is not supported');
6294
+ const code = flags >>> 5;
6295
+ const codec = BLOSC_CODECS[code] ?? `code ${code}`;
6296
+ if (codec !== 'lz4' && codec !== 'zstd' && codec !== 'zlib') {
6297
+ throw new Error(`blosc: inner codec ${codec} is not supported (lz4, zstd or zlib)`);
6298
+ }
6299
+ const shuffle = (flags & 0x1) !== 0 && typesize > 1;
6300
+ const noSplit = (flags & 0x10) !== 0;
6301
+ const nblocks = Math.ceil(nbytes / blocksize);
6302
+ const scratch = shuffle ? new Uint8Array(blocksize) : null;
6303
+ for (let blk = 0; blk < nblocks; blk++) {
6304
+ const bsize = Math.min(blocksize, nbytes - blk * blocksize);
6305
+ const leftover = bsize !== blocksize;
6306
+ const nstreams = !noSplit && !leftover ? typesize : 1;
6307
+ const ssize = Math.floor(bsize / nstreams);
6308
+ let p = view.getUint32(16 + blk * 4, true);
6309
+ const block = scratch ?? out;
6310
+ const base = scratch ? 0 : blk * blocksize;
6311
+ for (let s = 0; s < nstreams; s++) {
6312
+ const csize = view.getInt32(p, true);
6313
+ p += 4;
6314
+ const dst = base + s * ssize;
6315
+ if (csize < 0) {
6316
+ block.fill(-csize & 0xff, dst, dst + ssize);
6317
+ continue;
6318
+ }
6319
+ const src = frame.subarray(p, p + csize);
6320
+ if (csize === ssize) {
6321
+ block.set(src, dst);
6322
+ }
6323
+ else {
6324
+ const dec = codec === 'lz4'
6325
+ ? lz4BlockDecompress(src, ssize)
6326
+ : codec === 'zstd'
6327
+ ? decompress(src)
6328
+ : await inflate(src, 'deflate');
6329
+ if (dec.length !== ssize)
6330
+ throw new Error(`blosc: ${codec} produced ${dec.length} of ${ssize} bytes`);
6331
+ block.set(dec, dst);
6332
+ }
6333
+ p += csize;
6334
+ }
6335
+ if (scratch)
6336
+ unshuffle(scratch, bsize, typesize, out, blk * blocksize);
6337
+ }
6338
+ return out;
6339
+ }
6340
+ /** Decode one LZ4 block (no frame) of `size` bytes. */
6341
+ function lz4BlockDecompress(src, size) {
6342
+ const out = new Uint8Array(size);
6343
+ let i = 0;
6344
+ let o = 0;
6345
+ while (i < src.length) {
6346
+ const token = src[i++];
6347
+ let literals = token >>> 4;
6348
+ if (literals === 15) {
6349
+ let b;
6350
+ do {
6351
+ b = src[i++];
6352
+ literals += b;
6353
+ } while (b === 255);
6354
+ }
6355
+ if (o + literals > size || i + literals > src.length)
6356
+ throw new Error('lz4: literals overrun');
6357
+ out.set(src.subarray(i, i + literals), o);
6358
+ i += literals;
6359
+ o += literals;
6360
+ if (i >= src.length)
6361
+ break; // the last sequence has no match
6362
+ const offset = src[i] | (src[i + 1] << 8);
6363
+ i += 2;
6364
+ if (offset === 0 || offset > o)
6365
+ throw new Error('lz4: bad match offset');
6366
+ let length = (token & 15) + 4;
6367
+ if ((token & 15) === 15) {
6368
+ let b;
6369
+ do {
6370
+ b = src[i++];
6371
+ length += b;
6372
+ } while (b === 255);
6373
+ }
6374
+ if (o + length > size)
6375
+ throw new Error('lz4: match overrun');
6376
+ // Byte by byte: a match may overlap what it copies (a run).
6377
+ for (let k = 0; k < length; k++, o++)
6378
+ out[o] = out[o - offset];
6379
+ }
6380
+ if (o !== size)
6381
+ throw new Error(`lz4: produced ${o} of ${size} bytes`);
6382
+ return out;
6383
+ }
6384
+ /**
6385
+ * Decode a zarr chunk through `codecs`, outermost last as zarr lists them (v2: the one
6386
+ * `compressor`; v3: the bytes-to-bytes codecs after `bytes`).
6387
+ */
6388
+ async function decodeChunk(bytes, codecs, inflate = platformInflate) {
6389
+ let data = bytes;
6390
+ for (const codec of [...codecs].reverse()) {
6391
+ switch (codec.id) {
6392
+ case 'blosc':
6393
+ data = await bloscDecompress(data, inflate);
6394
+ break;
6395
+ case 'zstd':
6396
+ data = decompress(data);
6397
+ break;
6398
+ case 'gzip':
6399
+ data = await inflate(data, 'gzip');
6400
+ break;
6401
+ case 'zlib':
6402
+ data = await inflate(data, 'deflate');
6403
+ break;
6404
+ default:
6405
+ throw new Error(`zarr: codec ${codec.id} is not supported (blosc, zstd, gzip, zlib)`);
6406
+ }
6407
+ }
6408
+ return data;
6409
+ }
6410
+ function unshuffle(src, len, typesize, dst, off) {
6411
+ const n = Math.floor(len / typesize);
6412
+ for (let j = 0; j < typesize; j++) {
6413
+ const base = j * n;
6414
+ for (let i = 0; i < n; i++)
6415
+ dst[off + i * typesize + j] = src[base + i];
6416
+ }
6417
+ // Bytes past the last whole element are not shuffled.
6418
+ for (let k = n * typesize; k < len; k++)
6419
+ dst[off + k] = src[k];
6420
+ }
6421
+ async function platformInflate(bytes, format) {
6422
+ if (typeof DecompressionStream === 'undefined')
6423
+ throw new Error(`zarr: ${format} chunks need DecompressionStream`);
6424
+ const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream(format));
6425
+ return new Uint8Array(await new Response(stream).arrayBuffer());
6426
+ }
6427
+
6428
+ /**
6429
+ * A label image of an OME-Zarr store (`labels/<name>/`, NGFF 0.4 on zarr v2 or 0.5 on zarr v3),
6430
+ * read in the browser from the store's files (a folder chosen, or a zip's members): its full
6431
+ * resolution level, sliced along z, with the classes its `image-label` metadata names and colours.
6432
+ *
6433
+ * Chunks are decoded when a slice that needs them is read, so a store is never decoded whole.
6434
+ * Only what label images use: C-order chunks of integer (or integer-valued float) samples, through
6435
+ * the codecs {@link decodeChunk} reads, a missing chunk being the fill value. Sharded or transposed
6436
+ * arrays, F-order and filters throw rather than be read wrongly. Pure: usable from workers.
6437
+ */
6438
+ /** Whether `files` hold a zarr store (any zarr metadata among them). */
6439
+ function hasZarrMetadata(files) {
6440
+ return files.some((f) => /(^|\/)(\.zattrs|\.zarray|\.zgroup|zarr\.json)$/.test(norm(f.name)));
6441
+ }
6442
+ /**
6443
+ * The first label image of the zarr store among `files` (the order `labels/`'s listing gives,
6444
+ * else the first found), or null when the store has none.
6445
+ */
6446
+ async function readOmeZarrLabels(files, inflate) {
6447
+ const byPath = new Map(files.map((f) => [norm(f.name), f]));
6448
+ const groups = new Map();
6449
+ for (const [path, file] of byPath) {
6450
+ const m = /^(.*?)\/?(\.zattrs|zarr\.json)$/.exec(path);
6451
+ if (!m)
6452
+ continue;
6453
+ const doc = JSON.parse(await file.text());
6454
+ const attrs = (m[2] === 'zarr.json' ? doc['attributes'] : doc) ?? {};
6455
+ groups.set(m[1], (attrs['ome'] ?? attrs));
6456
+ }
6457
+ const listed = [];
6458
+ for (const [dir, ome] of groups) {
6459
+ for (const name of ome['labels'] ?? [])
6460
+ listed.push(join(dir, name));
6461
+ }
6462
+ const isLabel = (dir) => {
6463
+ const ome = groups.get(dir);
6464
+ return !!ome?.['multiscales'] && (!!ome['image-label'] || /(^|\/)labels\/[^/]+$/.test(dir));
6465
+ };
6466
+ const dir = [...listed, ...groups.keys()].find(isLabel);
6467
+ if (dir === undefined)
6468
+ return null;
6469
+ const ome = groups.get(dir);
6470
+ const ms = ome['multiscales'][0];
6471
+ const dataset = ms.datasets[0];
6472
+ const array = await readArrayMeta(byPath, join(dir, dataset.path));
6473
+ const axes = (ms.axes ?? ['z', 'y', 'x'].slice(3 - array.shape.length)).map((a) => typeof a === 'string' ? { name: a } : a);
6474
+ if (axes.length !== array.shape.length)
6475
+ throw new Error(`OME-Zarr ${dir}: ${axes.length} axes for a ${array.shape.length}-D array`);
6476
+ const at = (n) => axes.findIndex((a) => a.name.toLowerCase() === n);
6477
+ const ix = at('x');
6478
+ const iy = at('y');
6479
+ const iz = at('z');
6480
+ if (ix < 0 || iy < 0)
6481
+ throw new Error(`OME-Zarr ${dir}: no x and y axes`);
6482
+ const scale = dataset.coordinateTransformations?.find((t) => t.type === 'scale')?.scale;
6483
+ const um = (i) => (i < 0 ? 1 : (scale?.[i] ?? 1) * unitToMicrons(axes[i].unit));
6484
+ const stack = labelStack(byPath, join(dir, dataset.path), array, { ix, iy, iz }, inflate);
6485
+ stack.spacing = [um(ix), um(iy), iz < 0 ? 1 : um(iz)];
6486
+ const name = ms.name ?? dir.split('/').pop() ?? 'labels';
6487
+ return { name, stack, classes: imageLabelClasses(ome['image-label']) };
6488
+ }
6489
+ async function readArrayMeta(byPath, path) {
6490
+ const v2 = byPath.get(join(path, '.zarray'));
6491
+ if (v2) {
6492
+ const m = JSON.parse(await v2.text());
6493
+ if (m.order && m.order !== 'C')
6494
+ throw new Error(`zarr ${path}: ${m.order}-order chunks are not supported`);
6495
+ if (m.filters?.length)
6496
+ throw new Error(`zarr ${path}: filters are not supported`);
6497
+ const sep = m.dimension_separator ?? '.';
6498
+ return {
6499
+ shape: m.shape,
6500
+ chunks: m.chunks,
6501
+ dtype: v2Dtype(m.dtype, path),
6502
+ codecs: m.compressor ? [m.compressor] : [],
6503
+ fill: Number(m.fill_value ?? 0) || 0,
6504
+ key: (index) => index.join(sep),
6505
+ };
6506
+ }
6507
+ const v3 = byPath.get(join(path, 'zarr.json'));
6508
+ if (!v3)
6509
+ throw new Error(`zarr ${path}: no array metadata`);
6510
+ const m = JSON.parse(await v3.text());
6511
+ if (m.chunk_grid?.name !== 'regular')
6512
+ throw new Error(`zarr ${path}: chunk grid ${m.chunk_grid?.name} is not supported`);
6513
+ const codecs = (m.codecs ?? []);
6514
+ const bytes = codecs[0];
6515
+ if (bytes?.name !== 'bytes') {
6516
+ throw new Error(`zarr ${path}: codec ${bytes?.name ?? 'none'} before bytes is not supported (sharding, transpose)`);
6517
+ }
6518
+ const little = (bytes.configuration?.['endian'] ?? 'little') === 'little';
6519
+ const enc = m.chunk_key_encoding ?? { name: 'default' };
6520
+ const sep = enc.configuration?.separator ?? (enc.name === 'v2' ? '.' : '/');
6521
+ return {
6522
+ shape: m.shape,
6523
+ chunks: m.chunk_grid.configuration.chunk_shape,
6524
+ dtype: v3Dtype(m.data_type, little, path),
6525
+ codecs: codecs.slice(1).map((c) => ({ id: c.name, ...(c.configuration ?? {}) })),
6526
+ fill: Number(m.fill_value ?? 0) || 0,
6527
+ key: (index) => (enc.name === 'v2' ? index.join(sep) : ['c', ...index].join(sep)),
6528
+ };
6529
+ }
6530
+ /** The slices of the array at `path`, decoding (and keeping) the chunk layers a slice spans. */
6531
+ function labelStack(byPath, path, a, axes, inflate) {
6532
+ const { ix, iy, iz } = axes;
6533
+ const width = a.shape[ix];
6534
+ const height = a.shape[iy];
6535
+ const slices = iz < 0 ? 1 : a.shape[iz];
6536
+ const cx = a.chunks[ix];
6537
+ const cy = a.chunks[iy];
6538
+ // The chunks (their decodes, pending or done) of the last chunk layer along z read: a slice's
6539
+ // neighbours share it. A read keeps the layer it started with, so a read of another layer
6540
+ // replacing this cache while it awaits a chunk cannot change what it reads.
6541
+ let layer = null;
6542
+ const chunk = async (index) => {
6543
+ const file = byPath.get(join(path, a.key(index)));
6544
+ if (!file)
6545
+ return null;
6546
+ return decodeChunk(await file.bytes(), a.codecs, inflate);
6547
+ };
6548
+ return {
6549
+ width,
6550
+ height,
6551
+ slices,
6552
+ async labels(z) {
6553
+ if (z < 0 || z >= slices)
6554
+ throw new Error(`slice ${z} of ${slices}`);
6555
+ const kz = iz < 0 ? 0 : Math.floor(z / a.chunks[iz]);
6556
+ if (layer?.kz !== kz)
6557
+ layer = { kz, chunks: new Map() };
6558
+ const mine = layer;
6559
+ const out = new Uint32Array(width * height);
6560
+ const strides = a.chunks.map((_, d) => a.chunks.slice(d + 1).reduce((p, n) => p * n, 1));
6561
+ for (let gy = 0; gy * cy < height; gy++) {
6562
+ for (let gx = 0; gx * cx < width; gx++) {
6563
+ // Every other axis (time, channel) at its first index.
6564
+ const index = a.shape.map((_, d) => (d === ix ? gx : d === iy ? gy : d === iz ? kz : 0));
6565
+ const key = index.join(',');
6566
+ let pending = mine.chunks.get(key);
6567
+ if (!pending) {
6568
+ pending = chunk(index);
6569
+ mine.chunks.set(key, pending);
6570
+ // A chunk that failed to decode is not kept: the next read tries it again.
6571
+ pending.catch(() => mine.chunks.delete(key));
6572
+ }
6573
+ const data = await pending;
6574
+ const view = data ? new DataView(data.buffer, data.byteOffset, data.byteLength) : null;
6575
+ const inZ = iz < 0 ? 0 : z - kz * a.chunks[iz];
6576
+ for (let y = gy * cy; y < Math.min(height, (gy + 1) * cy); y++) {
6577
+ for (let x = gx * cx; x < Math.min(width, (gx + 1) * cx); x++) {
6578
+ let v = a.fill;
6579
+ if (view) {
6580
+ const local = a.shape.map((_, d) => d === ix ? x - gx * cx : d === iy ? y - gy * cy : d === iz ? inZ : 0);
6581
+ const i = local.reduce((p, n, d) => p + n * strides[d], 0);
6582
+ v = a.dtype.read(view, i * a.dtype.bytes);
6583
+ }
6584
+ if (v > 0xffffffff) {
6585
+ // A label id past 32 bits would be another label once narrowed: refuse it.
6586
+ throw new Error(`zarr ${path}: label ${v} does not fit 32 bits`);
6587
+ }
6588
+ out[y * width + x] = v > 0 ? v : 0;
6589
+ }
6590
+ }
6591
+ }
6592
+ }
6593
+ return out;
6594
+ },
6595
+ };
6596
+ }
6597
+ const SAMPLE = {
6598
+ u1: { bytes: 1, read: (v, at) => v.getUint8(at) },
6599
+ i1: { bytes: 1, read: (v, at) => v.getInt8(at) },
6600
+ u2: { bytes: 2, read: (v, at, le) => v.getUint16(at, le) },
6601
+ i2: { bytes: 2, read: (v, at, le) => v.getInt16(at, le) },
6602
+ u4: { bytes: 4, read: (v, at, le) => v.getUint32(at, le) },
6603
+ i4: { bytes: 4, read: (v, at, le) => v.getInt32(at, le) },
6604
+ u8: { bytes: 8, read: (v, at, le) => Number(v.getBigUint64(at, le)) },
6605
+ i8: { bytes: 8, read: (v, at, le) => Number(v.getBigInt64(at, le)) },
6606
+ f4: { bytes: 4, read: (v, at, le) => Math.round(v.getFloat32(at, le)) },
6607
+ f8: { bytes: 8, read: (v, at, le) => Math.round(v.getFloat64(at, le)) },
6608
+ };
6609
+ function v2Dtype(dtype, path) {
6610
+ const m = /^([<>|])([uif])(\d)$/.exec(dtype);
6611
+ const s = m ? SAMPLE[m[2] + m[3]] : undefined;
6612
+ if (!m || !s)
6613
+ throw new Error(`zarr ${path}: dtype ${dtype} is not a label type`);
6614
+ const le = m[1] !== '>';
6615
+ return { bytes: s.bytes, read: (v, at) => s.read(v, at, le) };
6616
+ }
6617
+ function v3Dtype(type, little, path) {
6618
+ const m = /^(u?int|float)(8|16|32|64)$/.exec(type);
6619
+ const code = m ? (m[1] === 'uint' ? 'u' : m[1] === 'int' ? 'i' : 'f') + Number(m[2]) / 8 : '';
6620
+ const s = SAMPLE[code];
6621
+ if (!s)
6622
+ throw new Error(`zarr ${path}: data type ${type} is not a label type`);
6623
+ return { bytes: s.bytes, read: (v, at) => s.read(v, at, little) };
6624
+ }
6625
+ /** The classes `image-label` names (a property naming each value) and colours; others unnamed. */
6626
+ function imageLabelClasses(meta) {
6627
+ const named = new Map();
6628
+ for (const p of meta?.properties ?? []) {
6629
+ const name = ['name', 'class', 'label', 'acronym', 'structure', 'type']
6630
+ .map((k) => p[k])
6631
+ .find((v) => typeof v === 'string');
6632
+ named.set(p['label-value'], { ...named.get(p['label-value']), name: name });
6633
+ }
6634
+ for (const c of meta?.colors ?? []) {
6635
+ if (!c.rgba)
6636
+ continue;
6637
+ const hex = `#${c.rgba
6638
+ .slice(0, 3)
6639
+ .map((n) => Math.round(n).toString(16).padStart(2, '0'))
6640
+ .join('')}`.toUpperCase();
6641
+ named.set(c['label-value'], { ...named.get(c['label-value']), color: hex });
6642
+ }
6643
+ if (!named.size)
6644
+ return labelClasses(undefined);
6645
+ const classHierarchy = [];
6646
+ const seen = new Set();
6647
+ for (const [value, { name, color }] of named) {
6648
+ const cls = name ?? `label ${value}`;
6649
+ // One class per name: an instance segmentation names many values alike.
6650
+ if (!seen.has(cls))
6651
+ classHierarchy.push({ name: cls, ...(color ? { color } : {}), value });
6652
+ seen.add(cls);
6653
+ }
6654
+ const labels = Object.fromEntries([...named].map(([v, { name }]) => [v, name ?? `label ${v}`]));
6655
+ return labelClasses({ classHierarchy, labels });
6656
+ }
6657
+ function unitToMicrons(unit) {
6658
+ switch (unit?.toLowerCase()) {
6659
+ case 'nanometer':
6660
+ return 1e-3;
6661
+ case 'millimeter':
6662
+ return 1e3;
6663
+ case 'centimeter':
6664
+ return 1e4;
6665
+ case 'meter':
6666
+ return 1e6;
6667
+ default:
6668
+ return 1; // micrometer, or none said
6669
+ }
6670
+ }
6671
+ /** A path relative to the store, '/'-separated, without a leading './'. */
6672
+ function norm(name) {
6673
+ return name.replace(/\\/g, '/').replace(/^\.\//, '');
6674
+ }
6675
+ function join(dir, name) {
6676
+ return dir ? `${dir}/${name}` : name;
6677
+ }
6678
+
6679
+ const EOCD = 0x06054b50;
6680
+ const ZIP64_LOCATOR = 0x07064b50;
6681
+ const ZIP64_EOCD = 0x06064b50;
6682
+ const CENTRAL = 0x02014b50;
6683
+ const LOCAL = 0x04034b50;
6684
+ const U32 = 0xffffffff;
6685
+ /** Whether a file name is a zip archive. */
6686
+ function isZipName(name) {
6687
+ return /\.zip$/i.test(name);
6688
+ }
6689
+ /** The members of the archive in `bytes` (directories and macOS metadata left out). */
6690
+ function readZip(bytes, inflateRaw = platformInflateRaw) {
6691
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
6692
+ const eocd = findEocd(view);
6693
+ let count = view.getUint16(eocd + 10, true);
6694
+ let dirOffset = view.getUint32(eocd + 16, true);
6695
+ if (count === 0xffff || dirOffset === U32) {
6696
+ const locator = eocd - 20;
6697
+ if (locator < 0 || view.getUint32(locator, true) !== ZIP64_LOCATOR)
6698
+ throw new Error('zip: ZIP64 locator missing');
6699
+ const z64 = num(view, locator + 8);
6700
+ if (view.getUint32(z64, true) !== ZIP64_EOCD)
6701
+ throw new Error('zip: ZIP64 directory end missing');
6702
+ count = num(view, z64 + 32);
6703
+ dirOffset = num(view, z64 + 48);
6704
+ }
6705
+ const entries = [];
6706
+ let at = dirOffset;
6707
+ for (let i = 0; i < count; i++) {
6708
+ if (view.getUint32(at, true) !== CENTRAL)
6709
+ throw new Error('zip: bad central directory');
6710
+ const flags = view.getUint16(at + 8, true);
6711
+ const method = view.getUint16(at + 10, true);
6712
+ const crc = view.getUint32(at + 16, true);
6713
+ let compressed = view.getUint32(at + 20, true);
6714
+ let size = view.getUint32(at + 24, true);
6715
+ const nameLen = view.getUint16(at + 28, true);
6716
+ const extraLen = view.getUint16(at + 30, true);
6717
+ const commentLen = view.getUint16(at + 32, true);
6718
+ let local = view.getUint32(at + 42, true);
6719
+ const nameBytes = bytes.subarray(at + 46, at + 46 + nameLen);
6720
+ const name = (flags & 0x800 ? new TextDecoder() : new TextDecoder('latin1'))
6721
+ .decode(nameBytes)
6722
+ .replace(/\\/g, '/');
6723
+ // ZIP64: the 32-bit fields that overflowed are in the extra field, in this order.
6724
+ let x = at + 46 + nameLen;
6725
+ const extraEnd = x + extraLen;
6726
+ while (x + 4 <= extraEnd) {
6727
+ const id = view.getUint16(x, true);
6728
+ const len = view.getUint16(x + 2, true);
6729
+ if (id === 0x0001) {
6730
+ let p = x + 4;
6731
+ if (size === U32)
6732
+ ((size = num(view, p)), (p += 8));
6733
+ if (compressed === U32)
6734
+ ((compressed = num(view, p)), (p += 8));
6735
+ if (local === U32)
6736
+ local = num(view, p);
6737
+ }
6738
+ x += 4 + len;
6739
+ }
6740
+ at = extraEnd + commentLen;
6741
+ if (name.endsWith('/') || /(^|\/)__MACOSX\//.test(name) || /(^|\/)\.DS_Store$/.test(name))
6742
+ continue;
6743
+ const entry = { flags, method, crc, compressed, size, local };
6744
+ entries.push({ name, size, bytes: () => readMember(bytes, view, name, entry, inflateRaw) });
6745
+ }
6746
+ return entries;
6747
+ }
6748
+ /**
6749
+ * `files` with every `.zip` among them replaced by its members, named by their paths in it (a
6750
+ * single top folder the archive wraps everything in is kept, as a folder pick would keep it).
6751
+ */
6752
+ async function expandZips(files, inflateRaw) {
6753
+ const out = [];
6754
+ for (const file of files) {
6755
+ if (!isZipName(file.name)) {
6756
+ out.push(file);
6757
+ continue;
6758
+ }
6759
+ for (const entry of readZip(await file.bytes(), inflateRaw)) {
6760
+ out.push({
6761
+ name: entry.name,
6762
+ bytes: () => entry.bytes(),
6763
+ text: async () => new TextDecoder().decode(await entry.bytes()),
6764
+ });
6765
+ }
6766
+ }
6767
+ return out;
6768
+ }
6769
+ async function readMember(bytes, view, name, m, inflateRaw) {
6770
+ if (m.flags & 0x1)
6771
+ throw new Error(`zip: ${name} is encrypted`);
6772
+ if (view.getUint32(m.local, true) !== LOCAL)
6773
+ throw new Error(`zip: ${name}: bad local header`);
6774
+ const start = m.local + 30 + view.getUint16(m.local + 26, true) + view.getUint16(m.local + 28, true);
6775
+ const raw = bytes.subarray(start, start + m.compressed);
6776
+ if (raw.length !== m.compressed)
6777
+ throw new Error(`zip: ${name} is truncated`);
6778
+ let out;
6779
+ if (m.method === 0)
6780
+ out = raw;
6781
+ else if (m.method === 8)
6782
+ out = await inflateRaw(raw);
6783
+ else
6784
+ throw new Error(`zip: ${name} uses compression method ${m.method}, not stored or deflate`);
6785
+ if (out.length !== m.size)
6786
+ throw new Error(`zip: ${name} is ${out.length} bytes, expected ${m.size}`);
6787
+ if (crc32(out) !== m.crc)
6788
+ throw new Error(`zip: ${name} fails its CRC check`);
6789
+ return out;
6790
+ }
6791
+ /** The end-of-central-directory record: the last one, within the 64 KiB a comment may take. */
6792
+ function findEocd(view) {
6793
+ const stop = Math.max(0, view.byteLength - 22 - 0xffff);
6794
+ for (let at = view.byteLength - 22; at >= stop; at--) {
6795
+ if (view.getUint32(at, true) === EOCD)
6796
+ return at;
6797
+ }
6798
+ throw new Error('zip: not a zip archive (no central directory)');
6799
+ }
6800
+ /** A little-endian u64 that fits a double (archives under 8 PB). */
6801
+ function num(view, at) {
6802
+ return view.getUint32(at, true) + view.getUint32(at + 4, true) * 0x100000000;
6803
+ }
6804
+ let crcTable = null;
6805
+ /** CRC-32 (IEEE), as zip records it. */
6806
+ function crc32(data) {
6807
+ if (!crcTable) {
6808
+ crcTable = new Uint32Array(256);
6809
+ for (let n = 0; n < 256; n++) {
6810
+ let c = n;
6811
+ for (let k = 0; k < 8; k++)
6812
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
6813
+ crcTable[n] = c >>> 0;
6814
+ }
6815
+ }
6816
+ let c = 0xffffffff;
6817
+ for (let i = 0; i < data.length; i++)
6818
+ c = crcTable[(c ^ data[i]) & 0xff] ^ (c >>> 8);
6819
+ return (c ^ 0xffffffff) >>> 0;
6820
+ }
6821
+ async function platformInflateRaw(bytes) {
6822
+ if (typeof DecompressionStream === 'undefined')
6823
+ throw new Error('zip: deflated members need DecompressionStream');
6824
+ let inflater;
6825
+ try {
6826
+ // Compression Streams' raw DEFLATE (Chrome 103, Firefox 113, Safari 16.4 on).
6827
+ inflater = new DecompressionStream('deflate-raw');
6828
+ }
6829
+ catch {
6830
+ throw new Error('zip: this browser cannot inflate zip members (no DecompressionStream "deflate-raw")');
6831
+ }
6832
+ const stream = new Blob([bytes]).stream().pipeThrough(inflater);
6833
+ return new Uint8Array(await new Response(stream).arrayBuffer());
6834
+ }
6835
+
6836
+ /**
6837
+ * Annotations opened by themselves, with no image under them: a label volume (`.nii`, `.nii.gz`,
6838
+ * `.nrrd`, with the ontology JSON chosen with it), an OME-Zarr store's label image, a folder of
6839
+ * per-slice GeoJSON (with its `regionset.json`), one GeoJSON (z-indexed by `geometry.plane.z` or
6840
+ * not), or a `.zip` of any of these.
6841
+ *
6842
+ * {@link openAnnotations} builds the image they are shown on, which every view takes as it takes
6843
+ * any other (OpenSeadragon, napari, Plotly; a stack when there are several slices; the Volume and
6844
+ * Isosurface views and their 3D region surfaces): a label volume's slices in gray levels, its
6845
+ * regions filled over them in their classes' colours, else a blank stack the size of the regions.
6846
+ * The image carries the annotations as its own region sources, one per slice
6847
+ * ({@link IImageInfo.regionSlices}), traced or parsed the first time a slice is shown.
6848
+ * The host publishes it as it publishes any image, and disposes it when another replaces it.
6849
+ *
6850
+ * {@link readAnnotationSlices} is the part the Region Editor's import shares: the sources, by slice.
6851
+ */
6852
+ /** The longest side an image built for annotations may have: OpenSeadragon's non-tiled limit. */
6853
+ const ANNOTATION_IMAGE_MAX_DIM = 8192;
6854
+ /**
6855
+ * The annotations among `files` (zips expanded), as one region source per slice; null when there
6856
+ * are none to read. A label image's slices are traced, and a GeoJSON parsed, when the slice is
6857
+ * first shown.
6858
+ */
6859
+ async function readAnnotationSlices(chosen, decoders = {}) {
6860
+ const files = await expandZips(chosen, decoders.inflateRaw);
6861
+ const openGeoJson = decoders.openGeoJson ?? ((text) => openGeoJsonRegionSource(text));
6862
+ if (hasZarrMetadata(files)) {
6863
+ const zarr = await readOmeZarrLabels(files, decoders.inflate);
6864
+ if (!zarr)
6865
+ throw new Error('The zarr store has no label image (an OME-Zarr labels/ group).');
6866
+ return labelSlices('labels', zarr.name, zarr.stack, styled(zarr.classes, decoders), openGeoJson);
6867
+ }
6868
+ const plan = planRegionImport(files);
6869
+ if (!plan)
6870
+ return null;
6871
+ if (plan.kind === 'volume') {
6872
+ const volume = await readLabelVolume(plan.volume.name, await plan.volume.bytes(), decoders.gunzip);
6873
+ const classes = labelClasses(plan.ontology ? JSON.parse(await plan.ontology.text()) : undefined);
6874
+ return labelSlices('labels', stem(plan.volume.name), stackOf(volume), styled(classes, decoders), openGeoJson);
6875
+ }
6876
+ if (plan.kind === 'folder') {
6877
+ const regionSet = plan.regionSet ? JSON.parse(await plan.regionSet.text()) : null;
6878
+ const openers = new Map();
6879
+ plan.slices.forEach((file, z) => openers.set(z, async () => openGeoJson(await folderSliceGeoJson(file, regionSet))));
6880
+ const folder = plan.slices[0].name.split('/').slice(-2, -1)[0] || 'regions';
6881
+ return {
6882
+ kind: 'folder',
6883
+ name: folder,
6884
+ slices: plan.slices.length,
6885
+ openers,
6886
+ summary: `${plan.slices.length} slice files`,
6887
+ };
6888
+ }
6889
+ const slices = splitByPlane(await plan.file.text());
6890
+ const openers = new Map();
6891
+ slices.forEach((text, z) => openers.set(z, () => openGeoJson(text)));
6892
+ return {
6893
+ kind: 'single',
6894
+ name: stem(plan.file.name),
6895
+ slices: slices.length,
6896
+ openers,
6897
+ summary: slices.length > 1 ? `${slices.length} slices` : 'one plane',
6898
+ };
6899
+ }
6900
+ let opened = 0;
6901
+ /**
6902
+ * The image `files`' annotations are shown on by themselves (see the module comment). Throws when
6903
+ * there are none, or when their regions span more than {@link ANNOTATION_IMAGE_MAX_DIM} pixels
6904
+ * (they are then imported onto the image they were drawn on).
6905
+ */
6906
+ async function openAnnotations(files, decoders = {}) {
6907
+ const set = await readAnnotationSlices(files, { outlineLabels: true, ...decoders });
6908
+ if (!set) {
6909
+ throw new Error('Choose a label volume (.nii, .nii.gz, .nrrd, with its ontology JSON), an OME-Zarr store, ' +
6910
+ 'GeoJSON files or a folder of them, or a .zip of any of these.');
6911
+ }
6912
+ let urls = [];
6913
+ let width;
6914
+ let height;
6915
+ let openers = set.openers;
6916
+ // Releases the sources measuring opened that the store has not taken (none for a label image).
6917
+ let release = () => undefined;
6918
+ try {
6919
+ if (set.labels && set.size) {
6920
+ [width, height] = set.size;
6921
+ checkSize(width, height);
6922
+ urls = await labelSliceUrls(set.labels.stack, set.labels.classes);
6923
+ }
6924
+ else {
6925
+ // Regions alone: a blank stack as large as they reach, measured on the sources (kept open
6926
+ // for their slices, so each is parsed once).
6927
+ const measured = await measure(set.openers);
6928
+ openers = measured.openers;
6929
+ release = measured.release;
6930
+ [width, height] = measured.size;
6931
+ checkSize(width, height);
6932
+ const png = encode({ width, height, data: new Uint8Array(width * height), channels: 1, depth: 8 });
6933
+ const blank = new Blob([png], { type: 'image/png' });
6934
+ urls = Array.from({ length: set.slices }, () => URL.createObjectURL(blank));
6935
+ }
6936
+ }
6937
+ catch (err) {
6938
+ urls.forEach((u) => URL.revokeObjectURL(u));
6939
+ release();
6940
+ throw err;
6941
+ }
6942
+ const [mx, my, mz] = set.spacing ?? [];
6943
+ const isStack = set.slices > 1;
6944
+ const info = {
6945
+ isGrayscale: true,
6946
+ trueImageSize: [width, height],
6947
+ urls,
6948
+ isStack,
6949
+ showStack: false,
6950
+ scaleRatio: true,
6951
+ // Unique: the slice cache keys on the name, and two opened sets must not share slices.
6952
+ fileName: `${set.name} · annotations [${++opened}]`,
6953
+ imageMeta: [
6954
+ {
6955
+ channelCount: 1,
6956
+ rgbChannels: 1,
6957
+ x: width,
6958
+ y: height,
6959
+ z: set.slices,
6960
+ mppX: mx ?? null,
6961
+ mppY: my ?? null,
6962
+ ...(mz != null ? { mppZ: mz } : {}),
6963
+ },
6964
+ ],
6965
+ tiled: false,
6966
+ // Open mid-stack: an atlas's first and last sections are mostly empty.
6967
+ initialZIndex: isStack ? set.slices >> 1 : undefined,
6968
+ regionSlices: openers,
6969
+ };
6970
+ return {
6971
+ info,
6972
+ summary: `${set.summary}, ${width} × ${height}`,
6973
+ dispose: () => {
6974
+ urls.forEach((u) => URL.revokeObjectURL(u));
6975
+ release();
6976
+ },
6977
+ };
6978
+ }
6979
+ function labelSlices(kind, name, stack, classes, openGeoJson) {
6980
+ const openers = new Map();
6981
+ for (let z = 0; z < stack.slices; z++) {
6982
+ openers.set(z, async () => openGeoJson(labelSliceGeoJson(oneSlice(stack, await stack.labels(z)), 0, classes)));
6983
+ }
6984
+ return {
6985
+ kind,
6986
+ name,
6987
+ slices: stack.slices,
6988
+ openers,
6989
+ labels: { stack, classes },
6990
+ size: [stack.width, stack.height],
6991
+ ...(stack.spacing ? { spacing: stack.spacing } : {}),
6992
+ summary: `${stack.slices} slice${stack.slices === 1 ? '' : 's'} of ${stack.width} × ${stack.height}`,
6993
+ };
6994
+ }
6995
+ /** `classes`, with the atlas look when asked for and they name no style of their own. */
6996
+ function styled(classes, decoders) {
6997
+ return decoders.outlineLabels && !classes.style ? { ...classes, style: ATLAS_STYLE } : classes;
6998
+ }
6999
+ /** A label volume as a stack read on demand. */
7000
+ function stackOf(volume) {
7001
+ return {
7002
+ width: volume.width,
7003
+ height: volume.height,
7004
+ slices: volume.slices,
7005
+ ...(volume.spacing ? { spacing: volume.spacing } : {}),
7006
+ labels: async (z) => volume.sliceLabels(z),
7007
+ };
7008
+ }
7009
+ /** One slice's labels as the one-slice volume the tracer reads. */
7010
+ function oneSlice(stack, labels) {
7011
+ return { format: 'nrrd', width: stack.width, height: stack.height, slices: 1, sliceLabels: () => labels };
7012
+ }
7013
+ /**
7014
+ * Every slice as a grayscale PNG blob URL, each label at its class colour's luminance (never 0),
7015
+ * background black. Grayscale, not colour: the Volume and Isosurface views take a grayscale stack,
7016
+ * and the colour is the regions' own, filled over it in their classes' colours (the atlas look).
7017
+ */
7018
+ async function labelSliceUrls(stack, classes) {
7019
+ const levels = new Map();
7020
+ const levelOf = (v) => {
7021
+ let l = levels.get(v);
7022
+ if (l === undefined) {
7023
+ const hex = classes.classOf(v).color;
7024
+ const [r, g, b] = [1, 3, 5].map((o) => parseInt(hex.slice(o, o + 2), 16));
7025
+ l = Math.max(1, Math.round(0.299 * r + 0.587 * g + 0.114 * b));
7026
+ levels.set(v, l);
7027
+ }
7028
+ return l;
7029
+ };
7030
+ const urls = [];
7031
+ const gray = new Uint8Array(stack.width * stack.height);
7032
+ for (let z = 0; z < stack.slices; z++) {
7033
+ const labels = await stack.labels(z);
7034
+ for (let i = 0; i < labels.length; i++)
7035
+ gray[i] = labels[i] ? levelOf(labels[i]) : 0;
7036
+ const png = encode({ width: stack.width, height: stack.height, data: gray, channels: 1, depth: 8 });
7037
+ urls.push(URL.createObjectURL(new Blob([png], { type: 'image/png' })));
7038
+ }
7039
+ return urls;
7040
+ }
7041
+ /**
7042
+ * Open every slice's source to learn how far the regions reach (the image is as large, from the
7043
+ * origin, as region coordinates are image pixels). Each opener then hands its already-open source
7044
+ * over the first time (the region store owns it from then on) and opens anew after. `release`
7045
+ * disposes the sources not handed over yet; they are released here when measuring fails.
7046
+ */
7047
+ async function measure(openers) {
7048
+ // Sources opened here and not yet handed to the store: theirs to release until then.
7049
+ const held = new Map();
7050
+ const release = () => {
7051
+ for (const source of held.values())
7052
+ source.dispose();
7053
+ held.clear();
7054
+ };
7055
+ let x1 = 0;
7056
+ let y1 = 0;
7057
+ try {
7058
+ for (const [z, open] of openers) {
7059
+ const source = await open();
7060
+ held.set(z, source);
7061
+ const bounds = source.info.bounds;
7062
+ if (bounds) {
7063
+ x1 = Math.max(x1, bounds[2]);
7064
+ y1 = Math.max(y1, bounds[3]);
7065
+ }
7066
+ }
7067
+ if (!(x1 > 0 && y1 > 0))
7068
+ throw new Error('The regions have no extent to show.');
7069
+ }
7070
+ catch (err) {
7071
+ release();
7072
+ throw err;
7073
+ }
7074
+ const reused = new Map();
7075
+ for (const [z, open] of openers) {
7076
+ reused.set(z, () => {
7077
+ const first = held.get(z);
7078
+ if (!first)
7079
+ return open();
7080
+ held.delete(z);
7081
+ return Promise.resolve(first);
7082
+ });
7083
+ }
7084
+ return { openers: reused, size: [Math.ceil(x1), Math.ceil(y1)], release };
7085
+ }
7086
+ function checkSize(width, height) {
7087
+ if (Math.max(width, height) > ANNOTATION_IMAGE_MAX_DIM) {
7088
+ throw new Error(`The annotations span ${width} × ${height} pixels, more than ${ANNOTATION_IMAGE_MAX_DIM} on a side: ` +
7089
+ 'open the image they were drawn on, and import them onto it from the Region Editor.');
7090
+ }
7091
+ }
7092
+ /**
7093
+ * One GeoJSON's slices: a collection whose features name planes (`geometry.plane.z`) split into
7094
+ * one collection per plane, each keeping the set's class tree and style, its features moved to
7095
+ * plane 0 (a slice's source shows plane 0); any other GeoJSON is one slice as it is.
7096
+ */
7097
+ function splitByPlane(text) {
7098
+ const doc = JSON.parse(text);
7099
+ const features = doc.type === 'FeatureCollection' ? (doc.features ?? []) : [];
7100
+ const zOf = (f) => Math.max(0, Math.floor(f.geometry?.plane?.z ?? 0));
7101
+ const last = features.reduce((m, f) => Math.max(m, zOf(f)), 0);
7102
+ if (last === 0)
7103
+ return [text];
7104
+ const slices = Array.from({ length: last + 1 }, () => []);
7105
+ for (const f of features) {
7106
+ const z = zOf(f);
7107
+ const geometry = f.geometry ? { ...f.geometry } : f.geometry;
7108
+ if (geometry)
7109
+ delete geometry.plane;
7110
+ slices[z].push({ ...f, geometry });
7111
+ }
7112
+ return slices.map((list) => JSON.stringify({ ...doc, features: list }));
7113
+ }
7114
+ function stem(name) {
7115
+ return (name.split('/').pop() ?? name).replace(/\.(nii\.gz|nii|nrrd|geojson|json|zip)$/i, '');
7116
+ }
7117
+
6229
7118
  /**
6230
7119
  * The largest bbox (in pixels) {@link rasterizePolygon} allocates before it
6231
7120
  * falls back to clipping at the window. Also the cap the wand/brush use to
@@ -9510,7 +10399,7 @@ function frameToRgba(cached, frameIndex) {
9510
10399
  /** Lazy-import so onnxruntime-web is never pulled into unit tests or the
9511
10400
  * initial bundle — only when segmentation actually runs. */
9512
10401
  const onnxSessionFactory = async () => {
9513
- const { OnnxSamSession } = await import('./jax-data-science-sci-image-visualizer-onnx-sam-session-DEDSm_NL.mjs');
10402
+ const { OnnxSamSession } = await import('./jax-data-science-sci-image-visualizer-onnx-sam-session-CvadfzjS.mjs');
9514
10403
  return new OnnxSamSession();
9515
10404
  };
9516
10405
  /** Thrown inside a load that a model switch superseded. */
@@ -10449,6 +11338,21 @@ function applyImageRois(info, target, z, minSourceChars = REGION_SOURCE_MIN_CHAR
10449
11338
  const load = (loads.get(target) ?? 0) + 1;
10450
11339
  loads.set(target, load);
10451
11340
  const asSource = (json) => !!attach && !!json && json.length >= minSourceChars;
11341
+ if (info.regionSlices && attach) {
11342
+ // An image built from annotations: its regions are its own sources, one per slice.
11343
+ if (info.isStack) {
11344
+ target.enterStackMode(new Map(), z, 'per-slice-file');
11345
+ for (const [s, open] of info.regionSlices)
11346
+ attach(open, { slice: s });
11347
+ return;
11348
+ }
11349
+ target.setRegions([]);
11350
+ target.resetUndoHistory();
11351
+ const first = info.regionSlices.get(0) ?? info.regionSlices.values().next().value;
11352
+ if (first)
11353
+ attach(first, { slice: 0 });
11354
+ return;
11355
+ }
10452
11356
  if (info.isStack && info.tiled === false) {
10453
11357
  const perSlice = info.roiJsonStrs;
10454
11358
  const sliceCount = info.urls?.length ?? perSlice?.length ?? 0;
@@ -15908,53 +16812,47 @@ class RegionEditorComponent {
15908
16812
  });
15909
16813
  }
15910
16814
  /**
15911
- * Import `files` (see planRegionImport): one GeoJSON file as before; a label volume as one
15912
- * region source per slice, traced from it the first time the slice is shown, its classes named
15913
- * (and treed) by the ontology chosen with it; a folder as one source per slice file. Either
15914
- * replaces the regions and sources of every slice.
16815
+ * Import `files`: one GeoJSON file as before (its regions, editable); anything else (see
16816
+ * readAnnotationSlices) as one region source per slice, replacing the regions and sources of
16817
+ * every slice: a label volume or an OME-Zarr label image traced the first time a slice is shown,
16818
+ * its classes named (and treed) by its ontology; a folder as one source per slice file; a
16819
+ * z-indexed GeoJSON one per plane; a zip of any of these.
15915
16820
  */
15916
16821
  async importRegionFiles(files) {
15917
- const plan = planRegionImport(files);
15918
- if (!plan) {
15919
- this.importFailed(new Error('Choose a GeoJSON file, a folder of GeoJSON files, or a .nii, .nii.gz or .nrrd label volume.'));
15920
- return;
15921
- }
15922
- if (plan.kind === 'single') {
15923
- await this.applyImportedRois(await plan.file.text());
15924
- return;
15925
- }
15926
- const attach = this.regionApi.importSliceRegionSources?.bind(this.regionApi);
15927
- if (!attach) {
15928
- this.importFailed(new Error('This viewer cannot hold region sources.'));
15929
- return;
15930
- }
15931
- // Taken before the files are read: decoding a volume takes a while, and the import must land
15932
- // on the image it was started for, not on one shown meanwhile.
15933
- const ticket = this.regionApi.regionImportTicket?.();
16822
+ // Every step, reading the files included, reports its failure here rather than rejecting:
16823
+ // the inputs call this without waiting on it.
15934
16824
  try {
15935
- const openers = new Map();
15936
- let summary;
15937
- if (plan.kind === 'volume') {
15938
- const volume = await readLabelVolume(plan.volume.name, await plan.volume.bytes());
15939
- const classes = labelClasses(plan.ontology ? JSON.parse(await plan.ontology.text()) : undefined);
15940
- for (let z = 0; z < volume.slices; z++) {
15941
- openers.set(z, () => openGeoJsonRegionSource(labelSliceGeoJson(volume, z, classes)));
16825
+ const plan = files.some((f) => isZipName(f.name)) ? null : planRegionImport(files);
16826
+ if (plan?.kind === 'single') {
16827
+ const text = await plan.file.text();
16828
+ if (onePlane(text)) {
16829
+ await this.applyImportedRois(text);
16830
+ return;
15942
16831
  }
15943
- summary = `${volume.slices} slices of ${volume.width} × ${volume.height}`;
15944
- this.warnIfSizeDiffers(volume.width, volume.height);
15945
16832
  }
15946
- else {
15947
- const regionSet = plan.regionSet ? JSON.parse(await plan.regionSet.text()) : null;
15948
- plan.slices.forEach((file, z) => openers.set(z, async () => openGeoJsonRegionSource(await folderSliceGeoJson(file, regionSet))));
15949
- summary = `${plan.slices.length} slice files`;
16833
+ const attach = this.regionApi.importSliceRegionSources?.bind(this.regionApi);
16834
+ if (!attach) {
16835
+ this.importFailed(new Error('This viewer cannot hold region sources.'));
16836
+ return;
16837
+ }
16838
+ // Taken before the files are read: decoding a volume takes a while, and the import must land
16839
+ // on the image it was started for, not on one shown meanwhile.
16840
+ const ticket = this.regionApi.regionImportTicket?.();
16841
+ const set = await readAnnotationSlices(files);
16842
+ if (!set) {
16843
+ this.importFailed(new Error('Choose a GeoJSON file, a folder of GeoJSON files, a .nii, .nii.gz or .nrrd label volume, ' +
16844
+ 'an OME-Zarr store, or a .zip of any of these.'));
16845
+ return;
15950
16846
  }
15951
- const attached = attach(openers, ticket);
16847
+ if (set.size)
16848
+ this.warnIfSizeDiffers(set.size[0], set.size[1]);
16849
+ const attached = attach(set.openers, ticket);
15952
16850
  if (attached === null) {
15953
16851
  this.importStale();
15954
16852
  return;
15955
16853
  }
15956
16854
  this.afterSourceImport();
15957
- this.toast('success', 'Regions imported', `${summary}; ${attached} slice${attached === 1 ? '' : 's'} annotated.`);
16855
+ this.toast('success', 'Regions imported', `${set.summary}; ${attached} slice${attached === 1 ? '' : 's'} annotated.`);
15958
16856
  }
15959
16857
  catch (err) {
15960
16858
  this.importFailed(err);
@@ -16289,6 +17187,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
16289
17187
  }], active: [{
16290
17188
  type: Input
16291
17189
  }] } });
17190
+ /**
17191
+ * Whether one GeoJSON file holds a single plane (its regions imported as they always were). One
17192
+ * that does not parse is taken as one too, so the regions import reports it as it always has.
17193
+ */
17194
+ function onePlane(text) {
17195
+ try {
17196
+ return splitByPlane(text).length === 1;
17197
+ }
17198
+ catch {
17199
+ return true;
17200
+ }
17201
+ }
16292
17202
 
16293
17203
  /**
16294
17204
  * Shared intensity helpers (refactoring plan, Step 5). The two backends use
@@ -40407,6 +41317,16 @@ class VolumeRegionMeshes {
40407
41317
  this.remove(key, e.layer);
40408
41318
  this.wanted = [];
40409
41319
  }
41320
+ /**
41321
+ * The volume the surfaces sat in is gone (or no longer theirs): take every surface down, keeping
41322
+ * the meshes wanted, so they come back when a frame is set again.
41323
+ */
41324
+ clearFrame() {
41325
+ this.frame = null;
41326
+ for (const [key, e] of [...this.layers])
41327
+ this.remove(key, e.layer);
41328
+ this.viewer.requestRender();
41329
+ }
40410
41330
  sync() {
40411
41331
  const keep = new Map(this.wanted.map((m) => [m.key, m]));
40412
41332
  for (const [key, e] of [...this.layers]) {
@@ -44725,8 +45645,13 @@ function slabAt(dataset, slice) {
44725
45645
  * shape while keeping the selection. The first 3D layer added frames the camera (`fit3d: 'once'`).
44726
45646
  */
44727
45647
  class Spatial3dScene extends SpatialSceneBase {
44728
- constructor(ctx, session) {
45648
+ /**
45649
+ * @param info the image plotted with the cloud: the reference volume's stack (the 2D view's
45650
+ * image, see showVolumeAsImage) when the dataset has a volume, which the regions are on
45651
+ */
45652
+ constructor(ctx, session, info) {
44729
45653
  super(ctx, session, true);
45654
+ this.info = info;
44730
45655
  /** The 3D point cloud. One layer: selection is a per-point alpha on it, not a second. */
44731
45656
  this.points = null;
44732
45657
  this.layerKey = null;
@@ -44768,9 +45693,64 @@ class Spatial3dScene extends SpatialSceneBase {
44768
45693
  * one; a rebuild that leaves the key alone lets it finish. */
44769
45694
  this.densityLoads = new Supersede();
44770
45695
  this.geneMapLoads = new Supersede();
45696
+ /** Whether the reference volume lets every region surface through (see setRegionsThroughVolume). */
45697
+ this.regionsThrough = false;
44771
45698
  /** The store's invert setting, which the density volumes follow (no other subscription of
44772
45699
  * this scene carries it). */
44773
45700
  this.invertSub = null;
45701
+ this.regionMeshes = new VolumeRegionMeshes(ctx.viewer);
45702
+ }
45703
+ /** Show these region reconstructions with the cloud (replacing the ones shown). */
45704
+ setRegionMeshes(meshes) {
45705
+ this.regionMeshes.set(meshes);
45706
+ }
45707
+ setRegionHighlight(keys) {
45708
+ this.regionMeshes.setHighlight(keys);
45709
+ }
45710
+ setRegionHidden(keys) {
45711
+ this.regionMeshes.setHidden(keys);
45712
+ }
45713
+ /** As the Volume view: the reference volume hides the surfaces behind what it shows, or none. */
45714
+ setRegionsThroughVolume(through) {
45715
+ this.regionsThrough = through;
45716
+ if (this.volume)
45717
+ this.volume.blending = through ? 'translucent_no_depth' : 'translucent';
45718
+ this.ctx.viewer.requestRender();
45719
+ }
45720
+ /**
45721
+ * Place the region surfaces in the reference volume's box, as the Volume view places them in its
45722
+ * own: image pixel (x, y) is voxel (x, y), slice k is voxel plane k, and the box is the volume's
45723
+ * voxels times its voxel size, centred on the origin as napari centres the volume (and the cloud
45724
+ * is offset to sit in it). Only when the plotted image IS that volume's stack: regions drawn on
45725
+ * another image (a tissue image) have no place in it, and are not shown - nor are surfaces
45726
+ * placed in a volume this one replaced.
45727
+ */
45728
+ placeRegionMeshes(dataset) {
45729
+ const meta = dataset.volume;
45730
+ if (!meta || !this.isVolumeStack(meta)) {
45731
+ this.regionMeshes.clearFrame();
45732
+ return;
45733
+ }
45734
+ const [vx, vy, vz] = meta.voxelSize;
45735
+ this.regionMeshes.setFrame({
45736
+ size: [meta.width, meta.height, meta.depth],
45737
+ world: [meta.width * vx, meta.height * vy, meta.depth * vz],
45738
+ });
45739
+ }
45740
+ /**
45741
+ * Image px per voxel of the volume the surfaces sit in: 1, the reference volume being the stack's
45742
+ * own voxels, as the Volume view's is at full resolution, so both reconstruct a class alike (and
45743
+ * share its cached surface). Null while there is no volume stack to place them in.
45744
+ */
45745
+ voxelStep() {
45746
+ const meta = this.session.latest?.[0]?.volume;
45747
+ return this.volume && meta && this.isVolumeStack(meta) ? 1 : null;
45748
+ }
45749
+ /** Whether the plotted image is the reference volume's stack: its size and slice count. */
45750
+ isVolumeStack(meta) {
45751
+ const info = this.info;
45752
+ const [w, h] = info?.trueImageSize ?? [0, 0];
45753
+ return !!info?.isStack && w === meta.width && h === meta.height && (info.urls?.length ?? 0) === meta.depth;
44774
45754
  }
44775
45755
  async mount() {
44776
45756
  // Before the spatial subscription, so the first build already reads the current value.
@@ -44821,6 +45801,7 @@ class Spatial3dScene extends SpatialSceneBase {
44821
45801
  return out;
44822
45802
  }
44823
45803
  dispose() {
45804
+ this.regionMeshes.clear();
44824
45805
  this.disposeSpatial();
44825
45806
  this.invertSub?.unsubscribe();
44826
45807
  this.invertSub = null;
@@ -45356,6 +46337,8 @@ class Spatial3dScene extends SpatialSceneBase {
45356
46337
  this.volume = null;
45357
46338
  this.volumeKey = null;
45358
46339
  }
46340
+ // The surfaces sat in the volume just removed (or there was none): down until one is placed.
46341
+ this.regionMeshes.clearFrame();
45359
46342
  this.origin = [0, 0, 0];
45360
46343
  if (!meta || !port?.getVolume)
45361
46344
  return;
@@ -45382,14 +46365,18 @@ class Spatial3dScene extends SpatialSceneBase {
45382
46365
  opacity: view.volumeOpacity,
45383
46366
  voxelSize: [vx, vy, vz],
45384
46367
  });
46368
+ if (this.regionsThrough)
46369
+ this.volume.blending = 'translucent_no_depth';
45385
46370
  // Half the box, negated: the observations' origin is the box's near corner,
45386
46371
  // and the box is centred on the world origin.
45387
46372
  this.origin = [-(meta.width * vx) / 2, -(meta.height * vy) / 2, -(meta.depth * vz) / 2];
46373
+ this.placeRegionMeshes(dataset);
45388
46374
  // Force a geometry rebuild: the offset changed, so cached positions are stale.
45389
46375
  this.layerKey = null;
45390
46376
  }
45391
46377
  /** Drop every layer of the cloud (the dataset went away, or has no z). */
45392
46378
  removeLayers(viewer) {
46379
+ this.regionMeshes.clearFrame();
45393
46380
  for (const layer of [this.points, this.volume, this.geneMapLayer, ...this.densityLayers]) {
45394
46381
  if (layer)
45395
46382
  viewer.layers.remove(layer);
@@ -45733,7 +46720,7 @@ class NapariVisualizerService extends BaseStoreVisualizer {
45733
46720
  /** The scene a plot type mounts. */
45734
46721
  createScene(plotType, ctx, info, noImage) {
45735
46722
  if (isSpatialOmics3d(plotType))
45736
- return new Spatial3dScene(ctx, this.spatial);
46723
+ return new Spatial3dScene(ctx, this.spatial, info);
45737
46724
  if (isSpatialOmics(plotType))
45738
46725
  return new Spatial2dScene(ctx, this.spatial, noImage);
45739
46726
  if (isNapariScatter(plotType))
@@ -46654,8 +47641,14 @@ class RoutingVisualizerService {
46654
47641
  this.napariFellBack = false;
46655
47642
  /** The div the backends render into (every backend shares it); '' before the first plot. */
46656
47643
  this.plotDiv = '';
46657
- /** Whether a napari volume view is the current plot type. */
47644
+ /**
47645
+ * Whether regions can be shown in 3D on screen: a napari volume (or isosurface) view, or the 3D
47646
+ * spatial cloud over a stack image (its reference volume's, see showVolumeAsImage). Both draw the
47647
+ * region reconstructions the same way (VolumeRegionMeshes), in the volume's box.
47648
+ */
46658
47649
  this.volumeView$ = new BehaviorSubject(false);
47650
+ /** Whether a napari volume (or isosurface) view is the current plot type: the slice pane's. */
47651
+ this.imageVolumeView = false;
46659
47652
  /** Whether the volume view's slice pane is wanted (see {@link setSliceEditor}). */
46660
47653
  this.sliceEditor$ = new BehaviorSubject(false);
46661
47654
  /** Whether the volume view draws the region reconstructions through the volume. */
@@ -46881,13 +47874,13 @@ class RoutingVisualizerService {
46881
47874
  }
46882
47875
  plot(plotDiv, imageLoaded, imageInfo, screenHeight, plotType, inPlace) {
46883
47876
  this.currentPlotType = plotType;
46884
- this.volumeView$.next(isNapariVolume(plotType) || isNapariIsosurface(plotType));
46885
47877
  this.plottedFileName = imageInfo?.fileName;
46886
47878
  this.plottedInfo = imageInfo ?? null;
46887
47879
  this.plottedHeight = screenHeight;
47880
+ this.updateVolumeView(plotType);
46888
47881
  // The slice pane belongs to the volume view: another plot type takes it down (OSD may be
46889
47882
  // about to draw the main view, which must not find the pane's viewer still mounted).
46890
- if (!this.volumeView$.value)
47883
+ if (!this.imageVolumeView)
46891
47884
  this.unmountSlice();
46892
47885
  // Region surfaces belong to the image they were built from: another image drops them.
46893
47886
  if (this.classes3d$.value.length && this.plottedFileName !== this.classes3dImage)
@@ -46910,7 +47903,7 @@ class RoutingVisualizerService {
46910
47903
  }
46911
47904
  this.lastRendered = next;
46912
47905
  const plotted = next.plot(plotDiv, imageLoaded, imageInfo, screenHeight, plotType, inPlace);
46913
- if (!this.sliceDiv || !this.volumeView$.value)
47906
+ if (!this.sliceDiv || !this.imageVolumeView)
46914
47907
  return plotted;
46915
47908
  // The split volume view: the slice pane follows the image (a new one, or its sharper pass).
46916
47909
  return plotted.then((ok) => {
@@ -46942,7 +47935,7 @@ class RoutingVisualizerService {
46942
47935
  async mountSlice(inPlace) {
46943
47936
  const div = this.sliceDiv;
46944
47937
  const info = this.plottedInfo;
46945
- if (!div || !info?.urls?.length || !this.volumeView$.value)
47938
+ if (!div || !info?.urls?.length || !this.imageVolumeView)
46946
47939
  return false;
46947
47940
  const request = ++this.sliceRequest;
46948
47941
  try {
@@ -47064,7 +48057,7 @@ class RoutingVisualizerService {
47064
48057
  }
47065
48058
  setPlotType(plotType) {
47066
48059
  this.currentPlotType = plotType;
47067
- this.volumeView$.next(isNapariVolume(plotType) || isNapariIsosurface(plotType));
48060
+ this.updateVolumeView(plotType);
47068
48061
  this.plotly.setPlotType(plotType);
47069
48062
  }
47070
48063
  /** @deprecated Use `getSurface3dControls()`. */
@@ -47367,6 +48360,13 @@ class RoutingVisualizerService {
47367
48360
  this.regionStore.setRegionStyle(patch);
47368
48361
  }
47369
48362
  // ── Regions in 3D: their reconstructions shown in the volume view ─────
48363
+ /** Recompute which 3D view is on screen for `plotType` (see {@link volumeView$}). */
48364
+ updateVolumeView(plotType) {
48365
+ this.imageVolumeView = isNapariVolume(plotType) || isNapariIsosurface(plotType);
48366
+ const regions3d = this.imageVolumeView || (isSpatialOmics3d(plotType) && !!this.plottedInfo?.isStack);
48367
+ // Every plot, as before: the Region Editor drops its per-class volumes on each (a new image).
48368
+ this.volumeView$.next(regions3d);
48369
+ }
47370
48370
  /** Whether a napari volume (or isosurface) view is on screen: regions can be shown in 3D. */
47371
48371
  getVolumeView$() {
47372
48372
  return this.volumeView$.asObservable();
@@ -48026,5 +49026,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
48026
49026
  * Generated bundle index. Do not edit.
48027
49027
  */
48028
49028
 
48029
- export { TOOLBAR_TOOLS as $, ALL_GENES as A, REGION_IO_PORT as B, CANVAS_TOOL_IDS as C, DEFAULT_CATEGORICAL_PALETTE as D, REGION_SOURCE_MIN_CHARS as E, Rectangle as F, Region as G, HexColorPickerComponent as H, IMAGE_STATE_PORT as I, RegionEditorComponent as J, RoutingVisualizerService as K, LUT_COLORS as L, MISSING_COLOR as M, NAPARI_DECIMATE_OPTIONS as N, SEMANTIC_SEGMENTER as O, PLOT_MODE_CONTEXT as P, SPATIAL_DATA_PORT as Q, REGION_EDITOR_API as R, SAM_MODELS as S, SPATIAL_WIRE_VERSION as T, ShapeSelection as U, SpatialChartsComponent as V, SpatialControlsComponent as W, SpatialDataHttpService as X, SpatialSelectionStore as Y, SupersededError as Z, TILE_ACCESS_PORT as _, runDecoder as a, provideVisualization as a$, VISUALIZER as a0, VIZ_ALERT_TOAST_KEY as a1, VIZ_CONFIG as a2, VIZ_TOAST_KEY as a3, ViewerFeature as a4, VisualizationModule as a5, VisualizerComponent as a6, assertManifestVersion as a7, benefitsFromGrouping as a8, buildOmicsTraces as a9, isContinuousColumn as aA, isDialogToolContribution as aB, isEmptyPresetSet as aC, isLabelVolumeName as aD, isLittleEndian as aE, isNapari3d as aF, isNapariIsosurface as aG, isNapariScatter as aH, isNapariScatter3d as aI, isNapariSurface as aJ, isNapariVolume as aK, isSamModelReady as aL, isSpatialOmics as aM, isSpatialOmics3d as aN, isThreeDimensional as aO, labelClasses as aP, labelSliceGeoJson as aQ, lutFor as aR, markerDiameters as aS, maskToIndices as aT, mutedFromSelection as aU, omicsLayout as aV, openGeoJsonRegionSource as aW, openHttpRegionSource as aX, parsePresetSet as aY, planRegionImport as aZ, pointInRing as a_, capabilitiesOf as aa, clearSamModelCache as ab, contrastWindow as ac, contributedPlotTypeOption as ad, countMask as ae, cropImage as af, datasetFromManifest as ag, decodeColumn as ah, decodeCoords as ai, decodeFeatureVector as aj, decodePolygons as ak, decodeRadius as al, defaultPresetSet as am, dialogToolContributions as an, emptySelection as ao, encodeCategorical as ap, encodeContinuous as aq, findColumnMeta as ar, getDefaultSamModelId as as, getPlotTypeDescriptor as at, getSamModel as au, hydrateBounds as av, importFile as aw, isBuiltinPlotType as ax, isCanvasToolId as ay, isCategoricalColumn as az, ALL_TOOLBAR_TOOLS as b, readLabelVolume as b0, rendererOwnsWheel as b1, resolveCategoryColors as b2, selectByCategory as b3, selectInRegions as b4, setDefaultSamModel as b5, setOrtWasmBase as b6, setSamModelUrls as b7, sortToolContributions as b8, visibleToolContributions as b9, CELL_SEGMENTER as c, CHANNEL_HISTOGRAM_API as d, CellposeSegmenterService as e, fetchModel as f, getOrtWasmBase as g, ChannelHistogramComponent as h, DEFAULT_FALLBACK_PALETTE as i, DEFAULT_MUTED_OPACITY as j, DEFAULT_SAM_MODEL_ID as k, DEFAULT_SPATIAL_VIEW as l, INSTANCE_SEGMENTER as m, ImageConverterService as n, MultiPolygon as o, NAPARI_DEFAULT_DECIMATE as p, NO_CATEGORY as q, runEncoder as r, NO_OBSERVATION as s, PLOT_MODE_SESSION as t, PLOT_TYPE_CONTRIBUTIONS as u, PLOT_TYPE_DESCRIPTORS as v, PREFERENCES_PORT as w, PlotType as x, Polygon as y, ProcessingImage as z };
48030
- //# sourceMappingURL=jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-BELcx0jv.mjs.map
49029
+ export { TILE_ACCESS_PORT as $, ALL_GENES as A, ProcessingImage as B, CANVAS_TOOL_IDS as C, DEFAULT_CATEGORICAL_PALETTE as D, REGION_IO_PORT as E, REGION_SOURCE_MIN_CHARS as F, Rectangle as G, HexColorPickerComponent as H, IMAGE_STATE_PORT as I, Region as J, RegionEditorComponent as K, LUT_COLORS as L, MISSING_COLOR as M, NAPARI_DECIMATE_OPTIONS as N, RoutingVisualizerService as O, PLOT_MODE_CONTEXT as P, SEMANTIC_SEGMENTER as Q, REGION_EDITOR_API as R, SAM_MODELS as S, SPATIAL_DATA_PORT as T, SPATIAL_WIRE_VERSION as U, ShapeSelection as V, SpatialChartsComponent as W, SpatialControlsComponent as X, SpatialDataHttpService as Y, SpatialSelectionStore as Z, SupersededError as _, runDecoder as a, openGeoJsonRegionSource as a$, TOOLBAR_TOOLS as a0, VISUALIZER as a1, VIZ_ALERT_TOAST_KEY as a2, VIZ_CONFIG as a3, VIZ_TOAST_KEY as a4, ViewerFeature as a5, VisualizationModule as a6, VisualizerComponent as a7, assertManifestVersion as a8, benefitsFromGrouping as a9, isBuiltinPlotType as aA, isCanvasToolId as aB, isCategoricalColumn as aC, isContinuousColumn as aD, isDialogToolContribution as aE, isEmptyPresetSet as aF, isLabelVolumeName as aG, isLittleEndian as aH, isNapari3d as aI, isNapariIsosurface as aJ, isNapariScatter as aK, isNapariScatter3d as aL, isNapariSurface as aM, isNapariVolume as aN, isSamModelReady as aO, isSpatialOmics as aP, isSpatialOmics3d as aQ, isThreeDimensional as aR, isZipName as aS, labelClasses as aT, labelSliceGeoJson as aU, lutFor as aV, markerDiameters as aW, maskToIndices as aX, mutedFromSelection as aY, omicsLayout as aZ, openAnnotations as a_, buildOmicsTraces as aa, capabilitiesOf as ab, clearSamModelCache as ac, contrastWindow as ad, contributedPlotTypeOption as ae, countMask as af, cropImage as ag, datasetFromManifest as ah, decodeColumn as ai, decodeCoords as aj, decodeFeatureVector as ak, decodePolygons as al, decodeRadius as am, defaultPresetSet as an, dialogToolContributions as ao, emptySelection as ap, encodeCategorical as aq, encodeContinuous as ar, expandZips as as, findColumnMeta as at, getDefaultSamModelId as au, getPlotTypeDescriptor as av, getSamModel as aw, hasZarrMetadata as ax, hydrateBounds as ay, importFile as az, ALL_TOOLBAR_TOOLS as b, openHttpRegionSource as b0, parsePresetSet as b1, planRegionImport as b2, pointInRing as b3, provideVisualization as b4, readAnnotationSlices as b5, readLabelVolume as b6, readOmeZarrLabels as b7, readZip as b8, rendererOwnsWheel as b9, resolveCategoryColors as ba, selectByCategory as bb, selectInRegions as bc, setDefaultSamModel as bd, setOrtWasmBase as be, setSamModelUrls as bf, sortToolContributions as bg, visibleToolContributions as bh, ANNOTATION_IMAGE_MAX_DIM as c, CELL_SEGMENTER as d, CHANNEL_HISTOGRAM_API as e, fetchModel as f, getOrtWasmBase as g, CellposeSegmenterService as h, ChannelHistogramComponent as i, DEFAULT_FALLBACK_PALETTE as j, DEFAULT_MUTED_OPACITY as k, DEFAULT_SAM_MODEL_ID as l, DEFAULT_SPATIAL_VIEW as m, INSTANCE_SEGMENTER as n, ImageConverterService as o, MultiPolygon as p, NAPARI_DEFAULT_DECIMATE as q, runEncoder as r, NO_CATEGORY as s, NO_OBSERVATION as t, PLOT_MODE_SESSION as u, PLOT_TYPE_CONTRIBUTIONS as v, PLOT_TYPE_DESCRIPTORS as w, PREFERENCES_PORT as x, PlotType as y, Polygon as z };
49030
+ //# sourceMappingURL=jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-a68Hw5rw.mjs.map