@niivue/niivue 0.67.1-dev.0 → 0.67.1-dev.1
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 +89 -13
- package/build/niivue/index.js.map +1 -1
- package/build/{nvdocument-CcP6istx.d.ts → nvdocument-B64_4nuJ.d.ts} +18 -1
- package/build/utils/index.d.ts +1 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.js +89 -13
- 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 +28 -28
- package/package.json +2 -2
- package/src/index_zarr_viewer.html +255 -68
- package/src/niivue/descriptives.ts +1 -1
- package/src/niivue/index.ts +22 -2
- package/src/nvimage/index.ts +1 -1
- package/src/nvimage/zarr/NVZarrHelper.ts +103 -12
- package/src/index_zarr.html +0 -152
|
@@ -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:
|
|
@@ -249,19 +251,23 @@ export class NVZarrHelper {
|
|
|
249
251
|
helper.updateLevelInfo()
|
|
250
252
|
|
|
251
253
|
// Center on the level
|
|
252
|
-
helper.centerX = helper.
|
|
253
|
-
helper.centerY = helper.
|
|
254
|
-
helper.centerZ = helper.
|
|
254
|
+
helper.centerX = helper.volumeDims.width / 2
|
|
255
|
+
helper.centerY = helper.volumeDims.height / 2
|
|
256
|
+
helper.centerZ = helper.volumeDims.depth / 2
|
|
255
257
|
|
|
256
258
|
// Configure the host NVImage
|
|
257
259
|
helper.configureHostImage()
|
|
258
260
|
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
+
// Don't load chunks here — loadInitialChunks() must be called after
|
|
262
|
+
// the onChunksUpdated callback is registered (see addVolume in Niivue).
|
|
261
263
|
|
|
262
264
|
return helper
|
|
263
265
|
}
|
|
264
266
|
|
|
267
|
+
async loadInitialChunks(): Promise<void> {
|
|
268
|
+
await this.updateVolume()
|
|
269
|
+
}
|
|
270
|
+
|
|
265
271
|
private updateLevelInfo(): void {
|
|
266
272
|
const levelInfo = this.pyramidInfo.levels[this.pyramidLevel]
|
|
267
273
|
const shape = levelInfo.shape
|
|
@@ -352,12 +358,12 @@ export class NVZarrHelper {
|
|
|
352
358
|
// Store original affine so resetVolumeAffine() works on zarr volumes
|
|
353
359
|
img.originalAffine = copyAffine(hdr.affine)
|
|
354
360
|
|
|
355
|
-
img.cal_min =
|
|
356
|
-
img.cal_max =
|
|
357
|
-
img.robust_min =
|
|
358
|
-
img.robust_max =
|
|
359
|
-
img.global_min =
|
|
360
|
-
img.global_max =
|
|
361
|
+
img.cal_min = 0
|
|
362
|
+
img.cal_max = 0
|
|
363
|
+
img.robust_min = 0
|
|
364
|
+
img.robust_max = 0
|
|
365
|
+
img.global_min = 0
|
|
366
|
+
img.global_max = 0
|
|
361
367
|
}
|
|
362
368
|
|
|
363
369
|
/** Get unit-converted voxel scales */
|
|
@@ -505,6 +511,10 @@ export class NVZarrHelper {
|
|
|
505
511
|
this.centerAtDragStart = { x: this.centerX, y: this.centerY, z: this.centerZ }
|
|
506
512
|
}
|
|
507
513
|
|
|
514
|
+
endDrag(): void {
|
|
515
|
+
this.centerAtDragStart = null
|
|
516
|
+
}
|
|
517
|
+
|
|
508
518
|
async panBy(dx: number, dy: number, dz: number = 0): Promise<void> {
|
|
509
519
|
this.centerX -= dx
|
|
510
520
|
this.centerY -= dy
|
|
@@ -515,12 +525,22 @@ export class NVZarrHelper {
|
|
|
515
525
|
}
|
|
516
526
|
|
|
517
527
|
async panTo(newCenterX: number, newCenterY: number, newCenterZ?: number): Promise<void> {
|
|
528
|
+
const prevX = this.centerX
|
|
529
|
+
const prevY = this.centerY
|
|
530
|
+
const prevZ = this.centerZ
|
|
531
|
+
|
|
518
532
|
this.centerX = newCenterX
|
|
519
533
|
this.centerY = newCenterY
|
|
520
534
|
if (newCenterZ !== undefined) {
|
|
521
535
|
this.centerZ = newCenterZ
|
|
522
536
|
}
|
|
523
537
|
this.clampCenter()
|
|
538
|
+
|
|
539
|
+
// Skip if clamped position is same as before (prevents infinite loop at boundaries)
|
|
540
|
+
if (Math.abs(this.centerX - prevX) < 0.001 && Math.abs(this.centerY - prevY) < 0.001 && Math.abs(this.centerZ - prevZ) < 0.001) {
|
|
541
|
+
return
|
|
542
|
+
}
|
|
543
|
+
|
|
524
544
|
this.updateAffine()
|
|
525
545
|
await this.updateVolume()
|
|
526
546
|
}
|
|
@@ -571,6 +591,77 @@ export class NVZarrHelper {
|
|
|
571
591
|
return { ...this.volumeDims }
|
|
572
592
|
}
|
|
573
593
|
|
|
594
|
+
/**
|
|
595
|
+
* Convert physical (mm) coordinates back to real zarr level pixel coordinates.
|
|
596
|
+
* Inverts the affine: levelPixel = (mm - OME_translation) / scale
|
|
597
|
+
*/
|
|
598
|
+
mmToLevelCoords(
|
|
599
|
+
mmX: number,
|
|
600
|
+
mmY: number,
|
|
601
|
+
mmZ: number
|
|
602
|
+
): {
|
|
603
|
+
width: number
|
|
604
|
+
height: number
|
|
605
|
+
depth: number
|
|
606
|
+
level: number
|
|
607
|
+
levelDims: { width: number; height: number; depth: number }
|
|
608
|
+
} {
|
|
609
|
+
const { scaleW, scaleH, scaleD } = this.getConvertedScales()
|
|
610
|
+
const axisNames = this.pyramidInfo.axisMapping.spatialAxisNames
|
|
611
|
+
|
|
612
|
+
// Map physical mm (x/y/z) to OME spatial dimensions (width/height/depth)
|
|
613
|
+
// The affine maps NIfTI cols to physical rows by axis name:
|
|
614
|
+
// col 0 (width) → axis[-1], col 1 (height) → axis[-2], col 2 (depth) → axis[-3]
|
|
615
|
+
// We need the reverse: given physical x/y/z, find which OME dim it maps to
|
|
616
|
+
const mmByAxis: Record<string, number> = { x: mmX, y: mmY, z: mmZ }
|
|
617
|
+
|
|
618
|
+
let levelW = 0
|
|
619
|
+
let levelH = 0
|
|
620
|
+
let levelD = 0
|
|
621
|
+
|
|
622
|
+
if (axisNames.length >= 2) {
|
|
623
|
+
// Width maps to last spatial axis name
|
|
624
|
+
const wAxisName = axisNames[axisNames.length - 1]
|
|
625
|
+
const hAxisName = axisNames[axisNames.length - 2]
|
|
626
|
+
const dAxisName = axisNames.length >= 3 ? axisNames[axisNames.length - 3] : 'z'
|
|
627
|
+
|
|
628
|
+
const mmW = mmByAxis[wAxisName] ?? mmX
|
|
629
|
+
const mmH = mmByAxis[hAxisName] ?? mmY
|
|
630
|
+
const mmD = mmByAxis[dAxisName] ?? mmZ
|
|
631
|
+
|
|
632
|
+
if (this.hasTranslations) {
|
|
633
|
+
const { transW, transH, transD } = this.getConvertedTranslations()
|
|
634
|
+
levelW = scaleW !== 0 ? (mmW - transW) / scaleW : 0
|
|
635
|
+
levelH = scaleH !== 0 ? (mmH - transH) / scaleH : 0
|
|
636
|
+
levelD = scaleD !== 0 ? (mmD - transD) / scaleD : 0
|
|
637
|
+
} else {
|
|
638
|
+
levelW = scaleW !== 0 ? mmW / scaleW : 0
|
|
639
|
+
levelH = scaleH !== 0 ? mmH / scaleH : 0
|
|
640
|
+
levelD = scaleD !== 0 ? mmD / scaleD : 0
|
|
641
|
+
}
|
|
642
|
+
} else {
|
|
643
|
+
// Fallback: simple diagonal
|
|
644
|
+
if (this.hasTranslations) {
|
|
645
|
+
const { transW, transH, transD } = this.getConvertedTranslations()
|
|
646
|
+
levelW = scaleW !== 0 ? (mmX - transW) / scaleW : 0
|
|
647
|
+
levelH = scaleH !== 0 ? (mmY - transH) / scaleH : 0
|
|
648
|
+
levelD = scaleD !== 0 ? (mmZ - transD) / scaleD : 0
|
|
649
|
+
} else {
|
|
650
|
+
levelW = scaleW !== 0 ? mmX / scaleW : 0
|
|
651
|
+
levelH = scaleH !== 0 ? mmY / scaleH : 0
|
|
652
|
+
levelD = scaleD !== 0 ? mmZ / scaleD : 0
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return {
|
|
657
|
+
width: Math.round(levelW),
|
|
658
|
+
height: Math.round(levelH),
|
|
659
|
+
depth: Math.round(levelD),
|
|
660
|
+
level: this.pyramidLevel,
|
|
661
|
+
levelDims: { ...this.levelDims }
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
574
665
|
private clampCenter(): void {
|
|
575
666
|
const halfW = this.volumeDims.width / 2
|
|
576
667
|
const halfH = this.volumeDims.height / 2
|
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>
|