@jax-data-science/sci-image-visualizer 0.8.2 → 0.8.3

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.
@@ -29393,6 +29393,10 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29393
29393
  this.loadingBadge = null;
29394
29394
  this.tileLoading = [];
29395
29395
  this.observationsLoading = 0;
29396
+ /** Image tiles in flight, and the scene they belong to: a fetch that outlives {@link reset}
29397
+ * settles against the old generation and leaves the new scene's count alone. */
29398
+ this.imageTilesLoading = 0;
29399
+ this.imageTilesGeneration = 0;
29396
29400
  /** Monotonic load generation. Bumped by {@link reset} and {@link cancelLoading}; the frame-loading
29397
29401
  * loops (volume assembly, surface preload) capture it and bail when it changes, so a Cancel (or a
29398
29402
  * new plot) actually stops fetching frames instead of running to completion in the background. */
@@ -29711,6 +29715,9 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29711
29715
  return false;
29712
29716
  }
29713
29717
  this.reset();
29718
+ // This plot's scene. A newer plot resets into the next one while this one still awaits, and
29719
+ // a superseded plot must not go on to draw (or count image tiles) into the newer scene.
29720
+ const scene = this.imageTilesGeneration;
29714
29721
  this.host = host;
29715
29722
  this.plotDivId = plotDiv;
29716
29723
  this.currentPlotType = plotType;
@@ -29752,6 +29759,8 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29752
29759
  });
29753
29760
  this.viewer = viewer;
29754
29761
  await viewer.ready;
29762
+ if (scene !== this.imageTilesGeneration)
29763
+ return false;
29755
29764
  if (isSpatialOmics3d(plotType)) {
29756
29765
  await this.mountSpatialOmics3d(viewer, host);
29757
29766
  }
@@ -29848,11 +29857,15 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29848
29857
  const v = this.viewer;
29849
29858
  if (!v)
29850
29859
  return;
29860
+ const scene = this.imageTilesGeneration;
29851
29861
  const desc = await this.ensureDescriptor();
29862
+ // Reset into a newer scene while the descriptor was in flight: this render is superseded.
29863
+ if (scene !== this.imageTilesGeneration)
29864
+ return;
29852
29865
  if (desc && desc.levels?.length) {
29853
29866
  if (token != null && token !== this.sliceReq)
29854
29867
  return;
29855
- await this.renderImageTiled(z, desc);
29868
+ await this.renderImageTiled(z, desc, scene);
29856
29869
  return;
29857
29870
  }
29858
29871
  return this.renderImageStitched(z, token);
@@ -29945,7 +29958,7 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29945
29958
  * coordinates so regions align. Same three modes as the stitch path. Per-channel layers use the
29946
29959
  * REAL pyramid levels (per-channel tiles only exist there); the composite uses all levels.
29947
29960
  */
29948
- async renderImageTiled(z, desc) {
29961
+ async renderImageTiled(z, desc, scene) {
29949
29962
  const v = this.viewer;
29950
29963
  if (!v)
29951
29964
  return;
@@ -29962,7 +29975,7 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29962
29975
  const st = states.find((s) => s.index === c);
29963
29976
  const color = st?.color ?? desc.channelInfo?.[c]?.color ?? tintFor(c);
29964
29977
  views.push({
29965
- source: this.buildTiledSource(desc, c, 1),
29978
+ source: this.buildTiledSource(desc, c, 1, scene),
29966
29979
  tint: color,
29967
29980
  name: st?.name ?? `ch${c}`,
29968
29981
  contrastLimits: [st?.min ?? 0, st?.max ?? 255],
@@ -29978,7 +29991,7 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29978
29991
  const st = states[0];
29979
29992
  this.channelView.render('grayscale', [
29980
29993
  {
29981
- source: this.buildTiledSource(desc, undefined, 1),
29994
+ source: this.buildTiledSource(desc, undefined, 1, scene),
29982
29995
  colormap: this.grayscaleColormap(),
29983
29996
  contrastLimits: [st?.min ?? 0, st?.max ?? 255],
29984
29997
  gamma: st?.gamma ?? 1,
@@ -29988,7 +30001,7 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29988
30001
  }
29989
30002
  else {
29990
30003
  this.imageMode = 'rgb';
29991
- this.channelView.render('rgb', [{ source: this.buildTiledSource(desc, undefined, 4) }], {
30004
+ this.channelView.render('rgb', [{ source: this.buildTiledSource(desc, undefined, 4, scene) }], {
29992
30005
  interpolation,
29993
30006
  });
29994
30007
  }
@@ -29999,8 +30012,9 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29999
30012
  await this.refreshHistogramSamples(z, desc);
30000
30013
  }
30001
30014
  /** Build a pyramidal TiledSource backed by the server `/tile` endpoint. `channel` selects a band
30002
- * (grayscale luminance, real levels only); omit it for the composite (RGBA, all levels). */
30003
- buildTiledSource(desc, channel, channels) {
30015
+ * (grayscale luminance, real levels only); omit it for the composite (RGBA, all levels).
30016
+ * `scene` is the generation of the render that asked for it, taken before that render's awaits. */
30017
+ buildTiledSource(desc, channel, channels, scene) {
30004
30018
  const infoB64 = this.tiles.getSelectedInfoB64() ?? '';
30005
30019
  // Per-channel tiles exist only at REAL Bio-Formats levels; the composite exists at all levels.
30006
30020
  const usable = channel == null
@@ -30010,6 +30024,9 @@ class NapariVisualizerService extends BaseStoreVisualizer {
30010
30024
  const tileSize = desc.tileSize || TILE_SIZE;
30011
30025
  const ch = channel == null ? '' : `&channel=${channel}`;
30012
30026
  const api = this.api;
30027
+ // The scene this source draws: its tiles count on the badge only while that scene is current,
30028
+ // so a request the disposed source still issues after a reset never shows on the new one.
30029
+ const generation = scene;
30013
30030
  return {
30014
30031
  kind: 'tiled',
30015
30032
  width: desc.width,
@@ -30023,17 +30040,31 @@ class NapariVisualizerService extends BaseStoreVisualizer {
30023
30040
  fetchTile: async (key) => {
30024
30041
  const res = usable[key.level]?.res ?? key.level;
30025
30042
  const url = `${api}tile?info=${infoB64}&res=${res}&col=${key.col}&row=${key.row}&z=${key.z}&tileSize=${tileSize}${ch}`;
30026
- const headers = await this.tiles
30027
- .getAuthHeaders()
30028
- .catch(() => ({}));
30029
- const resp = await fetch(url, { headers });
30030
- if (!resp.ok) {
30031
- throw new Error(`[napari-js] tile ${key.level}/${key.col}/${key.row} → ${resp.status}`);
30043
+ // "Image reloading…" on the loading badge while any tile of the image is in flight.
30044
+ const counted = generation === this.imageTilesGeneration;
30045
+ if (counted) {
30046
+ this.imageTilesLoading++;
30047
+ this.showLoading();
30048
+ }
30049
+ try {
30050
+ const headers = await this.tiles
30051
+ .getAuthHeaders()
30052
+ .catch(() => ({}));
30053
+ const resp = await fetch(url, { headers });
30054
+ if (!resp.ok) {
30055
+ throw new Error(`[napari-js] tile ${key.level}/${key.col}/${key.row} → ${resp.status}`);
30056
+ }
30057
+ const bmp = await createImageBitmap(await resp.blob());
30058
+ if (channels === 4)
30059
+ return { width: bmp.width, height: bmp.height, data: bmp };
30060
+ return this.bitmapToLuminance(bmp);
30061
+ }
30062
+ finally {
30063
+ if (counted && generation === this.imageTilesGeneration) {
30064
+ this.imageTilesLoading--;
30065
+ this.showLoading();
30066
+ }
30032
30067
  }
30033
- const bmp = await createImageBitmap(await resp.blob());
30034
- if (channels === 4)
30035
- return { width: bmp.width, height: bmp.height, data: bmp };
30036
- return this.bitmapToLuminance(bmp);
30037
30068
  },
30038
30069
  };
30039
30070
  }
@@ -30195,7 +30226,11 @@ class NapariVisualizerService extends BaseStoreVisualizer {
30195
30226
  if (!this.host)
30196
30227
  return;
30197
30228
  this.loadingBadge ??= new NapariLoadingBadge(this.host);
30198
- this.loadingBadge.set([...(this.observationsLoading > 0 ? ['Observations'] : []), ...this.tileLoading]);
30229
+ this.loadingBadge.set([
30230
+ ...(this.imageTilesLoading > 0 ? ['Image'] : []),
30231
+ ...(this.observationsLoading > 0 ? ['Observations'] : []),
30232
+ ...this.tileLoading,
30233
+ ]);
30199
30234
  }
30200
30235
  installNavigator(z) {
30201
30236
  this.navigator?.destroy();
@@ -32744,6 +32779,8 @@ class NapariVisualizerService extends BaseStoreVisualizer {
32744
32779
  this.loadingBadge = null;
32745
32780
  this.tileLoading = [];
32746
32781
  this.observationsLoading = 0;
32782
+ this.imageTilesLoading = 0;
32783
+ this.imageTilesGeneration++;
32747
32784
  this.regionOverlay?.destroy();
32748
32785
  this.regionOverlay = null;
32749
32786
  this.axesLabels?.destroy();