@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
package/src/nvimage/utils.ts
CHANGED
|
@@ -210,6 +210,8 @@ export type ImageFromUrlOptions = {
|
|
|
210
210
|
zarrChannel?: number
|
|
211
211
|
/** Convert OME spatial units to millimeters for NIfTI compatibility (default: true) */
|
|
212
212
|
zarrConvertUnits?: boolean
|
|
213
|
+
/** World-space center [x, y, z] in mm where the zarr volume center should be positioned */
|
|
214
|
+
zarrCenterMM?: [number, number, number]
|
|
213
215
|
}
|
|
214
216
|
|
|
215
217
|
// TODO centralize shared options
|
|
@@ -95,9 +95,11 @@ function createTypedVoxelArray(datatypeCode: number, size: number): TypedVoxelAr
|
|
|
95
95
|
case NiiDataType.DT_UINT16:
|
|
96
96
|
return new Uint16Array(size)
|
|
97
97
|
case NiiDataType.DT_INT16:
|
|
98
|
-
case NiiDataType.DT_INT32:
|
|
99
98
|
return new Int16Array(size)
|
|
99
|
+
case NiiDataType.DT_INT32:
|
|
100
|
+
return new Int32Array(size)
|
|
100
101
|
case NiiDataType.DT_UINT32:
|
|
102
|
+
return new Uint32Array(size)
|
|
101
103
|
case NiiDataType.DT_FLOAT32:
|
|
102
104
|
return new Float32Array(size)
|
|
103
105
|
case NiiDataType.DT_FLOAT64:
|
|
@@ -156,6 +158,7 @@ export class NVZarrHelper {
|
|
|
156
158
|
private voxelTranslations: { width: number; height: number; depth: number }
|
|
157
159
|
private hasTranslations: boolean
|
|
158
160
|
private convertUnitsToMm: boolean
|
|
161
|
+
private worldOffsetMM: [number, number, number] = [0, 0, 0]
|
|
159
162
|
|
|
160
163
|
private centerX: number
|
|
161
164
|
private centerY: number
|
|
@@ -169,6 +172,7 @@ export class NVZarrHelper {
|
|
|
169
172
|
private currentAbortController: AbortController | null = null
|
|
170
173
|
private runningMin = Infinity
|
|
171
174
|
private runningMax = -Infinity
|
|
175
|
+
private calibrationDone = false
|
|
172
176
|
|
|
173
177
|
// Debounce state for batching chunk updates
|
|
174
178
|
private updateDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
@@ -248,7 +252,7 @@ export class NVZarrHelper {
|
|
|
248
252
|
helper.pyramidLevel = level
|
|
249
253
|
helper.updateLevelInfo()
|
|
250
254
|
|
|
251
|
-
// Center on the level
|
|
255
|
+
// Center on the level (use levelDims so initial view shows the center of the zarr)
|
|
252
256
|
helper.centerX = helper.levelDims.width / 2
|
|
253
257
|
helper.centerY = helper.levelDims.height / 2
|
|
254
258
|
helper.centerZ = helper.levelDims.depth / 2
|
|
@@ -256,12 +260,16 @@ export class NVZarrHelper {
|
|
|
256
260
|
// Configure the host NVImage
|
|
257
261
|
helper.configureHostImage()
|
|
258
262
|
|
|
259
|
-
//
|
|
260
|
-
|
|
263
|
+
// Don't load chunks here — loadInitialChunks() must be called after
|
|
264
|
+
// the onChunksUpdated callback is registered (see addVolume in Niivue).
|
|
261
265
|
|
|
262
266
|
return helper
|
|
263
267
|
}
|
|
264
268
|
|
|
269
|
+
async loadInitialChunks(): Promise<void> {
|
|
270
|
+
await this.updateVolume()
|
|
271
|
+
}
|
|
272
|
+
|
|
265
273
|
private updateLevelInfo(): void {
|
|
266
274
|
const levelInfo = this.pyramidInfo.levels[this.pyramidLevel]
|
|
267
275
|
const shape = levelInfo.shape
|
|
@@ -352,12 +360,12 @@ export class NVZarrHelper {
|
|
|
352
360
|
// Store original affine so resetVolumeAffine() works on zarr volumes
|
|
353
361
|
img.originalAffine = copyAffine(hdr.affine)
|
|
354
362
|
|
|
355
|
-
img.cal_min =
|
|
356
|
-
img.cal_max =
|
|
357
|
-
img.robust_min =
|
|
358
|
-
img.robust_max =
|
|
359
|
-
img.global_min =
|
|
360
|
-
img.global_max =
|
|
363
|
+
img.cal_min = 0
|
|
364
|
+
img.cal_max = 0
|
|
365
|
+
img.robust_min = 0
|
|
366
|
+
img.robust_max = 0
|
|
367
|
+
img.global_min = 0
|
|
368
|
+
img.global_max = 0
|
|
361
369
|
}
|
|
362
370
|
|
|
363
371
|
/** Get unit-converted voxel scales */
|
|
@@ -451,6 +459,9 @@ export class NVZarrHelper {
|
|
|
451
459
|
affine[row][col] = niftiCols[col].scale
|
|
452
460
|
affine[row][3] = niftiCols[col].trans
|
|
453
461
|
}
|
|
462
|
+
affine[0][3] += this.worldOffsetMM[0]
|
|
463
|
+
affine[1][3] += this.worldOffsetMM[1]
|
|
464
|
+
affine[2][3] += this.worldOffsetMM[2]
|
|
454
465
|
hdr.affine = affine
|
|
455
466
|
} else {
|
|
456
467
|
// No OME translations — calculate offset based on pan position
|
|
@@ -481,13 +492,16 @@ export class NVZarrHelper {
|
|
|
481
492
|
affine[row][col] = niftiCols[col].scale
|
|
482
493
|
affine[row][3] = niftiCols[col].trans
|
|
483
494
|
}
|
|
495
|
+
affine[0][3] += this.worldOffsetMM[0]
|
|
496
|
+
affine[1][3] += this.worldOffsetMM[1]
|
|
497
|
+
affine[2][3] += this.worldOffsetMM[2]
|
|
484
498
|
hdr.affine = affine
|
|
485
499
|
} else {
|
|
486
500
|
// Fallback: simple diagonal affine with pan position
|
|
487
501
|
hdr.affine = [
|
|
488
|
-
[scaleW, 0, 0, volStartW * scaleW],
|
|
489
|
-
[0, -scaleH, 0, volStartH * scaleH],
|
|
490
|
-
[0, 0, -scaleD, volStartD * scaleD],
|
|
502
|
+
[scaleW, 0, 0, volStartW * scaleW + this.worldOffsetMM[0]],
|
|
503
|
+
[0, -scaleH, 0, volStartH * scaleH + this.worldOffsetMM[1]],
|
|
504
|
+
[0, 0, -scaleD, volStartD * scaleD + this.worldOffsetMM[2]],
|
|
491
505
|
[0, 0, 0, 1]
|
|
492
506
|
]
|
|
493
507
|
}
|
|
@@ -505,6 +519,10 @@ export class NVZarrHelper {
|
|
|
505
519
|
this.centerAtDragStart = { x: this.centerX, y: this.centerY, z: this.centerZ }
|
|
506
520
|
}
|
|
507
521
|
|
|
522
|
+
endDrag(): void {
|
|
523
|
+
this.centerAtDragStart = null
|
|
524
|
+
}
|
|
525
|
+
|
|
508
526
|
async panBy(dx: number, dy: number, dz: number = 0): Promise<void> {
|
|
509
527
|
this.centerX -= dx
|
|
510
528
|
this.centerY -= dy
|
|
@@ -515,12 +533,22 @@ export class NVZarrHelper {
|
|
|
515
533
|
}
|
|
516
534
|
|
|
517
535
|
async panTo(newCenterX: number, newCenterY: number, newCenterZ?: number): Promise<void> {
|
|
536
|
+
const prevX = this.centerX
|
|
537
|
+
const prevY = this.centerY
|
|
538
|
+
const prevZ = this.centerZ
|
|
539
|
+
|
|
518
540
|
this.centerX = newCenterX
|
|
519
541
|
this.centerY = newCenterY
|
|
520
542
|
if (newCenterZ !== undefined) {
|
|
521
543
|
this.centerZ = newCenterZ
|
|
522
544
|
}
|
|
523
545
|
this.clampCenter()
|
|
546
|
+
|
|
547
|
+
// Skip if clamped position is same as before (prevents infinite loop at boundaries)
|
|
548
|
+
if (Math.abs(this.centerX - prevX) < 0.001 && Math.abs(this.centerY - prevY) < 0.001 && Math.abs(this.centerZ - prevZ) < 0.001) {
|
|
549
|
+
return
|
|
550
|
+
}
|
|
551
|
+
|
|
524
552
|
this.updateAffine()
|
|
525
553
|
await this.updateVolume()
|
|
526
554
|
}
|
|
@@ -571,6 +599,124 @@ export class NVZarrHelper {
|
|
|
571
599
|
return { ...this.volumeDims }
|
|
572
600
|
}
|
|
573
601
|
|
|
602
|
+
getWorldOffset(): [number, number, number] {
|
|
603
|
+
return [...this.worldOffsetMM]
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Set the world-space offset so the full level's center maps to targetMM in world space.
|
|
608
|
+
* Computes the native physical center of the zarr level, then sets worldOffsetMM
|
|
609
|
+
* so that center aligns with targetMM. Also centers the viewport on the level center.
|
|
610
|
+
*/
|
|
611
|
+
setWorldCenter(targetMM: [number, number, number]): void {
|
|
612
|
+
const { scaleW, scaleH, scaleD } = this.getConvertedScales()
|
|
613
|
+
const { transW, transH, transD } = this.getConvertedTranslations()
|
|
614
|
+
const axisNames = this.pyramidInfo.axisMapping.spatialAxisNames
|
|
615
|
+
|
|
616
|
+
// Native physical center per OME dim (width=last, height=2nd-to-last, depth=3rd-to-last)
|
|
617
|
+
const physW = (this.levelDims.width / 2) * scaleW + transW
|
|
618
|
+
const physH = (this.levelDims.height / 2) * scaleH + transH
|
|
619
|
+
const physD = (this.levelDims.depth / 2) * scaleD + transD
|
|
620
|
+
|
|
621
|
+
// Map OME dims to x/y/z rows using axis names
|
|
622
|
+
const nativeCenter: [number, number, number] = [0, 0, 0]
|
|
623
|
+
const dims = [
|
|
624
|
+
{ name: axisNames[axisNames.length - 1], phys: physW },
|
|
625
|
+
{ name: axisNames[axisNames.length - 2], phys: physH },
|
|
626
|
+
{ name: axisNames.length >= 3 ? axisNames[axisNames.length - 3] : 'z', phys: physD }
|
|
627
|
+
]
|
|
628
|
+
for (const d of dims) {
|
|
629
|
+
const row = d.name === 'x' ? 0 : d.name === 'y' ? 1 : 2
|
|
630
|
+
nativeCenter[row] = d.phys
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
this.worldOffsetMM = [targetMM[0] - nativeCenter[0], targetMM[1] - nativeCenter[1], targetMM[2] - nativeCenter[2]]
|
|
634
|
+
|
|
635
|
+
// Center viewport on level center so visible region is at the target
|
|
636
|
+
this.centerX = this.levelDims.width / 2
|
|
637
|
+
this.centerY = this.levelDims.height / 2
|
|
638
|
+
this.centerZ = this.levelDims.depth / 2
|
|
639
|
+
this.clampCenter()
|
|
640
|
+
|
|
641
|
+
this.updateAffine()
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Convert physical (mm) coordinates back to real zarr level pixel coordinates.
|
|
646
|
+
* Inverts the affine: levelPixel = (mm - OME_translation) / scale
|
|
647
|
+
*/
|
|
648
|
+
mmToLevelCoords(
|
|
649
|
+
mmX: number,
|
|
650
|
+
mmY: number,
|
|
651
|
+
mmZ: number
|
|
652
|
+
): {
|
|
653
|
+
width: number
|
|
654
|
+
height: number
|
|
655
|
+
depth: number
|
|
656
|
+
level: number
|
|
657
|
+
levelDims: { width: number; height: number; depth: number }
|
|
658
|
+
} {
|
|
659
|
+
// Subtract world offset to convert from offset world-space back to native zarr space
|
|
660
|
+
mmX -= this.worldOffsetMM[0]
|
|
661
|
+
mmY -= this.worldOffsetMM[1]
|
|
662
|
+
mmZ -= this.worldOffsetMM[2]
|
|
663
|
+
|
|
664
|
+
const { scaleW, scaleH, scaleD } = this.getConvertedScales()
|
|
665
|
+
const axisNames = this.pyramidInfo.axisMapping.spatialAxisNames
|
|
666
|
+
|
|
667
|
+
// Map physical mm (x/y/z) to OME spatial dimensions (width/height/depth)
|
|
668
|
+
// The affine maps NIfTI cols to physical rows by axis name:
|
|
669
|
+
// col 0 (width) → axis[-1], col 1 (height) → axis[-2], col 2 (depth) → axis[-3]
|
|
670
|
+
// We need the reverse: given physical x/y/z, find which OME dim it maps to
|
|
671
|
+
const mmByAxis: Record<string, number> = { x: mmX, y: mmY, z: mmZ }
|
|
672
|
+
|
|
673
|
+
let levelW = 0
|
|
674
|
+
let levelH = 0
|
|
675
|
+
let levelD = 0
|
|
676
|
+
|
|
677
|
+
if (axisNames.length >= 2) {
|
|
678
|
+
// Width maps to last spatial axis name
|
|
679
|
+
const wAxisName = axisNames[axisNames.length - 1]
|
|
680
|
+
const hAxisName = axisNames[axisNames.length - 2]
|
|
681
|
+
const dAxisName = axisNames.length >= 3 ? axisNames[axisNames.length - 3] : 'z'
|
|
682
|
+
|
|
683
|
+
const mmW = mmByAxis[wAxisName] ?? mmX
|
|
684
|
+
const mmH = mmByAxis[hAxisName] ?? mmY
|
|
685
|
+
const mmD = mmByAxis[dAxisName] ?? mmZ
|
|
686
|
+
|
|
687
|
+
if (this.hasTranslations) {
|
|
688
|
+
const { transW, transH, transD } = this.getConvertedTranslations()
|
|
689
|
+
levelW = scaleW !== 0 ? (mmW - transW) / scaleW : 0
|
|
690
|
+
levelH = scaleH !== 0 ? (mmH - transH) / scaleH : 0
|
|
691
|
+
levelD = scaleD !== 0 ? (mmD - transD) / scaleD : 0
|
|
692
|
+
} else {
|
|
693
|
+
levelW = scaleW !== 0 ? mmW / scaleW : 0
|
|
694
|
+
levelH = scaleH !== 0 ? mmH / scaleH : 0
|
|
695
|
+
levelD = scaleD !== 0 ? mmD / scaleD : 0
|
|
696
|
+
}
|
|
697
|
+
} else {
|
|
698
|
+
// Fallback: simple diagonal
|
|
699
|
+
if (this.hasTranslations) {
|
|
700
|
+
const { transW, transH, transD } = this.getConvertedTranslations()
|
|
701
|
+
levelW = scaleW !== 0 ? (mmX - transW) / scaleW : 0
|
|
702
|
+
levelH = scaleH !== 0 ? (mmY - transH) / scaleH : 0
|
|
703
|
+
levelD = scaleD !== 0 ? (mmZ - transD) / scaleD : 0
|
|
704
|
+
} else {
|
|
705
|
+
levelW = scaleW !== 0 ? mmX / scaleW : 0
|
|
706
|
+
levelH = scaleH !== 0 ? mmY / scaleH : 0
|
|
707
|
+
levelD = scaleD !== 0 ? mmZ / scaleD : 0
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return {
|
|
712
|
+
width: Math.round(levelW),
|
|
713
|
+
height: Math.round(levelH),
|
|
714
|
+
depth: Math.round(levelD),
|
|
715
|
+
level: this.pyramidLevel,
|
|
716
|
+
levelDims: { ...this.levelDims }
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
574
720
|
private clampCenter(): void {
|
|
575
721
|
const halfW = this.volumeDims.width / 2
|
|
576
722
|
const halfH = this.volumeDims.height / 2
|
|
@@ -651,9 +797,6 @@ export class NVZarrHelper {
|
|
|
651
797
|
const abortController = new AbortController()
|
|
652
798
|
this.currentAbortController = abortController
|
|
653
799
|
|
|
654
|
-
this.runningMin = Infinity
|
|
655
|
-
this.runningMax = -Infinity
|
|
656
|
-
|
|
657
800
|
await new Promise<void>((resolve) => {
|
|
658
801
|
requestAnimationFrame(() => resolve())
|
|
659
802
|
})
|
|
@@ -800,15 +943,17 @@ export class NVZarrHelper {
|
|
|
800
943
|
return
|
|
801
944
|
}
|
|
802
945
|
|
|
803
|
-
// Track min/max for calibration
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
this.runningMin
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
this.runningMax
|
|
946
|
+
// Track min/max for initial calibration (skipped once calibration is done)
|
|
947
|
+
if (!this.calibrationDone) {
|
|
948
|
+
const step = Math.max(1, Math.floor(data.length / 2000))
|
|
949
|
+
for (let i = 0; i < data.length; i += step) {
|
|
950
|
+
const val = data[i]
|
|
951
|
+
if (val < this.runningMin) {
|
|
952
|
+
this.runningMin = val
|
|
953
|
+
}
|
|
954
|
+
if (val > this.runningMax) {
|
|
955
|
+
this.runningMax = val
|
|
956
|
+
}
|
|
812
957
|
}
|
|
813
958
|
}
|
|
814
959
|
|
|
@@ -849,6 +994,9 @@ export class NVZarrHelper {
|
|
|
849
994
|
}
|
|
850
995
|
|
|
851
996
|
private updateCalibration(): void {
|
|
997
|
+
if (this.calibrationDone) {
|
|
998
|
+
return
|
|
999
|
+
}
|
|
852
1000
|
if (this.runningMin < Infinity && this.runningMax > -Infinity) {
|
|
853
1001
|
this.hostImage.cal_min = this.runningMin
|
|
854
1002
|
this.hostImage.cal_max = this.runningMax
|
|
@@ -856,6 +1004,7 @@ export class NVZarrHelper {
|
|
|
856
1004
|
this.hostImage.robust_max = this.runningMax
|
|
857
1005
|
this.hostImage.global_min = this.runningMin
|
|
858
1006
|
this.hostImage.global_max = this.runningMax
|
|
1007
|
+
this.calibrationDone = true
|
|
859
1008
|
}
|
|
860
1009
|
}
|
|
861
1010
|
|
package/src/index_zarr.html
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
6
|
-
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
7
|
-
<title>NiiVue</title>
|
|
8
|
-
<link rel="stylesheet" href="niivue.css" />
|
|
9
|
-
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
|
|
10
|
-
</head>
|
|
11
|
-
<header>
|
|
12
|
-
|
|
13
|
-
<label for="dragMode">Drag mode</label>
|
|
14
|
-
<select id="dragMode">
|
|
15
|
-
<option value="contrast">contrast</option>
|
|
16
|
-
<option value="measurement">measurement</option>
|
|
17
|
-
<option value="none">none</option>
|
|
18
|
-
<option value="pan" selected>pan/zoom</option>
|
|
19
|
-
</select>
|
|
20
|
-
|
|
21
|
-
<label for="thickSelect"> Thickness</label>
|
|
22
|
-
<select id="thickSelect">
|
|
23
|
-
<option>0.5</option>
|
|
24
|
-
<option>1.0</option>
|
|
25
|
-
<option>1.5</option>
|
|
26
|
-
<option selected>2.0</option>
|
|
27
|
-
<option>2.5</option>
|
|
28
|
-
<option>3.0</option>
|
|
29
|
-
</select>
|
|
30
|
-
</header>
|
|
31
|
-
<main id="canvas-container">
|
|
32
|
-
<div style="display: flex; width: 100%; height: 100%">
|
|
33
|
-
<canvas id="gl1"></canvas>
|
|
34
|
-
</div>
|
|
35
|
-
</main>
|
|
36
|
-
<footer>
|
|
37
|
-
<label id="statusBar"> </label>
|
|
38
|
-
</footer>
|
|
39
|
-
</html>
|
|
40
|
-
<script type="module" async>
|
|
41
|
-
import { Niivue, NVImage, NVMesh, NVMeshLoaders, SHOW_RENDER, DRAG_MODE, SLICE_TYPE } from './niivue/index.ts'
|
|
42
|
-
thickSelect.onchange = function() {
|
|
43
|
-
nv1.setMeshThicknessOn2D(parseFloat(this.options[this.selectedIndex].text))
|
|
44
|
-
}
|
|
45
|
-
dragMode.onchange = function () {
|
|
46
|
-
switch (this.value) {
|
|
47
|
-
case "none":
|
|
48
|
-
nv1.opts.dragMode = nv1.dragModes.none
|
|
49
|
-
break
|
|
50
|
-
case "contrast":
|
|
51
|
-
nv1.opts.dragMode = nv1.dragModes.contrast
|
|
52
|
-
break
|
|
53
|
-
case "measurement":
|
|
54
|
-
nv1.opts.dragMode = nv1.dragModes.measurement
|
|
55
|
-
break
|
|
56
|
-
case "pan":
|
|
57
|
-
nv1.opts.dragMode = nv1.dragModes.pan
|
|
58
|
-
break
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
var volumeList1 = [
|
|
62
|
-
{ url: "../demos/images/fs/brainmask.mgz" },
|
|
63
|
-
]
|
|
64
|
-
const onLocationChange = (data) => {
|
|
65
|
-
statusBar.innerHTML = data.string
|
|
66
|
-
}
|
|
67
|
-
let defaults = {
|
|
68
|
-
loglevel: 'debug',
|
|
69
|
-
show3Dcrosshair: true,
|
|
70
|
-
onLocationChange: onLocationChange,
|
|
71
|
-
}
|
|
72
|
-
var nv1 = new Niivue(defaults)
|
|
73
|
-
await nv1.attachToCanvas(gl1)
|
|
74
|
-
nv1.opts.multiplanarShowRender = SHOW_RENDER.ALWAYS
|
|
75
|
-
nv1.opts.isOrientCube = true
|
|
76
|
-
nv1.opts.yoke3Dto2DZoom = true
|
|
77
|
-
nv1.opts.dragMode = nv1.dragModes.pan
|
|
78
|
-
nv1.opts.crosshairGap = 6
|
|
79
|
-
await nv1.loadVolumes(volumeList1)
|
|
80
|
-
|
|
81
|
-
await nv1.loadMeshes([
|
|
82
|
-
{ url: "../demos/images/fs/rh.white", rgba255: [255, 0, 255, 255] },
|
|
83
|
-
{ url: "../demos/images/fs/rh.pial", rgba255: [0, 255, 64, 255] },
|
|
84
|
-
])
|
|
85
|
-
nv1.setMeshShader(nv1.meshes[0].id, 'crosscut')
|
|
86
|
-
nv1.setMeshShader(nv1.meshes[1].id, 'crosscut')
|
|
87
|
-
nv1.setClipPlane([0.0, 180, 20])
|
|
88
|
-
|
|
89
|
-
// Chunk-based random volume data animation
|
|
90
|
-
function startChunkAnimation() {
|
|
91
|
-
const vol = nv1.volumes[0]
|
|
92
|
-
if (!vol || !vol.img) return
|
|
93
|
-
|
|
94
|
-
const nx = vol.hdr.dims[1]
|
|
95
|
-
const ny = vol.hdr.dims[2]
|
|
96
|
-
const nz = vol.hdr.dims[3]
|
|
97
|
-
const datatype = vol.hdr.datatypeCode
|
|
98
|
-
|
|
99
|
-
const CHUNK_SIZE = 8
|
|
100
|
-
|
|
101
|
-
// Pre-compute chunks (only include voxels with value > 0)
|
|
102
|
-
const chunks = []
|
|
103
|
-
for (let cz = 0; cz < nz; cz += CHUNK_SIZE) {
|
|
104
|
-
for (let cy = 0; cy < ny; cy += CHUNK_SIZE) {
|
|
105
|
-
for (let cx = 0; cx < nx; cx += CHUNK_SIZE) {
|
|
106
|
-
const indices = []
|
|
107
|
-
for (let z = cz; z < Math.min(cz + CHUNK_SIZE, nz); z++) {
|
|
108
|
-
for (let y = cy; y < Math.min(cy + CHUNK_SIZE, ny); y++) {
|
|
109
|
-
for (let x = cx; x < Math.min(cx + CHUNK_SIZE, nx); x++) {
|
|
110
|
-
const idx = x + y * nx + z * nx * ny
|
|
111
|
-
if (vol.img[idx] > 0) {
|
|
112
|
-
indices.push(idx)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
// Only add chunk if it has non-zero voxels
|
|
118
|
-
if (indices.length > 0) {
|
|
119
|
-
chunks.push(indices)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Get random value generator based on data type
|
|
126
|
-
function getRandomValue() {
|
|
127
|
-
switch (datatype) {
|
|
128
|
-
case 2: return Math.floor(Math.random() * 256) // UINT8
|
|
129
|
-
case 4: return Math.floor(Math.random() * 65536) - 32768 // INT16
|
|
130
|
-
case 512: return Math.floor(Math.random() * 65536) // UINT16
|
|
131
|
-
case 16: return Math.random() // FLOAT32
|
|
132
|
-
default: return Math.floor(Math.random() * 256)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Animation loop - run as fast as possible
|
|
137
|
-
function animate() {
|
|
138
|
-
for (const chunkIndices of chunks) {
|
|
139
|
-
const value = getRandomValue()
|
|
140
|
-
for (const idx of chunkIndices) {
|
|
141
|
-
vol.img[idx] = value
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
nv1.updateGLVolume()
|
|
145
|
-
requestAnimationFrame(animate)
|
|
146
|
-
}
|
|
147
|
-
requestAnimationFrame(animate)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Auto-execute on page load
|
|
151
|
-
//startChunkAnimation()
|
|
152
|
-
</script>
|