@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.
- package/build/drawing/index.d.ts +1 -1
- package/build/drawing/index.js.map +1 -1
- package/build/index.min.js +1 -1
- package/build/metafile-esm.json +1 -1
- package/build/niivue/index.d.ts +3 -2
- package/build/niivue/index.js +171 -26
- package/build/niivue/index.js.map +1 -1
- package/build/{nvdocument-CcP6istx.d.ts → nvdocument-DrX6RoEG.d.ts} +32 -2
- package/build/utils/index.d.ts +1 -1
- package/dist/index.d.ts +33 -2
- package/dist/index.js +171 -26
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/metafile-esm.json +1 -1
- package/dist/niivue.umd.js +31 -31
- package/package.json +108 -108
- package/src/index_zarr_viewer.html +255 -68
- package/src/niivue/descriptives.ts +1 -1
- package/src/niivue/index.ts +40 -3
- package/src/nvimage/index.ts +11 -3
- package/src/nvimage/utils.ts +2 -0
- package/src/nvimage/zarr/NVZarrHelper.ts +174 -25
- package/src/index_zarr.html +0 -152
|
@@ -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
|
/**
|
package/build/utils/index.d.ts
CHANGED
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.
|
|
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 =
|
|
31435
|
-
img.cal_max =
|
|
31436
|
-
img.robust_min =
|
|
31437
|
-
img.robust_max =
|
|
31438
|
-
img.global_min =
|
|
31439
|
-
img.global_max =
|
|
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
|
-
|
|
31795
|
-
|
|
31796
|
-
|
|
31797
|
-
|
|
31798
|
-
this.runningMin
|
|
31799
|
-
|
|
31800
|
-
|
|
31801
|
-
this.runningMax
|
|
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
|
}
|