@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.
@@ -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;
@@ -770,6 +774,7 @@ declare class NVZarrHelper {
770
774
  onAllChunksLoaded?: () => void;
771
775
  private constructor();
772
776
  static create(hostImage: NVImage, url: string, options: NVZarrHelperOptions): Promise<NVZarrHelper>;
777
+ loadInitialChunks(): Promise<void>;
773
778
  private updateLevelInfo;
774
779
  private configureHostImage;
775
780
  /** Get unit-converted voxel scales */
@@ -791,6 +796,7 @@ declare class NVZarrHelper {
791
796
  */
792
797
  private updateAffine;
793
798
  beginDrag(): void;
799
+ endDrag(): void;
794
800
  panBy(dx: number, dy: number, dz?: number): Promise<void>;
795
801
  panTo(newCenterX: number, newCenterY: number, newCenterZ?: number): Promise<void>;
796
802
  setPyramidLevel(level: number): Promise<void>;
@@ -812,6 +818,28 @@ declare class NVZarrHelper {
812
818
  height: number;
813
819
  depth: number;
814
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;
828
+ /**
829
+ * Convert physical (mm) coordinates back to real zarr level pixel coordinates.
830
+ * Inverts the affine: levelPixel = (mm - OME_translation) / scale
831
+ */
832
+ mmToLevelCoords(mmX: number, mmY: number, mmZ: number): {
833
+ width: number;
834
+ height: number;
835
+ depth: number;
836
+ level: number;
837
+ levelDims: {
838
+ width: number;
839
+ height: number;
840
+ depth: number;
841
+ };
842
+ };
815
843
  private clampCenter;
816
844
  private getVisibleChunks;
817
845
  private updateVolume;
@@ -879,7 +907,7 @@ declare function copyAffine(affine: number[][]): number[][];
879
907
  */
880
908
  declare function transformsEqual(a: AffineTransform, b: AffineTransform, epsilon?: number): boolean;
881
909
 
882
- type TypedVoxelArray = Float32Array | Uint8Array | Int16Array | Float64Array | Uint16Array;
910
+ type TypedVoxelArray = Float32Array | Uint8Array | Int16Array | Float64Array | Uint16Array | Int32Array | Uint32Array;
883
911
  /**
884
912
  * a NVImage encapsulates some image data and provides methods to query and operate on images
885
913
  */
@@ -936,6 +964,7 @@ declare class NVImage {
936
964
  onColormapChange: (img: NVImage) => void;
937
965
  onOpacityChange: (img: NVImage) => void;
938
966
  zarrHelper: NVZarrHelper | null;
967
+ _hasExplicitZarrCenter: boolean;
939
968
  mm000?: vec3;
940
969
  mm100?: vec3;
941
970
  mm010?: vec3;
@@ -1039,7 +1068,7 @@ declare class NVImage {
1039
1068
  /**
1040
1069
  * factory function to load and return a new NVImage instance from a given URL
1041
1070
  */
1042
- 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'>> & {
1043
1072
  url?: string | Uint8Array | ArrayBuffer;
1044
1073
  }): Promise<NVImage>;
1045
1074
  /**
@@ -1054,6 +1083,7 @@ declare class NVImage {
1054
1083
  convertUnitsToMm?: boolean;
1055
1084
  colormap?: string;
1056
1085
  opacity?: number;
1086
+ zarrCenterMM?: [number, number, number];
1057
1087
  }): Promise<NVImage>;
1058
1088
  static readFileAsync(file: File, bytesToLoad?: number): Promise<ArrayBuffer>;
1059
1089
  /**
@@ -1,4 +1,4 @@
1
- import { ar as Volume, x as NiftiHeader } from '../nvdocument-CcP6istx.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;
@@ -567,6 +571,7 @@ declare class NVZarrHelper {
567
571
  onAllChunksLoaded?: () => void;
568
572
  private constructor();
569
573
  static create(hostImage: NVImage, url: string, options: NVZarrHelperOptions): Promise<NVZarrHelper>;
574
+ loadInitialChunks(): Promise<void>;
570
575
  private updateLevelInfo;
571
576
  private configureHostImage;
572
577
  /** Get unit-converted voxel scales */
@@ -588,6 +593,7 @@ declare class NVZarrHelper {
588
593
  */
589
594
  private updateAffine;
590
595
  beginDrag(): void;
596
+ endDrag(): void;
591
597
  panBy(dx: number, dy: number, dz?: number): Promise<void>;
592
598
  panTo(newCenterX: number, newCenterY: number, newCenterZ?: number): Promise<void>;
593
599
  setPyramidLevel(level: number): Promise<void>;
@@ -609,6 +615,28 @@ declare class NVZarrHelper {
609
615
  height: number;
610
616
  depth: number;
611
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;
625
+ /**
626
+ * Convert physical (mm) coordinates back to real zarr level pixel coordinates.
627
+ * Inverts the affine: levelPixel = (mm - OME_translation) / scale
628
+ */
629
+ mmToLevelCoords(mmX: number, mmY: number, mmZ: number): {
630
+ width: number;
631
+ height: number;
632
+ depth: number;
633
+ level: number;
634
+ levelDims: {
635
+ width: number;
636
+ height: number;
637
+ depth: number;
638
+ };
639
+ };
612
640
  private clampCenter;
613
641
  private getVisibleChunks;
614
642
  private updateVolume;
@@ -676,7 +704,7 @@ declare function copyAffine(affine: number[][]): number[][];
676
704
  */
677
705
  declare function transformsEqual(a: AffineTransform, b: AffineTransform, epsilon?: number): boolean;
678
706
 
679
- type TypedVoxelArray = Float32Array | Uint8Array | Int16Array | Float64Array | Uint16Array;
707
+ type TypedVoxelArray = Float32Array | Uint8Array | Int16Array | Float64Array | Uint16Array | Int32Array | Uint32Array;
680
708
  /**
681
709
  * a NVImage encapsulates some image data and provides methods to query and operate on images
682
710
  */
@@ -733,6 +761,7 @@ declare class NVImage {
733
761
  onColormapChange: (img: NVImage) => void;
734
762
  onOpacityChange: (img: NVImage) => void;
735
763
  zarrHelper: NVZarrHelper | null;
764
+ _hasExplicitZarrCenter: boolean;
736
765
  mm000?: vec3;
737
766
  mm100?: vec3;
738
767
  mm010?: vec3;
@@ -836,7 +865,7 @@ declare class NVImage {
836
865
  /**
837
866
  * factory function to load and return a new NVImage instance from a given URL
838
867
  */
839
- 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'>> & {
840
869
  url?: string | Uint8Array | ArrayBuffer;
841
870
  }): Promise<NVImage>;
842
871
  /**
@@ -851,6 +880,7 @@ declare class NVImage {
851
880
  convertUnitsToMm?: boolean;
852
881
  colormap?: string;
853
882
  opacity?: number;
883
+ zarrCenterMM?: [number, number, number];
854
884
  }): Promise<NVImage>;
855
885
  static readFileAsync(file: File, bytesToLoad?: number): Promise<ArrayBuffer>;
856
886
  /**
@@ -2314,6 +2344,7 @@ declare class Niivue {
2314
2344
  private canvasObserver;
2315
2345
  syncOpts: SyncOpts;
2316
2346
  readyForSync: boolean;
2347
+ private _skipDragInDraw;
2317
2348
  uiData: UIData;
2318
2349
  back: NVImage | null;
2319
2350
  overlays: NVImage[];
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.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",
@@ -31227,9 +31227,11 @@ function createTypedVoxelArray(datatypeCode, size) {
31227
31227
  case 512 /* DT_UINT16 */:
31228
31228
  return new Uint16Array(size);
31229
31229
  case 4 /* DT_INT16 */:
31230
- case 8 /* DT_INT32 */:
31231
31230
  return new Int16Array(size);
31231
+ case 8 /* DT_INT32 */:
31232
+ return new Int32Array(size);
31232
31233
  case 768 /* DT_UINT32 */:
31234
+ return new Uint32Array(size);
31233
31235
  case 16 /* DT_FLOAT32 */:
31234
31236
  return new Float32Array(size);
31235
31237
  case 64 /* DT_FLOAT64 */:
@@ -31281,6 +31283,7 @@ var NVZarrHelper = class _NVZarrHelper {
31281
31283
  __publicField(this, "voxelTranslations");
31282
31284
  __publicField(this, "hasTranslations");
31283
31285
  __publicField(this, "convertUnitsToMm");
31286
+ __publicField(this, "worldOffsetMM", [0, 0, 0]);
31284
31287
  __publicField(this, "centerX");
31285
31288
  __publicField(this, "centerY");
31286
31289
  __publicField(this, "centerZ");
@@ -31291,6 +31294,7 @@ var NVZarrHelper = class _NVZarrHelper {
31291
31294
  __publicField(this, "currentAbortController", null);
31292
31295
  __publicField(this, "runningMin", Infinity);
31293
31296
  __publicField(this, "runningMax", -Infinity);
31297
+ __publicField(this, "calibrationDone", false);
31294
31298
  // Debounce state for batching chunk updates
31295
31299
  __publicField(this, "updateDebounceTimer", null);
31296
31300
  __publicField(this, "UPDATE_DEBOUNCE_MS", 50);
@@ -31357,9 +31361,11 @@ var NVZarrHelper = class _NVZarrHelper {
31357
31361
  helper.centerY = helper.levelDims.height / 2;
31358
31362
  helper.centerZ = helper.levelDims.depth / 2;
31359
31363
  helper.configureHostImage();
31360
- await helper.updateVolume();
31361
31364
  return helper;
31362
31365
  }
31366
+ async loadInitialChunks() {
31367
+ await this.updateVolume();
31368
+ }
31363
31369
  updateLevelInfo() {
31364
31370
  const levelInfo = this.pyramidInfo.levels[this.pyramidLevel];
31365
31371
  const shape = levelInfo.shape;
@@ -31431,12 +31437,12 @@ var NVZarrHelper = class _NVZarrHelper {
31431
31437
  img.img = createTypedVoxelArray(this.datatypeCode, width * height * depth);
31432
31438
  this.updateAffine();
31433
31439
  img.originalAffine = copyAffine(hdr.affine);
31434
- img.cal_min = NaN;
31435
- img.cal_max = NaN;
31436
- img.robust_min = NaN;
31437
- img.robust_max = NaN;
31438
- img.global_min = NaN;
31439
- img.global_max = NaN;
31440
+ img.cal_min = 0;
31441
+ img.cal_max = 0;
31442
+ img.robust_min = 0;
31443
+ img.robust_max = 0;
31444
+ img.global_min = 0;
31445
+ img.global_max = 0;
31440
31446
  }
31441
31447
  /** Get unit-converted voxel scales */
31442
31448
  getConvertedScales() {
@@ -31516,6 +31522,9 @@ var NVZarrHelper = class _NVZarrHelper {
31516
31522
  affine[row][col] = niftiCols[col].scale;
31517
31523
  affine[row][3] = niftiCols[col].trans;
31518
31524
  }
31525
+ affine[0][3] += this.worldOffsetMM[0];
31526
+ affine[1][3] += this.worldOffsetMM[1];
31527
+ affine[2][3] += this.worldOffsetMM[2];
31519
31528
  hdr.affine = affine;
31520
31529
  } else {
31521
31530
  const { scaleW, scaleH, scaleD } = this.getConvertedScales();
@@ -31539,12 +31548,15 @@ var NVZarrHelper = class _NVZarrHelper {
31539
31548
  affine[row][col] = niftiCols[col].scale;
31540
31549
  affine[row][3] = niftiCols[col].trans;
31541
31550
  }
31551
+ affine[0][3] += this.worldOffsetMM[0];
31552
+ affine[1][3] += this.worldOffsetMM[1];
31553
+ affine[2][3] += this.worldOffsetMM[2];
31542
31554
  hdr.affine = affine;
31543
31555
  } else {
31544
31556
  hdr.affine = [
31545
- [scaleW, 0, 0, volStartW * scaleW],
31546
- [0, -scaleH, 0, volStartH * scaleH],
31547
- [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]],
31548
31560
  [0, 0, 0, 1]
31549
31561
  ];
31550
31562
  }
@@ -31555,6 +31567,9 @@ var NVZarrHelper = class _NVZarrHelper {
31555
31567
  beginDrag() {
31556
31568
  this.centerAtDragStart = { x: this.centerX, y: this.centerY, z: this.centerZ };
31557
31569
  }
31570
+ endDrag() {
31571
+ this.centerAtDragStart = null;
31572
+ }
31558
31573
  async panBy(dx, dy, dz = 0) {
31559
31574
  this.centerX -= dx;
31560
31575
  this.centerY -= dy;
@@ -31564,12 +31579,18 @@ var NVZarrHelper = class _NVZarrHelper {
31564
31579
  await this.updateVolume();
31565
31580
  }
31566
31581
  async panTo(newCenterX, newCenterY, newCenterZ) {
31582
+ const prevX = this.centerX;
31583
+ const prevY = this.centerY;
31584
+ const prevZ = this.centerZ;
31567
31585
  this.centerX = newCenterX;
31568
31586
  this.centerY = newCenterY;
31569
31587
  if (newCenterZ !== void 0) {
31570
31588
  this.centerZ = newCenterZ;
31571
31589
  }
31572
31590
  this.clampCenter();
31591
+ if (Math.abs(this.centerX - prevX) < 1e-3 && Math.abs(this.centerY - prevY) < 1e-3 && Math.abs(this.centerZ - prevZ) < 1e-3) {
31592
+ return;
31593
+ }
31573
31594
  this.updateAffine();
31574
31595
  await this.updateVolume();
31575
31596
  }
@@ -31609,6 +31630,89 @@ var NVZarrHelper = class _NVZarrHelper {
31609
31630
  getVolumeDims() {
31610
31631
  return { ...this.volumeDims };
31611
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
+ }
31665
+ /**
31666
+ * Convert physical (mm) coordinates back to real zarr level pixel coordinates.
31667
+ * Inverts the affine: levelPixel = (mm - OME_translation) / scale
31668
+ */
31669
+ mmToLevelCoords(mmX, mmY, mmZ) {
31670
+ mmX -= this.worldOffsetMM[0];
31671
+ mmY -= this.worldOffsetMM[1];
31672
+ mmZ -= this.worldOffsetMM[2];
31673
+ const { scaleW, scaleH, scaleD } = this.getConvertedScales();
31674
+ const axisNames = this.pyramidInfo.axisMapping.spatialAxisNames;
31675
+ const mmByAxis = { x: mmX, y: mmY, z: mmZ };
31676
+ let levelW = 0;
31677
+ let levelH = 0;
31678
+ let levelD = 0;
31679
+ if (axisNames.length >= 2) {
31680
+ const wAxisName = axisNames[axisNames.length - 1];
31681
+ const hAxisName = axisNames[axisNames.length - 2];
31682
+ const dAxisName = axisNames.length >= 3 ? axisNames[axisNames.length - 3] : "z";
31683
+ const mmW = mmByAxis[wAxisName] ?? mmX;
31684
+ const mmH = mmByAxis[hAxisName] ?? mmY;
31685
+ const mmD = mmByAxis[dAxisName] ?? mmZ;
31686
+ if (this.hasTranslations) {
31687
+ const { transW, transH, transD } = this.getConvertedTranslations();
31688
+ levelW = scaleW !== 0 ? (mmW - transW) / scaleW : 0;
31689
+ levelH = scaleH !== 0 ? (mmH - transH) / scaleH : 0;
31690
+ levelD = scaleD !== 0 ? (mmD - transD) / scaleD : 0;
31691
+ } else {
31692
+ levelW = scaleW !== 0 ? mmW / scaleW : 0;
31693
+ levelH = scaleH !== 0 ? mmH / scaleH : 0;
31694
+ levelD = scaleD !== 0 ? mmD / scaleD : 0;
31695
+ }
31696
+ } else {
31697
+ if (this.hasTranslations) {
31698
+ const { transW, transH, transD } = this.getConvertedTranslations();
31699
+ levelW = scaleW !== 0 ? (mmX - transW) / scaleW : 0;
31700
+ levelH = scaleH !== 0 ? (mmY - transH) / scaleH : 0;
31701
+ levelD = scaleD !== 0 ? (mmZ - transD) / scaleD : 0;
31702
+ } else {
31703
+ levelW = scaleW !== 0 ? mmX / scaleW : 0;
31704
+ levelH = scaleH !== 0 ? mmY / scaleH : 0;
31705
+ levelD = scaleD !== 0 ? mmZ / scaleD : 0;
31706
+ }
31707
+ }
31708
+ return {
31709
+ width: Math.round(levelW),
31710
+ height: Math.round(levelH),
31711
+ depth: Math.round(levelD),
31712
+ level: this.pyramidLevel,
31713
+ levelDims: { ...this.levelDims }
31714
+ };
31715
+ }
31612
31716
  clampCenter() {
31613
31717
  const halfW = this.volumeDims.width / 2;
31614
31718
  const halfH = this.volumeDims.height / 2;
@@ -31677,8 +31781,6 @@ var NVZarrHelper = class _NVZarrHelper {
31677
31781
  this.currentAbortController?.abort();
31678
31782
  const abortController = new AbortController();
31679
31783
  this.currentAbortController = abortController;
31680
- this.runningMin = Infinity;
31681
- this.runningMax = -Infinity;
31682
31784
  await new Promise((resolve2) => {
31683
31785
  requestAnimationFrame(() => resolve2());
31684
31786
  });
@@ -31791,14 +31893,16 @@ var NVZarrHelper = class _NVZarrHelper {
31791
31893
  if (actualChunkW <= 0 || actualChunkH <= 0 || actualChunkD <= 0) {
31792
31894
  return;
31793
31895
  }
31794
- const step = Math.max(1, Math.floor(data.length / 2e3));
31795
- for (let i = 0; i < data.length; i += step) {
31796
- const val = data[i];
31797
- if (val < this.runningMin) {
31798
- this.runningMin = val;
31799
- }
31800
- if (val > this.runningMax) {
31801
- 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
+ }
31802
31906
  }
31803
31907
  }
31804
31908
  const fullChunkVol = this.chunkSize.width * this.chunkSize.height * this.chunkSize.depth;
@@ -31829,6 +31933,9 @@ var NVZarrHelper = class _NVZarrHelper {
31829
31933
  }
31830
31934
  }
31831
31935
  updateCalibration() {
31936
+ if (this.calibrationDone) {
31937
+ return;
31938
+ }
31832
31939
  if (this.runningMin < Infinity && this.runningMax > -Infinity) {
31833
31940
  this.hostImage.cal_min = this.runningMin;
31834
31941
  this.hostImage.cal_max = this.runningMax;
@@ -31836,6 +31943,7 @@ var NVZarrHelper = class _NVZarrHelper {
31836
31943
  this.hostImage.robust_max = this.runningMax;
31837
31944
  this.hostImage.global_min = this.runningMin;
31838
31945
  this.hostImage.global_max = this.runningMax;
31946
+ this.calibrationDone = true;
31839
31947
  }
31840
31948
  }
31841
31949
  /**
@@ -31929,6 +32037,7 @@ var NVImage = class _NVImage {
31929
32037
  __publicField(this, "onOpacityChange", () => {
31930
32038
  });
31931
32039
  __publicField(this, "zarrHelper", null);
32040
+ __publicField(this, "_hasExplicitZarrCenter", false);
31932
32041
  __publicField(this, "mm000");
31933
32042
  __publicField(this, "mm100");
31934
32043
  __publicField(this, "mm010");
@@ -32565,7 +32674,8 @@ var NVImage = class _NVImage {
32565
32674
  zarrLevel,
32566
32675
  zarrMaxVolumeSize,
32567
32676
  zarrChannel,
32568
- zarrConvertUnits
32677
+ zarrConvertUnits,
32678
+ zarrCenterMM
32569
32679
  } = {}) {
32570
32680
  if (url === "") {
32571
32681
  throw Error("url must not be empty");
@@ -32618,7 +32728,8 @@ var NVImage = class _NVImage {
32618
32728
  channel: zarrChannel,
32619
32729
  convertUnitsToMm: zarrConvertUnits,
32620
32730
  colormap,
32621
- opacity
32731
+ opacity,
32732
+ zarrCenterMM
32622
32733
  });
32623
32734
  }
32624
32735
  const zarrResult = await loadZarrData(url);
@@ -32697,6 +32808,10 @@ var NVImage = class _NVImage {
32697
32808
  cacheSize: options.cacheSize,
32698
32809
  convertUnitsToMm: options.convertUnitsToMm
32699
32810
  });
32811
+ if (options.zarrCenterMM) {
32812
+ nvimage.zarrHelper.setWorldCenter(options.zarrCenterMM);
32813
+ nvimage._hasExplicitZarrCenter = true;
32814
+ }
32700
32815
  if (options.colormap) {
32701
32816
  nvimage._colormap = options.colormap;
32702
32817
  }
@@ -43414,6 +43529,7 @@ var Niivue = class {
43414
43529
  crosshair: false
43415
43530
  });
43416
43531
  __publicField(this, "readyForSync", false);
43532
+ __publicField(this, "_skipDragInDraw", false);
43417
43533
  // UI Data
43418
43534
  __publicField(this, "uiData", {
43419
43535
  mousedown: false,
@@ -44510,6 +44626,9 @@ var Niivue = class {
44510
44626
  }
44511
44627
  if (this.uiData.isDragging) {
44512
44628
  this.uiData.isDragging = false;
44629
+ for (const vol of this.getZarrVolumes()) {
44630
+ vol.zarrHelper?.endDrag();
44631
+ }
44513
44632
  if (currentDragMode === 7 /* angle */) {
44514
44633
  if (this.uiData.angleState === "drawing_first_line") {
44515
44634
  this.uiData.angleFirstLine = [this.uiData.dragStart[0], this.uiData.dragStart[1], this.uiData.dragEnd[0], this.uiData.dragEnd[1]];
@@ -44655,6 +44774,9 @@ var Niivue = class {
44655
44774
  }
44656
44775
  if (this.uiData.isDragging) {
44657
44776
  this.uiData.isDragging = false;
44777
+ for (const vol of this.getZarrVolumes()) {
44778
+ vol.zarrHelper?.endDrag();
44779
+ }
44658
44780
  if (this.getCurrentDragMode() === 1 /* contrast */) {
44659
44781
  this.calculateNewRange();
44660
44782
  this.refreshLayers(this.volumes[0], 0);
@@ -44714,6 +44836,9 @@ var Niivue = class {
44714
44836
  if (this.uiData.isDragging || this.uiData.mousedown) {
44715
44837
  log.debug("Mouse left canvas during drag, resetting drag state.");
44716
44838
  this.uiData.isDragging = false;
44839
+ for (const vol of this.getZarrVolumes()) {
44840
+ vol.zarrHelper?.endDrag();
44841
+ }
44717
44842
  const resetState = createResetButtonState();
44718
44843
  this.uiData.mouseButtonLeftDown = resetState.mouseButtonLeftDown;
44719
44844
  this.uiData.mouseButtonCenterDown = resetState.mouseButtonCenterDown;
@@ -45284,7 +45409,8 @@ var Niivue = class {
45284
45409
  zarrLevel: imageItem.zarrLevel,
45285
45410
  zarrMaxVolumeSize: imageItem.zarrMaxVolumeSize,
45286
45411
  zarrChannel: imageItem.zarrChannel,
45287
- zarrConvertUnits: imageItem.zarrConvertUnits
45412
+ zarrConvertUnits: imageItem.zarrConvertUnits,
45413
+ zarrCenterMM: imageItem.zarrCenterMM
45288
45414
  };
45289
45415
  const volume = await NVImage.loadFromUrl(imageOptions);
45290
45416
  this.document.addImageOptions(volume, imageOptions);
@@ -45864,10 +45990,27 @@ var Niivue = class {
45864
45990
  this.setVolume(volume, result.index);
45865
45991
  this.onImageLoaded(volume);
45866
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
+ }
45867
46007
  volume.zarrHelper.onChunksUpdated = () => {
45868
46008
  this.updateGLVolume();
46009
+ this._skipDragInDraw = true;
45869
46010
  this.drawScene();
46011
+ this._skipDragInDraw = false;
45870
46012
  };
46013
+ volume.zarrHelper.loadInitialChunks().catch((err2) => log.error("Failed to load initial zarr chunks", err2));
45871
46014
  }
45872
46015
  log.debug("loaded volume", volume.name);
45873
46016
  log.debug(volume);
@@ -49994,7 +50137,9 @@ var Niivue = class {
49994
50137
  }
49995
50138
  if (panPromises.length > 0) {
49996
50139
  Promise.all(panPromises).then(() => {
50140
+ this._skipDragInDraw = true;
49997
50141
  this.drawScene();
50142
+ this._skipDragInDraw = false;
49998
50143
  }).catch((err2) => {
49999
50144
  log.error("Failed to pan Zarr:", err2);
50000
50145
  });
@@ -53271,7 +53416,7 @@ var Niivue = class {
53271
53416
  this.dragForSlicer3D([this.uiData.dragStart[0], this.uiData.dragStart[1], this.uiData.dragEnd[0], this.uiData.dragEnd[1]]);
53272
53417
  return;
53273
53418
  }
53274
- if (this.getCurrentDragMode() === 3 /* pan */) {
53419
+ if (this.getCurrentDragMode() === 3 /* pan */ && !this._skipDragInDraw) {
53275
53420
  this.dragForPanZoom([this.uiData.dragStart[0], this.uiData.dragStart[1], this.uiData.dragEnd[0], this.uiData.dragEnd[1]]);
53276
53421
  return;
53277
53422
  }