@niivue/niivue 0.67.1-dev.0 → 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.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",
@@ -22535,9 +22535,11 @@ function createTypedVoxelArray(datatypeCode, size) {
22535
22535
  case 512 /* DT_UINT16 */:
22536
22536
  return new Uint16Array(size);
22537
22537
  case 4 /* DT_INT16 */:
22538
- case 8 /* DT_INT32 */:
22539
22538
  return new Int16Array(size);
22539
+ case 8 /* DT_INT32 */:
22540
+ return new Int32Array(size);
22540
22541
  case 768 /* DT_UINT32 */:
22542
+ return new Uint32Array(size);
22541
22543
  case 16 /* DT_FLOAT32 */:
22542
22544
  return new Float32Array(size);
22543
22545
  case 64 /* DT_FLOAT64 */:
@@ -22589,6 +22591,7 @@ var NVZarrHelper = class _NVZarrHelper {
22589
22591
  __publicField(this, "voxelTranslations");
22590
22592
  __publicField(this, "hasTranslations");
22591
22593
  __publicField(this, "convertUnitsToMm");
22594
+ __publicField(this, "worldOffsetMM", [0, 0, 0]);
22592
22595
  __publicField(this, "centerX");
22593
22596
  __publicField(this, "centerY");
22594
22597
  __publicField(this, "centerZ");
@@ -22599,6 +22602,7 @@ var NVZarrHelper = class _NVZarrHelper {
22599
22602
  __publicField(this, "currentAbortController", null);
22600
22603
  __publicField(this, "runningMin", Infinity);
22601
22604
  __publicField(this, "runningMax", -Infinity);
22605
+ __publicField(this, "calibrationDone", false);
22602
22606
  // Debounce state for batching chunk updates
22603
22607
  __publicField(this, "updateDebounceTimer", null);
22604
22608
  __publicField(this, "UPDATE_DEBOUNCE_MS", 50);
@@ -22665,9 +22669,11 @@ var NVZarrHelper = class _NVZarrHelper {
22665
22669
  helper.centerY = helper.levelDims.height / 2;
22666
22670
  helper.centerZ = helper.levelDims.depth / 2;
22667
22671
  helper.configureHostImage();
22668
- await helper.updateVolume();
22669
22672
  return helper;
22670
22673
  }
22674
+ async loadInitialChunks() {
22675
+ await this.updateVolume();
22676
+ }
22671
22677
  updateLevelInfo() {
22672
22678
  const levelInfo = this.pyramidInfo.levels[this.pyramidLevel];
22673
22679
  const shape = levelInfo.shape;
@@ -22739,12 +22745,12 @@ var NVZarrHelper = class _NVZarrHelper {
22739
22745
  img.img = createTypedVoxelArray(this.datatypeCode, width * height * depth);
22740
22746
  this.updateAffine();
22741
22747
  img.originalAffine = copyAffine(hdr.affine);
22742
- img.cal_min = NaN;
22743
- img.cal_max = NaN;
22744
- img.robust_min = NaN;
22745
- img.robust_max = NaN;
22746
- img.global_min = NaN;
22747
- img.global_max = NaN;
22748
+ img.cal_min = 0;
22749
+ img.cal_max = 0;
22750
+ img.robust_min = 0;
22751
+ img.robust_max = 0;
22752
+ img.global_min = 0;
22753
+ img.global_max = 0;
22748
22754
  }
22749
22755
  /** Get unit-converted voxel scales */
22750
22756
  getConvertedScales() {
@@ -22824,6 +22830,9 @@ var NVZarrHelper = class _NVZarrHelper {
22824
22830
  affine[row][col] = niftiCols[col].scale;
22825
22831
  affine[row][3] = niftiCols[col].trans;
22826
22832
  }
22833
+ affine[0][3] += this.worldOffsetMM[0];
22834
+ affine[1][3] += this.worldOffsetMM[1];
22835
+ affine[2][3] += this.worldOffsetMM[2];
22827
22836
  hdr.affine = affine;
22828
22837
  } else {
22829
22838
  const { scaleW, scaleH, scaleD } = this.getConvertedScales();
@@ -22847,12 +22856,15 @@ var NVZarrHelper = class _NVZarrHelper {
22847
22856
  affine[row][col] = niftiCols[col].scale;
22848
22857
  affine[row][3] = niftiCols[col].trans;
22849
22858
  }
22859
+ affine[0][3] += this.worldOffsetMM[0];
22860
+ affine[1][3] += this.worldOffsetMM[1];
22861
+ affine[2][3] += this.worldOffsetMM[2];
22850
22862
  hdr.affine = affine;
22851
22863
  } else {
22852
22864
  hdr.affine = [
22853
- [scaleW, 0, 0, volStartW * scaleW],
22854
- [0, -scaleH, 0, volStartH * scaleH],
22855
- [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]],
22856
22868
  [0, 0, 0, 1]
22857
22869
  ];
22858
22870
  }
@@ -22863,6 +22875,9 @@ var NVZarrHelper = class _NVZarrHelper {
22863
22875
  beginDrag() {
22864
22876
  this.centerAtDragStart = { x: this.centerX, y: this.centerY, z: this.centerZ };
22865
22877
  }
22878
+ endDrag() {
22879
+ this.centerAtDragStart = null;
22880
+ }
22866
22881
  async panBy(dx, dy, dz = 0) {
22867
22882
  this.centerX -= dx;
22868
22883
  this.centerY -= dy;
@@ -22872,12 +22887,18 @@ var NVZarrHelper = class _NVZarrHelper {
22872
22887
  await this.updateVolume();
22873
22888
  }
22874
22889
  async panTo(newCenterX, newCenterY, newCenterZ) {
22890
+ const prevX = this.centerX;
22891
+ const prevY = this.centerY;
22892
+ const prevZ = this.centerZ;
22875
22893
  this.centerX = newCenterX;
22876
22894
  this.centerY = newCenterY;
22877
22895
  if (newCenterZ !== void 0) {
22878
22896
  this.centerZ = newCenterZ;
22879
22897
  }
22880
22898
  this.clampCenter();
22899
+ if (Math.abs(this.centerX - prevX) < 1e-3 && Math.abs(this.centerY - prevY) < 1e-3 && Math.abs(this.centerZ - prevZ) < 1e-3) {
22900
+ return;
22901
+ }
22881
22902
  this.updateAffine();
22882
22903
  await this.updateVolume();
22883
22904
  }
@@ -22917,6 +22938,89 @@ var NVZarrHelper = class _NVZarrHelper {
22917
22938
  getVolumeDims() {
22918
22939
  return { ...this.volumeDims };
22919
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
+ }
22973
+ /**
22974
+ * Convert physical (mm) coordinates back to real zarr level pixel coordinates.
22975
+ * Inverts the affine: levelPixel = (mm - OME_translation) / scale
22976
+ */
22977
+ mmToLevelCoords(mmX, mmY, mmZ) {
22978
+ mmX -= this.worldOffsetMM[0];
22979
+ mmY -= this.worldOffsetMM[1];
22980
+ mmZ -= this.worldOffsetMM[2];
22981
+ const { scaleW, scaleH, scaleD } = this.getConvertedScales();
22982
+ const axisNames = this.pyramidInfo.axisMapping.spatialAxisNames;
22983
+ const mmByAxis = { x: mmX, y: mmY, z: mmZ };
22984
+ let levelW = 0;
22985
+ let levelH = 0;
22986
+ let levelD = 0;
22987
+ if (axisNames.length >= 2) {
22988
+ const wAxisName = axisNames[axisNames.length - 1];
22989
+ const hAxisName = axisNames[axisNames.length - 2];
22990
+ const dAxisName = axisNames.length >= 3 ? axisNames[axisNames.length - 3] : "z";
22991
+ const mmW = mmByAxis[wAxisName] ?? mmX;
22992
+ const mmH = mmByAxis[hAxisName] ?? mmY;
22993
+ const mmD = mmByAxis[dAxisName] ?? mmZ;
22994
+ if (this.hasTranslations) {
22995
+ const { transW, transH, transD } = this.getConvertedTranslations();
22996
+ levelW = scaleW !== 0 ? (mmW - transW) / scaleW : 0;
22997
+ levelH = scaleH !== 0 ? (mmH - transH) / scaleH : 0;
22998
+ levelD = scaleD !== 0 ? (mmD - transD) / scaleD : 0;
22999
+ } else {
23000
+ levelW = scaleW !== 0 ? mmW / scaleW : 0;
23001
+ levelH = scaleH !== 0 ? mmH / scaleH : 0;
23002
+ levelD = scaleD !== 0 ? mmD / scaleD : 0;
23003
+ }
23004
+ } else {
23005
+ if (this.hasTranslations) {
23006
+ const { transW, transH, transD } = this.getConvertedTranslations();
23007
+ levelW = scaleW !== 0 ? (mmX - transW) / scaleW : 0;
23008
+ levelH = scaleH !== 0 ? (mmY - transH) / scaleH : 0;
23009
+ levelD = scaleD !== 0 ? (mmZ - transD) / scaleD : 0;
23010
+ } else {
23011
+ levelW = scaleW !== 0 ? mmX / scaleW : 0;
23012
+ levelH = scaleH !== 0 ? mmY / scaleH : 0;
23013
+ levelD = scaleD !== 0 ? mmZ / scaleD : 0;
23014
+ }
23015
+ }
23016
+ return {
23017
+ width: Math.round(levelW),
23018
+ height: Math.round(levelH),
23019
+ depth: Math.round(levelD),
23020
+ level: this.pyramidLevel,
23021
+ levelDims: { ...this.levelDims }
23022
+ };
23023
+ }
22920
23024
  clampCenter() {
22921
23025
  const halfW = this.volumeDims.width / 2;
22922
23026
  const halfH = this.volumeDims.height / 2;
@@ -22985,8 +23089,6 @@ var NVZarrHelper = class _NVZarrHelper {
22985
23089
  this.currentAbortController?.abort();
22986
23090
  const abortController = new AbortController();
22987
23091
  this.currentAbortController = abortController;
22988
- this.runningMin = Infinity;
22989
- this.runningMax = -Infinity;
22990
23092
  await new Promise((resolve) => {
22991
23093
  requestAnimationFrame(() => resolve());
22992
23094
  });
@@ -23099,14 +23201,16 @@ var NVZarrHelper = class _NVZarrHelper {
23099
23201
  if (actualChunkW <= 0 || actualChunkH <= 0 || actualChunkD <= 0) {
23100
23202
  return;
23101
23203
  }
23102
- const step = Math.max(1, Math.floor(data.length / 2e3));
23103
- for (let i = 0; i < data.length; i += step) {
23104
- const val = data[i];
23105
- if (val < this.runningMin) {
23106
- this.runningMin = val;
23107
- }
23108
- if (val > this.runningMax) {
23109
- 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
+ }
23110
23214
  }
23111
23215
  }
23112
23216
  const fullChunkVol = this.chunkSize.width * this.chunkSize.height * this.chunkSize.depth;
@@ -23137,6 +23241,9 @@ var NVZarrHelper = class _NVZarrHelper {
23137
23241
  }
23138
23242
  }
23139
23243
  updateCalibration() {
23244
+ if (this.calibrationDone) {
23245
+ return;
23246
+ }
23140
23247
  if (this.runningMin < Infinity && this.runningMax > -Infinity) {
23141
23248
  this.hostImage.cal_min = this.runningMin;
23142
23249
  this.hostImage.cal_max = this.runningMax;
@@ -23144,6 +23251,7 @@ var NVZarrHelper = class _NVZarrHelper {
23144
23251
  this.hostImage.robust_max = this.runningMax;
23145
23252
  this.hostImage.global_min = this.runningMin;
23146
23253
  this.hostImage.global_max = this.runningMax;
23254
+ this.calibrationDone = true;
23147
23255
  }
23148
23256
  }
23149
23257
  /**
@@ -23237,6 +23345,7 @@ var NVImage = class _NVImage {
23237
23345
  __publicField(this, "onOpacityChange", () => {
23238
23346
  });
23239
23347
  __publicField(this, "zarrHelper", null);
23348
+ __publicField(this, "_hasExplicitZarrCenter", false);
23240
23349
  __publicField(this, "mm000");
23241
23350
  __publicField(this, "mm100");
23242
23351
  __publicField(this, "mm010");
@@ -23873,7 +23982,8 @@ var NVImage = class _NVImage {
23873
23982
  zarrLevel,
23874
23983
  zarrMaxVolumeSize,
23875
23984
  zarrChannel,
23876
- zarrConvertUnits
23985
+ zarrConvertUnits,
23986
+ zarrCenterMM
23877
23987
  } = {}) {
23878
23988
  if (url === "") {
23879
23989
  throw Error("url must not be empty");
@@ -23926,7 +24036,8 @@ var NVImage = class _NVImage {
23926
24036
  channel: zarrChannel,
23927
24037
  convertUnitsToMm: zarrConvertUnits,
23928
24038
  colormap,
23929
- opacity
24039
+ opacity,
24040
+ zarrCenterMM
23930
24041
  });
23931
24042
  }
23932
24043
  const zarrResult = await loadZarrData(url);
@@ -24005,6 +24116,10 @@ var NVImage = class _NVImage {
24005
24116
  cacheSize: options.cacheSize,
24006
24117
  convertUnitsToMm: options.convertUnitsToMm
24007
24118
  });
24119
+ if (options.zarrCenterMM) {
24120
+ nvimage.zarrHelper.setWorldCenter(options.zarrCenterMM);
24121
+ nvimage._hasExplicitZarrCenter = true;
24122
+ }
24008
24123
  if (options.colormap) {
24009
24124
  nvimage._colormap = options.colormap;
24010
24125
  }
@@ -34659,6 +34774,7 @@ var Niivue = class {
34659
34774
  crosshair: false
34660
34775
  });
34661
34776
  __publicField(this, "readyForSync", false);
34777
+ __publicField(this, "_skipDragInDraw", false);
34662
34778
  // UI Data
34663
34779
  __publicField(this, "uiData", {
34664
34780
  mousedown: false,
@@ -35755,6 +35871,9 @@ var Niivue = class {
35755
35871
  }
35756
35872
  if (this.uiData.isDragging) {
35757
35873
  this.uiData.isDragging = false;
35874
+ for (const vol of this.getZarrVolumes()) {
35875
+ vol.zarrHelper?.endDrag();
35876
+ }
35758
35877
  if (currentDragMode === 7 /* angle */) {
35759
35878
  if (this.uiData.angleState === "drawing_first_line") {
35760
35879
  this.uiData.angleFirstLine = [this.uiData.dragStart[0], this.uiData.dragStart[1], this.uiData.dragEnd[0], this.uiData.dragEnd[1]];
@@ -35900,6 +36019,9 @@ var Niivue = class {
35900
36019
  }
35901
36020
  if (this.uiData.isDragging) {
35902
36021
  this.uiData.isDragging = false;
36022
+ for (const vol of this.getZarrVolumes()) {
36023
+ vol.zarrHelper?.endDrag();
36024
+ }
35903
36025
  if (this.getCurrentDragMode() === 1 /* contrast */) {
35904
36026
  this.calculateNewRange();
35905
36027
  this.refreshLayers(this.volumes[0], 0);
@@ -35959,6 +36081,9 @@ var Niivue = class {
35959
36081
  if (this.uiData.isDragging || this.uiData.mousedown) {
35960
36082
  log.debug("Mouse left canvas during drag, resetting drag state.");
35961
36083
  this.uiData.isDragging = false;
36084
+ for (const vol of this.getZarrVolumes()) {
36085
+ vol.zarrHelper?.endDrag();
36086
+ }
35962
36087
  const resetState = createResetButtonState();
35963
36088
  this.uiData.mouseButtonLeftDown = resetState.mouseButtonLeftDown;
35964
36089
  this.uiData.mouseButtonCenterDown = resetState.mouseButtonCenterDown;
@@ -36529,7 +36654,8 @@ var Niivue = class {
36529
36654
  zarrLevel: imageItem.zarrLevel,
36530
36655
  zarrMaxVolumeSize: imageItem.zarrMaxVolumeSize,
36531
36656
  zarrChannel: imageItem.zarrChannel,
36532
- zarrConvertUnits: imageItem.zarrConvertUnits
36657
+ zarrConvertUnits: imageItem.zarrConvertUnits,
36658
+ zarrCenterMM: imageItem.zarrCenterMM
36533
36659
  };
36534
36660
  const volume = await NVImage.loadFromUrl(imageOptions);
36535
36661
  this.document.addImageOptions(volume, imageOptions);
@@ -37109,10 +37235,27 @@ var Niivue = class {
37109
37235
  this.setVolume(volume, result.index);
37110
37236
  this.onImageLoaded(volume);
37111
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
+ }
37112
37252
  volume.zarrHelper.onChunksUpdated = () => {
37113
37253
  this.updateGLVolume();
37254
+ this._skipDragInDraw = true;
37114
37255
  this.drawScene();
37256
+ this._skipDragInDraw = false;
37115
37257
  };
37258
+ volume.zarrHelper.loadInitialChunks().catch((err) => log.error("Failed to load initial zarr chunks", err));
37116
37259
  }
37117
37260
  log.debug("loaded volume", volume.name);
37118
37261
  log.debug(volume);
@@ -41239,7 +41382,9 @@ var Niivue = class {
41239
41382
  }
41240
41383
  if (panPromises.length > 0) {
41241
41384
  Promise.all(panPromises).then(() => {
41385
+ this._skipDragInDraw = true;
41242
41386
  this.drawScene();
41387
+ this._skipDragInDraw = false;
41243
41388
  }).catch((err) => {
41244
41389
  log.error("Failed to pan Zarr:", err);
41245
41390
  });
@@ -44516,7 +44661,7 @@ var Niivue = class {
44516
44661
  this.dragForSlicer3D([this.uiData.dragStart[0], this.uiData.dragStart[1], this.uiData.dragEnd[0], this.uiData.dragEnd[1]]);
44517
44662
  return;
44518
44663
  }
44519
- if (this.getCurrentDragMode() === 3 /* pan */) {
44664
+ if (this.getCurrentDragMode() === 3 /* pan */ && !this._skipDragInDraw) {
44520
44665
  this.dragForPanZoom([this.uiData.dragStart[0], this.uiData.dragStart[1], this.uiData.dragEnd[0], this.uiData.dragEnd[1]]);
44521
44666
  return;
44522
44667
  }