@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.
@@ -448,6 +448,8 @@ type ImageFromUrlOptions = {
448
448
  zarrChannel?: number;
449
449
  /** Convert OME spatial units to millimeters for NIfTI compatibility (default: true) */
450
450
  zarrConvertUnits?: boolean;
451
+ /** World-space center [x, y, z] in mm where the zarr volume center should be positioned */
452
+ zarrCenterMM?: [number, number, number];
451
453
  };
452
454
  type ImageFromFileOptions = {
453
455
  file: File | File[];
@@ -747,6 +749,7 @@ declare class NVZarrHelper {
747
749
  private voxelTranslations;
748
750
  private hasTranslations;
749
751
  private convertUnitsToMm;
752
+ private worldOffsetMM;
750
753
  private centerX;
751
754
  private centerY;
752
755
  private centerZ;
@@ -757,6 +760,7 @@ declare class NVZarrHelper {
757
760
  private currentAbortController;
758
761
  private runningMin;
759
762
  private runningMax;
763
+ private calibrationDone;
760
764
  private updateDebounceTimer;
761
765
  private readonly UPDATE_DEBOUNCE_MS;
762
766
  private pendingChunkCount;
@@ -814,6 +818,13 @@ declare class NVZarrHelper {
814
818
  height: number;
815
819
  depth: number;
816
820
  };
821
+ getWorldOffset(): [number, number, number];
822
+ /**
823
+ * Set the world-space offset so the full level's center maps to targetMM in world space.
824
+ * Computes the native physical center of the zarr level, then sets worldOffsetMM
825
+ * so that center aligns with targetMM. Also centers the viewport on the level center.
826
+ */
827
+ setWorldCenter(targetMM: [number, number, number]): void;
817
828
  /**
818
829
  * Convert physical (mm) coordinates back to real zarr level pixel coordinates.
819
830
  * Inverts the affine: levelPixel = (mm - OME_translation) / scale
@@ -953,6 +964,7 @@ declare class NVImage {
953
964
  onColormapChange: (img: NVImage) => void;
954
965
  onOpacityChange: (img: NVImage) => void;
955
966
  zarrHelper: NVZarrHelper | null;
967
+ _hasExplicitZarrCenter: boolean;
956
968
  mm000?: vec3;
957
969
  mm100?: vec3;
958
970
  mm010?: vec3;
@@ -1056,7 +1068,7 @@ declare class NVImage {
1056
1068
  /**
1057
1069
  * factory function to load and return a new NVImage instance from a given URL
1058
1070
  */
1059
- static loadFromUrl({ url, urlImgData, headers, name, colormap, opacity, cal_min, cal_max, trustCalMinMax, percentileFrac, ignoreZeroVoxels, useQFormNotSForm, colormapNegative, frame4D, isManifest, limitFrames4D, imageType, colorbarVisible, buffer, zarrLevel, zarrMaxVolumeSize, zarrChannel, zarrConvertUnits }?: Partial<Omit<ImageFromUrlOptions, 'url'>> & {
1071
+ static loadFromUrl({ url, urlImgData, headers, name, colormap, opacity, cal_min, cal_max, trustCalMinMax, percentileFrac, ignoreZeroVoxels, useQFormNotSForm, colormapNegative, frame4D, isManifest, limitFrames4D, imageType, colorbarVisible, buffer, zarrLevel, zarrMaxVolumeSize, zarrChannel, zarrConvertUnits, zarrCenterMM }?: Partial<Omit<ImageFromUrlOptions, 'url'>> & {
1060
1072
  url?: string | Uint8Array | ArrayBuffer;
1061
1073
  }): Promise<NVImage>;
1062
1074
  /**
@@ -1071,6 +1083,7 @@ declare class NVImage {
1071
1083
  convertUnitsToMm?: boolean;
1072
1084
  colormap?: string;
1073
1085
  opacity?: number;
1086
+ zarrCenterMM?: [number, number, number];
1074
1087
  }): Promise<NVImage>;
1075
1088
  static readFileAsync(file: File, bytesToLoad?: number): Promise<ArrayBuffer>;
1076
1089
  /**
@@ -1,4 +1,4 @@
1
- import { ar as Volume, x as NiftiHeader } from '../nvdocument-B64_4nuJ.js';
1
+ import { ar as Volume, x as NiftiHeader } from '../nvdocument-DrX6RoEG.js';
2
2
  import { vec3, mat4, vec4 } from 'gl-matrix';
3
3
  import 'nifti-reader-js';
4
4
 
package/dist/index.d.ts CHANGED
@@ -245,6 +245,8 @@ type ImageFromUrlOptions = {
245
245
  zarrChannel?: number;
246
246
  /** Convert OME spatial units to millimeters for NIfTI compatibility (default: true) */
247
247
  zarrConvertUnits?: boolean;
248
+ /** World-space center [x, y, z] in mm where the zarr volume center should be positioned */
249
+ zarrCenterMM?: [number, number, number];
248
250
  };
249
251
  type ImageFromFileOptions = {
250
252
  file: File | File[];
@@ -544,6 +546,7 @@ declare class NVZarrHelper {
544
546
  private voxelTranslations;
545
547
  private hasTranslations;
546
548
  private convertUnitsToMm;
549
+ private worldOffsetMM;
547
550
  private centerX;
548
551
  private centerY;
549
552
  private centerZ;
@@ -554,6 +557,7 @@ declare class NVZarrHelper {
554
557
  private currentAbortController;
555
558
  private runningMin;
556
559
  private runningMax;
560
+ private calibrationDone;
557
561
  private updateDebounceTimer;
558
562
  private readonly UPDATE_DEBOUNCE_MS;
559
563
  private pendingChunkCount;
@@ -611,6 +615,13 @@ declare class NVZarrHelper {
611
615
  height: number;
612
616
  depth: number;
613
617
  };
618
+ getWorldOffset(): [number, number, number];
619
+ /**
620
+ * Set the world-space offset so the full level's center maps to targetMM in world space.
621
+ * Computes the native physical center of the zarr level, then sets worldOffsetMM
622
+ * so that center aligns with targetMM. Also centers the viewport on the level center.
623
+ */
624
+ setWorldCenter(targetMM: [number, number, number]): void;
614
625
  /**
615
626
  * Convert physical (mm) coordinates back to real zarr level pixel coordinates.
616
627
  * Inverts the affine: levelPixel = (mm - OME_translation) / scale
@@ -750,6 +761,7 @@ declare class NVImage {
750
761
  onColormapChange: (img: NVImage) => void;
751
762
  onOpacityChange: (img: NVImage) => void;
752
763
  zarrHelper: NVZarrHelper | null;
764
+ _hasExplicitZarrCenter: boolean;
753
765
  mm000?: vec3;
754
766
  mm100?: vec3;
755
767
  mm010?: vec3;
@@ -853,7 +865,7 @@ declare class NVImage {
853
865
  /**
854
866
  * factory function to load and return a new NVImage instance from a given URL
855
867
  */
856
- static loadFromUrl({ url, urlImgData, headers, name, colormap, opacity, cal_min, cal_max, trustCalMinMax, percentileFrac, ignoreZeroVoxels, useQFormNotSForm, colormapNegative, frame4D, isManifest, limitFrames4D, imageType, colorbarVisible, buffer, zarrLevel, zarrMaxVolumeSize, zarrChannel, zarrConvertUnits }?: Partial<Omit<ImageFromUrlOptions, 'url'>> & {
868
+ static loadFromUrl({ url, urlImgData, headers, name, colormap, opacity, cal_min, cal_max, trustCalMinMax, percentileFrac, ignoreZeroVoxels, useQFormNotSForm, colormapNegative, frame4D, isManifest, limitFrames4D, imageType, colorbarVisible, buffer, zarrLevel, zarrMaxVolumeSize, zarrChannel, zarrConvertUnits, zarrCenterMM }?: Partial<Omit<ImageFromUrlOptions, 'url'>> & {
857
869
  url?: string | Uint8Array | ArrayBuffer;
858
870
  }): Promise<NVImage>;
859
871
  /**
@@ -868,6 +880,7 @@ declare class NVImage {
868
880
  convertUnitsToMm?: boolean;
869
881
  colormap?: string;
870
882
  opacity?: number;
883
+ zarrCenterMM?: [number, number, number];
871
884
  }): Promise<NVImage>;
872
885
  static readFileAsync(file: File, bytesToLoad?: number): Promise<ArrayBuffer>;
873
886
  /**
package/dist/index.js CHANGED
@@ -5249,7 +5249,7 @@ var forEach3 = (function() {
5249
5249
  // package.json
5250
5250
  var package_default = {
5251
5251
  name: "@niivue/niivue",
5252
- version: "0.67.1-dev.0",
5252
+ version: "0.67.1-dev.2",
5253
5253
  description: "minimal webgl2 nifti image viewer",
5254
5254
  types: "./build/niivue/index.d.ts",
5255
5255
  main: "./build/niivue/index.js",
@@ -31283,6 +31283,7 @@ var NVZarrHelper = class _NVZarrHelper {
31283
31283
  __publicField(this, "voxelTranslations");
31284
31284
  __publicField(this, "hasTranslations");
31285
31285
  __publicField(this, "convertUnitsToMm");
31286
+ __publicField(this, "worldOffsetMM", [0, 0, 0]);
31286
31287
  __publicField(this, "centerX");
31287
31288
  __publicField(this, "centerY");
31288
31289
  __publicField(this, "centerZ");
@@ -31293,6 +31294,7 @@ var NVZarrHelper = class _NVZarrHelper {
31293
31294
  __publicField(this, "currentAbortController", null);
31294
31295
  __publicField(this, "runningMin", Infinity);
31295
31296
  __publicField(this, "runningMax", -Infinity);
31297
+ __publicField(this, "calibrationDone", false);
31296
31298
  // Debounce state for batching chunk updates
31297
31299
  __publicField(this, "updateDebounceTimer", null);
31298
31300
  __publicField(this, "UPDATE_DEBOUNCE_MS", 50);
@@ -31355,9 +31357,9 @@ var NVZarrHelper = class _NVZarrHelper {
31355
31357
  const level = Math.max(0, Math.min(options.level, helper.pyramidInfo.levels.length - 1));
31356
31358
  helper.pyramidLevel = level;
31357
31359
  helper.updateLevelInfo();
31358
- helper.centerX = helper.volumeDims.width / 2;
31359
- helper.centerY = helper.volumeDims.height / 2;
31360
- helper.centerZ = helper.volumeDims.depth / 2;
31360
+ helper.centerX = helper.levelDims.width / 2;
31361
+ helper.centerY = helper.levelDims.height / 2;
31362
+ helper.centerZ = helper.levelDims.depth / 2;
31361
31363
  helper.configureHostImage();
31362
31364
  return helper;
31363
31365
  }
@@ -31520,6 +31522,9 @@ var NVZarrHelper = class _NVZarrHelper {
31520
31522
  affine[row][col] = niftiCols[col].scale;
31521
31523
  affine[row][3] = niftiCols[col].trans;
31522
31524
  }
31525
+ affine[0][3] += this.worldOffsetMM[0];
31526
+ affine[1][3] += this.worldOffsetMM[1];
31527
+ affine[2][3] += this.worldOffsetMM[2];
31523
31528
  hdr.affine = affine;
31524
31529
  } else {
31525
31530
  const { scaleW, scaleH, scaleD } = this.getConvertedScales();
@@ -31543,12 +31548,15 @@ var NVZarrHelper = class _NVZarrHelper {
31543
31548
  affine[row][col] = niftiCols[col].scale;
31544
31549
  affine[row][3] = niftiCols[col].trans;
31545
31550
  }
31551
+ affine[0][3] += this.worldOffsetMM[0];
31552
+ affine[1][3] += this.worldOffsetMM[1];
31553
+ affine[2][3] += this.worldOffsetMM[2];
31546
31554
  hdr.affine = affine;
31547
31555
  } else {
31548
31556
  hdr.affine = [
31549
- [scaleW, 0, 0, volStartW * scaleW],
31550
- [0, -scaleH, 0, volStartH * scaleH],
31551
- [0, 0, -scaleD, volStartD * scaleD],
31557
+ [scaleW, 0, 0, volStartW * scaleW + this.worldOffsetMM[0]],
31558
+ [0, -scaleH, 0, volStartH * scaleH + this.worldOffsetMM[1]],
31559
+ [0, 0, -scaleD, volStartD * scaleD + this.worldOffsetMM[2]],
31552
31560
  [0, 0, 0, 1]
31553
31561
  ];
31554
31562
  }
@@ -31622,11 +31630,46 @@ var NVZarrHelper = class _NVZarrHelper {
31622
31630
  getVolumeDims() {
31623
31631
  return { ...this.volumeDims };
31624
31632
  }
31633
+ getWorldOffset() {
31634
+ return [...this.worldOffsetMM];
31635
+ }
31636
+ /**
31637
+ * Set the world-space offset so the full level's center maps to targetMM in world space.
31638
+ * Computes the native physical center of the zarr level, then sets worldOffsetMM
31639
+ * so that center aligns with targetMM. Also centers the viewport on the level center.
31640
+ */
31641
+ setWorldCenter(targetMM) {
31642
+ const { scaleW, scaleH, scaleD } = this.getConvertedScales();
31643
+ const { transW, transH, transD } = this.getConvertedTranslations();
31644
+ const axisNames = this.pyramidInfo.axisMapping.spatialAxisNames;
31645
+ const physW = this.levelDims.width / 2 * scaleW + transW;
31646
+ const physH = this.levelDims.height / 2 * scaleH + transH;
31647
+ const physD = this.levelDims.depth / 2 * scaleD + transD;
31648
+ const nativeCenter = [0, 0, 0];
31649
+ const dims = [
31650
+ { name: axisNames[axisNames.length - 1], phys: physW },
31651
+ { name: axisNames[axisNames.length - 2], phys: physH },
31652
+ { name: axisNames.length >= 3 ? axisNames[axisNames.length - 3] : "z", phys: physD }
31653
+ ];
31654
+ for (const d of dims) {
31655
+ const row = d.name === "x" ? 0 : d.name === "y" ? 1 : 2;
31656
+ nativeCenter[row] = d.phys;
31657
+ }
31658
+ this.worldOffsetMM = [targetMM[0] - nativeCenter[0], targetMM[1] - nativeCenter[1], targetMM[2] - nativeCenter[2]];
31659
+ this.centerX = this.levelDims.width / 2;
31660
+ this.centerY = this.levelDims.height / 2;
31661
+ this.centerZ = this.levelDims.depth / 2;
31662
+ this.clampCenter();
31663
+ this.updateAffine();
31664
+ }
31625
31665
  /**
31626
31666
  * Convert physical (mm) coordinates back to real zarr level pixel coordinates.
31627
31667
  * Inverts the affine: levelPixel = (mm - OME_translation) / scale
31628
31668
  */
31629
31669
  mmToLevelCoords(mmX, mmY, mmZ) {
31670
+ mmX -= this.worldOffsetMM[0];
31671
+ mmY -= this.worldOffsetMM[1];
31672
+ mmZ -= this.worldOffsetMM[2];
31630
31673
  const { scaleW, scaleH, scaleD } = this.getConvertedScales();
31631
31674
  const axisNames = this.pyramidInfo.axisMapping.spatialAxisNames;
31632
31675
  const mmByAxis = { x: mmX, y: mmY, z: mmZ };
@@ -31738,8 +31781,6 @@ var NVZarrHelper = class _NVZarrHelper {
31738
31781
  this.currentAbortController?.abort();
31739
31782
  const abortController = new AbortController();
31740
31783
  this.currentAbortController = abortController;
31741
- this.runningMin = Infinity;
31742
- this.runningMax = -Infinity;
31743
31784
  await new Promise((resolve2) => {
31744
31785
  requestAnimationFrame(() => resolve2());
31745
31786
  });
@@ -31852,14 +31893,16 @@ var NVZarrHelper = class _NVZarrHelper {
31852
31893
  if (actualChunkW <= 0 || actualChunkH <= 0 || actualChunkD <= 0) {
31853
31894
  return;
31854
31895
  }
31855
- const step = Math.max(1, Math.floor(data.length / 2e3));
31856
- for (let i = 0; i < data.length; i += step) {
31857
- const val = data[i];
31858
- if (val < this.runningMin) {
31859
- this.runningMin = val;
31860
- }
31861
- if (val > this.runningMax) {
31862
- this.runningMax = val;
31896
+ if (!this.calibrationDone) {
31897
+ const step = Math.max(1, Math.floor(data.length / 2e3));
31898
+ for (let i = 0; i < data.length; i += step) {
31899
+ const val = data[i];
31900
+ if (val < this.runningMin) {
31901
+ this.runningMin = val;
31902
+ }
31903
+ if (val > this.runningMax) {
31904
+ this.runningMax = val;
31905
+ }
31863
31906
  }
31864
31907
  }
31865
31908
  const fullChunkVol = this.chunkSize.width * this.chunkSize.height * this.chunkSize.depth;
@@ -31890,6 +31933,9 @@ var NVZarrHelper = class _NVZarrHelper {
31890
31933
  }
31891
31934
  }
31892
31935
  updateCalibration() {
31936
+ if (this.calibrationDone) {
31937
+ return;
31938
+ }
31893
31939
  if (this.runningMin < Infinity && this.runningMax > -Infinity) {
31894
31940
  this.hostImage.cal_min = this.runningMin;
31895
31941
  this.hostImage.cal_max = this.runningMax;
@@ -31897,6 +31943,7 @@ var NVZarrHelper = class _NVZarrHelper {
31897
31943
  this.hostImage.robust_max = this.runningMax;
31898
31944
  this.hostImage.global_min = this.runningMin;
31899
31945
  this.hostImage.global_max = this.runningMax;
31946
+ this.calibrationDone = true;
31900
31947
  }
31901
31948
  }
31902
31949
  /**
@@ -31990,6 +32037,7 @@ var NVImage = class _NVImage {
31990
32037
  __publicField(this, "onOpacityChange", () => {
31991
32038
  });
31992
32039
  __publicField(this, "zarrHelper", null);
32040
+ __publicField(this, "_hasExplicitZarrCenter", false);
31993
32041
  __publicField(this, "mm000");
31994
32042
  __publicField(this, "mm100");
31995
32043
  __publicField(this, "mm010");
@@ -32626,7 +32674,8 @@ var NVImage = class _NVImage {
32626
32674
  zarrLevel,
32627
32675
  zarrMaxVolumeSize,
32628
32676
  zarrChannel,
32629
- zarrConvertUnits
32677
+ zarrConvertUnits,
32678
+ zarrCenterMM
32630
32679
  } = {}) {
32631
32680
  if (url === "") {
32632
32681
  throw Error("url must not be empty");
@@ -32679,7 +32728,8 @@ var NVImage = class _NVImage {
32679
32728
  channel: zarrChannel,
32680
32729
  convertUnitsToMm: zarrConvertUnits,
32681
32730
  colormap,
32682
- opacity
32731
+ opacity,
32732
+ zarrCenterMM
32683
32733
  });
32684
32734
  }
32685
32735
  const zarrResult = await loadZarrData(url);
@@ -32758,6 +32808,10 @@ var NVImage = class _NVImage {
32758
32808
  cacheSize: options.cacheSize,
32759
32809
  convertUnitsToMm: options.convertUnitsToMm
32760
32810
  });
32811
+ if (options.zarrCenterMM) {
32812
+ nvimage.zarrHelper.setWorldCenter(options.zarrCenterMM);
32813
+ nvimage._hasExplicitZarrCenter = true;
32814
+ }
32761
32815
  if (options.colormap) {
32762
32816
  nvimage._colormap = options.colormap;
32763
32817
  }
@@ -45355,7 +45409,8 @@ var Niivue = class {
45355
45409
  zarrLevel: imageItem.zarrLevel,
45356
45410
  zarrMaxVolumeSize: imageItem.zarrMaxVolumeSize,
45357
45411
  zarrChannel: imageItem.zarrChannel,
45358
- zarrConvertUnits: imageItem.zarrConvertUnits
45412
+ zarrConvertUnits: imageItem.zarrConvertUnits,
45413
+ zarrCenterMM: imageItem.zarrCenterMM
45359
45414
  };
45360
45415
  const volume = await NVImage.loadFromUrl(imageOptions);
45361
45416
  this.document.addImageOptions(volume, imageOptions);
@@ -45935,6 +45990,20 @@ var Niivue = class {
45935
45990
  this.setVolume(volume, result.index);
45936
45991
  this.onImageLoaded(volume);
45937
45992
  if (volume.zarrHelper) {
45993
+ if (!volume._hasExplicitZarrCenter && this.back && this.back !== volume && this.back.hdr?.dims) {
45994
+ const bg = this.back;
45995
+ const dims = bg.hdr.dims;
45996
+ const affine = bg.hdr.affine;
45997
+ const ci = (dims[1] - 1) / 2;
45998
+ const cj = (dims[2] - 1) / 2;
45999
+ const ck = (dims[3] - 1) / 2;
46000
+ const bgCenter = [
46001
+ affine[0][0] * ci + affine[0][1] * cj + affine[0][2] * ck + affine[0][3],
46002
+ affine[1][0] * ci + affine[1][1] * cj + affine[1][2] * ck + affine[1][3],
46003
+ affine[2][0] * ci + affine[2][1] * cj + affine[2][2] * ck + affine[2][3]
46004
+ ];
46005
+ volume.zarrHelper.setWorldCenter(bgCenter);
46006
+ }
45938
46007
  volume.zarrHelper.onChunksUpdated = () => {
45939
46008
  this.updateGLVolume();
45940
46009
  this._skipDragInDraw = true;