@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.
@@ -12,30 +12,50 @@
12
12
  display: flex;
13
13
  gap: 16px;
14
14
  align-items: center;
15
- padding: 8px;
16
- background: #2a2a2a;
15
+ padding: 10px 12px;
16
+ background: #111;
17
17
  flex-wrap: wrap;
18
+ border-bottom: 1px solid #222;
18
19
  }
19
20
  #controls label {
20
- color: #ccc;
21
+ color: #999;
22
+ font-size: 13px;
21
23
  }
22
24
  #controls input,
23
25
  #controls select {
24
- padding: 4px 8px;
26
+ padding: 5px 8px;
27
+ background: #222;
28
+ color: #eee;
29
+ border: 1px solid #444;
30
+ border-radius: 3px;
31
+ }
32
+ #datasetSelect {
33
+ max-width: 360px;
34
+ }
35
+ #datasetSelect option {
36
+ padding: 2px 4px;
37
+ }
38
+ #datasetSelect optgroup {
39
+ color: #aaa;
40
+ font-style: normal;
25
41
  }
26
42
  #loadBtn {
27
- background: #4a9eff;
43
+ background: #fff;
28
44
  border: none;
29
- color: white;
45
+ color: #000;
30
46
  padding: 8px 16px;
31
47
  cursor: pointer;
32
48
  border-radius: 4px;
49
+ font-weight: 600;
50
+ font-size: 13px;
51
+ letter-spacing: 0.01em;
33
52
  }
34
53
  #loadBtn:hover {
35
- background: #3a8eef;
54
+ background: #ddd;
36
55
  }
37
56
  #loadBtn:disabled {
38
- background: #666;
57
+ background: #555;
58
+ color: #888;
39
59
  cursor: not-allowed;
40
60
  }
41
61
  #navControls {
@@ -44,15 +64,15 @@
44
64
  align-items: center;
45
65
  }
46
66
  #navControls button {
47
- background: #444;
48
- border: 1px solid #666;
49
- color: white;
67
+ background: #222;
68
+ border: 1px solid #444;
69
+ color: #eee;
50
70
  padding: 4px 12px;
51
71
  cursor: pointer;
52
- border-radius: 4px;
72
+ border-radius: 3px;
53
73
  }
54
74
  #navControls button:hover {
55
- background: #555;
75
+ background: #333;
56
76
  }
57
77
  #levelSelectorsContainer {
58
78
  display: flex;
@@ -64,38 +84,44 @@
64
84
  display: flex;
65
85
  gap: 4px;
66
86
  align-items: center;
67
- background: #333;
87
+ background: #1a1a1a;
68
88
  padding: 4px 8px;
69
- border-radius: 4px;
89
+ border-radius: 3px;
90
+ border: 1px solid #333;
70
91
  }
71
92
  .level-selector label {
72
- color: #aaa;
93
+ color: #777;
73
94
  font-size: 11px;
74
95
  }
75
96
  .level-selector select {
76
97
  padding: 2px 4px;
98
+ background: #222;
99
+ color: #eee;
100
+ border: 1px solid #444;
101
+ border-radius: 3px;
77
102
  }
78
103
  #infoPanel {
79
- padding: 8px;
80
- background: #1a1a2e;
81
- color: #aaa;
104
+ padding: 8px 12px;
105
+ background: #0a0a0a;
106
+ color: #999;
82
107
  font-family: monospace;
83
108
  font-size: 12px;
84
109
  display: none;
110
+ border-bottom: 1px solid #222;
85
111
  }
86
112
  .hint {
87
- color: #888;
113
+ color: #666;
88
114
  font-size: 11px;
89
115
  font-style: italic;
90
116
  }
91
117
  #affineControls {
92
118
  display: none;
93
119
  padding: 8px 12px;
94
- background: #1e1e2e;
95
- border-top: 1px solid #333;
120
+ background: #0d0d0d;
121
+ border-top: 1px solid #222;
96
122
  }
97
123
  #affineControls h3 {
98
- color: #ccc;
124
+ color: #bbb;
99
125
  margin: 0 0 8px 0;
100
126
  font-size: 13px;
101
127
  display: flex;
@@ -103,22 +129,26 @@
103
129
  gap: 12px;
104
130
  }
105
131
  #affineControls h3 button {
106
- background: #c44;
107
- border: none;
108
- color: white;
132
+ background: #333;
133
+ border: 1px solid #555;
134
+ color: #eee;
109
135
  padding: 2px 10px;
110
136
  cursor: pointer;
111
137
  border-radius: 3px;
112
138
  font-size: 11px;
113
139
  }
114
140
  #affineControls h3 button:hover {
115
- background: #d55;
141
+ background: #444;
116
142
  }
117
143
  #affineSaveBtn, #affineLoadBtn {
118
- background: #4a9eff !important;
144
+ background: #fff !important;
145
+ color: #000 !important;
146
+ border-color: #fff !important;
147
+ font-weight: 600;
119
148
  }
120
149
  #affineSaveBtn:hover, #affineLoadBtn:hover {
121
- background: #3a8eef !important;
150
+ background: #ddd !important;
151
+ border-color: #ddd !important;
122
152
  }
123
153
  .affine-group {
124
154
  display: flex;
@@ -127,7 +157,7 @@
127
157
  margin-bottom: 4px;
128
158
  }
129
159
  .affine-group label {
130
- color: #999;
160
+ color: #777;
131
161
  font-size: 11px;
132
162
  font-family: monospace;
133
163
  }
@@ -140,7 +170,7 @@
140
170
  width: 120px;
141
171
  }
142
172
  .affine-slider .val {
143
- color: #6cf;
173
+ color: #ccc;
144
174
  font-family: monospace;
145
175
  font-size: 11px;
146
176
  min-width: 50px;
@@ -151,15 +181,79 @@
151
181
  <body>
152
182
  <header>
153
183
  <div id="controls">
154
- <label for="storeUrl">Zarr URL:</label>
184
+ <label for="datasetSelect"><a href="https://github.com/InsightSoftwareConsortium/OMEZarrOpenSciVisDatasets" target="_blank" rel="noopener" style="color:#999;text-decoration:underline">Dataset</a>:</label>
185
+ <select id="datasetSelect">
186
+ <option value="">-- Custom URL --</option>
187
+ <optgroup label="CT Scans">
188
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/aneurism.ome.zarr">Aneurism - head arteries, 256³, uint8</option>
189
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/backpack.ome.zarr">Backpack - CT of backpack, 512x512x373, uint16</option>
190
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/beechnut.ome.zarr">Beechnut - microCT, 1024x1024x1546, uint16</option>
191
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/bonsai.ome.zarr">Bonsai - CT of tree, 256³, uint8</option>
192
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/boston_teapot.ome.zarr">Boston Teapot - SIGGRAPH teapot, 256x256x178, uint8</option>
193
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/bunny.ome.zarr">Bunny - Stanford Bunny CT, 512x512x361, uint16</option>
194
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/carp.ome.zarr">Carp - fish CT, 256x256x512, uint16</option>
195
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/chameleon.ome.zarr">Chameleon - CT, 1024x1024x1080, uint16</option>
196
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/christmas_tree.ome.zarr">Christmas Tree - CT, 512x499x512, uint16</option>
197
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/engine.ome.zarr">Engine - engine block CT, 256x256x128, uint8</option>
198
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/foot.ome.zarr">Foot - human foot, 256³, uint8</option>
199
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/kingsnake.ome.zarr">Kingsnake - snake egg CT, 1024x1024x795, uint8</option>
200
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/lobster.ome.zarr">Lobster - CT in resin, 301x324x56, uint8</option>
201
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/pancreas.ome.zarr">Pancreas - abdominal CT, 240x512x512, int16</option>
202
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/pawpawsaurus.ome.zarr" selected>Pawpawsaurus - fossil CT, 958x646x1088, uint16</option>
203
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/pig_heart.ome.zarr">Pig Heart - microCT, 2048x2048x2612, int16</option>
204
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/present.ome.zarr">Present - industrial CT, 492x492x442, uint16</option>
205
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/prone.ome.zarr">Prone - abdomen CT, 512x512x463, uint16</option>
206
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/skull.ome.zarr">Skull - phantom skull, 256³, uint8</option>
207
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/spathorhynchus.ome.zarr">Spathorhynchus - fossil CT, 1024x1024x750, uint16</option>
208
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/stag_beetle.ome.zarr">Stag Beetle - industrial CT, 832x832x494, uint16</option>
209
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/statue_leg.ome.zarr">Statue Leg - bronze statue CT, 341x341x93, uint8</option>
210
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/stent.ome.zarr">Stent - abdomen with stent, 512x512x174, uint16</option>
211
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/synthetic_truss_with_five_defects.ome.zarr">Synthetic Truss - simulated CT, 1200³, float32</option>
212
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/vertebra.ome.zarr">Vertebra - angiography, 512³, uint16</option>
213
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/vis_male.ome.zarr">Visible Male - head scan, 128x256x256, uint8</option>
214
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/woodbranch.ome.zarr">Woodbranch - microCT, 2048³, uint16</option>
215
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/zeiss.ome.zarr">Zeiss - car part CT, 680³, uint8</option>
216
+ </optgroup>
217
+ <optgroup label="MRI Scans">
218
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/frog.ome.zarr">Frog - MRI, 256x256x44, uint8</option>
219
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/mri_ventricles.ome.zarr">MRI Ventricles - head CSF, 256x256x124, uint8</option>
220
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/mri_woman.ome.zarr">MRI Woman - head MRI, 256x256x109, uint16</option>
221
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/mrt_angio.ome.zarr">MRT Angio - head angiography, 416x512x112, uint16</option>
222
+ </optgroup>
223
+ <optgroup label="Microscopy">
224
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/marmoset_neurons.ome.zarr">Marmoset Neurons - V1 cortex, 1024x1024x314, uint8</option>
225
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/neocortical_layer_1_axons.ome.zarr">Neocortical Axons - barrel cortex, 1464x1033x76, uint8</option>
226
+ </optgroup>
227
+ <optgroup label="Simulations">
228
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/blunt_fin.ome.zarr">Blunt Fin - flow sim, 256x128x64, uint8</option>
229
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/csafe_heptane.ome.zarr">CSAFE Heptane - combustion sim, 302³, uint8</option>
230
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/duct.ome.zarr">Duct - wall-bounded flow, 193x194x1000, float32</option>
231
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/fuel.ome.zarr">Fuel - fuel injection sim, 64³, uint8</option>
232
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/hcci_oh.ome.zarr">HCCI OH - autoignition sim, 560³, float32</option>
233
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/hydrogen_atom.ome.zarr">Hydrogen Atom - electron dist, 128³, uint8</option>
234
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/jicf_q.ome.zarr">JICF Q - jet crossflow, 1408x1080x1100, float32</option>
235
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/magnetic_reconnection.ome.zarr">Magnetic Reconnection - sim, 512³, float32</option>
236
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/marschner_lobb.ome.zarr">Marschner-Lobb - high freq test, 41³, uint8</option>
237
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/miranda.ome.zarr">Miranda - Rayleigh-Taylor sim, 1024³, float32</option>
238
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/neghip.ome.zarr">Neghip - protein electron dist, 64³, uint8</option>
239
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/nucleon.ome.zarr">Nucleon - nuclear sim, 41³, uint8</option>
240
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/richtmyer_meshkov.ome.zarr">Richtmyer-Meshkov - instability, 2048x2048x1920, uint8</option>
241
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/shockwave.ome.zarr">Shockwave - planar shock sim, 64x64x512, uint8</option>
242
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/silicium.ome.zarr">Silicium - grid sim, 98x34x34, uint8</option>
243
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/tacc_turbulence.ome.zarr">TACC Turbulence - enstrophy, 256³, float32</option>
244
+ </optgroup>
245
+ <optgroup label="Other">
246
+ <option value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/tooth.ome.zarr">Tooth - 103x94x161, uint8</option>
247
+ </optgroup>
248
+ </select>
249
+
250
+ <label for="storeUrl">URL:</label>
155
251
  <input
156
252
  type="text"
157
253
  id="storeUrl"
158
254
  value="https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/pawpawsaurus.ome.zarr"
159
255
  style="width: 400px"
160
256
  />
161
- <!-- https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/pawpawsaurus.ome.zarr -->
162
- <!-- https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.5/96x2/chameleon.ome.zarr -->
163
257
 
164
258
  <label for="maxVolSize">Max Volume Size:</label>
165
259
  <select id="maxVolSize">
@@ -178,7 +272,26 @@
178
272
  <option value="5">5</option>
179
273
  </select>
180
274
 
275
+ <label for="colormapSelect">Colormap:</label>
276
+ <select id="colormapSelect"></select>
277
+
278
+ <label for="primaryDrag">Left Drag:</label>
279
+ <select id="primaryDrag">
280
+ <option value="crosshair">crosshair</option>
281
+ <option value="contrast">brightness/contrast</option>
282
+ </select>
283
+
284
+ <label for="secondaryDrag">Right Drag:</label>
285
+ <select id="secondaryDrag">
286
+ <option value="contrast">contrast</option>
287
+ <option value="pan" selected>pan</option>
288
+ <option value="measurement">measurement</option>
289
+ <option value="slicer3D">slicer3D</option>
290
+ <option value="none">none</option>
291
+ </select>
292
+
181
293
  <button id="loadBtn">Load Zarr</button>
294
+ <button id="toggleAffineBtn" style="display:none;background:#222;border:1px solid #444;color:#eee;padding:8px 12px;cursor:pointer;border-radius:4px;font-size:13px;">Registration</button>
182
295
 
183
296
  <div id="navControls">
184
297
  <span class="hint">Drag to pan</span>
@@ -262,6 +375,7 @@
262
375
  <script type="module" async>
263
376
  import { Niivue, DRAG_MODE, SLICE_TYPE, SHOW_RENDER, MULTIPLANAR_TYPE, copyAffine } from './niivue/index.ts'
264
377
 
378
+ const datasetSelect = document.getElementById('datasetSelect')
265
379
  const storeUrlInput = document.getElementById('storeUrl')
266
380
  const maxVolSizeSelect = document.getElementById('maxVolSize')
267
381
  const zarrLevelSelect = document.getElementById('zarrLevel')
@@ -274,6 +388,14 @@
274
388
  const affineResetBtn = document.getElementById('affineResetBtn')
275
389
  const affineSaveBtn = document.getElementById('affineSaveBtn')
276
390
  const affineLoadBtn = document.getElementById('affineLoadBtn')
391
+ const toggleAffineBtn = document.getElementById('toggleAffineBtn')
392
+
393
+ toggleAffineBtn.addEventListener('click', () => {
394
+ const visible = affineControls.style.display === 'block'
395
+ affineControls.style.display = visible ? 'none' : 'block'
396
+ toggleAffineBtn.textContent = visible ? 'Registration' : 'Hide Registration'
397
+ nv.resizeListener()
398
+ })
277
399
 
278
400
  // Affine slider elements
279
401
  const sliders = {
@@ -299,6 +421,19 @@
299
421
  sz: document.getElementById('szVal'),
300
422
  }
301
423
 
424
+ // Sync dataset select with URL input
425
+ datasetSelect.addEventListener('change', () => {
426
+ if (datasetSelect.value) {
427
+ storeUrlInput.value = datasetSelect.value
428
+ }
429
+ })
430
+
431
+ // When URL input is manually edited, switch dropdown to "Custom URL"
432
+ storeUrlInput.addEventListener('input', () => {
433
+ const match = [...datasetSelect.options].find(o => o.value === storeUrlInput.value.trim())
434
+ datasetSelect.value = match ? match.value : ''
435
+ })
436
+
302
437
  const ZARR_VOL_IDX = 1
303
438
 
304
439
  function applyAffineFromSliders() {
@@ -399,11 +534,55 @@
399
534
  multiplanarLayout: MULTIPLANAR_TYPE.GRID,
400
535
  // isSliceMM: true
401
536
  })
402
-
403
537
  await nv.attachToCanvas(document.getElementById('gl1'))
404
538
  nv.opts.multiplanarShowRender = SHOW_RENDER.ALWAYS
405
539
  window.nv = nv
406
540
 
541
+ nv.onLocationChange = (data) => {
542
+ let text = data.string
543
+ const zarrVolumes = nv.getZarrVolumes()
544
+ for (const vol of zarrVolumes) {
545
+ const helper = vol.zarrHelper
546
+ if (!helper) continue
547
+ const coords = helper.mmToLevelCoords(data.mm[0], data.mm[1], data.mm[2])
548
+ const dims = coords.levelDims
549
+ const inBounds = coords.width >= 0 && coords.width < dims.width &&
550
+ coords.height >= 0 && coords.height < dims.height &&
551
+ coords.depth >= 0 && coords.depth < dims.depth
552
+ const coordStr = `L${coords.level}: ${coords.width}, ${coords.height}, ${coords.depth} / ${dims.width}x${dims.height}x${dims.depth}`
553
+ text += ` [${coordStr}${inBounds ? '' : ' OOB'}]`
554
+ }
555
+ statusBar.textContent = text
556
+ }
557
+
558
+ // Populate colormap select
559
+ const colormapSelect = document.getElementById('colormapSelect')
560
+ const maps = nv.colormaps()
561
+ for (const name of maps) {
562
+ const opt = document.createElement('option')
563
+ opt.value = name
564
+ opt.textContent = name
565
+ if (name === 'gray') opt.selected = true
566
+ colormapSelect.appendChild(opt)
567
+ }
568
+ colormapSelect.addEventListener('change', () => {
569
+ if (nv.volumes.length > 0) {
570
+ nv.setColormap(nv.volumes[0].id, colormapSelect.value)
571
+ }
572
+ })
573
+
574
+ // Primary drag (left mouse button)
575
+ document.getElementById('primaryDrag').addEventListener('change', (e) => {
576
+ nv.opts.dragModePrimary = e.target.value === 'contrast' ? 1 : 8
577
+ })
578
+
579
+ // Secondary drag (right mouse button)
580
+ document.getElementById('secondaryDrag').addEventListener('change', (e) => {
581
+ if (nv.dragModes[e.target.value] !== undefined) {
582
+ nv.opts.dragMode = nv.dragModes[e.target.value]
583
+ }
584
+ })
585
+
407
586
  function updateInfoPanel() {
408
587
  const helper = nv.getZarrVolume()?.zarrHelper
409
588
  if (!helper) {
@@ -507,55 +686,62 @@
507
686
 
508
687
  try {
509
688
  await nv.loadVolumes([
510
- // mni zarr (LSM simulated)
511
- {
512
- // url: storeUrl,
513
- url: "http://localhost:8080/mni152.ome.zarr",
514
- zarrLevel: level,
515
- zarrMaxVolumeSize: maxVolSize,
516
- opacity: 1.0,
517
- colormap: "grey"
518
- },
519
689
  // mni nifti
520
- { url: "../demos/images/mni152.nii.gz", opacity: 1,colormap: "gray" },
521
- // mni WSI image
522
- {
523
- // url: storeUrl,
524
- url: "http://localhost:8080/mni152_axial.ome.zarr",
525
- // url: "http://localhost:3000/K_HE_2_ome.zarr",
526
- // url: "http://localhost:3000/E23191_25-1A.1.ome.zarr",
527
- zarrLevel: level,
528
- zarrMaxVolumeSize: maxVolSize,
529
- channel: 0,
530
- opacity: 1.0,
531
- colormap: "red"
532
- },
690
+ // { url: "../demos/images/mni152.nii.gz", opacity: 1,colormap: "gray" },
691
+
692
+ // mni zarr (LSM simulated)
533
693
  // {
534
- // url: "http://localhost:3000/export/nifti?zarr_path=K_HE_2_ome.zarr&scale=virtual_scale1&c=0&z=0&y=0&x=0&xdim=128&ydim=128&zdim=128",
535
- // name: "nifti.nii.gz"
536
- // }
537
- // LSM image
694
+ // // url: storeUrl,
695
+ // url: "http://localhost:8080/K_HE_2_multiscale.zarr",
696
+ // zarrLevel: level,
697
+ // zarrMaxVolumeSize: maxVolSize,
698
+ // opacity: 1.0,
699
+ // channel:1,
700
+ // colormap: "gray",
701
+ // cal_min:150,
702
+ // cal_max: 261
703
+ // },
704
+ // mni WSI image
538
705
  // {
539
706
  // // url: storeUrl,
540
- // // url: "http://localhost:8080/mni152_axial.ome.zarr",
541
- // url: "http://localhost:3000/K_HE_2_ome.zarr",
542
- // // url: "https://dandiarchive.s3.amazonaws.com/zarr/7723d02f-1f71-4553-a7b0-47bda1ae8b42",
707
+ // url: "http://localhost:8080/mni152_axial.ome.zarr",
708
+ // // url: "http://localhost:3000/K_HE_2_ome.zarr",
543
709
  // // url: "http://localhost:3000/E23191_25-1A.1.ome.zarr",
544
- // // url: "http://localhost:3000/test-image.zarr",
545
710
  // zarrLevel: level,
546
711
  // zarrMaxVolumeSize: maxVolSize,
712
+ // channel: 0,
547
713
  // opacity: 1.0,
548
- // colormap: "gray"
714
+ // colormap: "red"
549
715
  // },
716
+ // {
717
+ // url: "http://localhost:3000/export/nifti?zarr_path=K_HE_2_ome.zarr&scale=virtual_scale1&c=0&z=0&y=0&x=0&xdim=128&ydim=128&zdim=128",
718
+ // name: "nifti.nii.gz"
719
+ // }
720
+ // LSM image
721
+ {
722
+ url: storeUrl,
723
+ // url: "http://localhost:8080/mni152_axial.ome.zarr",
724
+ // url: "http://localhost:3000/K_HE_2_ome.zarr",
725
+ // url: "https://dandiarchive.s3.amazonaws.com/zarr/7723d02f-1f71-4553-a7b0-47bda1ae8b42",
726
+ // url: "http://localhost:3000/E23191_25-1A.1.ome.zarr",
727
+ // url: "http://localhost:3000/test-image.zarr",
728
+ zarrLevel: level,
729
+ zarrMaxVolumeSize: maxVolSize,
730
+ opacity: 1.0,
731
+ colormap: "gray",
732
+ name: "demo.ome.zarr"
733
+ },
550
734
 
551
735
 
552
736
  ])
553
737
  // await nv.addVolumeFromUrl({ url: "../demos/images/mni152.nii.gz", opacity: 0.4,colormap: "red" })
554
738
  // nv.setColormap(nv.volumes[0].id, 'gray')
555
739
 
556
- // Show navigation and affine controls
740
+ // Show navigation controls and registration toggle
557
741
  navControls.style.display = 'flex'
558
- affineControls.style.display = 'block'
742
+ toggleAffineBtn.style.display = 'inline-block'
743
+ affineControls.style.display = 'none'
744
+ toggleAffineBtn.textContent = 'Registration'
559
745
  resetAffineSliders()
560
746
 
561
747
  // Create level selectors for all zarr volumes
@@ -581,6 +767,7 @@
581
767
  navControls.style.display = 'none'
582
768
  infoPanel.style.display = 'none'
583
769
  affineControls.style.display = 'none'
770
+ toggleAffineBtn.style.display = 'none'
584
771
  } finally {
585
772
  loadBtn.disabled = false
586
773
  loadBtn.textContent = 'Load Zarr'
@@ -1,7 +1,7 @@
1
1
  import { NiftiHeader } from '@/types'
2
2
 
3
3
  /** Typed array types supported for voxel data */
4
- export type TypedVoxelArray = Float32Array | Uint8Array | Int16Array | Float64Array | Uint16Array
4
+ export type TypedVoxelArray = Float32Array | Uint8Array | Int16Array | Float64Array | Uint16Array | Int32Array | Uint32Array
5
5
 
6
6
  /**
7
7
  * Options for creating a mask array
@@ -258,6 +258,8 @@ export class Niivue {
258
258
 
259
259
  readyForSync = false
260
260
 
261
+ private _skipDragInDraw = false
262
+
261
263
  // UI Data
262
264
  uiData: UIData = {
263
265
  mousedown: false,
@@ -1528,6 +1530,9 @@ export class Niivue {
1528
1530
  }
1529
1531
  if (this.uiData.isDragging) {
1530
1532
  this.uiData.isDragging = false
1533
+ for (const vol of this.getZarrVolumes()) {
1534
+ vol.zarrHelper?.endDrag()
1535
+ }
1531
1536
 
1532
1537
  // Handle angle measurement workflow
1533
1538
  if (currentDragMode === DRAG_MODE.angle) {
@@ -1713,6 +1718,9 @@ export class Niivue {
1713
1718
  // Handle drag completion
1714
1719
  if (this.uiData.isDragging) {
1715
1720
  this.uiData.isDragging = false
1721
+ for (const vol of this.getZarrVolumes()) {
1722
+ vol.zarrHelper?.endDrag()
1723
+ }
1716
1724
  // if drag mode is contrast, and the user double taps and drags...
1717
1725
  if (this.getCurrentDragMode() === DRAG_MODE.contrast) {
1718
1726
  this.calculateNewRange()
@@ -1791,6 +1799,9 @@ export class Niivue {
1791
1799
  if (this.uiData.isDragging || this.uiData.mousedown) {
1792
1800
  log.debug('Mouse left canvas during drag, resetting drag state.')
1793
1801
  this.uiData.isDragging = false
1802
+ for (const vol of this.getZarrVolumes()) {
1803
+ vol.zarrHelper?.endDrag()
1804
+ }
1794
1805
  const resetState = MouseController.createResetButtonState()
1795
1806
  this.uiData.mouseButtonLeftDown = resetState.mouseButtonLeftDown
1796
1807
  this.uiData.mouseButtonCenterDown = resetState.mouseButtonCenterDown
@@ -2497,7 +2508,8 @@ export class Niivue {
2497
2508
  zarrLevel: imageItem.zarrLevel,
2498
2509
  zarrMaxVolumeSize: imageItem.zarrMaxVolumeSize,
2499
2510
  zarrChannel: imageItem.zarrChannel,
2500
- zarrConvertUnits: imageItem.zarrConvertUnits
2511
+ zarrConvertUnits: imageItem.zarrConvertUnits,
2512
+ zarrCenterMM: imageItem.zarrCenterMM
2501
2513
  }
2502
2514
  const volume = await NVImage.loadFromUrl(imageOptions)
2503
2515
  this.document.addImageOptions(volume, imageOptions)
@@ -3150,12 +3162,35 @@ export class Niivue {
3150
3162
  this.volumes = result.volumes
3151
3163
  this.setVolume(volume, result.index)
3152
3164
  this.onImageLoaded(volume)
3153
- // Hook zarr chunk updates to trigger GL refresh
3165
+ // Hook zarr chunk updates to trigger GL refresh.
3166
+ // Register the callback BEFORE loading initial chunks so the first
3167
+ // batch of data triggers a proper GPU upload and render.
3154
3168
  if (volume.zarrHelper) {
3169
+ // Auto-center zarr overlay on background volume if no explicit position was given
3170
+ if (!volume._hasExplicitZarrCenter && this.back && this.back !== volume && this.back.hdr?.dims) {
3171
+ const bg = this.back
3172
+ const dims = bg.hdr.dims
3173
+ const affine = bg.hdr.affine
3174
+ const ci = (dims[1] - 1) / 2
3175
+ const cj = (dims[2] - 1) / 2
3176
+ const ck = (dims[3] - 1) / 2
3177
+ const bgCenter: [number, number, number] = [
3178
+ affine[0][0] * ci + affine[0][1] * cj + affine[0][2] * ck + affine[0][3],
3179
+ affine[1][0] * ci + affine[1][1] * cj + affine[1][2] * ck + affine[1][3],
3180
+ affine[2][0] * ci + affine[2][1] * cj + affine[2][2] * ck + affine[2][3]
3181
+ ]
3182
+ volume.zarrHelper.setWorldCenter(bgCenter)
3183
+ }
3184
+
3155
3185
  volume.zarrHelper.onChunksUpdated = (): void => {
3156
3186
  this.updateGLVolume()
3187
+ this._skipDragInDraw = true
3157
3188
  this.drawScene()
3189
+ this._skipDragInDraw = false
3158
3190
  }
3191
+ // Load initial chunks now (with callback registered).
3192
+ // Not awaited — progressive rendering via onChunksUpdated handles GPU updates.
3193
+ volume.zarrHelper.loadInitialChunks().catch((err) => log.error('Failed to load initial zarr chunks', err))
3159
3194
  }
3160
3195
  log.debug('loaded volume', volume.name)
3161
3196
  log.debug(volume)
@@ -8012,7 +8047,9 @@ export class Niivue {
8012
8047
  if (panPromises.length > 0) {
8013
8048
  Promise.all(panPromises)
8014
8049
  .then(() => {
8050
+ this._skipDragInDraw = true
8015
8051
  this.drawScene()
8052
+ this._skipDragInDraw = false
8016
8053
  })
8017
8054
  .catch((err) => {
8018
8055
  log.error('Failed to pan Zarr:', err)
@@ -11800,7 +11837,7 @@ export class Niivue {
11800
11837
  this.dragForSlicer3D([this.uiData.dragStart[0], this.uiData.dragStart[1], this.uiData.dragEnd[0], this.uiData.dragEnd[1]])
11801
11838
  return
11802
11839
  }
11803
- if (this.getCurrentDragMode() === DRAG_MODE.pan) {
11840
+ if (this.getCurrentDragMode() === DRAG_MODE.pan && !this._skipDragInDraw) {
11804
11841
  this.dragForPanZoom([this.uiData.dragStart[0], this.uiData.dragStart[1], this.uiData.dragEnd[0], this.uiData.dragEnd[1]])
11805
11842
  return
11806
11843
  }
@@ -22,7 +22,7 @@ import { NVZarrHelper } from '@/nvimage/zarr/NVZarrHelper'
22
22
  import { AffineTransform, copyAffine, createTransformMatrix, multiplyAffine } from '@/nvimage/affineUtils'
23
23
 
24
24
  export * from '@/nvimage/utils'
25
- export type TypedVoxelArray = Float32Array | Uint8Array | Int16Array | Float64Array | Uint16Array
25
+ export type TypedVoxelArray = Float32Array | Uint8Array | Int16Array | Float64Array | Uint16Array | Int32Array | Uint32Array
26
26
 
27
27
  /**
28
28
  * a NVImage encapsulates some image data and provides methods to query and operate on images
@@ -89,6 +89,7 @@ export class NVImage {
89
89
  onOpacityChange: (img: NVImage) => void = () => {}
90
90
 
91
91
  zarrHelper: NVZarrHelper | null = null
92
+ _hasExplicitZarrCenter = false
92
93
 
93
94
  mm000?: vec3
94
95
  mm100?: vec3
@@ -865,7 +866,8 @@ export class NVImage {
865
866
  zarrLevel,
866
867
  zarrMaxVolumeSize,
867
868
  zarrChannel,
868
- zarrConvertUnits
869
+ zarrConvertUnits,
870
+ zarrCenterMM
869
871
  }: Partial<Omit<ImageFromUrlOptions, 'url'>> & { url?: string | Uint8Array | ArrayBuffer } = {}): Promise<NVImage> {
870
872
  if (url === '') {
871
873
  throw Error('url must not be empty')
@@ -924,7 +926,8 @@ export class NVImage {
924
926
  channel: zarrChannel,
925
927
  convertUnitsToMm: zarrConvertUnits,
926
928
  colormap,
927
- opacity
929
+ opacity,
930
+ zarrCenterMM
928
931
  })
929
932
  }
930
933
  // Light path: load entire array (unchanged)
@@ -1014,6 +1017,7 @@ export class NVImage {
1014
1017
  convertUnitsToMm?: boolean
1015
1018
  colormap?: string
1016
1019
  opacity?: number
1020
+ zarrCenterMM?: [number, number, number]
1017
1021
  }
1018
1022
  ): Promise<NVImage> {
1019
1023
  const nvimage = new NVImage()
@@ -1026,6 +1030,10 @@ export class NVImage {
1026
1030
  cacheSize: options.cacheSize,
1027
1031
  convertUnitsToMm: options.convertUnitsToMm
1028
1032
  })
1033
+ if (options.zarrCenterMM) {
1034
+ nvimage.zarrHelper.setWorldCenter(options.zarrCenterMM)
1035
+ nvimage._hasExplicitZarrCenter = true
1036
+ }
1029
1037
  if (options.colormap) {
1030
1038
  nvimage._colormap = options.colormap
1031
1039
  }