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.
- package/dist/CmrComponents/niivue-viewer/CloudMrNiivueViewer.js +142 -84
- package/dist/CmrComponents/niivue-viewer/NiivuePatcher.js +24 -12
- package/dist/CmrComponents/niivue-viewer/Toolbar.d.ts +4 -0
- package/dist/CmrComponents/niivue-viewer/Toolbar.js +16 -5
- package/dist/CmrComponents/niivue-viewer/penDraftUtils.js +11 -2
- package/dist/CmrComponents/niivue-viewer/polylinePenUtils.js +12 -1
- package/dist/core/common/components/NiivueTools/NiivuePatcher.js +13 -5
- package/package.json +1 -1
|
@@ -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
|
|
127
|
-
var
|
|
128
|
-
var
|
|
129
|
-
var
|
|
130
|
-
var
|
|
131
|
-
var
|
|
132
|
-
var
|
|
133
|
-
var
|
|
134
|
-
var
|
|
135
|
-
var
|
|
136
|
-
var
|
|
137
|
-
var
|
|
138
|
-
var
|
|
139
|
-
var
|
|
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
|
|
142
|
-
var
|
|
143
|
-
var
|
|
144
|
-
var
|
|
145
|
-
var
|
|
146
|
-
var
|
|
147
|
-
var
|
|
148
|
-
var
|
|
149
|
-
var
|
|
150
|
-
var
|
|
151
|
-
var
|
|
152
|
-
var
|
|
153
|
-
var
|
|
154
|
-
var
|
|
155
|
-
var
|
|
156
|
-
var
|
|
157
|
-
var
|
|
158
|
-
var
|
|
159
|
-
var
|
|
160
|
-
var
|
|
161
|
-
var
|
|
162
|
-
var
|
|
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
|
|
166
|
-
var
|
|
167
|
-
var
|
|
168
|
-
var
|
|
169
|
-
var
|
|
170
|
-
var
|
|
171
|
-
var
|
|
172
|
-
var
|
|
173
|
-
var
|
|
174
|
-
var
|
|
175
|
-
var
|
|
176
|
-
var
|
|
177
|
-
var
|
|
178
|
-
var
|
|
179
|
-
var
|
|
180
|
-
var
|
|
181
|
-
var
|
|
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
|
|
197
|
-
var
|
|
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
|
|
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
|
|
247
|
-
var
|
|
248
|
-
var
|
|
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
|
-
(
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1464
|
-
|
|
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
|
|
1514
|
-
var
|
|
1515
|
-
var
|
|
1516
|
-
}), saveConfirmCallback =
|
|
1517
|
-
var
|
|
1518
|
-
var
|
|
1519
|
-
var
|
|
1520
|
-
var
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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) {
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|