cloudmr-ux 4.8.7 → 4.8.9

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.
@@ -118,67 +118,69 @@ window.nv = nv;
118
118
  // It is exported so that it can be used in other projects easily
119
119
  export default function CloudMrNiivueViewer(props) {
120
120
  var _this = this;
121
+ var _a, _b;
121
122
  var endpoints = getEndpoints(); // CloudMR core endpoints (mode1/mode2 helper)
122
123
  var selectedVolume = props.selectedVolume;
123
124
  var setSelectedVolume = props.setSelectedVolume;
124
125
  var setWarning = props.setWarning, setWarningOpen = props.setWarningOpen;
125
126
  // const nv = props.nv;
126
- var _a = React.useState(false), openSettings = _a[0], setOpenSettings = _a[1];
127
- var _b = React.useState(false), openLayers = _b[0], setOpenLayers = _b[1];
128
- var _c = React.useState(nv.opts.crosshairColor), crosshairColor = _c[0], setCrosshairColor = _c[1];
129
- var _d = React.useState(nv.opts.selectionBoxColor), selectionBoxColor = _d[0], setSelectionBoxColor = _d[1];
130
- var _e = React.useState(nv.opts.backColor), backColor = _e[0], setBackColor = _e[1];
131
- var _f = React.useState(nv.opts.clipPlaneColor), clipPlaneColor = _f[0], setClipPlaneColor = _f[1];
132
- var _g = React.useState(nv.volumes), layers = _g[0], setLayers = _g[1];
133
- var _h = React.useState(false), cornerText = _h[0], setCornerText = _h[1];
134
- var _j = React.useState(true), radiological = _j[0], setRadiological = _j[1];
135
- var _k = React.useState(false), crosshair3D = _k[0], setCrosshair3D = _k[1];
136
- var _l = React.useState(nv.opts.textHeight), textSize = _l[0], setTextSize = _l[1];
137
- var _m = React.useState(nv.opts.isColorbar), colorBar = _m[0], setColorBar = _m[1];
138
- var _o = React.useState(nv.opts.isSliceMM), worldSpace = _o[0], setWorldSpace = _o[1];
139
- var _p = React.useState(nv.currentClipPlaneIndex > 0 ? true : false), clipPlane = _p[0], setClipPlane = _p[1];
127
+ var _c = React.useState(false), openSettings = _c[0], setOpenSettings = _c[1];
128
+ var _d = React.useState(false), openLayers = _d[0], setOpenLayers = _d[1];
129
+ var _e = React.useState(nv.opts.crosshairColor), crosshairColor = _e[0], setCrosshairColor = _e[1];
130
+ var _f = React.useState(nv.opts.selectionBoxColor), selectionBoxColor = _f[0], setSelectionBoxColor = _f[1];
131
+ var _g = React.useState(nv.opts.backColor), backColor = _g[0], setBackColor = _g[1];
132
+ var _h = React.useState(nv.opts.clipPlaneColor), clipPlaneColor = _h[0], setClipPlaneColor = _h[1];
133
+ var _j = React.useState(nv.volumes), layers = _j[0], setLayers = _j[1];
134
+ var _k = React.useState(false), cornerText = _k[0], setCornerText = _k[1];
135
+ var _l = React.useState(true), radiological = _l[0], setRadiological = _l[1];
136
+ var _m = React.useState(false), crosshair3D = _m[0], setCrosshair3D = _m[1];
137
+ var _o = React.useState(nv.opts.textHeight), textSize = _o[0], setTextSize = _o[1];
138
+ var _p = React.useState(nv.opts.isColorbar), colorBar = _p[0], setColorBar = _p[1];
139
+ var _q = React.useState(nv.opts.isSliceMM), worldSpace = _q[0], setWorldSpace = _q[1];
140
+ var _r = React.useState(nv.currentClipPlaneIndex > 0 ? true : false), clipPlane = _r[0], setClipPlane = _r[1];
140
141
  // TODO: add crosshair size state and setter
141
- var _q = React.useState(1.0), opacity = _q[0], setopacity = _q[1];
142
- var _r = React.useState(nv.opts.drawingEnabled), drawingEnabled = _r[0], setDrawingEnabled = _r[1];
143
- var _s = React.useState(nv.opts.penValue), drawPen = _s[0], setDrawPen = _s[1];
144
- var _t = React.useState(0.8), drawOpacity = _t[0], setDrawOpacity = _t[1];
145
- var _u = React.useState(nv.opts.crosshairColor[3]), crosshairOpacity = _u[0], setCrosshairOpacity = _u[1];
146
- var _v = React.useState(nv.opts.clipPlaneColor[3]), clipPlaneOpacity = _v[0], setClipPlaneOpacity = _v[1];
147
- var _w = React.useState(true), locationTableVisible = _w[0], setLocationTableVisible = _w[1];
148
- var _x = React.useState([]), locationData = _x[0], setLocationData = _x[1];
149
- var _y = React.useState(2), decimalPrecision = _y[0], setDecimalPrecision = _y[1];
150
- var _z = React.useState(nv.opts.isOrientCube), orientCube = _z[0], setOrientCube = _z[1];
151
- var _0 = React.useState(nv.opts.isRuler), ruler = _0[0], setRuler = _0[1];
152
- var _1 = React.useState(nv.opts.multiplanarPadPixels), multiplanarPadPixels = _1[0], setMultiplanarPadPixels = _1[1];
153
- var _2 = React.useState(nv.opts.maxDrawUndoBitmaps), maxDrawUndoBitmaps = _2[0], setMaxDrawUndoBitmaps = _2[1];
154
- var _3 = React.useState(nv.opts.sagittalNoseLeft), sagittalNoseLeft = _3[0], setSagittalNoseLeft = _3[1];
155
- var _4 = React.useState(nv.opts.rulerWidth), rulerWidth = _4[0], setRulerWidth = _4[1];
156
- var _5 = React.useState(nv.opts.longTouchTimeout), longTouchTimeout = _5[0], setLongTouchTimeout = _5[1];
157
- var _6 = React.useState(nv.opts.doubleTouchTimeout), doubleTouchTimeout = _6[0], setDoubleTouchTimeout = _6[1];
158
- var _7 = React.useState(nv.opts.isDragShowsMeasurementTool), dragToMeasure = _7[0], setDragToMeasure = _7[1];
159
- var _8 = React.useState(nv.opts.rulerColor), rulerColor = _8[0], setRulerColor = _8[1];
160
- var _9 = React.useState(nv.opts.rulerColor[3]), rulerOpacity = _9[0], setRulerOpacity = _9[1];
161
- var _10 = React.useState(false), highDPI = _10[0], setHighDPI = _10[1];
162
- var _11 = React.useState([]), rois = _11[0], setROIs = _11[1];
142
+ var _s = React.useState(1.0), opacity = _s[0], setopacity = _s[1];
143
+ var _t = React.useState(nv.opts.drawingEnabled), drawingEnabled = _t[0], setDrawingEnabled = _t[1];
144
+ var _u = React.useState(nv.opts.penValue), drawPen = _u[0], setDrawPen = _u[1];
145
+ var _v = React.useState(0.8), drawOpacity = _v[0], setDrawOpacity = _v[1];
146
+ var _w = React.useState(nv.opts.crosshairColor[3]), crosshairOpacity = _w[0], setCrosshairOpacity = _w[1];
147
+ var _x = React.useState(nv.opts.clipPlaneColor[3]), clipPlaneOpacity = _x[0], setClipPlaneOpacity = _x[1];
148
+ var _y = React.useState(true), locationTableVisible = _y[0], setLocationTableVisible = _y[1];
149
+ var _z = React.useState([]), locationData = _z[0], setLocationData = _z[1];
150
+ var _0 = React.useState(2), decimalPrecision = _0[0], setDecimalPrecision = _0[1];
151
+ var _1 = React.useState(nv.opts.isOrientCube), orientCube = _1[0], setOrientCube = _1[1];
152
+ var _2 = React.useState(nv.opts.isRuler), ruler = _2[0], setRuler = _2[1];
153
+ var _3 = React.useState(nv.opts.multiplanarPadPixels), multiplanarPadPixels = _3[0], setMultiplanarPadPixels = _3[1];
154
+ var _4 = React.useState(nv.opts.maxDrawUndoBitmaps), maxDrawUndoBitmaps = _4[0], setMaxDrawUndoBitmaps = _4[1];
155
+ var _5 = React.useState(nv.opts.sagittalNoseLeft), sagittalNoseLeft = _5[0], setSagittalNoseLeft = _5[1];
156
+ var _6 = React.useState(nv.opts.rulerWidth), rulerWidth = _6[0], setRulerWidth = _6[1];
157
+ var _7 = React.useState(nv.opts.longTouchTimeout), longTouchTimeout = _7[0], setLongTouchTimeout = _7[1];
158
+ var _8 = React.useState(nv.opts.doubleTouchTimeout), doubleTouchTimeout = _8[0], setDoubleTouchTimeout = _8[1];
159
+ var _9 = React.useState(nv.opts.isDragShowsMeasurementTool), dragToMeasure = _9[0], setDragToMeasure = _9[1];
160
+ var _10 = React.useState(nv.opts.rulerColor), rulerColor = _10[0], setRulerColor = _10[1];
161
+ var _11 = React.useState(nv.opts.rulerColor[3]), rulerOpacity = _11[0], setRulerOpacity = _11[1];
162
+ var _12 = React.useState(false), highDPI = _12[0], setHighDPI = _12[1];
163
+ var _13 = React.useState([]), rois = _13[0], setROIs = _13[1];
163
164
  // Persists zoom, gamma and opacity across volume/channel switches so they are not reset
164
165
  var savedViewStateRef = React.useRef(null);
165
- var _12 = React.useState(false), showCrosshair = _12[0], setShowCrosshair = _12[1];
166
- var _13 = useState('absolute'), complexMode = _13[0], setComplexMode = _13[1];
167
- var _14 = useState(['absolute']), complexOptions = _14[0], setComplexOptions = _14[1];
168
- var _15 = useState(true), roiVisible = _15[0], setROIVisible = _15[1];
169
- var _16 = useState(0.8), drawingOpacity = _16[0], setDrawingOpacity = _16[1];
170
- var _17 = useState(0), min = _17[0], setMin = _17[1];
171
- var _18 = useState(1), max = _18[0], setMax = _18[1];
172
- var _19 = useState(false), textsVisible = _19[0], setTextsVisible = _19[1];
173
- var _20 = useState({ a: 1, b: 0 }), transformFactors = _20[0], setTransformFactors = _20[1];
174
- var _21 = useState(false), saving = _21[0], setSaving = _21[1];
175
- var _22 = useState(null), drawShapeTool = _22[0], setDrawShapeTool = _22[1];
176
- var _23 = useState(null), shapeDraft = _23[0], setShapeDraft = _23[1];
177
- var _24 = useState("freehand"), penDrawMode = _24[0], setPenDrawMode = _24[1];
178
- var _25 = useState(0), polylineVertexCount = _25[0], setPolylineVertexCount = _25[1];
179
- var _26 = useState(null), penDraft = _26[0], setPenDraft = _26[1];
180
- var _27 = useState(1), brushSize = _27[0], setBrushSize = _27[1];
181
- var _28 = useState(1), eraserSize = _28[0], setEraserSize = _28[1];
166
+ var loadingVolumeIndexRef = React.useRef(props.selectedVolume);
167
+ var _14 = React.useState(false), showCrosshair = _14[0], setShowCrosshair = _14[1];
168
+ var _15 = useState('absolute'), complexMode = _15[0], setComplexMode = _15[1];
169
+ var _16 = useState(['absolute']), complexOptions = _16[0], setComplexOptions = _16[1];
170
+ var _17 = useState(true), roiVisible = _17[0], setROIVisible = _17[1];
171
+ var _18 = useState(0.8), drawingOpacity = _18[0], setDrawingOpacity = _18[1];
172
+ var _19 = useState(0), min = _19[0], setMin = _19[1];
173
+ var _20 = useState(1), max = _20[0], setMax = _20[1];
174
+ var _21 = useState(false), textsVisible = _21[0], setTextsVisible = _21[1];
175
+ var _22 = useState({ a: 1, b: 0 }), transformFactors = _22[0], setTransformFactors = _22[1];
176
+ var _23 = useState(false), saving = _23[0], setSaving = _23[1];
177
+ var _24 = useState(null), drawShapeTool = _24[0], setDrawShapeTool = _24[1];
178
+ var _25 = useState(null), shapeDraft = _25[0], setShapeDraft = _25[1];
179
+ var _26 = useState("freehand"), penDrawMode = _26[0], setPenDrawMode = _26[1];
180
+ var _27 = useState(0), polylineVertexCount = _27[0], setPolylineVertexCount = _27[1];
181
+ var _28 = useState(null), penDraft = _28[0], setPenDraft = _28[1];
182
+ var _29 = useState(1), brushSize = _29[0], setBrushSize = _29[1];
183
+ var _30 = useState(1), eraserSize = _30[0], setEraserSize = _30[1];
182
184
  var brushSizeRef = React.useRef(1);
183
185
  var eraserSizeRef = React.useRef(1);
184
186
  var shapeDraftRef = React.useRef(null);
@@ -193,8 +195,8 @@ export default function CloudMrNiivueViewer(props) {
193
195
  nv.opts.penBounds = 0;
194
196
  nv.opts.penSize = 1;
195
197
  }, []);
196
- var _29 = React.useState(1.0), gamma = _29[0], setGamma = _29[1];
197
- var _30 = React.useState(0), gammaKey = _30[0], setGammaKey = _30[1];
198
+ var _31 = React.useState(1.0), gamma = _31[0], setGamma = _31[1];
199
+ var _32 = React.useState(0), gammaKey = _32[0], setGammaKey = _32[1];
198
200
  // Niivue → React bridge so other places (Toolbar) can force the UI to reset
199
201
  nv.onResetGamma = function () {
200
202
  setGamma(1.0);
@@ -217,6 +219,9 @@ export default function CloudMrNiivueViewer(props) {
217
219
  setTimeout(function (args) { return nv.resizeListener(); }, 700);
218
220
  }
219
221
  }, []);
222
+ React.useEffect(function () {
223
+ loadingVolumeIndexRef.current = props.selectedVolume;
224
+ }, [props.selectedVolume]);
220
225
  React.useEffect(function () {
221
226
  var _a;
222
227
  var nii = (_a = props.niis) === null || _a === void 0 ? void 0 : _a[props.selectedVolume];
@@ -239,18 +244,18 @@ export default function CloudMrNiivueViewer(props) {
239
244
  // setLayers([...nv.volumes])
240
245
  // }, [])
241
246
  // values dualslider
242
- var _31 = useState(0), rangeKey = _31[0], setRangeKey = _31[1];
247
+ var _33 = useState(0), rangeKey = _33[0], setRangeKey = _33[1];
243
248
  nv.onResetContrast = function () {
244
249
  setRangeKey(rangeKey + 1);
245
250
  };
246
- var _32 = useState([0, 0, 0]), boundMins = _32[0], setBoundMins = _32[1];
247
- var _33 = useState([1, 1, 1]), boundMaxs = _33[0], setBoundMaxs = _33[1];
248
- var _34 = useState([0.5, 0.5, 0.5]), mms = _34[0], setMMs = _34[1];
251
+ var _34 = useState([0, 0, 0]), boundMins = _34[0], setBoundMins = _34[1];
252
+ var _35 = useState([1, 1, 1]), boundMaxs = _35[0], setBoundMaxs = _35[1];
253
+ var _36 = useState([0.5, 0.5, 0.5]), mms = _36[0], setMMs = _36[1];
249
254
  nv.onImageLoaded = function () {
250
- var _a, _b;
255
+ var _a, _b, _c, _d, _e, _f, _g;
251
256
  var oldCrosshairPos = __spreadArray([], nv.scene.crosshairPos, true);
252
257
  if (nv.volumes.length > 1) {
253
- var nii = (_a = props.niis) === null || _a === void 0 ? void 0 : _a[props.selectedVolume];
258
+ var nii = (_b = (_a = props.niis) === null || _a === void 0 ? void 0 : _a[loadingVolumeIndexRef.current]) !== null && _b !== void 0 ? _b : (_c = props.niis) === null || _c === void 0 ? void 0 : _c[props.selectedVolume];
254
259
  if (nii === null || nii === void 0 ? void 0 : nii.link) {
255
260
  nv.loadVolumes([niiToVolume(nii)]);
256
261
  setWarning("Error loading results, please check internet connectivity");
@@ -298,17 +303,16 @@ export default function CloudMrNiivueViewer(props) {
298
303
  }
299
304
  else {
300
305
  nv.setGamma(1.0);
301
- (_b = nv.onResetGamma) === null || _b === void 0 ? void 0 : _b.call(nv);
306
+ (_d = nv.onResetGamma) === null || _d === void 0 ? void 0 : _d.call(nv);
302
307
  nv.resetScene();
303
308
  }
304
309
  setLayers(__spreadArray([], nv.volumes, true));
305
310
  setBoundMins(nv.frac2mm([0, 0, 0]));
306
311
  setBoundMaxs(nv.frac2mm([1, 1, 1]));
307
312
  // setMMs(nv.frac2mm([0.5, 0.5, 0.5])); // Commented to prevent recentering on volume load; we now re-apply saved crosshair if available
308
- if (verifyComplex(nv.volumes[0])) //Check if there are complex components
309
- nvSetDisplayedVoxels('absolute');
310
- else
311
- nvSetDisplayedVoxels('absolute');
313
+ var currentNii = (_f = (_e = props.niis) === null || _e === void 0 ? void 0 : _e[loadingVolumeIndexRef.current]) !== null && _f !== void 0 ? _f : (_g = props.niis) === null || _g === void 0 ? void 0 : _g[props.selectedVolume];
314
+ verifyComplex(nv.volumes[0], currentNii);
315
+ nvSetDisplayedVoxels('absolute');
312
316
  // let volume = nv.volumes[0];
313
317
  nvSetDragMode(dragMode); // keep engine behavior in sync with dropdown
314
318
  // Re-apply world/voxel mode and last crosshair after resets
@@ -357,9 +361,18 @@ export default function CloudMrNiivueViewer(props) {
357
361
  return numbers;
358
362
  }
359
363
  }
360
- function verifyComplex(volume) {
364
+ function verifyComplex(volume, nii) {
365
+ var _a, _b;
361
366
  volume.real = volume.img;
362
367
  setComplexMode('absolute');
368
+ if (isAbsoluteOnlyVolume(nii)) {
369
+ volume.absolute = new volume.img.constructor(volume.img.length);
370
+ for (var i = 0; i < volume.img.length; i++) {
371
+ var realPart = volume.real[i];
372
+ var imaginaryPart = (_b = (_a = volume.imaginary) === null || _a === void 0 ? void 0 : _a[i]) !== null && _b !== void 0 ? _b : 0;
373
+ volume.absolute[i] = Math.sqrt(realPart * realPart + imaginaryPart * imaginaryPart);
374
+ }
375
+ }
363
376
  // Ensure volume.imaginary is defined and has the same length as volume.img
364
377
  if (!volume.imaginary || volume.imaginary.length !== volume.img.length) {
365
378
  setComplexOptions(['absolute', 'real']);
@@ -397,6 +410,10 @@ export default function CloudMrNiivueViewer(props) {
397
410
  return !allZero;
398
411
  }
399
412
  function nvSetDisplayedVoxels(voxelType) {
413
+ var _a, _b, _c;
414
+ var nii = (_b = (_a = props.niis) === null || _a === void 0 ? void 0 : _a[loadingVolumeIndexRef.current]) !== null && _b !== void 0 ? _b : (_c = props.niis) === null || _c === void 0 ? void 0 : _c[selectedVolume];
415
+ if (isAbsoluteOnlyVolume(nii) && voxelType !== 'absolute')
416
+ return;
400
417
  setComplexMode(voxelType);
401
418
  var volume = nv.volumes[0];
402
419
  switch (voxelType) {
@@ -533,7 +550,7 @@ export default function CloudMrNiivueViewer(props) {
533
550
  setTextsVisible(true);
534
551
  }
535
552
  }
536
- var _35 = useState("pan"), dragMode = _35[0], setDragMode = _35[1];
553
+ var _37 = useState("pan"), dragMode = _37[0], setDragMode = _37[1];
537
554
  function nvSetDragMode(dragMode) {
538
555
  switch (dragMode) {
539
556
  case "none":
@@ -847,7 +864,7 @@ export default function CloudMrNiivueViewer(props) {
847
864
  nv.opts.crosshairWidth = w;
848
865
  nv.drawScene();
849
866
  }
850
- var _36 = useState({}), labelMapping = _36[0], setLabelMapping = _36[1];
867
+ var _38 = useState({}), labelMapping = _38[0], setLabelMapping = _38[1];
851
868
  function resampleImage(mapping) {
852
869
  if (mapping === void 0) { mapping = labelMapping; }
853
870
  var el = typeof document !== "undefined" ? document.getElementById("histoplot") : null;
@@ -1370,8 +1387,12 @@ export default function CloudMrNiivueViewer(props) {
1370
1387
  setSelectionBoxColor(__spreadArray(__spreadArray([], rgb01, true), [0.5], false));
1371
1388
  nv.setSelectionBoxColor(__spreadArray(__spreadArray([], rgb01, true), [0.5], false));
1372
1389
  }
1373
- var _37 = React.useState('axial'), sliceType = _37[0], setSliceType = _37[1];
1390
+ var _39 = React.useState('axial'), sliceType = _39[0], setSliceType = _39[1];
1391
+ var axialOnlyView = isAxialOnlyVolume((_a = props.niis) === null || _a === void 0 ? void 0 : _a[selectedVolume]);
1392
+ var absoluteOnlyView = isAbsoluteOnlyVolume((_b = props.niis) === null || _b === void 0 ? void 0 : _b[selectedVolume]);
1374
1393
  function nvUpdateSliceType(newSliceType) {
1394
+ if (axialOnlyView && newSliceType !== 'axial')
1395
+ return;
1375
1396
  setSliceType(newSliceType);
1376
1397
  if (newSliceType === 'axial') {
1377
1398
  nv.setSliceType(nv.sliceTypeAxial);
@@ -1408,7 +1429,13 @@ export default function CloudMrNiivueViewer(props) {
1408
1429
  setLayers(__spreadArray([], nv.volumes, true));
1409
1430
  }
1410
1431
  function stylingProxy(nii) {
1411
- if (nii.dim === 2) {
1432
+ var _a;
1433
+ if (isAbsoluteOnlyVolume(nii)) {
1434
+ if (complexMode !== 'absolute' && ((_a = nv.volumes[0]) === null || _a === void 0 ? void 0 : _a.absolute)) {
1435
+ nvSetDisplayedVoxels('absolute');
1436
+ }
1437
+ }
1438
+ if (isAxialOnlyVolume(nii)) {
1412
1439
  nvUpdateSliceType('axial');
1413
1440
  setShowCrosshair(false);
1414
1441
  setTextsVisible(false);
@@ -1432,11 +1459,12 @@ export default function CloudMrNiivueViewer(props) {
1432
1459
  var _this = this;
1433
1460
  return __generator(this, function (_a) {
1434
1461
  openVolume = function () { return __awaiter(_this, void 0, void 0, function () {
1435
- var vol, e_1;
1462
+ var vol, nextNii, e_1;
1436
1463
  var _a;
1437
1464
  return __generator(this, function (_b) {
1438
1465
  switch (_b.label) {
1439
1466
  case 0:
1467
+ loadingVolumeIndexRef.current = volumeIndex;
1440
1468
  nv.closeDrawing();
1441
1469
  setDrawingChanged(false);
1442
1470
  if (drawingEnabled)
@@ -1460,9 +1488,8 @@ export default function CloudMrNiivueViewer(props) {
1460
1488
  nvSetDragMode(dragMode); // re-apply user's drag mode after Niivue resets
1461
1489
  // Re-apply world/voxel mode and last crosshair after loading a new volume
1462
1490
  nv.setSliceMM(worldSpace);
1463
- //applySavedCrosshairIfAny();
1464
- // ensure engine mode matches the remembered selection
1465
- nvUpdateSliceType(sliceType);
1491
+ nextNii = props.niis[volumeIndex];
1492
+ nvUpdateSliceType(isAxialOnlyVolume(nextNii) ? 'axial' : sliceType);
1466
1493
  nv.opts.crosshairWidth = showCrosshair ? 1 : 0;
1467
1494
  return [3 /*break*/, 4];
1468
1495
  case 3:
@@ -1510,14 +1537,14 @@ export default function CloudMrNiivueViewer(props) {
1510
1537
  return [2 /*return*/];
1511
1538
  });
1512
1539
  }); };
1513
- var _38 = useState(''), selectedROI = _38[0], setSelectedDrawingLayer = _38[1];
1514
- var _39 = useState(false), saveDialogOpen = _39[0], setSaveDialogOpen = _39[1];
1515
- var _40 = useState(function () {
1516
- }), saveConfirmCallback = _40[0], setSaveConfirmCallback = _40[1];
1517
- var _41 = useState(false), confirmationOpen = _41[0], setConfirmationOpen = _41[1];
1518
- var _42 = useState(function () { }), warningConfirmationCallback = _42[0], setWarningConfirmationCallback = _42[1];
1519
- var _43 = useState(function () { }), warningCancelCallback = _43[0], setWarningCancelCallback = _43[1];
1520
- var _44 = useState(false), drawingChanged = _44[0], setDrawingChanged = _44[1];
1540
+ var _40 = useState(''), selectedROI = _40[0], setSelectedDrawingLayer = _40[1];
1541
+ var _41 = useState(false), saveDialogOpen = _41[0], setSaveDialogOpen = _41[1];
1542
+ var _42 = useState(function () {
1543
+ }), saveConfirmCallback = _42[0], setSaveConfirmCallback = _42[1];
1544
+ var _43 = useState(false), confirmationOpen = _43[0], setConfirmationOpen = _43[1];
1545
+ var _44 = useState(function () { }), warningConfirmationCallback = _44[0], setWarningConfirmationCallback = _44[1];
1546
+ var _45 = useState(function () { }), warningCancelCallback = _45[0], setWarningCancelCallback = _45[1];
1547
+ var _46 = useState(false), drawingChanged = _46[0], setDrawingChanged = _46[1];
1521
1548
  // When new drawing strokes are made on top of a saved ROI, reset the dropdown
1522
1549
  // so it shows the "ROI Layer" placeholder instead of the stale saved name.
1523
1550
  React.useEffect(function () {
@@ -1887,7 +1914,7 @@ export default function CloudMrNiivueViewer(props) {
1887
1914
  width: '100%',
1888
1915
  alignItems: 'center',
1889
1916
  justifyContent: 'center'
1890
- } }, { children: [_jsxs(SettingsPanel, __assign({ open: openSettings, width: 300, toggleMenu: toggleSettings }, { children: [_jsx(ColorPicker, { colorRGB01: backColor, onSetColor: nvUpdateBackColor, title: 'Background color' }), _jsx(ColorPicker, { colorRGB01: clipPlaneColor, onSetColor: nvUpdateClipPlaneColor, title: 'Clip plane color' }), _jsx(NumberPicker, { value: clipPlaneOpacity, onChange: nvUpdateClipPlaneOpacity, title: 'Clip plane opacity', min: 0, max: 1, step: 0.1 }), _jsx(ColorPicker, { colorRGB01: crosshairColor, onSetColor: nvUpdateCrosshairColor, title: 'Crosshair color' }), _jsx(NumberPicker, { value: crosshairOpacity, onChange: nvUpdateCrosshairOpacity, title: 'Crosshair opacity', min: 0, max: 1, step: 0.1 }), _jsx(ColorPicker, { colorRGB01: selectionBoxColor, onSetColor: nvUpdateSelectionBoxColor, title: 'Selection box color' }), _jsx(NumberPicker, { value: nv.opts.crosshairWidth, onChange: nvUpdateCrosshairSize, title: 'Crosshair size', min: 0, max: 10, step: 1 }), _jsx(NumberPicker, { value: textSize, onChange: nvUpdateTextSize, title: 'Text size', min: 0, max: 0.2, step: 0.01 }), _jsx(ColorPicker, { colorRGB01: rulerColor, onSetColor: nvUpdateRulerColor, title: 'Ruler color' }), _jsx(NumberPicker, { value: rulerWidth, onChange: nvUpdateRulerWidth, title: 'Ruler thickness', min: 0, max: 10, step: 1 }), _jsx(NumberPicker, { value: rulerOpacity, onChange: nvUpdateRulerOpacity, title: 'Ruler opacity', min: 0, max: 1, step: 0.1 }), _jsx(NumberPicker, { value: opacity, onChange: nvUpdateOpacity, title: 'Opacity', min: 0, max: 1, step: 0.01 }), _jsx(NumberPicker, { value: drawOpacity, onChange: nvUpdateDrawOpacity, title: 'Draw Opacity', min: 0, max: 1, step: 0.01 }), _jsx("label", __assign({ htmlFor: "drawPen" }, { children: "Draw color:" })), _jsxs("select", __assign({ name: "drawPen", id: "drawPen", onChange: nvUpdateDrawPen, value: drawPen }, { children: [_jsx("option", __assign({ value: "0" }, { children: "Erase" })), _jsx("option", __assign({ value: "1" }, { children: "Red" })), _jsx("option", __assign({ value: "2" }, { children: "Green" })), _jsx("option", __assign({ value: "3" }, { children: "Blue" })), _jsx("option", __assign({ value: "8" }, { children: "Filled Erase" })), _jsx("option", __assign({ value: "9" }, { children: "Filled Red" })), _jsx("option", __assign({ value: "10" }, { children: "Filled Green" })), _jsx("option", __assign({ value: "11" }, { children: "Filled Blue" })), _jsx("option", __assign({ value: "12" }, { children: "Erase Selected Cluster" }))] })), _jsx(Button, __assign({ title: 'Save image', onClick: nvSaveImage }, { children: "Save image" })), _jsx(NVSwitch, { checked: locationTableVisible, title: 'Location table', onChange: toggleLocationTable }), _jsx(NVSwitch, { checked: drawingEnabled, title: 'Enable drawing', onChange: nvUpdateDrawingEnabled }), _jsx(NVSwitch, { checked: orientCube, title: 'Orientation cube', onChange: nvUpdateOrientCube }), _jsx(NVSwitch, { checked: ruler, title: 'Ruler', onChange: nvUpdateRuler }), _jsx(NVSwitch, { checked: clipPlane, title: 'Clip plane', onChange: nvUpdateClipPlane }), _jsx(NVSwitch, { checked: cornerText, title: 'Corner text', onChange: nvUpdateCornerText }), _jsx(NVSwitch, { checked: radiological, title: 'radiological', onChange: nvUpdateRadiological }), _jsx(NVSwitch, { checked: crosshair3D, title: '3D crosshair', onChange: nvUpdateCrosshair3D }), _jsx(NVSwitch, { checked: colorBar, title: 'Show color bar', onChange: nvUpdateColorBar }), _jsx(NVSwitch, { checked: worldSpace, title: 'World space', onChange: nvUpdateWorldSpace }), _jsx(NVSwitch, { checked: sagittalNoseLeft, title: 'Nose left', onChange: nvUpdateSagittalNoseLeft }), _jsx(NVSwitch, { checked: dragToMeasure, title: 'Drag to measure', onChange: nvUpdateDragToMeasure }), _jsx(NVSwitch, { checked: highDPI, title: 'High DPI', onChange: nvUpdateHighDPI }), _jsx(NumberPicker, { value: decimalPrecision, onChange: updateDecimalPrecision, title: 'Decimal precision', min: 0, max: 8, step: 1 }), _jsx(NumberPicker, { value: multiplanarPadPixels, onChange: nvUpdateMultiplanarPadPixels, title: 'Multiplanar padding', min: 0, max: 20, step: 2 }), _jsx(NumberPicker, { value: maxDrawUndoBitmaps, onChange: nvUpdateMaxDrawUndoBitmaps, title: 'Max Draw Undos', min: 8, max: 28, step: 1 }), _jsx(NumberPicker, { value: longTouchTimeout, onChange: nvUpdateLongTouchTimeout, title: 'Long touch timeout msec', min: 1000, max: 5000, step: 100 }), _jsx(NumberPicker, { value: doubleTouchTimeout, onChange: nvUpdateDoubleTouchTimeout, title: 'Double touch timeout msec', min: 500, max: 999, step: 25 })] })), _jsx(LayersPanel, __assign({ open: openLayers, width: 320, onToggleMenu: toggleLayers, onAddLayer: addLayer }, { children: layerList })), _jsx(Toolbar, { nv: nv, nvUpdateSliceType: nvUpdateSliceType, sliceType: sliceType, toggleSettings: toggleSettings, toggleLayers: toggleLayers, volumes: props.niis.map(niiToVolume), selectedVolume: selectedVolume, setSelectedVolume: selectVolume, showColorBar: colorBar, toggleColorBar: nvUpdateColorBar, rois: props.rois, selectedROI: selectedROI, refreshROI: refreshROI, setSelectedROI: selectDrawingLayer, toggleShowCrosshair: nvUpdateCrosshair, showCrosshair: showCrosshair, dragMode: dragMode, setDragMode: nvSetDragMode, toggleRadiological: nvUpdateRadiological, radiological: radiological, saveROI: saveDrawingLayer, complexMode: complexMode, setComplexMode: nvSetDisplayedVoxels, complexOptions: complexOptions, labelsVisible: textsVisible, toggleLabelsVisible: nvToggleLabelVisible, saving: saving, setSaving: setSaving, drawingChanged: drawingChanged, resampleImage: resampleImage, accessToken: props.accessToken, pipelineId: props.pipelineID, roiDeleteUrl: props.roiDeleteUrl, refreshPipelineRois: props.refreshPipelineRois }), _jsx(CmrConfirmation, { name: 'New Changes Made', message: "Consider saving your drawing before switching.", open: confirmationOpen, setOpen: setConfirmationOpen, cancellable: true, confirmCallback: warningConfirmationCallback, cancelCallback: warningCancelCallback, cancelText: "Don't save" }), _jsx(CmrEditConfirmation, { name: 'Save drawings', message: 'Please enter the name of the saved drawing', open: saveDialogOpen, setOpen: setSaveDialogOpen, confirmCallback: saveConfirmCallback, cancellable: true, cancelCallback: function () {
1917
+ } }, { children: [_jsxs(SettingsPanel, __assign({ open: openSettings, width: 300, toggleMenu: toggleSettings }, { children: [_jsx(ColorPicker, { colorRGB01: backColor, onSetColor: nvUpdateBackColor, title: 'Background color' }), _jsx(ColorPicker, { colorRGB01: clipPlaneColor, onSetColor: nvUpdateClipPlaneColor, title: 'Clip plane color' }), _jsx(NumberPicker, { value: clipPlaneOpacity, onChange: nvUpdateClipPlaneOpacity, title: 'Clip plane opacity', min: 0, max: 1, step: 0.1 }), _jsx(ColorPicker, { colorRGB01: crosshairColor, onSetColor: nvUpdateCrosshairColor, title: 'Crosshair color' }), _jsx(NumberPicker, { value: crosshairOpacity, onChange: nvUpdateCrosshairOpacity, title: 'Crosshair opacity', min: 0, max: 1, step: 0.1 }), _jsx(ColorPicker, { colorRGB01: selectionBoxColor, onSetColor: nvUpdateSelectionBoxColor, title: 'Selection box color' }), _jsx(NumberPicker, { value: nv.opts.crosshairWidth, onChange: nvUpdateCrosshairSize, title: 'Crosshair size', min: 0, max: 10, step: 1 }), _jsx(NumberPicker, { value: textSize, onChange: nvUpdateTextSize, title: 'Text size', min: 0, max: 0.2, step: 0.01 }), _jsx(ColorPicker, { colorRGB01: rulerColor, onSetColor: nvUpdateRulerColor, title: 'Ruler color' }), _jsx(NumberPicker, { value: rulerWidth, onChange: nvUpdateRulerWidth, title: 'Ruler thickness', min: 0, max: 10, step: 1 }), _jsx(NumberPicker, { value: rulerOpacity, onChange: nvUpdateRulerOpacity, title: 'Ruler opacity', min: 0, max: 1, step: 0.1 }), _jsx(NumberPicker, { value: opacity, onChange: nvUpdateOpacity, title: 'Opacity', min: 0, max: 1, step: 0.01 }), _jsx(NumberPicker, { value: drawOpacity, onChange: nvUpdateDrawOpacity, title: 'Draw Opacity', min: 0, max: 1, step: 0.01 }), _jsx("label", __assign({ htmlFor: "drawPen" }, { children: "Draw color:" })), _jsxs("select", __assign({ name: "drawPen", id: "drawPen", onChange: nvUpdateDrawPen, value: drawPen }, { children: [_jsx("option", __assign({ value: "0" }, { children: "Erase" })), _jsx("option", __assign({ value: "1" }, { children: "Red" })), _jsx("option", __assign({ value: "2" }, { children: "Green" })), _jsx("option", __assign({ value: "3" }, { children: "Blue" })), _jsx("option", __assign({ value: "8" }, { children: "Filled Erase" })), _jsx("option", __assign({ value: "9" }, { children: "Filled Red" })), _jsx("option", __assign({ value: "10" }, { children: "Filled Green" })), _jsx("option", __assign({ value: "11" }, { children: "Filled Blue" })), _jsx("option", __assign({ value: "12" }, { children: "Erase Selected Cluster" }))] })), _jsx(Button, __assign({ title: 'Save image', onClick: nvSaveImage }, { children: "Save image" })), _jsx(NVSwitch, { checked: locationTableVisible, title: 'Location table', onChange: toggleLocationTable }), _jsx(NVSwitch, { checked: drawingEnabled, title: 'Enable drawing', onChange: nvUpdateDrawingEnabled }), _jsx(NVSwitch, { checked: orientCube, title: 'Orientation cube', onChange: nvUpdateOrientCube }), _jsx(NVSwitch, { checked: ruler, title: 'Ruler', onChange: nvUpdateRuler }), _jsx(NVSwitch, { checked: clipPlane, title: 'Clip plane', onChange: nvUpdateClipPlane }), _jsx(NVSwitch, { checked: cornerText, title: 'Corner text', onChange: nvUpdateCornerText }), _jsx(NVSwitch, { checked: radiological, title: 'radiological', onChange: nvUpdateRadiological }), _jsx(NVSwitch, { checked: crosshair3D, title: '3D crosshair', onChange: nvUpdateCrosshair3D }), _jsx(NVSwitch, { checked: colorBar, title: 'Show color bar', onChange: nvUpdateColorBar }), _jsx(NVSwitch, { checked: worldSpace, title: 'World space', onChange: nvUpdateWorldSpace }), _jsx(NVSwitch, { checked: sagittalNoseLeft, title: 'Nose left', onChange: nvUpdateSagittalNoseLeft }), _jsx(NVSwitch, { checked: dragToMeasure, title: 'Drag to measure', onChange: nvUpdateDragToMeasure }), _jsx(NVSwitch, { checked: highDPI, title: 'High DPI', onChange: nvUpdateHighDPI }), _jsx(NumberPicker, { value: decimalPrecision, onChange: updateDecimalPrecision, title: 'Decimal precision', min: 0, max: 8, step: 1 }), _jsx(NumberPicker, { value: multiplanarPadPixels, onChange: nvUpdateMultiplanarPadPixels, title: 'Multiplanar padding', min: 0, max: 20, step: 2 }), _jsx(NumberPicker, { value: maxDrawUndoBitmaps, onChange: nvUpdateMaxDrawUndoBitmaps, title: 'Max Draw Undos', min: 8, max: 28, step: 1 }), _jsx(NumberPicker, { value: longTouchTimeout, onChange: nvUpdateLongTouchTimeout, title: 'Long touch timeout msec', min: 1000, max: 5000, step: 100 }), _jsx(NumberPicker, { value: doubleTouchTimeout, onChange: nvUpdateDoubleTouchTimeout, title: 'Double touch timeout msec', min: 500, max: 999, step: 25 })] })), _jsx(LayersPanel, __assign({ open: openLayers, width: 320, onToggleMenu: toggleLayers, onAddLayer: addLayer }, { children: layerList })), _jsx(Toolbar, { nv: nv, nvUpdateSliceType: nvUpdateSliceType, sliceType: sliceType, axialOnlyView: axialOnlyView, absoluteOnlyView: absoluteOnlyView, toggleSettings: toggleSettings, toggleLayers: toggleLayers, volumes: props.niis.map(niiToVolume), selectedVolume: selectedVolume, setSelectedVolume: selectVolume, showColorBar: colorBar, toggleColorBar: nvUpdateColorBar, rois: props.rois, selectedROI: selectedROI, refreshROI: refreshROI, setSelectedROI: selectDrawingLayer, toggleShowCrosshair: nvUpdateCrosshair, showCrosshair: showCrosshair, dragMode: dragMode, setDragMode: nvSetDragMode, toggleRadiological: nvUpdateRadiological, radiological: radiological, saveROI: saveDrawingLayer, complexMode: complexMode, setComplexMode: nvSetDisplayedVoxels, complexOptions: complexOptions, labelsVisible: textsVisible, toggleLabelsVisible: nvToggleLabelVisible, saving: saving, setSaving: setSaving, drawingChanged: drawingChanged, resampleImage: resampleImage, accessToken: props.accessToken, pipelineId: props.pipelineID, roiDeleteUrl: props.roiDeleteUrl, refreshPipelineRois: props.refreshPipelineRois }), _jsx(CmrConfirmation, { name: 'New Changes Made', message: "Consider saving your drawing before switching.", open: confirmationOpen, setOpen: setConfirmationOpen, cancellable: true, confirmCallback: warningConfirmationCallback, cancelCallback: warningCancelCallback, cancelText: "Don't save" }), _jsx(CmrEditConfirmation, { name: 'Save drawings', message: 'Please enter the name of the saved drawing', open: saveDialogOpen, setOpen: setSaveDialogOpen, confirmCallback: saveConfirmCallback, cancellable: true, cancelCallback: function () {
1891
1918
  },
1892
1919
  // suffix={'.zip'}
1893
1920
  defaultText: (props.rois[selectedROI] !== undefined ?
@@ -1906,3 +1933,34 @@ function niiToVolume(nii) {
1906
1933
  alias: nii.name
1907
1934
  };
1908
1935
  }
1936
+ /** 2D matrix results (noise covariance, noise coefficients, etc.) only support axial view. */
1937
+ function isAxialOnlyVolume(nii) {
1938
+ if (!nii)
1939
+ return false;
1940
+ if (nii.dim === 2)
1941
+ return true;
1942
+ var label = "".concat(nii.name || "", " ").concat(nii.type || "").toLowerCase();
1943
+ return (label.includes("noise covariance") ||
1944
+ label.includes("noise coefficients") ||
1945
+ label.includes("noise coefficient"));
1946
+ }
1947
+ /** SNR, g-factor maps, inverse g-factor, and noise coefficients are magnitude-only. */
1948
+ function isAbsoluteOnlyVolume(nii) {
1949
+ if (!nii)
1950
+ return false;
1951
+ var label = "".concat(nii.name || "", " ").concat(nii.type || "", " ").concat(nii.filename || "").toLowerCase();
1952
+ if (label.includes("noise coefficient"))
1953
+ return true;
1954
+ if (label.includes("inverse g factor") ||
1955
+ label.includes("inverse g-factor") ||
1956
+ label.includes("inverse gfactor") ||
1957
+ label.includes("inverse g"))
1958
+ return true;
1959
+ if (label.includes("g factor") ||
1960
+ label.includes("g-factor") ||
1961
+ /\bgfactor\b/.test(label))
1962
+ return true;
1963
+ if (label.includes("snr"))
1964
+ return true;
1965
+ return false;
1966
+ }
@@ -849,7 +849,15 @@ Niivue.prototype.groupLabelsFromSelection = function (sourceLabels = [], targetL
849
849
  };
850
850
 
851
851
  Niivue.prototype.resetScene = function () {
852
- this.scene.pan2Dxyzmm = [0, 0, 0, 1]
852
+ // Reset 2D pan + zoom
853
+ this.scene.pan2Dxyzmm = [0, 0, 0, 1];
854
+ // Reset 3D camera to Niivue defaults (azimuth=110, elevation=10, scale=1)
855
+ if (typeof this.setRenderAzimuthElevation === "function") {
856
+ this.setRenderAzimuthElevation(110, 10);
857
+ }
858
+ if (typeof this.setScale === "function") {
859
+ this.setScale(1.0);
860
+ }
853
861
  this.drawScene();
854
862
  }
855
863
 
@@ -871,19 +879,19 @@ Niivue.prototype.recenter = function () {
871
879
 
872
880
 
873
881
  Niivue.prototype.resetZoom = function () {
874
- // this.scene.pan2Dxyzmm[0] = 0;
875
- // this.scene.pan2Dxyzmm[1] = 0;
876
- // this.scene.pan2Dxyzmm[2] = 0;
877
-
882
+ // Reset 2D zoom (pan stays, only zoom component adjusted)
878
883
  const zoom = 1;
879
-
880
- const zoomChange = this.scene.pan2Dxyzmm[3] - zoom
884
+ const zoomChange = this.scene.pan2Dxyzmm[3] - zoom;
881
885
  this.scene.pan2Dxyzmm[3] = zoom;
882
- const mm = this.frac2mm(this.scene.crosshairPos)
883
- this.scene.pan2Dxyzmm[0] += zoomChange * mm[0]
884
- this.scene.pan2Dxyzmm[1] += zoomChange * mm[1]
885
- this.scene.pan2Dxyzmm[2] += zoomChange * mm[2]
886
- this.drawScene()
886
+ const mm = this.frac2mm(this.scene.crosshairPos);
887
+ this.scene.pan2Dxyzmm[0] += zoomChange * mm[0];
888
+ this.scene.pan2Dxyzmm[1] += zoomChange * mm[1];
889
+ this.scene.pan2Dxyzmm[2] += zoomChange * mm[2];
890
+ // Reset 3D zoom to default scale
891
+ if (typeof this.setScale === "function") {
892
+ this.setScale(1.0);
893
+ }
894
+ this.drawScene();
887
895
  }
888
896
 
889
897
  Niivue.prototype.setCenteredZoom = function (zoom) {
@@ -1528,6 +1536,10 @@ function clickedVoxelToolKind(nv) {
1528
1536
  function cloudMrTryReopenDraftOnClick(nv) {
1529
1537
  if (nv._cloudMrShapeDraftActive || nv._cloudMrPenDraftActive) return;
1530
1538
  if (!isClickWithoutDrag(nv.uiData)) return;
1539
+ // Never reopen a draft when the eraser is the active tool — the user is
1540
+ // trying to erase, not re-select. The eraser sets penValue to 0 with
1541
+ // drawing enabled; that combination uniquely identifies the eraser here.
1542
+ if (nv.opts.drawingEnabled && nv.opts.penValue === 0) return;
1531
1543
 
1532
1544
  const kind = clickedVoxelToolKind(nv);
1533
1545
  if (kind === 1) {
@@ -4,6 +4,10 @@ interface ToolbarProps {
4
4
  nv: any;
5
5
  nvUpdateSliceType: any;
6
6
  sliceType: string;
7
+ /** When true, only axial orientation is valid (e.g. noise covariance / coefficients). */
8
+ axialOnlyView?: boolean;
9
+ /** When true, only absolute display mode is valid (e.g. SNR, g-factor maps). */
10
+ absoluteOnlyView?: boolean;
7
11
  toggleLayers: React.MouseEventHandler<HTMLButtonElement> | undefined;
8
12
  toggleSettings: React.MouseEventHandler<HTMLButtonElement> | undefined;
9
13
  volumes: {
@@ -66,8 +66,9 @@ export default function Toolbar(props) {
66
66
  var viewerTheme = useNiivueViewerTheme();
67
67
  function handleSliceTypeChange(e) {
68
68
  var newSliceType = e.target.value;
69
- var nvUpdateSliceType = props.nvUpdateSliceType;
70
- nvUpdateSliceType(newSliceType);
69
+ if (props.axialOnlyView && newSliceType !== "axial")
70
+ return;
71
+ props.nvUpdateSliceType(newSliceType);
71
72
  }
72
73
  // let dragModes = ["Pan","Measurement","Contrast",'None'];
73
74
  var dragModes = [
@@ -76,6 +77,7 @@ export default function Toolbar(props) {
76
77
  { value: "contrast", label: "Slice and Contrast" },
77
78
  { value: "none", label: "Slice and None" }
78
79
  ];
80
+ var displayModes = ["absolute", "real", "imaginary", "phase"];
79
81
  var _a = useState(false), roiDeleteOpen = _a[0], setRoiDeleteOpen = _a[1];
80
82
  var _b = useState(undefined), roiDeleteMsg = _b[0], setRoiDeleteMsg = _b[1];
81
83
  var _c = useState(function () { return function () { }; }), roiDeleteConfirm = _c[0], setRoiDeleteConfirm = _c[1];
@@ -111,7 +113,7 @@ export default function Toolbar(props) {
111
113
  }) }))] })), _jsxs(FormControl, __assign({ size: 'small', sx: {
112
114
  m: 2,
113
115
  minWidth: 120
114
- } }, { children: [_jsx(InputLabel, __assign({ id: "slice-type-label" }, { children: "Orientation" })), _jsxs(Select, __assign({ labelId: "slice-type-label", id: "slice-type", value: props.sliceType, label: "Orientation", onChange: handleSliceTypeChange }, { children: [_jsx(MenuItem, __assign({ value: 'axial' }, { children: "Axial" })), _jsx(MenuItem, __assign({ value: 'coronal' }, { children: "Coronal" })), _jsx(MenuItem, __assign({ value: 'sagittal' }, { children: "Sagittal" })), _jsx(MenuItem, __assign({ value: 'multi' }, { children: "Multi" })), _jsx(MenuItem, __assign({ value: '3d' }, { children: "3D" }))] }))] })), _jsxs(FormControl, __assign({ size: 'small', sx: {
116
+ } }, { children: [_jsx(InputLabel, __assign({ id: "slice-type-label" }, { children: "Orientation" })), _jsxs(Select, __assign({ labelId: "slice-type-label", id: "slice-type", value: props.sliceType, label: "Orientation", onChange: handleSliceTypeChange }, { children: [_jsx(MenuItem, __assign({ value: 'axial' }, { children: "Axial" })), _jsx(MenuItem, __assign({ value: 'coronal', disabled: props.axialOnlyView }, { children: "Coronal" })), _jsx(MenuItem, __assign({ value: 'sagittal', disabled: props.axialOnlyView }, { children: "Sagittal" })), _jsx(MenuItem, __assign({ value: 'multi', disabled: props.axialOnlyView }, { children: "Multi" })), _jsx(MenuItem, __assign({ value: '3d', disabled: props.axialOnlyView }, { children: "3D" }))] }))] })), _jsxs(FormControl, __assign({ size: 'small', sx: {
115
117
  m: 2,
116
118
  minWidth: 180
117
119
  } }, { children: [_jsx(InputLabel, __assign({ id: "drag-mode-label" }, { children: "Scroll and Right Click Drag" })), _jsx(Select, __assign({ labelId: "drag-mode-label", id: "drag-mode", value: props.dragMode, label: "Scroll and Right Click Drag", onChange: function (e) {
@@ -120,8 +122,17 @@ export default function Toolbar(props) {
120
122
  } }, { children: dragModes.map(function (mode, index) { return (_jsx(MenuItem, __assign({ value: mode.value }, { children: mode.label }), index)); }) }))] })), _jsxs(FormControl, __assign({ size: 'small', sx: {
121
123
  m: 2,
122
124
  minWidth: 120
123
- } }, { children: [_jsx(InputLabel, __assign({ id: "slice-type-label" }, { children: "Display Mode" })), _jsx(Select, __assign({ labelId: "slice-type-label", id: "slice-type", value: props.complexMode, label: "Display Mode", onChange: function (e) { return props.setComplexMode(e.target.value); } }, { children: props.complexOptions.map(function (value) {
124
- return _jsx(MenuItem, __assign({ value: value }, { children: value.charAt(0).toUpperCase() + value.slice(1) }));
125
+ } }, { children: [_jsx(InputLabel, __assign({ id: "slice-type-label" }, { children: "Display Mode" })), _jsx(Select, __assign({ labelId: "slice-type-label", id: "slice-type", value: props.complexMode, label: "Display Mode", onChange: function (e) {
126
+ var next = e.target.value;
127
+ if (props.absoluteOnlyView && next !== "absolute")
128
+ return;
129
+ if (!props.complexOptions.includes(next))
130
+ return;
131
+ props.setComplexMode(next);
132
+ } }, { children: displayModes.map(function (value) {
133
+ var unavailable = !props.complexOptions.includes(value);
134
+ var absoluteOnlyLocked = props.absoluteOnlyView && value !== "absolute";
135
+ return (_jsx(MenuItem, __assign({ value: value, disabled: unavailable || absoluteOnlyLocked }, { children: value.charAt(0).toUpperCase() + value.slice(1) }), value));
125
136
  }) }))] })), _jsxs(FormControl, __assign({ size: "small", sx: { m: 2, minWidth: 160 } }, { children: [_jsx(InputLabel, __assign({ id: "roi-layer-label" }, { children: "ROI Layer" })), _jsx(Select, __assign({ labelId: "roi-layer-label", id: "roi-layer", value: props.selectedROI, label: "Opened ROIs",
126
137
  // Only text in the closed preview (no icon)
127
138
  renderValue: function (selected) {
@@ -440,11 +440,18 @@ export function capturePolylineDraftFromClick(nv) {
440
440
  return null;
441
441
  var dx = nv.back.dims[1];
442
442
  var dy = nv.back.dims[2];
443
- // Decode stored linear indices back to [x, y, z] triples.
443
+ // Decode stored linear indices back to [x, y, z] triples, but only include
444
+ // voxels that are still present in the current bitmap. Registered indices
445
+ // that were cleared by the eraser must not be restored — otherwise re-opening
446
+ // the draft would silently undo the user's erasure.
444
447
  var strokeVoxels = [];
448
+ var presentIndices = new Set();
445
449
  var x1 = Infinity, y1 = Infinity, z1 = Infinity;
446
450
  var x2 = -Infinity, y2 = -Infinity, z2 = -Infinity;
447
451
  entry.voxelIndices.forEach(function (idx) {
452
+ if (nv.drawBitmap[idx] === 0)
453
+ return; // already erased — skip
454
+ presentIndices.add(idx);
448
455
  var z = Math.floor(idx / (dx * dy));
449
456
  var rem = idx - z * dx * dy;
450
457
  var y = Math.floor(rem / dx);
@@ -463,7 +470,9 @@ export function capturePolylineDraftFromClick(nv) {
463
470
  if (z > z2)
464
471
  z2 = z;
465
472
  });
466
- var baseBitmap = eraseClusterFromBitmap(nv.drawBitmap, entry.voxelIndices);
473
+ if (strokeVoxels.length === 0)
474
+ return null;
475
+ var baseBitmap = eraseClusterFromBitmap(nv.drawBitmap, presentIndices);
467
476
  return {
468
477
  kind: "freehand",
469
478
  baseBitmap: baseBitmap,
@@ -44,6 +44,7 @@ export function notifyPolylineChange(nv, count) {
44
44
  export function resetPolylineState(nv) {
45
45
  nv._cloudMrPolylineVertices = [];
46
46
  nv._cloudMrPolylineBaseBitmap = null;
47
+ nv._cloudMrPolylinePrevBaseBitmap = null;
47
48
  nv._cloudMrPolylineSessionStartBitmap = null;
48
49
  nv._cloudMrPolylineAxCorSag = -1;
49
50
  nv._cloudMrLastPolylineClickMs = null;
@@ -90,12 +91,20 @@ export function addPolylineVertex(nv) {
90
91
  now - nv._cloudMrLastPolylineClickMs <= DOUBLE_CLICK_MS;
91
92
  nv._cloudMrLastPolylineClickMs = now;
92
93
  if (isDoubleClick) {
93
- // Don't add this click as a vertex signal close instead.
94
+ // The first click of the double-click already placed an extra vertex.
95
+ // Roll it back using the snapshot saved before that click, then close.
96
+ if (nv._cloudMrPolylinePrevBaseBitmap) {
97
+ verts.pop();
98
+ nv._cloudMrPolylineBaseBitmap = nv._cloudMrPolylinePrevBaseBitmap;
99
+ nv.drawBitmap.set(nv._cloudMrPolylinePrevBaseBitmap);
100
+ nv.refreshDrawing(false, false);
101
+ }
94
102
  return POLYLINE_CLOSE;
95
103
  }
96
104
  if (verts.length === 0) {
97
105
  nv._cloudMrPolylineSessionStartBitmap = nv.drawBitmap.slice();
98
106
  nv._cloudMrPolylineBaseBitmap = nv._cloudMrPolylineSessionStartBitmap.slice();
107
+ nv._cloudMrPolylinePrevBaseBitmap = null;
99
108
  nv._cloudMrPolylineAxCorSag = axCorSagFromMouse(nv);
100
109
  nv.drawPenAxCorSag = nv._cloudMrPolylineAxCorSag;
101
110
  verts.push(pt);
@@ -108,6 +117,8 @@ export function addPolylineVertex(nv) {
108
117
  return false;
109
118
  }
110
119
  nv.drawPenAxCorSag = nv._cloudMrPolylineAxCorSag;
120
+ // Save the pre-segment bitmap so a subsequent double-click can roll back.
121
+ nv._cloudMrPolylinePrevBaseBitmap = nv._cloudMrPolylineBaseBitmap;
111
122
  nv.drawPenLine(pt, prev, penValue);
112
123
  verts.push(pt);
113
124
  nv._cloudMrPolylineBaseBitmap = nv.drawBitmap.slice();
@@ -1469,7 +1469,15 @@ Niivue.prototype.groupLabelsFromSelection = function (
1469
1469
  };
1470
1470
 
1471
1471
  Niivue.prototype.resetScene = function () {
1472
+ // Reset 2D pan + zoom
1472
1473
  this.scene.pan2Dxyzmm = [0, 0, 0, 1];
1474
+ // Reset 3D camera to Niivue defaults (azimuth=110, elevation=10, scale=1)
1475
+ if (typeof this.setRenderAzimuthElevation === "function") {
1476
+ this.setRenderAzimuthElevation(110, 10);
1477
+ }
1478
+ if (typeof this.setScale === "function") {
1479
+ this.setScale(1.0);
1480
+ }
1473
1481
  this.drawScene();
1474
1482
  };
1475
1483
 
@@ -1490,18 +1498,18 @@ Niivue.prototype.recenter = function () {
1490
1498
  };
1491
1499
 
1492
1500
  Niivue.prototype.resetZoom = function () {
1493
- // this.scene.pan2Dxyzmm[0] = 0;
1494
- // this.scene.pan2Dxyzmm[1] = 0;
1495
- // this.scene.pan2Dxyzmm[2] = 0;
1496
-
1501
+ // Reset 2D zoom (pan stays, only zoom component adjusted)
1497
1502
  const zoom = 1;
1498
-
1499
1503
  const zoomChange = this.scene.pan2Dxyzmm[3] - zoom;
1500
1504
  this.scene.pan2Dxyzmm[3] = zoom;
1501
1505
  const mm = this.frac2mm(this.scene.crosshairPos);
1502
1506
  this.scene.pan2Dxyzmm[0] += zoomChange * mm[0];
1503
1507
  this.scene.pan2Dxyzmm[1] += zoomChange * mm[1];
1504
1508
  this.scene.pan2Dxyzmm[2] += zoomChange * mm[2];
1509
+ // Reset 3D zoom to default scale
1510
+ if (typeof this.setScale === "function") {
1511
+ this.setScale(1.0);
1512
+ }
1505
1513
  this.drawScene();
1506
1514
  };
1507
1515
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudmr-ux",
3
- "version": "4.8.7",
3
+ "version": "4.8.9",
4
4
  "author": "erosmontin@gmail.com",
5
5
  "license": "MIT",
6
6
  "repository": "erosmontin/cloudmr-ux",