@niivue/niivue 0.67.1-dev.1 → 0.67.1-dev.2

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.
@@ -19,7 +19,7 @@ import { mat4 as mat418, vec2, vec3 as vec324, vec4 as vec417 } from "gl-matrix"
19
19
  // package.json
20
20
  var package_default = {
21
21
  name: "@niivue/niivue",
22
- version: "0.67.1-dev.0",
22
+ version: "0.67.1-dev.2",
23
23
  description: "minimal webgl2 nifti image viewer",
24
24
  types: "./build/niivue/index.d.ts",
25
25
  main: "./build/niivue/index.js",
@@ -22591,6 +22591,7 @@ var NVZarrHelper = class _NVZarrHelper {
22591
22591
  __publicField(this, "voxelTranslations");
22592
22592
  __publicField(this, "hasTranslations");
22593
22593
  __publicField(this, "convertUnitsToMm");
22594
+ __publicField(this, "worldOffsetMM", [0, 0, 0]);
22594
22595
  __publicField(this, "centerX");
22595
22596
  __publicField(this, "centerY");
22596
22597
  __publicField(this, "centerZ");
@@ -22601,6 +22602,7 @@ var NVZarrHelper = class _NVZarrHelper {
22601
22602
  __publicField(this, "currentAbortController", null);
22602
22603
  __publicField(this, "runningMin", Infinity);
22603
22604
  __publicField(this, "runningMax", -Infinity);
22605
+ __publicField(this, "calibrationDone", false);
22604
22606
  // Debounce state for batching chunk updates
22605
22607
  __publicField(this, "updateDebounceTimer", null);
22606
22608
  __publicField(this, "UPDATE_DEBOUNCE_MS", 50);
@@ -22663,9 +22665,9 @@ var NVZarrHelper = class _NVZarrHelper {
22663
22665
  const level = Math.max(0, Math.min(options.level, helper.pyramidInfo.levels.length - 1));
22664
22666
  helper.pyramidLevel = level;
22665
22667
  helper.updateLevelInfo();
22666
- helper.centerX = helper.volumeDims.width / 2;
22667
- helper.centerY = helper.volumeDims.height / 2;
22668
- helper.centerZ = helper.volumeDims.depth / 2;
22668
+ helper.centerX = helper.levelDims.width / 2;
22669
+ helper.centerY = helper.levelDims.height / 2;
22670
+ helper.centerZ = helper.levelDims.depth / 2;
22669
22671
  helper.configureHostImage();
22670
22672
  return helper;
22671
22673
  }
@@ -22828,6 +22830,9 @@ var NVZarrHelper = class _NVZarrHelper {
22828
22830
  affine[row][col] = niftiCols[col].scale;
22829
22831
  affine[row][3] = niftiCols[col].trans;
22830
22832
  }
22833
+ affine[0][3] += this.worldOffsetMM[0];
22834
+ affine[1][3] += this.worldOffsetMM[1];
22835
+ affine[2][3] += this.worldOffsetMM[2];
22831
22836
  hdr.affine = affine;
22832
22837
  } else {
22833
22838
  const { scaleW, scaleH, scaleD } = this.getConvertedScales();
@@ -22851,12 +22856,15 @@ var NVZarrHelper = class _NVZarrHelper {
22851
22856
  affine[row][col] = niftiCols[col].scale;
22852
22857
  affine[row][3] = niftiCols[col].trans;
22853
22858
  }
22859
+ affine[0][3] += this.worldOffsetMM[0];
22860
+ affine[1][3] += this.worldOffsetMM[1];
22861
+ affine[2][3] += this.worldOffsetMM[2];
22854
22862
  hdr.affine = affine;
22855
22863
  } else {
22856
22864
  hdr.affine = [
22857
- [scaleW, 0, 0, volStartW * scaleW],
22858
- [0, -scaleH, 0, volStartH * scaleH],
22859
- [0, 0, -scaleD, volStartD * scaleD],
22865
+ [scaleW, 0, 0, volStartW * scaleW + this.worldOffsetMM[0]],
22866
+ [0, -scaleH, 0, volStartH * scaleH + this.worldOffsetMM[1]],
22867
+ [0, 0, -scaleD, volStartD * scaleD + this.worldOffsetMM[2]],
22860
22868
  [0, 0, 0, 1]
22861
22869
  ];
22862
22870
  }
@@ -22930,11 +22938,46 @@ var NVZarrHelper = class _NVZarrHelper {
22930
22938
  getVolumeDims() {
22931
22939
  return { ...this.volumeDims };
22932
22940
  }
22941
+ getWorldOffset() {
22942
+ return [...this.worldOffsetMM];
22943
+ }
22944
+ /**
22945
+ * Set the world-space offset so the full level's center maps to targetMM in world space.
22946
+ * Computes the native physical center of the zarr level, then sets worldOffsetMM
22947
+ * so that center aligns with targetMM. Also centers the viewport on the level center.
22948
+ */
22949
+ setWorldCenter(targetMM) {
22950
+ const { scaleW, scaleH, scaleD } = this.getConvertedScales();
22951
+ const { transW, transH, transD } = this.getConvertedTranslations();
22952
+ const axisNames = this.pyramidInfo.axisMapping.spatialAxisNames;
22953
+ const physW = this.levelDims.width / 2 * scaleW + transW;
22954
+ const physH = this.levelDims.height / 2 * scaleH + transH;
22955
+ const physD = this.levelDims.depth / 2 * scaleD + transD;
22956
+ const nativeCenter = [0, 0, 0];
22957
+ const dims = [
22958
+ { name: axisNames[axisNames.length - 1], phys: physW },
22959
+ { name: axisNames[axisNames.length - 2], phys: physH },
22960
+ { name: axisNames.length >= 3 ? axisNames[axisNames.length - 3] : "z", phys: physD }
22961
+ ];
22962
+ for (const d of dims) {
22963
+ const row = d.name === "x" ? 0 : d.name === "y" ? 1 : 2;
22964
+ nativeCenter[row] = d.phys;
22965
+ }
22966
+ this.worldOffsetMM = [targetMM[0] - nativeCenter[0], targetMM[1] - nativeCenter[1], targetMM[2] - nativeCenter[2]];
22967
+ this.centerX = this.levelDims.width / 2;
22968
+ this.centerY = this.levelDims.height / 2;
22969
+ this.centerZ = this.levelDims.depth / 2;
22970
+ this.clampCenter();
22971
+ this.updateAffine();
22972
+ }
22933
22973
  /**
22934
22974
  * Convert physical (mm) coordinates back to real zarr level pixel coordinates.
22935
22975
  * Inverts the affine: levelPixel = (mm - OME_translation) / scale
22936
22976
  */
22937
22977
  mmToLevelCoords(mmX, mmY, mmZ) {
22978
+ mmX -= this.worldOffsetMM[0];
22979
+ mmY -= this.worldOffsetMM[1];
22980
+ mmZ -= this.worldOffsetMM[2];
22938
22981
  const { scaleW, scaleH, scaleD } = this.getConvertedScales();
22939
22982
  const axisNames = this.pyramidInfo.axisMapping.spatialAxisNames;
22940
22983
  const mmByAxis = { x: mmX, y: mmY, z: mmZ };
@@ -23046,8 +23089,6 @@ var NVZarrHelper = class _NVZarrHelper {
23046
23089
  this.currentAbortController?.abort();
23047
23090
  const abortController = new AbortController();
23048
23091
  this.currentAbortController = abortController;
23049
- this.runningMin = Infinity;
23050
- this.runningMax = -Infinity;
23051
23092
  await new Promise((resolve) => {
23052
23093
  requestAnimationFrame(() => resolve());
23053
23094
  });
@@ -23160,14 +23201,16 @@ var NVZarrHelper = class _NVZarrHelper {
23160
23201
  if (actualChunkW <= 0 || actualChunkH <= 0 || actualChunkD <= 0) {
23161
23202
  return;
23162
23203
  }
23163
- const step = Math.max(1, Math.floor(data.length / 2e3));
23164
- for (let i = 0; i < data.length; i += step) {
23165
- const val = data[i];
23166
- if (val < this.runningMin) {
23167
- this.runningMin = val;
23168
- }
23169
- if (val > this.runningMax) {
23170
- this.runningMax = val;
23204
+ if (!this.calibrationDone) {
23205
+ const step = Math.max(1, Math.floor(data.length / 2e3));
23206
+ for (let i = 0; i < data.length; i += step) {
23207
+ const val = data[i];
23208
+ if (val < this.runningMin) {
23209
+ this.runningMin = val;
23210
+ }
23211
+ if (val > this.runningMax) {
23212
+ this.runningMax = val;
23213
+ }
23171
23214
  }
23172
23215
  }
23173
23216
  const fullChunkVol = this.chunkSize.width * this.chunkSize.height * this.chunkSize.depth;
@@ -23198,6 +23241,9 @@ var NVZarrHelper = class _NVZarrHelper {
23198
23241
  }
23199
23242
  }
23200
23243
  updateCalibration() {
23244
+ if (this.calibrationDone) {
23245
+ return;
23246
+ }
23201
23247
  if (this.runningMin < Infinity && this.runningMax > -Infinity) {
23202
23248
  this.hostImage.cal_min = this.runningMin;
23203
23249
  this.hostImage.cal_max = this.runningMax;
@@ -23205,6 +23251,7 @@ var NVZarrHelper = class _NVZarrHelper {
23205
23251
  this.hostImage.robust_max = this.runningMax;
23206
23252
  this.hostImage.global_min = this.runningMin;
23207
23253
  this.hostImage.global_max = this.runningMax;
23254
+ this.calibrationDone = true;
23208
23255
  }
23209
23256
  }
23210
23257
  /**
@@ -23298,6 +23345,7 @@ var NVImage = class _NVImage {
23298
23345
  __publicField(this, "onOpacityChange", () => {
23299
23346
  });
23300
23347
  __publicField(this, "zarrHelper", null);
23348
+ __publicField(this, "_hasExplicitZarrCenter", false);
23301
23349
  __publicField(this, "mm000");
23302
23350
  __publicField(this, "mm100");
23303
23351
  __publicField(this, "mm010");
@@ -23934,7 +23982,8 @@ var NVImage = class _NVImage {
23934
23982
  zarrLevel,
23935
23983
  zarrMaxVolumeSize,
23936
23984
  zarrChannel,
23937
- zarrConvertUnits
23985
+ zarrConvertUnits,
23986
+ zarrCenterMM
23938
23987
  } = {}) {
23939
23988
  if (url === "") {
23940
23989
  throw Error("url must not be empty");
@@ -23987,7 +24036,8 @@ var NVImage = class _NVImage {
23987
24036
  channel: zarrChannel,
23988
24037
  convertUnitsToMm: zarrConvertUnits,
23989
24038
  colormap,
23990
- opacity
24039
+ opacity,
24040
+ zarrCenterMM
23991
24041
  });
23992
24042
  }
23993
24043
  const zarrResult = await loadZarrData(url);
@@ -24066,6 +24116,10 @@ var NVImage = class _NVImage {
24066
24116
  cacheSize: options.cacheSize,
24067
24117
  convertUnitsToMm: options.convertUnitsToMm
24068
24118
  });
24119
+ if (options.zarrCenterMM) {
24120
+ nvimage.zarrHelper.setWorldCenter(options.zarrCenterMM);
24121
+ nvimage._hasExplicitZarrCenter = true;
24122
+ }
24069
24123
  if (options.colormap) {
24070
24124
  nvimage._colormap = options.colormap;
24071
24125
  }
@@ -36600,7 +36654,8 @@ var Niivue = class {
36600
36654
  zarrLevel: imageItem.zarrLevel,
36601
36655
  zarrMaxVolumeSize: imageItem.zarrMaxVolumeSize,
36602
36656
  zarrChannel: imageItem.zarrChannel,
36603
- zarrConvertUnits: imageItem.zarrConvertUnits
36657
+ zarrConvertUnits: imageItem.zarrConvertUnits,
36658
+ zarrCenterMM: imageItem.zarrCenterMM
36604
36659
  };
36605
36660
  const volume = await NVImage.loadFromUrl(imageOptions);
36606
36661
  this.document.addImageOptions(volume, imageOptions);
@@ -37180,6 +37235,20 @@ var Niivue = class {
37180
37235
  this.setVolume(volume, result.index);
37181
37236
  this.onImageLoaded(volume);
37182
37237
  if (volume.zarrHelper) {
37238
+ if (!volume._hasExplicitZarrCenter && this.back && this.back !== volume && this.back.hdr?.dims) {
37239
+ const bg = this.back;
37240
+ const dims = bg.hdr.dims;
37241
+ const affine = bg.hdr.affine;
37242
+ const ci = (dims[1] - 1) / 2;
37243
+ const cj = (dims[2] - 1) / 2;
37244
+ const ck = (dims[3] - 1) / 2;
37245
+ const bgCenter = [
37246
+ affine[0][0] * ci + affine[0][1] * cj + affine[0][2] * ck + affine[0][3],
37247
+ affine[1][0] * ci + affine[1][1] * cj + affine[1][2] * ck + affine[1][3],
37248
+ affine[2][0] * ci + affine[2][1] * cj + affine[2][2] * ck + affine[2][3]
37249
+ ];
37250
+ volume.zarrHelper.setWorldCenter(bgCenter);
37251
+ }
37183
37252
  volume.zarrHelper.onChunksUpdated = () => {
37184
37253
  this.updateGLVolume();
37185
37254
  this._skipDragInDraw = true;