cloudmr-ux 4.8.0 → 4.8.3
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/CloudMrNiivuePanel.d.ts +2 -2
- package/dist/CmrComponents/niivue-viewer/CloudMrNiivuePanel.js +19 -9
- package/dist/CmrComponents/niivue-viewer/CloudMrNiivueViewer.js +91 -60
- package/dist/CmrComponents/niivue-viewer/NiivuePatcher.js +10 -0
- package/dist/CmrComponents/niivue-viewer/ShapeDraftOverlay.js +18 -7
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/DrawColorPlatte.d.ts +2 -4
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/DrawColorPlatte.js +17 -18
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/EraserPlatte.d.ts +3 -3
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/EraserPlatte.js +2 -2
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/MroDrawToolkit.js +81 -42
- package/dist/CmrComponents/niivue-viewer/shapeDraftUtils.d.ts +32 -0
- package/dist/CmrComponents/niivue-viewer/shapeDraftUtils.js +209 -12
- package/package.json +1 -1
|
@@ -25,8 +25,8 @@ export interface CloudMrNiivuePanelProps {
|
|
|
25
25
|
locationData: any[];
|
|
26
26
|
decimalPrecision: number;
|
|
27
27
|
drawToolkitProps: CloudMrDrawToolkitProps;
|
|
28
|
-
drawShapeTool: "pen" | "rectangle" | "ellipse" | null;
|
|
29
|
-
setDrawShapeTool: (t: "pen" | "rectangle" | "ellipse" | null) => void;
|
|
28
|
+
drawShapeTool: "pen" | "polyline" | "rectangle" | "ellipse" | null;
|
|
29
|
+
setDrawShapeTool: (t: "pen" | "polyline" | "rectangle" | "ellipse" | null) => void;
|
|
30
30
|
resampleImage: () => void;
|
|
31
31
|
layerList: React.ComponentProps<any>[];
|
|
32
32
|
mins: number[];
|
|
@@ -109,24 +109,24 @@ export function CloudMrNiivuePanel(props) {
|
|
|
109
109
|
props.resampleImage();
|
|
110
110
|
}, [histogram]);
|
|
111
111
|
function applyDrawShapeTool(tool) {
|
|
112
|
-
var _a, _b, _c
|
|
112
|
+
var _a, _b, _c;
|
|
113
113
|
if (props.shapeDraft) {
|
|
114
|
-
// Apply (commit) the current draft rather than discarding it
|
|
115
114
|
(_a = props.onApplyShapeDraftKeepTool) === null || _a === void 0 ? void 0 : _a.call(props);
|
|
116
115
|
}
|
|
117
116
|
if (props.penDraft) {
|
|
118
117
|
(_b = props.onApplyPenDraftKeepTool) === null || _b === void 0 ? void 0 : _b.call(props);
|
|
119
118
|
}
|
|
120
|
-
if (tool !== "pen") {
|
|
121
|
-
(_d = (_c = props.drawToolkitProps).onPenDrawModeChange) === null || _d === void 0 ? void 0 : _d.call(_c, "freehand");
|
|
122
|
-
}
|
|
123
119
|
props.setDrawShapeTool(tool);
|
|
124
120
|
var nv = props.nv;
|
|
125
|
-
var penMode = (_e = props.drawToolkitProps.penDrawMode) !== null && _e !== void 0 ? _e : "freehand";
|
|
126
121
|
nv.opts.deferShapeCommit = tool === "rectangle" || tool === "ellipse";
|
|
127
122
|
if (tool === "pen") {
|
|
128
|
-
nv.opts.polylinePenMode =
|
|
129
|
-
nv.opts.isFilledPen =
|
|
123
|
+
nv.opts.polylinePenMode = false;
|
|
124
|
+
nv.opts.isFilledPen = true;
|
|
125
|
+
nv.opts.deferFreehandCommit = false;
|
|
126
|
+
}
|
|
127
|
+
else if (tool === "polyline") {
|
|
128
|
+
nv.opts.polylinePenMode = true;
|
|
129
|
+
nv.opts.isFilledPen = false;
|
|
130
130
|
nv.opts.deferFreehandCommit = false;
|
|
131
131
|
}
|
|
132
132
|
else {
|
|
@@ -139,8 +139,18 @@ export function CloudMrNiivuePanel(props) {
|
|
|
139
139
|
: tool === "ellipse"
|
|
140
140
|
? NI_PEN_TYPE.ELLIPSE
|
|
141
141
|
: NI_PEN_TYPE.PEN;
|
|
142
|
+
if (tool === "rectangle" || tool === "ellipse") {
|
|
143
|
+
nv.opts.penBounds = 0;
|
|
144
|
+
nv.opts.penSize = 1;
|
|
145
|
+
}
|
|
146
|
+
else if (tool === "pen" || tool === "polyline") {
|
|
147
|
+
var penBrush = (_c = props.drawToolkitProps.brushSize) !== null && _c !== void 0 ? _c : 1;
|
|
148
|
+
nv.opts.penBounds = (penBrush - 1) / 2;
|
|
149
|
+
nv.opts.penSize = penBrush;
|
|
150
|
+
}
|
|
142
151
|
nv.drawScene();
|
|
143
|
-
if ((tool === "rectangle" || tool === "ellipse"
|
|
152
|
+
if ((tool === "rectangle" || tool === "ellipse" || tool === "pen" || tool === "polyline") &&
|
|
153
|
+
!props.drawToolkitProps.drawingEnabled) {
|
|
144
154
|
props.drawToolkitProps.setDrawingEnabled(true);
|
|
145
155
|
}
|
|
146
156
|
}
|
|
@@ -64,6 +64,7 @@ import { NumberPicker } from './NumberPicker';
|
|
|
64
64
|
import { ColorPicker } from './ColorPicker';
|
|
65
65
|
import { LayersPanel } from './LayersPanel';
|
|
66
66
|
import { applyPenDraft, cancelPenDraft, captureFreehandDraft, fillPolylineDraft, unfillPolylineDraft, polylineDraftFromNv, syncPolylineDraftToNv, registerAppliedPolyline, restoreCommittedPolyline, collectPolylineAppliedVoxelIndices, } from './penDraftUtils';
|
|
67
|
+
import { registerAppliedShape, updateShapeRegistryErosionState, removeShapeRegistryEntry, clearShapeRegistry, } from './shapeDraftUtils';
|
|
67
68
|
import { CloudMrNiivuePanel } from './CloudMrNiivuePanel';
|
|
68
69
|
import { Niivue } from './NiivuePatcher';
|
|
69
70
|
import NVSwitch from './Switch';
|
|
@@ -177,6 +178,9 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
177
178
|
var _25 = useState(0), polylineVertexCount = _25[0], setPolylineVertexCount = _25[1];
|
|
178
179
|
var _26 = useState(null), penDraft = _26[0], setPenDraft = _26[1];
|
|
179
180
|
var _27 = useState(1), brushSize = _27[0], setBrushSize = _27[1];
|
|
181
|
+
var _28 = useState(1), eraserSize = _28[0], setEraserSize = _28[1];
|
|
182
|
+
var brushSizeRef = React.useRef(1);
|
|
183
|
+
var eraserSizeRef = React.useRef(1);
|
|
180
184
|
var shapeDraftRef = React.useRef(null);
|
|
181
185
|
var penDraftRef = React.useRef(null);
|
|
182
186
|
var drawShapeToolRef = React.useRef(null);
|
|
@@ -189,8 +193,8 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
189
193
|
nv.opts.penBounds = 0;
|
|
190
194
|
nv.opts.penSize = 1;
|
|
191
195
|
}, []);
|
|
192
|
-
var
|
|
193
|
-
var
|
|
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];
|
|
194
198
|
// Niivue → React bridge so other places (Toolbar) can force the UI to reset
|
|
195
199
|
nv.onResetGamma = function () {
|
|
196
200
|
setGamma(1.0);
|
|
@@ -235,13 +239,13 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
235
239
|
// setLayers([...nv.volumes])
|
|
236
240
|
// }, [])
|
|
237
241
|
// values dualslider
|
|
238
|
-
var
|
|
242
|
+
var _31 = useState(0), rangeKey = _31[0], setRangeKey = _31[1];
|
|
239
243
|
nv.onResetContrast = function () {
|
|
240
244
|
setRangeKey(rangeKey + 1);
|
|
241
245
|
};
|
|
242
|
-
var
|
|
243
|
-
var
|
|
244
|
-
var
|
|
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];
|
|
245
249
|
nv.onImageLoaded = function () {
|
|
246
250
|
var _a, _b;
|
|
247
251
|
var oldCrosshairPos = __spreadArray([], nv.scene.crosshairPos, true);
|
|
@@ -437,6 +441,11 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
437
441
|
nv._cloudMrSuppressDrawingChangedMouseUp = false;
|
|
438
442
|
return;
|
|
439
443
|
}
|
|
444
|
+
if (nv.opts.drawingEnabled &&
|
|
445
|
+
nv.opts.penType === NI_PEN_TYPE.PEN &&
|
|
446
|
+
nv.opts.penValue === 0) {
|
|
447
|
+
updateShapeRegistryErosionState(nv);
|
|
448
|
+
}
|
|
440
449
|
if (nv.opts.drawingEnabled) {
|
|
441
450
|
setDrawingChanged(true);
|
|
442
451
|
resampleImage();
|
|
@@ -524,7 +533,7 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
524
533
|
setTextsVisible(true);
|
|
525
534
|
}
|
|
526
535
|
}
|
|
527
|
-
var
|
|
536
|
+
var _35 = useState("pan"), dragMode = _35[0], setDragMode = _35[1];
|
|
528
537
|
function nvSetDragMode(dragMode) {
|
|
529
538
|
switch (dragMode) {
|
|
530
539
|
case "none":
|
|
@@ -561,11 +570,25 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
561
570
|
nv.setDrawingEnabled(enabled);
|
|
562
571
|
nv.drawScene();
|
|
563
572
|
}
|
|
564
|
-
function
|
|
565
|
-
setBrushSize(size);
|
|
573
|
+
function applyNvBrushSize(size) {
|
|
566
574
|
nv.opts.penBounds = (size - 1) / 2;
|
|
567
575
|
nv.opts.penSize = size;
|
|
568
576
|
}
|
|
577
|
+
function nvUpdateBrushSize(size) {
|
|
578
|
+
setBrushSize(size);
|
|
579
|
+
brushSizeRef.current = size;
|
|
580
|
+
var tool = drawShapeToolRef.current;
|
|
581
|
+
if ((tool === "pen" || tool === "polyline") && drawPen !== 0 && drawPen !== 8) {
|
|
582
|
+
applyNvBrushSize(size);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function nvUpdateEraserSize(size) {
|
|
586
|
+
setEraserSize(size);
|
|
587
|
+
eraserSizeRef.current = size;
|
|
588
|
+
if (drawPen === 0 || drawPen === 8) {
|
|
589
|
+
applyNvBrushSize(size);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
569
592
|
function nvUpdateDrawPen(a) {
|
|
570
593
|
var _a;
|
|
571
594
|
var raw = Number(a.target.value);
|
|
@@ -583,21 +606,19 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
583
606
|
nv.opts.deferShapeCommit = false;
|
|
584
607
|
nv.opts.polylinePenMode = false;
|
|
585
608
|
(_a = nv.cloudMrResetPolyline) === null || _a === void 0 ? void 0 : _a.call(nv);
|
|
586
|
-
|
|
587
|
-
cancelPenDraft(nv, penDraftRef.current);
|
|
588
|
-
setPenDraft(null);
|
|
589
|
-
penDraftRef.current = null;
|
|
590
|
-
nv._cloudMrPenDraftActive = false;
|
|
591
|
-
setPolylineVertexCount(0);
|
|
592
|
-
}
|
|
593
|
-
if (shapeDraftRef.current) {
|
|
594
|
-
cancelShapeDraft();
|
|
595
|
-
}
|
|
609
|
+
applyActiveDraftIfAny();
|
|
596
610
|
}
|
|
597
611
|
else if (drawShapeToolRef.current === "pen") {
|
|
598
612
|
nv.opts.deferFreehandCommit = false;
|
|
599
|
-
nv.opts.polylinePenMode =
|
|
600
|
-
nv.opts.isFilledPen =
|
|
613
|
+
nv.opts.polylinePenMode = false;
|
|
614
|
+
nv.opts.isFilledPen = true;
|
|
615
|
+
applyNvBrushSize(brushSizeRef.current);
|
|
616
|
+
}
|
|
617
|
+
else if (drawShapeToolRef.current === "polyline") {
|
|
618
|
+
nv.opts.deferFreehandCommit = false;
|
|
619
|
+
nv.opts.polylinePenMode = true;
|
|
620
|
+
nv.opts.isFilledPen = false;
|
|
621
|
+
applyNvBrushSize(brushSizeRef.current);
|
|
601
622
|
}
|
|
602
623
|
}
|
|
603
624
|
function deactivateDrawTools() {
|
|
@@ -625,26 +646,17 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
625
646
|
nv.setPenValue(1, false);
|
|
626
647
|
nv.opts.isFilledPen = false;
|
|
627
648
|
}
|
|
649
|
+
applyNvBrushSize(1);
|
|
628
650
|
}
|
|
629
651
|
function activateEraser() {
|
|
630
|
-
|
|
652
|
+
applyActiveDraftIfAny();
|
|
631
653
|
setDrawShapeTool(null);
|
|
632
654
|
nv.opts.penType = NI_PEN_TYPE.PEN;
|
|
633
655
|
nv.opts.deferShapeCommit = false;
|
|
634
656
|
nv.opts.deferFreehandCommit = false;
|
|
635
657
|
nv.opts.polylinePenMode = false;
|
|
636
|
-
if (shapeDraftRef.current) {
|
|
637
|
-
cancelShapeDraft();
|
|
638
|
-
}
|
|
639
|
-
if (penDraftRef.current) {
|
|
640
|
-
cancelPenDraft(nv, penDraftRef.current);
|
|
641
|
-
(_a = nv.cloudMrResetPolyline) === null || _a === void 0 ? void 0 : _a.call(nv);
|
|
642
|
-
setPolylineVertexCount(0);
|
|
643
|
-
setPenDraft(null);
|
|
644
|
-
penDraftRef.current = null;
|
|
645
|
-
nv._cloudMrPenDraftActive = false;
|
|
646
|
-
}
|
|
647
658
|
nvUpdateDrawPen({ target: { value: 8 } });
|
|
659
|
+
applyNvBrushSize(eraserSizeRef.current);
|
|
648
660
|
nvSetDrawingEnabled(true);
|
|
649
661
|
}
|
|
650
662
|
function nvUpdateDrawOpacity(a) {
|
|
@@ -809,7 +821,7 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
809
821
|
nv.opts.crosshairWidth = w;
|
|
810
822
|
nv.drawScene();
|
|
811
823
|
}
|
|
812
|
-
var
|
|
824
|
+
var _36 = useState({}), labelMapping = _36[0], setLabelMapping = _36[1];
|
|
813
825
|
function resampleImage(mapping) {
|
|
814
826
|
if (mapping === void 0) { mapping = labelMapping; }
|
|
815
827
|
var el = typeof document !== "undefined" ? document.getElementById("histoplot") : null;
|
|
@@ -877,7 +889,8 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
877
889
|
setPenDraft(null);
|
|
878
890
|
penDraftRef.current = null;
|
|
879
891
|
nv._cloudMrPenDraftActive = false;
|
|
880
|
-
|
|
892
|
+
var tool = drawShapeToolRef.current;
|
|
893
|
+
if (tool === "pen" || tool === "polyline") {
|
|
881
894
|
nvSetDrawingEnabled(true);
|
|
882
895
|
}
|
|
883
896
|
}
|
|
@@ -910,9 +923,9 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
910
923
|
nv._cloudMrPenDraftActive = false;
|
|
911
924
|
setDrawingChanged(true);
|
|
912
925
|
if (keepTool) {
|
|
913
|
-
var
|
|
914
|
-
nv.opts.polylinePenMode =
|
|
915
|
-
nv.opts.isFilledPen =
|
|
926
|
+
var tool = drawShapeToolRef.current;
|
|
927
|
+
nv.opts.polylinePenMode = tool === "polyline";
|
|
928
|
+
nv.opts.isFilledPen = tool === "pen";
|
|
916
929
|
nv.opts.deferFreehandCommit = false;
|
|
917
930
|
nvSetDrawingEnabled(true);
|
|
918
931
|
}
|
|
@@ -947,6 +960,7 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
947
960
|
nv.onShapeCommitted = function (draft) {
|
|
948
961
|
nv.drawAddUndoBitmap(nv.drawFillOverwrites);
|
|
949
962
|
markShapeVoxelKind(draft);
|
|
963
|
+
registerAppliedShape(nv, draft);
|
|
950
964
|
setDrawingChanged(true);
|
|
951
965
|
resampleImage();
|
|
952
966
|
};
|
|
@@ -968,11 +982,10 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
968
982
|
setPenDraft(draft);
|
|
969
983
|
penDraftRef.current = draft;
|
|
970
984
|
nv._cloudMrPenDraftActive = true;
|
|
971
|
-
// Auto-select pen tool so the palette opens
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
penDrawModeRef.current = mode;
|
|
985
|
+
// Auto-select the correct pen tool so the right palette opens
|
|
986
|
+
var tool = draft.kind === "polyline" ? "polyline" : "pen";
|
|
987
|
+
setDrawShapeTool(tool);
|
|
988
|
+
penDrawModeRef.current = draft.kind === "polyline" ? "polyline" : "freehand";
|
|
976
989
|
if (draft.kind === "polyline") {
|
|
977
990
|
setPolylineVertexCount((_b = (_a = draft.vertices) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0);
|
|
978
991
|
}
|
|
@@ -983,7 +996,7 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
983
996
|
nv.onPolylineChange = function (count) {
|
|
984
997
|
var _a, _b;
|
|
985
998
|
setPolylineVertexCount(count);
|
|
986
|
-
if (
|
|
999
|
+
if (drawShapeToolRef.current === "polyline" && count >= 2) {
|
|
987
1000
|
var prev = penDraftRef.current;
|
|
988
1001
|
var preserveFill = (prev === null || prev === void 0 ? void 0 : prev.kind) === "polyline" &&
|
|
989
1002
|
prev.filled &&
|
|
@@ -1020,6 +1033,7 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1020
1033
|
nv._cloudMrPolylineBaseBitmap = null;
|
|
1021
1034
|
nv._cloudMrPolylineSessionStartBitmap = null;
|
|
1022
1035
|
nv._cloudMrToolKindBitmap = null;
|
|
1036
|
+
clearShapeRegistry(nv);
|
|
1023
1037
|
}
|
|
1024
1038
|
function clearDrawingHandler() {
|
|
1025
1039
|
var _a;
|
|
@@ -1057,6 +1071,9 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1057
1071
|
var draft = shapeDraftRef.current;
|
|
1058
1072
|
if (!(draft === null || draft === void 0 ? void 0 : draft.baseBitmap))
|
|
1059
1073
|
return;
|
|
1074
|
+
if (draft._registryId != null) {
|
|
1075
|
+
removeShapeRegistryEntry(nv, draft._registryId);
|
|
1076
|
+
}
|
|
1060
1077
|
// Clear tool-kind tags for voxels this shape occupied
|
|
1061
1078
|
if (nv._cloudMrToolKindBitmap) {
|
|
1062
1079
|
for (var i = 0; i < nv.drawBitmap.length; i++) {
|
|
@@ -1162,13 +1179,29 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1162
1179
|
}
|
|
1163
1180
|
}
|
|
1164
1181
|
}
|
|
1182
|
+
function applyActiveDraftIfAny() {
|
|
1183
|
+
if (shapeDraftRef.current) {
|
|
1184
|
+
applyShapeDraft();
|
|
1185
|
+
}
|
|
1186
|
+
else if (penDraftRef.current) {
|
|
1187
|
+
applyPenDraftHandler();
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1165
1190
|
function applyShapeDraft(_a) {
|
|
1166
|
-
var _b
|
|
1191
|
+
var _b, _c;
|
|
1192
|
+
var _d = _a === void 0 ? {} : _a, _e = _d.keepTool, keepTool = _e === void 0 ? false : _e;
|
|
1167
1193
|
var draft = shapeDraftRef.current;
|
|
1168
1194
|
if (!draft)
|
|
1169
1195
|
return;
|
|
1170
1196
|
nv.drawAddUndoBitmap(nv.drawFillOverwrites);
|
|
1171
1197
|
markShapeVoxelKind(draft);
|
|
1198
|
+
var registryId = registerAppliedShape(nv, draft, {
|
|
1199
|
+
existingId: draft._registryId,
|
|
1200
|
+
eroded: ((_c = (_b = draft.shapeVoxels) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0) > 0
|
|
1201
|
+
});
|
|
1202
|
+
if (registryId != null) {
|
|
1203
|
+
draft._registryId = registryId;
|
|
1204
|
+
}
|
|
1172
1205
|
setDrawingChanged(true);
|
|
1173
1206
|
setShapeDraft(null);
|
|
1174
1207
|
shapeDraftRef.current = null;
|
|
@@ -1187,6 +1220,7 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1187
1220
|
shapeDraftRef.current = draft;
|
|
1188
1221
|
}
|
|
1189
1222
|
nv.onShapeDraftReady = function (draft) {
|
|
1223
|
+
applyNvBrushSize(1);
|
|
1190
1224
|
setShapeDraft(draft);
|
|
1191
1225
|
shapeDraftRef.current = draft;
|
|
1192
1226
|
nv._cloudMrShapeDraftActive = true;
|
|
@@ -1198,12 +1232,7 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1198
1232
|
nvSetDrawingEnabled(false);
|
|
1199
1233
|
};
|
|
1200
1234
|
nv.onApplyActiveDraft = function () {
|
|
1201
|
-
|
|
1202
|
-
applyShapeDraft();
|
|
1203
|
-
}
|
|
1204
|
-
else if (penDraftRef.current) {
|
|
1205
|
-
applyPenDraftHandler();
|
|
1206
|
-
}
|
|
1235
|
+
applyActiveDraftIfAny();
|
|
1207
1236
|
};
|
|
1208
1237
|
React.useEffect(function () {
|
|
1209
1238
|
nv._cloudMrPenDraftActive = penDraft != null;
|
|
@@ -1259,7 +1288,7 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1259
1288
|
setSelectionBoxColor(__spreadArray(__spreadArray([], rgb01, true), [0.5], false));
|
|
1260
1289
|
nv.setSelectionBoxColor(__spreadArray(__spreadArray([], rgb01, true), [0.5], false));
|
|
1261
1290
|
}
|
|
1262
|
-
var
|
|
1291
|
+
var _37 = React.useState('axial'), sliceType = _37[0], setSliceType = _37[1];
|
|
1263
1292
|
function nvUpdateSliceType(newSliceType) {
|
|
1264
1293
|
setSliceType(newSliceType);
|
|
1265
1294
|
if (newSliceType === 'axial') {
|
|
@@ -1399,14 +1428,14 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1399
1428
|
return [2 /*return*/];
|
|
1400
1429
|
});
|
|
1401
1430
|
}); };
|
|
1402
|
-
var
|
|
1403
|
-
var
|
|
1404
|
-
var
|
|
1405
|
-
}), saveConfirmCallback =
|
|
1406
|
-
var
|
|
1407
|
-
var
|
|
1408
|
-
var
|
|
1409
|
-
var
|
|
1431
|
+
var _38 = useState(''), selectedROI = _38[0], setSelectedDrawingLayer = _38[1];
|
|
1432
|
+
var _39 = useState(false), saveDialogOpen = _39[0], setSaveDialogOpen = _39[1];
|
|
1433
|
+
var _40 = useState(function () {
|
|
1434
|
+
}), saveConfirmCallback = _40[0], setSaveConfirmCallback = _40[1];
|
|
1435
|
+
var _41 = useState(false), confirmationOpen = _41[0], setConfirmationOpen = _41[1];
|
|
1436
|
+
var _42 = useState(function () { }), warningConfirmationCallback = _42[0], setWarningConfirmationCallback = _42[1];
|
|
1437
|
+
var _43 = useState(function () { }), warningCancelCallback = _43[0], setWarningCancelCallback = _43[1];
|
|
1438
|
+
var _44 = useState(false), drawingChanged = _44[0], setDrawingChanged = _44[1];
|
|
1410
1439
|
// When new drawing strokes are made on top of a saved ROI, reset the dropdown
|
|
1411
1440
|
// so it shows the "ROI Layer" placeholder instead of the stale saved name.
|
|
1412
1441
|
React.useEffect(function () {
|
|
@@ -1763,6 +1792,8 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1763
1792
|
penDraftFilled: (penDraft === null || penDraft === void 0 ? void 0 : penDraft.filled) === true,
|
|
1764
1793
|
brushSize: brushSize,
|
|
1765
1794
|
updateBrushSize: nvUpdateBrushSize,
|
|
1795
|
+
eraserSize: eraserSize,
|
|
1796
|
+
updateEraserSize: nvUpdateEraserSize,
|
|
1766
1797
|
onActivateEraser: activateEraser,
|
|
1767
1798
|
onDeactivateDrawTools: deactivateDrawTools,
|
|
1768
1799
|
onClearDrawing: clearDrawingHandler
|
|
@@ -1673,6 +1673,16 @@ Niivue.prototype.mouseUpListener = function cloudMrMouseUpListener() {
|
|
|
1673
1673
|
}
|
|
1674
1674
|
|
|
1675
1675
|
if (!pendingDraft?.baseBitmap) {
|
|
1676
|
+
// If a draft is open and the user clicked (not dragged) on the canvas,
|
|
1677
|
+
// treat it as an "apply" — same behaviour as right-click or the Apply button.
|
|
1678
|
+
if (
|
|
1679
|
+
(this._cloudMrShapeDraftActive || this._cloudMrPenDraftActive) &&
|
|
1680
|
+
isClickWithoutDrag(this.uiData) &&
|
|
1681
|
+
typeof this.onApplyActiveDraft === "function"
|
|
1682
|
+
) {
|
|
1683
|
+
this.onApplyActiveDraft();
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1676
1686
|
cloudMrTryReopenDraftOnClick(this);
|
|
1677
1687
|
return;
|
|
1678
1688
|
}
|
|
@@ -28,7 +28,7 @@ var HANDLE_SIZE = 10;
|
|
|
28
28
|
* @param {{ nv: any, draft: import('./shapeDraftUtils').ShapeDraft, onDraftChange: (d: any) => void, onApplyDraft?: () => void, overlayKey?: unknown }} props
|
|
29
29
|
*/
|
|
30
30
|
export function ShapeDraftOverlay(_a) {
|
|
31
|
-
var _b;
|
|
31
|
+
var _b, _c, _d;
|
|
32
32
|
var nv = _a.nv, draft = _a.draft, onDraftChange = _a.onDraftChange, onApplyDraft = _a.onApplyDraft, overlayKey = _a.overlayKey;
|
|
33
33
|
var theme = useNiivueViewerTheme();
|
|
34
34
|
var dragRef = useRef(null);
|
|
@@ -38,7 +38,7 @@ export function ShapeDraftOverlay(_a) {
|
|
|
38
38
|
var finishDragRef = useRef(null);
|
|
39
39
|
var onApplyDraftRef = useRef(onApplyDraft);
|
|
40
40
|
onApplyDraftRef.current = onApplyDraft;
|
|
41
|
-
var
|
|
41
|
+
var _e = useState(0), setTick = _e[1];
|
|
42
42
|
var bump = useCallback(function () { return setTick(function (t) { return t + 1; }); }, []);
|
|
43
43
|
useEffect(function () {
|
|
44
44
|
var onMove = function () { return bump(); };
|
|
@@ -82,6 +82,7 @@ export function ShapeDraftOverlay(_a) {
|
|
|
82
82
|
return null;
|
|
83
83
|
return { left: left, top: top, width: width, height: height };
|
|
84
84
|
}, [cornerCss]);
|
|
85
|
+
var resizeDisabled = ((_d = (_c = draft === null || draft === void 0 ? void 0 : draft.shapeVoxels) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) > 0;
|
|
85
86
|
var applyDraft = useCallback(function (nextDraft) {
|
|
86
87
|
redrawDraftShape(nv, nextDraft);
|
|
87
88
|
onDraftChange(nextDraft);
|
|
@@ -101,6 +102,7 @@ export function ShapeDraftOverlay(_a) {
|
|
|
101
102
|
}
|
|
102
103
|
};
|
|
103
104
|
onPointerMoveRef.current = function (event) {
|
|
105
|
+
var _a;
|
|
104
106
|
var drag = dragRef.current;
|
|
105
107
|
var currentDraft = draftRef.current;
|
|
106
108
|
if (!drag || !currentDraft)
|
|
@@ -113,8 +115,11 @@ export function ShapeDraftOverlay(_a) {
|
|
|
113
115
|
return;
|
|
114
116
|
var startCanvas = clientToCanvasPos(canvas, drag.startClientX, drag.startClientY);
|
|
115
117
|
var endCanvas = clientToCanvasPos(canvas, event.clientX, event.clientY);
|
|
116
|
-
var
|
|
117
|
-
var next = __assign(__assign({}, currentDraft), { ptA: translatePt(drag.startPtA,
|
|
118
|
+
var delta_1 = canvasDeltaToVoxDelta(nv, startCanvas, endCanvas);
|
|
119
|
+
var next = __assign(__assign({}, currentDraft), { ptA: translatePt(drag.startPtA, delta_1), ptB: translatePt(drag.startPtB, delta_1), shapeVoxels: (_a = drag.startShapeVoxels) === null || _a === void 0 ? void 0 : _a.map(function (_a) {
|
|
120
|
+
var x = _a[0], y = _a[1], z = _a[2];
|
|
121
|
+
return translatePt([x, y, z], delta_1);
|
|
122
|
+
}) });
|
|
118
123
|
applyDraft(next);
|
|
119
124
|
return;
|
|
120
125
|
}
|
|
@@ -123,10 +128,11 @@ export function ShapeDraftOverlay(_a) {
|
|
|
123
128
|
if (!vox)
|
|
124
129
|
return;
|
|
125
130
|
var next = resizeDraftCorner(nv, currentDraft, drag.cornerIndex, vox);
|
|
126
|
-
applyDraft(next);
|
|
131
|
+
applyDraft(__assign(__assign({}, next), { shapeVoxels: undefined }));
|
|
127
132
|
}
|
|
128
133
|
};
|
|
129
134
|
var startDrag = useCallback(function (event, mode, cornerIndex) {
|
|
135
|
+
var _a;
|
|
130
136
|
if (cornerIndex === void 0) { cornerIndex = -1; }
|
|
131
137
|
event.preventDefault();
|
|
132
138
|
event.stopPropagation();
|
|
@@ -137,7 +143,11 @@ export function ShapeDraftOverlay(_a) {
|
|
|
137
143
|
startClientX: event.clientX,
|
|
138
144
|
startClientY: event.clientY,
|
|
139
145
|
startPtA: __spreadArray([], draft.ptA, true),
|
|
140
|
-
startPtB: __spreadArray([], draft.ptB, true)
|
|
146
|
+
startPtB: __spreadArray([], draft.ptB, true),
|
|
147
|
+
startShapeVoxels: (_a = draft.shapeVoxels) === null || _a === void 0 ? void 0 : _a.map(function (_a) {
|
|
148
|
+
var x = _a[0], y = _a[1], z = _a[2];
|
|
149
|
+
return [x, y, z];
|
|
150
|
+
})
|
|
141
151
|
};
|
|
142
152
|
window.addEventListener("pointermove", onPointerMoveRef.current);
|
|
143
153
|
window.addEventListener("pointerup", finishDragRef.current);
|
|
@@ -181,6 +191,7 @@ export function ShapeDraftOverlay(_a) {
|
|
|
181
191
|
background: theme.accentMutedBgLight,
|
|
182
192
|
boxSizing: "border-box",
|
|
183
193
|
pointerEvents: "none"
|
|
184
|
-
} }), centerCss && (_jsx("div", { role: "presentation", onPointerDown: function (e) { return startDrag(e, "move"); }, style: __assign(__assign({}, handleStyle), { left: centerCss.x, top: centerCss.y, cursor: "move", borderRadius: 2, width: 12, height: 12, marginLeft: -6, marginTop: -6 }), title: "Move shape
|
|
194
|
+
} }), centerCss && (_jsx("div", { role: "presentation", onPointerDown: function (e) { return startDrag(e, "move"); }, style: __assign(__assign({}, handleStyle), { left: centerCss.x, top: centerCss.y, cursor: "move", borderRadius: 2, width: 12, height: 12, marginLeft: -6, marginTop: -6 }), title: resizeDisabled ? "Move shape (resize unavailable after erasing)" : "Move shape" })), !resizeDisabled &&
|
|
195
|
+
cornerCss.map(function (pos, i) { return (_jsx("div", { role: "presentation", onPointerDown: function (e) { return startDrag(e, "corner", i); }, style: __assign(__assign({}, handleStyle), { left: pos.x, top: pos.y, cursor: "nwse-resize" }), title: "Resize shape" }, "corner-".concat(i))); })] })));
|
|
185
196
|
}
|
|
186
197
|
export default ShapeDraftOverlay;
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
export default function DrawColorPlatte({ expanded, updateDrawPen, setDrawingEnabled,
|
|
1
|
+
export default function DrawColorPlatte({ expanded, updateDrawPen, setDrawingEnabled, penToolKind, polylineVertexCount, penDraftActive, penDraftKind, penDraftFilled, onApplyPenDraft, onDeletePenDraft, onFillPenDraft, brushSize, updateBrushSize, shapeDraftActive, onApplyShapeDraft, onDeleteShapeDraft, }: {
|
|
2
2
|
expanded: any;
|
|
3
3
|
updateDrawPen: any;
|
|
4
4
|
setDrawingEnabled: any;
|
|
5
|
-
|
|
6
|
-
penDrawMode?: string | undefined;
|
|
7
|
-
onPenDrawModeChange: any;
|
|
5
|
+
penToolKind?: null | undefined;
|
|
8
6
|
polylineVertexCount?: number | undefined;
|
|
9
7
|
penDraftActive?: boolean | undefined;
|
|
10
8
|
penDraftKind: any;
|
|
@@ -11,7 +11,8 @@ var __assign = (this && this.__assign) || function () {
|
|
|
11
11
|
};
|
|
12
12
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Color palette for pen/shape ROI tools. Freehand and polyline each use their own
|
|
15
|
+
* toolbar button and pass `penToolKind` to show the matching options.
|
|
15
16
|
*/
|
|
16
17
|
import { Stack, IconButton, Button, Tooltip, Typography } from "@mui/material";
|
|
17
18
|
import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
|
|
@@ -28,16 +29,14 @@ var FILLED_COLORS = [
|
|
|
28
29
|
];
|
|
29
30
|
var ACTION_FONT_SIZE = "0.75rem";
|
|
30
31
|
var ACTION_ICON_SIZE = "0.875rem";
|
|
31
|
-
var modeBtnSx = function (active) { return ({
|
|
32
|
-
color: active ? "#c9a0e8" : "#bbb",
|
|
33
|
-
fontSize: ACTION_FONT_SIZE,
|
|
34
|
-
textTransform: "none",
|
|
35
|
-
minWidth: 0,
|
|
36
|
-
py: 0.25,
|
|
37
|
-
px: 0.75
|
|
38
|
-
}); };
|
|
39
32
|
export default function DrawColorPlatte(_a) {
|
|
40
|
-
var expanded = _a.expanded, updateDrawPen = _a.updateDrawPen, setDrawingEnabled = _a.setDrawingEnabled,
|
|
33
|
+
var expanded = _a.expanded, updateDrawPen = _a.updateDrawPen, setDrawingEnabled = _a.setDrawingEnabled,
|
|
34
|
+
/** @type {"freehand" | "polyline" | null} */
|
|
35
|
+
_b = _a.penToolKind,
|
|
36
|
+
/** @type {"freehand" | "polyline" | null} */
|
|
37
|
+
penToolKind = _b === void 0 ? null : _b, _c = _a.polylineVertexCount, polylineVertexCount = _c === void 0 ? 0 : _c, _d = _a.penDraftActive, penDraftActive = _d === void 0 ? false : _d, penDraftKind = _a.penDraftKind, _e = _a.penDraftFilled, penDraftFilled = _e === void 0 ? false : _e, onApplyPenDraft = _a.onApplyPenDraft, onDeletePenDraft = _a.onDeletePenDraft, onFillPenDraft = _a.onFillPenDraft, _f = _a.brushSize, brushSize = _f === void 0 ? 1 : _f, updateBrushSize = _a.updateBrushSize, _g = _a.shapeDraftActive, shapeDraftActive = _g === void 0 ? false : _g, onApplyShapeDraft = _a.onApplyShapeDraft, onDeleteShapeDraft = _a.onDeleteShapeDraft;
|
|
38
|
+
var isFreehandTool = penToolKind === "freehand";
|
|
39
|
+
var isPolylineTool = penToolKind === "polyline";
|
|
41
40
|
return (_jsxs(Stack, __assign({ style: {
|
|
42
41
|
position: "absolute",
|
|
43
42
|
top: "100%",
|
|
@@ -51,13 +50,13 @@ export default function DrawColorPlatte(_a) {
|
|
|
51
50
|
borderTopLeftRadius: "6pt",
|
|
52
51
|
borderTopRightRadius: "6pt",
|
|
53
52
|
background: "#333"
|
|
54
|
-
}, direction: "column", spacing: 0.5, sx: { py: expanded ? 0.5 : 0 } }, { children: [
|
|
53
|
+
}, direction: "column", spacing: 0.5, sx: { py: expanded ? 0.5 : 0 } }, { children: [(isFreehandTool || isPolylineTool) && expanded && updateBrushSize && (_jsx(BrushSizeSlider, { label: "Line thickness", brushSize: brushSize, updateBrushSize: updateBrushSize })), _jsx(Stack, __assign({ direction: "row" }, { children: FILLED_COLORS.map(function (color, index) { return (_jsx(IconButton, __assign({ onClick: function () {
|
|
55
54
|
updateDrawPen({ target: { value: index + 1 } });
|
|
56
55
|
setDrawingEnabled(true);
|
|
57
|
-
} }, { children: _jsx(FiberManualRecordIcon, { sx: color.sx }) }), index)); }) })),
|
|
58
|
-
|
|
59
|
-
(
|
|
60
|
-
|
|
56
|
+
} }, { children: _jsx(FiberManualRecordIcon, { sx: color.sx }) }), index)); }) })), isPolylineTool && expanded && polylineVertexCount === 0 && (_jsx(Typography, __assign({ sx: { px: 1, pb: 0.5, fontSize: "0.68rem", color: "#aaa", userSelect: "none" } }, { children: "Click each vertex to draw connected line segments" }))), penDraftActive &&
|
|
57
|
+
expanded &&
|
|
58
|
+
((isFreehandTool && penDraftKind === "freehand") ||
|
|
59
|
+
(isPolylineTool && penDraftKind === "polyline")) && (_jsxs(Stack, __assign({ direction: "row", alignItems: "center", justifyContent: "space-between", sx: { px: 1, py: 0.5, borderTop: "1px solid #555", width: "100%" } }, { children: [_jsx(Tooltip, __assign({ title: "Delete this ROI drawing" }, { children: _jsx(Button, __assign({ size: "small", "aria-label": "delete pen draft", onClick: function () { return onDeletePenDraft === null || onDeletePenDraft === void 0 ? void 0 : onDeletePenDraft(); }, startIcon: _jsx(DeleteOutlineIcon, { sx: { fontSize: ACTION_ICON_SIZE } }), sx: {
|
|
61
60
|
color: "#f44336",
|
|
62
61
|
fontSize: ACTION_FONT_SIZE,
|
|
63
62
|
textTransform: "none",
|
|
@@ -65,7 +64,7 @@ export default function DrawColorPlatte(_a) {
|
|
|
65
64
|
py: 0.25,
|
|
66
65
|
px: 0.75,
|
|
67
66
|
"& .MuiButton-startIcon": { mr: 0.5, ml: 0 }
|
|
68
|
-
} }, { children: "Delete" })) })), _jsxs(Stack, __assign({ direction: "row", spacing: 1, alignItems: "center" }, { children: [
|
|
67
|
+
} }, { children: "Delete" })) })), isPolylineTool && (_jsxs(Stack, __assign({ direction: "row", spacing: 1, alignItems: "center" }, { children: [polylineVertexCount >= 3 && (_jsx(Tooltip, __assign({ title: penDraftFilled
|
|
69
68
|
? "Remove fill (keeps outline editable)"
|
|
70
69
|
: "Fill interior (keeps outline editable until Apply)" }, { children: _jsx(Button, __assign({ size: "small", "aria-label": penDraftFilled ? "undo fill polyline" : "fill polyline", onClick: function () { return onFillPenDraft === null || onFillPenDraft === void 0 ? void 0 : onFillPenDraft(); }, sx: {
|
|
71
70
|
color: penDraftFilled ? "#ffb74d" : "#c9a0e8",
|
|
@@ -74,7 +73,7 @@ export default function DrawColorPlatte(_a) {
|
|
|
74
73
|
minWidth: 0,
|
|
75
74
|
py: 0.25,
|
|
76
75
|
px: 0.75
|
|
77
|
-
} }, { children: penDraftFilled ? "Undo Fill" : "Fill" })) }))),
|
|
76
|
+
} }, { children: penDraftFilled ? "Undo Fill" : "Fill" })) }))), _jsx(Tooltip, __assign({ title: "Apply polyline (Enter or right-click)" }, { children: _jsx(Button, __assign({ size: "small", "aria-label": "apply pen draft", onClick: function () { return onApplyPenDraft === null || onApplyPenDraft === void 0 ? void 0 : onApplyPenDraft(); }, startIcon: _jsx(CheckIcon, { sx: { fontSize: ACTION_ICON_SIZE } }), sx: {
|
|
78
77
|
color: "#c9a0e8",
|
|
79
78
|
fontSize: ACTION_FONT_SIZE,
|
|
80
79
|
textTransform: "none",
|
|
@@ -82,7 +81,7 @@ export default function DrawColorPlatte(_a) {
|
|
|
82
81
|
py: 0.25,
|
|
83
82
|
px: 0.75,
|
|
84
83
|
"& .MuiButton-startIcon": { mr: 0.5, ml: 0 }
|
|
85
|
-
} }, { children: "Apply" })) }))
|
|
84
|
+
} }, { children: "Apply" })) }))] })))] }))), shapeDraftActive && expanded && (_jsx(Stack, __assign({ direction: "row", alignItems: "center", justifyContent: "flex-start", sx: { px: 1, py: 0.5, borderTop: "1px solid #555", width: "100%" } }, { children: _jsx(Tooltip, __assign({ title: "Delete this ROI drawing" }, { children: _jsx(Button, __assign({ size: "small", "aria-label": "delete shape draft", onClick: function () { return onDeleteShapeDraft === null || onDeleteShapeDraft === void 0 ? void 0 : onDeleteShapeDraft(); }, startIcon: _jsx(DeleteOutlineIcon, { sx: { fontSize: ACTION_ICON_SIZE } }), sx: {
|
|
86
85
|
color: "#f44336",
|
|
87
86
|
fontSize: ACTION_FONT_SIZE,
|
|
88
87
|
textTransform: "none",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export default function EraserPlatte({ expandEraseOptions, updateDrawPen, setDrawingEnabled,
|
|
1
|
+
export default function EraserPlatte({ expandEraseOptions, updateDrawPen, setDrawingEnabled, eraserSize, updateEraserSize, }: {
|
|
2
2
|
expandEraseOptions: any;
|
|
3
3
|
updateDrawPen: any;
|
|
4
4
|
setDrawingEnabled: any;
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
eraserSize?: number | undefined;
|
|
6
|
+
updateEraserSize: any;
|
|
7
7
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -15,7 +15,7 @@ import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
|
|
|
15
15
|
import FiberManualRecordOutlinedIcon from "@mui/icons-material/FiberManualRecordOutlined";
|
|
16
16
|
import { BrushSizeSlider } from "./BrushSizeSlider";
|
|
17
17
|
export default function EraserPlatte(_a) {
|
|
18
|
-
var expandEraseOptions = _a.expandEraseOptions, updateDrawPen = _a.updateDrawPen, setDrawingEnabled = _a.setDrawingEnabled, _b = _a.
|
|
18
|
+
var expandEraseOptions = _a.expandEraseOptions, updateDrawPen = _a.updateDrawPen, setDrawingEnabled = _a.setDrawingEnabled, _b = _a.eraserSize, eraserSize = _b === void 0 ? 1 : _b, updateEraserSize = _a.updateEraserSize;
|
|
19
19
|
var eraseOptions = [
|
|
20
20
|
_jsx(FiberManualRecordIcon, { style: { color: "white" } }, "e0"),
|
|
21
21
|
_jsx(FiberManualRecordOutlinedIcon, { style: { color: "white" } }, "e1"),
|
|
@@ -33,7 +33,7 @@ export default function EraserPlatte(_a) {
|
|
|
33
33
|
borderTopRightRadius: "6pt",
|
|
34
34
|
background: "#333",
|
|
35
35
|
width: 150
|
|
36
|
-
}, direction: "column" }, { children: [
|
|
36
|
+
}, direction: "column" }, { children: [updateEraserSize && (_jsx(BrushSizeSlider, { label: "Eraser size", brushSize: eraserSize, updateBrushSize: updateEraserSize })), _jsx(Stack, __assign({ direction: "row", style: { justifyContent: "center" } }, { children: eraseOptions.map(function (value, index) { return (_jsx(IconButton, __assign({ onClick: function () {
|
|
37
37
|
updateDrawPen({ target: { value: index === 0 ? 8 : 0 } });
|
|
38
38
|
setDrawingEnabled(true);
|
|
39
39
|
} }, { children: value }), index)); }) }))] })));
|
|
@@ -20,6 +20,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
20
20
|
import React, { useState, useEffect } from "react";
|
|
21
21
|
import { Box, Card, CardContent, IconButton, Slider, Stack, Tooltip, Typography, } from "@mui/material";
|
|
22
22
|
import DrawIcon from "@mui/icons-material/Draw";
|
|
23
|
+
import TimelineIcon from "@mui/icons-material/Timeline";
|
|
23
24
|
import CropSquareOutlinedIcon from "@mui/icons-material/CropSquareOutlined";
|
|
24
25
|
import CircleOutlinedIcon from "@mui/icons-material/CircleOutlined";
|
|
25
26
|
import AutoFixNormalOutlinedIcon from "@mui/icons-material/AutoFixNormalOutlined";
|
|
@@ -42,6 +43,14 @@ function clickTargetIsNiivueCanvas(target) {
|
|
|
42
43
|
return false;
|
|
43
44
|
return !!target.closest("#niiCanvas");
|
|
44
45
|
}
|
|
46
|
+
/** Click-away scoped to one tool button + its dropdown palette. */
|
|
47
|
+
function ToolClickAway(_a) {
|
|
48
|
+
var active = _a.active, onClickAway = _a.onClickAway, sx = _a.sx, children = _a.children;
|
|
49
|
+
var box = _jsx(Box, __assign({ sx: sx }, { children: children }));
|
|
50
|
+
if (!active)
|
|
51
|
+
return box;
|
|
52
|
+
return _jsx(ClickAwayListener, __assign({ onClickAway: onClickAway }, { children: box }));
|
|
53
|
+
}
|
|
45
54
|
/** Same icon tone as typical MUI on-paper controls (matches slice panel body). */
|
|
46
55
|
var ICON_COLOR = "#212121";
|
|
47
56
|
function EraserIcon(props) {
|
|
@@ -73,9 +82,11 @@ export function MroDrawToolkit(props) {
|
|
|
73
82
|
var _d = useState(undefined), setMaskColor = _d[1];
|
|
74
83
|
var filled = props.drawPen > 7;
|
|
75
84
|
useEffect(function () {
|
|
76
|
-
|
|
85
|
+
var eraserSelected = props.drawPen === 0 || props.drawPen === 8;
|
|
86
|
+
// Close palette when tool is deactivated programmatically (e.g. after Apply).
|
|
87
|
+
// Keep eraser palette open when switching from shape/pen edit → eraser in one click.
|
|
77
88
|
if (drawShapeTool === null && !props.shapeDraftActive && !props.penDraftActive) {
|
|
78
|
-
setExpandedOption("n");
|
|
89
|
+
setExpandedOption(eraserSelected ? "e" : "n");
|
|
79
90
|
return;
|
|
80
91
|
}
|
|
81
92
|
if (!props.shapeDraftActive && !props.penDraftActive)
|
|
@@ -86,8 +97,14 @@ export function MroDrawToolkit(props) {
|
|
|
86
97
|
setExpandedOption("l");
|
|
87
98
|
else if (drawShapeTool === "pen")
|
|
88
99
|
setExpandedOption("d");
|
|
89
|
-
|
|
100
|
+
else if (drawShapeTool === "polyline")
|
|
101
|
+
setExpandedOption("p");
|
|
102
|
+
}, [props.shapeDraftActive, props.penDraftActive, drawShapeTool, props.drawPen]);
|
|
90
103
|
var shapeSelectedSx = function (shape) {
|
|
104
|
+
if (shape === "pen")
|
|
105
|
+
return (drawShapeTool === "pen") ? theme.selectedToolSx : {};
|
|
106
|
+
if (shape === "polyline")
|
|
107
|
+
return (drawShapeTool === "polyline") ? theme.selectedToolSx : {};
|
|
91
108
|
return drawShapeTool === shape ? theme.selectedToolSx : {};
|
|
92
109
|
};
|
|
93
110
|
var eraserActive = expandedOption === "e";
|
|
@@ -112,6 +129,19 @@ export function MroDrawToolkit(props) {
|
|
|
112
129
|
props.setDrawingEnabled(true);
|
|
113
130
|
}
|
|
114
131
|
}
|
|
132
|
+
function clickPolyline() {
|
|
133
|
+
leaveEraserIfActive();
|
|
134
|
+
onDrawShapeToolChange === null || onDrawShapeToolChange === void 0 ? void 0 : onDrawShapeToolChange("polyline");
|
|
135
|
+
if (expandedOption === "p") {
|
|
136
|
+
setExpandedOption("n");
|
|
137
|
+
props.setDrawingEnabled(false);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
setExpandedOption("p");
|
|
141
|
+
setExpandOpacityOptions(false);
|
|
142
|
+
props.setDrawingEnabled(true);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
115
145
|
function clickRectangle() {
|
|
116
146
|
leaveEraserIfActive();
|
|
117
147
|
onDrawShapeToolChange === null || onDrawShapeToolChange === void 0 ? void 0 : onDrawShapeToolChange("rectangle");
|
|
@@ -165,43 +195,52 @@ export function MroDrawToolkit(props) {
|
|
|
165
195
|
"&:hover": { backgroundColor: "rgba(0,0,0,0.04)" },
|
|
166
196
|
"&.Mui-disabled": { color: "rgba(0,0,0,0.26)" }
|
|
167
197
|
};
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
198
|
+
function shouldIgnoreToolClickAway(event) {
|
|
199
|
+
if (clickTargetIsNiivueCanvas(event.target))
|
|
200
|
+
return true;
|
|
201
|
+
if (props.shapeDraftActive || props.penDraftActive)
|
|
202
|
+
return true;
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
function handleDrawToolClickAway(event) {
|
|
206
|
+
var _a;
|
|
207
|
+
if (shouldIgnoreToolClickAway(event))
|
|
208
|
+
return;
|
|
209
|
+
if (expandedOption === "n" || expandedOption === "m")
|
|
210
|
+
return;
|
|
211
|
+
setExpandedOption("n");
|
|
212
|
+
setExpandOpacityOptions(false);
|
|
213
|
+
(_a = props.onDeactivateDrawTools) === null || _a === void 0 ? void 0 : _a.call(props);
|
|
214
|
+
}
|
|
215
|
+
var toolGroupSx = { position: "relative", display: "inline-flex", alignItems: "center" };
|
|
216
|
+
return (_jsxs("div", __assign({ style: __assign({ width: "100%", position: "relative", zIndex: 1080 }, props.style) }, { children: [_jsx("div", __assign({ className: "title", style: { width: "100%" } }, { children: "ROI Tools" })), _jsx(Card, __assign({ variant: "outlined", sx: {
|
|
217
|
+
mb: 2,
|
|
218
|
+
borderTopLeftRadius: 0,
|
|
219
|
+
borderTopRightRadius: 0,
|
|
220
|
+
overflow: "visible"
|
|
221
|
+
} }, { children: _jsx(CardContent, __assign({ sx: {
|
|
222
|
+
overflow: "visible",
|
|
223
|
+
"&:last-child": { paddingBottom: 2 }
|
|
224
|
+
} }, { children: _jsx("div", __assign({ style: { display: "flex", flexDirection: "column", overflow: "visible" } }, { children: _jsxs("div", __assign({ style: {
|
|
225
|
+
display: "flex",
|
|
226
|
+
flexDirection: "row",
|
|
227
|
+
flexWrap: "wrap",
|
|
228
|
+
alignItems: "center",
|
|
229
|
+
gap: 4,
|
|
230
|
+
overflow: "visible"
|
|
231
|
+
} }, { children: [_jsxs(ToolClickAway, __assign({ active: expandedOption === "d", onClickAway: handleDrawToolClickAway, sx: __assign(__assign({}, toolGroupSx), { zIndex: expandedOption === "d" ? 1600 : "auto" }) }, { children: [_jsx(Tooltip, __assign({ title: "Freehand" }, { children: _jsx(IconButton, __assign({ "aria-label": "freehand", size: "small", onClick: clickPen, sx: __assign(__assign({}, toolBtnSx), shapeSelectedSx("pen")) }, { children: _jsx(DrawIcon, { sx: { color: "inherit" } }) })) })), _jsx(DrawColorPlatte, { expanded: expandedOption === "d", updateDrawPen: props.updateDrawPen, setDrawingEnabled: props.setDrawingEnabled, penToolKind: "freehand", penDraftActive: props.penDraftActive, penDraftKind: props.penDraftKind, onDeletePenDraft: props.onDeletePenDraft, brushSize: props.brushSize, updateBrushSize: props.updateBrushSize })] })), _jsxs(ToolClickAway, __assign({ active: expandedOption === "p", onClickAway: handleDrawToolClickAway, sx: __assign(__assign({}, toolGroupSx), { zIndex: expandedOption === "p" ? 1600 : "auto" }) }, { children: [_jsx(Tooltip, __assign({ title: "Polyline" }, { children: _jsx(IconButton, __assign({ "aria-label": "polyline", size: "small", onClick: clickPolyline, sx: __assign(__assign({}, toolBtnSx), shapeSelectedSx("polyline")) }, { children: _jsx(TimelineIcon, { sx: { color: "inherit" } }) })) })), _jsx(DrawColorPlatte, { expanded: expandedOption === "p", updateDrawPen: props.updateDrawPen, setDrawingEnabled: props.setDrawingEnabled, penToolKind: "polyline", polylineVertexCount: props.polylineVertexCount, penDraftActive: props.penDraftActive, penDraftKind: props.penDraftKind, penDraftFilled: props.penDraftFilled, onApplyPenDraft: props.onApplyPenDraft, onDeletePenDraft: props.onDeletePenDraft, onFillPenDraft: props.onFillPenDraft, brushSize: props.brushSize, updateBrushSize: props.updateBrushSize })] })), _jsxs(ToolClickAway, __assign({ active: expandedOption === "r", onClickAway: handleDrawToolClickAway, sx: __assign(__assign({}, toolGroupSx), { zIndex: expandedOption === "r" ? 1600 : "auto" }) }, { children: [_jsx(Tooltip, __assign({ title: "Rectangle" }, { children: _jsx(IconButton, __assign({ "aria-label": "rectangle", size: "small", onClick: clickRectangle, sx: __assign(__assign({}, toolBtnSx), shapeSelectedSx("rectangle")) }, { children: _jsx(CropSquareOutlinedIcon, { sx: { color: "inherit" } }) })) })), _jsx(DrawColorPlatte, { expanded: expandedOption === "r", updateDrawPen: props.updateDrawPen, setDrawingEnabled: props.setDrawingEnabled, shapeDraftActive: props.shapeDraftActive && drawShapeTool === "rectangle", onApplyShapeDraft: props.onApplyShapeDraft, onDeleteShapeDraft: props.onDeleteShapeDraft })] })), _jsxs(ToolClickAway, __assign({ active: expandedOption === "l", onClickAway: handleDrawToolClickAway, sx: __assign(__assign({}, toolGroupSx), { zIndex: expandedOption === "l" ? 1600 : "auto" }) }, { children: [_jsx(Tooltip, __assign({ title: "Ellipse" }, { children: _jsx(IconButton, __assign({ "aria-label": "ellipse", size: "small", onClick: clickEllipse, sx: __assign(__assign({}, toolBtnSx), shapeSelectedSx("ellipse")) }, { children: _jsx(CircleOutlinedIcon, { sx: { color: "inherit" } }) })) })), _jsx(DrawColorPlatte, { expanded: expandedOption === "l", updateDrawPen: props.updateDrawPen, setDrawingEnabled: props.setDrawingEnabled, shapeDraftActive: props.shapeDraftActive && drawShapeTool === "ellipse", onApplyShapeDraft: props.onApplyShapeDraft, onDeleteShapeDraft: props.onDeleteShapeDraft })] })), _jsxs(ToolClickAway, __assign({ active: expandedOption === "e", onClickAway: handleDrawToolClickAway, sx: __assign(__assign({}, toolGroupSx), { zIndex: expandedOption === "e" ? 1600 : "auto" }) }, { children: [_jsx(Tooltip, __assign({ title: "Eraser" }, { children: _jsx(IconButton, __assign({ "aria-label": "erase", size: "small", onClick: clickEraser, sx: __assign(__assign({}, toolBtnSx), (eraserActive ? theme.selectedToolSx : {})) }, { children: filled || !eraserActive ? (_jsx(EraserIcon, {})) : (_jsx(AutoFixNormalOutlinedIcon, { sx: { color: ICON_COLOR } })) })) })), _jsx(EraserPlatte, { expandEraseOptions: expandedOption === "e", updateDrawPen: props.updateDrawPen, setDrawingEnabled: props.setDrawingEnabled, eraserSize: props.eraserSize, updateEraserSize: props.updateEraserSize })] })), _jsx(Tooltip, __assign({ title: "Undo" }, { children: _jsx(IconButton, __assign({ "aria-label": "revert", size: "small", onClick: function () { return props.drawUndo(); }, sx: toolBtnSx }, { children: _jsx(ReplyIcon, { sx: { color: ICON_COLOR } }) })) })), _jsx(Tooltip, __assign({ title: "Save screenshot" }, { children: _jsx("span", { children: _jsx(IconButton, __assign({ "aria-label": "capture", size: "small", disabled: !vol, onClick: function () { return vol && props.nv.saveScene("".concat(vol.name, "_drawing.png")); }, sx: toolBtnSx }, { children: _jsx(CameraAltIcon, { sx: { color: ICON_COLOR } }) })) }) })), _jsx(Tooltip, __assign({ title: "Clear drawing" }, { children: _jsx(IconButton, __assign({ "aria-label": "delete", size: "small", onClick: function () {
|
|
232
|
+
var _a;
|
|
233
|
+
(_a = props.onClearDrawing) === null || _a === void 0 ? void 0 : _a.call(props);
|
|
234
|
+
setExpandedOption("n");
|
|
235
|
+
setExpandOpacityOptions(false);
|
|
236
|
+
}, sx: toolBtnSx }, { children: _jsx(DeleteIcon, { sx: { color: ICON_COLOR } }) })) })), _jsx(Tooltip, __assign({ title: "ROI visibility" }, { children: _jsx(IconButton, __assign({ "aria-label": "visible", size: "small", onClick: function () { return props.toggleROIVisible(); }, sx: toolBtnSx }, { children: props.roiVisible ? (_jsx(VisibilityIcon, { sx: { color: ICON_COLOR } })) : (_jsx(VisibilityOffIcon, { sx: { color: ICON_COLOR, opacity: 0.45 } })) })) })), _jsxs(Box, __assign({ sx: {
|
|
237
|
+
position: "relative",
|
|
238
|
+
zIndex: expandOpacityOptions ? 1600 : "auto",
|
|
239
|
+
display: "inline-flex",
|
|
240
|
+
alignItems: "center",
|
|
241
|
+
color: ICON_COLOR
|
|
242
|
+
} }, { children: [_jsx(Tooltip, __assign({ title: "Drawing opacity" }, { children: _jsx(IconButton, __assign({ "aria-label": "opaque", size: "small", onClick: function () { return setExpandOpacityOptions(!expandOpacityOptions); }, sx: toolBtnSx }, { children: _jsx(OpacityIcon, { sx: { color: ICON_COLOR } }) })) })), _jsx(Typography, __assign({ component: "span", sx: { fontSize: "0.7rem", mr: 0.25, userSelect: "none", color: ICON_COLOR } }, { children: props.drawingOpacity.toFixed(2) })), _jsx(OpacityPlatte, { drawingOpacity: props.drawingOpacity, setDrawingOpacity: props.setDrawingOpacity, expanded: expandOpacityOptions })] })), _jsxs(Box, __assign({ sx: { position: "relative", zIndex: expandedOption === "m" ? 1600 : "auto", display: "inline-flex", alignItems: "center" } }, { children: [_jsx(Tooltip, __assign({ title: "Mask by intensity range" }, { children: _jsx(IconButton, __assign({ "aria-label": "fill", size: "small", onClick: clickMask, sx: toolBtnSx }, { children: _jsx(FormatColorFillIcon, { sx: { color: ICON_COLOR } }) })) })), _jsx(MaskPlatte, { resampleImage: function () {
|
|
243
|
+
props.resampleImage();
|
|
244
|
+
props.setDrawingChanged(true);
|
|
245
|
+
}, expanded: expandedOption === "m", nv: props.nv, setMaskColor: setMaskColor, unfocus: function () { return setExpandedOption("n"); } })] }))] })) })) })) }))] })));
|
|
207
246
|
}
|
|
@@ -7,7 +7,29 @@
|
|
|
7
7
|
* @property {number} axCorSag
|
|
8
8
|
* @property {number} penType
|
|
9
9
|
* @property {Uint8Array} baseBitmap
|
|
10
|
+
* @property {[number, number, number][]} [shapeVoxels] exact voxels when reopening a modified shape
|
|
11
|
+
* @property {number} [_registryId] links to _cloudMrShapeRegistry for erosion tracking
|
|
10
12
|
*/
|
|
13
|
+
export function collectShapeVoxelIndices(nv: any, draft: any): Set<any>;
|
|
14
|
+
export function findShapeRegistryEntryById(nv: any, registryId: any): any;
|
|
15
|
+
/**
|
|
16
|
+
* Match a flood-filled cluster to a registered shape using an IoU-style score:
|
|
17
|
+
* overlap / max(clusterSize, entrySize)
|
|
18
|
+
* This prevents a large eroded rectangle from matching a smaller new shape drawn
|
|
19
|
+
* inside its original footprint, while still correctly identifying the same shape
|
|
20
|
+
* being re-opened (where both sizes are nearly equal and overlap is near 1.0).
|
|
21
|
+
*/
|
|
22
|
+
export function findShapeRegistryEntry(nv: any, cluster: any): any;
|
|
23
|
+
/** Persist applied rectangle/ellipse voxels so eraser hits can be tracked per shape. */
|
|
24
|
+
export function registerAppliedShape(nv: any, draft: any, { existingId, eroded }?: {
|
|
25
|
+
existingId: any;
|
|
26
|
+
eroded?: boolean | undefined;
|
|
27
|
+
}): any;
|
|
28
|
+
/** Mark shapes whose stored voxels were cleared by the eraser. */
|
|
29
|
+
export function updateShapeRegistryErosionState(nv: any): void;
|
|
30
|
+
export function removeShapeRegistryEntry(nv: any, registryId: any): void;
|
|
31
|
+
export function clearShapeRegistry(nv: any): void;
|
|
32
|
+
export function paintShapeVoxels(nv: any, voxels: any, penValue: any): void;
|
|
11
33
|
export function penTypeToKind(penType: any): "rectangle" | "ellipse";
|
|
12
34
|
export function normalizeBounds(ptA: any, ptB: any, dims: any): {
|
|
13
35
|
x1: number;
|
|
@@ -70,6 +92,8 @@ export function eraseClusterFromBitmap(bitmap: any, visited: any): Uint8Array;
|
|
|
70
92
|
* reconstruct a ShapeDraft so the bounding-box overlay reappears for re-editing.
|
|
71
93
|
*/
|
|
72
94
|
export function captureShapeDraftFromClick(nv: any): {
|
|
95
|
+
shapeVoxels?: number[][] | undefined;
|
|
96
|
+
_registryId?: any;
|
|
73
97
|
ptA: any[];
|
|
74
98
|
ptB: any[];
|
|
75
99
|
penValue: number;
|
|
@@ -85,4 +109,12 @@ export type ShapeDraft = {
|
|
|
85
109
|
axCorSag: number;
|
|
86
110
|
penType: number;
|
|
87
111
|
baseBitmap: Uint8Array;
|
|
112
|
+
/**
|
|
113
|
+
* exact voxels when reopening a modified shape
|
|
114
|
+
*/
|
|
115
|
+
shapeVoxels?: [number, number, number][] | undefined;
|
|
116
|
+
/**
|
|
117
|
+
* links to _cloudMrShapeRegistry for erosion tracking
|
|
118
|
+
*/
|
|
119
|
+
_registryId?: number | undefined;
|
|
88
120
|
};
|
|
@@ -29,7 +29,136 @@ import { voxFromMouse } from "./polylinePenUtils";
|
|
|
29
29
|
* @property {number} axCorSag
|
|
30
30
|
* @property {number} penType
|
|
31
31
|
* @property {Uint8Array} baseBitmap
|
|
32
|
+
* @property {[number, number, number][]} [shapeVoxels] exact voxels when reopening a modified shape
|
|
33
|
+
* @property {number} [_registryId] links to _cloudMrShapeRegistry for erosion tracking
|
|
32
34
|
*/
|
|
35
|
+
export function collectShapeVoxelIndices(nv, draft) {
|
|
36
|
+
var _a, _b;
|
|
37
|
+
var indices = new Set();
|
|
38
|
+
if (!(nv === null || nv === void 0 ? void 0 : nv.drawBitmap))
|
|
39
|
+
return indices;
|
|
40
|
+
var dims = (_a = nv.back) === null || _a === void 0 ? void 0 : _a.dims;
|
|
41
|
+
if (((_b = draft.shapeVoxels) === null || _b === void 0 ? void 0 : _b.length) && dims) {
|
|
42
|
+
var dx = dims[1];
|
|
43
|
+
var dy = dims[2];
|
|
44
|
+
for (var _i = 0, _c = draft.shapeVoxels; _i < _c.length; _i++) {
|
|
45
|
+
var _d = _c[_i], x = _d[0], y = _d[1], z = _d[2];
|
|
46
|
+
indices.add(x + y * dx + z * dx * dy);
|
|
47
|
+
}
|
|
48
|
+
return indices;
|
|
49
|
+
}
|
|
50
|
+
if (!draft.baseBitmap)
|
|
51
|
+
return indices;
|
|
52
|
+
for (var i = 0; i < nv.drawBitmap.length; i++) {
|
|
53
|
+
if (nv.drawBitmap[i] === draft.penValue && draft.baseBitmap[i] !== draft.penValue) {
|
|
54
|
+
indices.add(i);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return indices;
|
|
58
|
+
}
|
|
59
|
+
export function findShapeRegistryEntryById(nv, registryId) {
|
|
60
|
+
var _a, _b;
|
|
61
|
+
return (_b = (_a = nv._cloudMrShapeRegistry) === null || _a === void 0 ? void 0 : _a.find(function (entry) { return entry.id === registryId; })) !== null && _b !== void 0 ? _b : null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Match a flood-filled cluster to a registered shape using an IoU-style score:
|
|
65
|
+
* overlap / max(clusterSize, entrySize)
|
|
66
|
+
* This prevents a large eroded rectangle from matching a smaller new shape drawn
|
|
67
|
+
* inside its original footprint, while still correctly identifying the same shape
|
|
68
|
+
* being re-opened (where both sizes are nearly equal and overlap is near 1.0).
|
|
69
|
+
*/
|
|
70
|
+
export function findShapeRegistryEntry(nv, cluster) {
|
|
71
|
+
var registry = nv._cloudMrShapeRegistry;
|
|
72
|
+
if (!(registry === null || registry === void 0 ? void 0 : registry.length) || !(cluster === null || cluster === void 0 ? void 0 : cluster.visited))
|
|
73
|
+
return null;
|
|
74
|
+
var clusterSize = cluster.visited.size;
|
|
75
|
+
if (clusterSize === 0)
|
|
76
|
+
return null;
|
|
77
|
+
var MATCH_THRESHOLD = 0.5;
|
|
78
|
+
var best = null;
|
|
79
|
+
var bestScore = 0;
|
|
80
|
+
for (var _i = 0, registry_1 = registry; _i < registry_1.length; _i++) {
|
|
81
|
+
var entry = registry_1[_i];
|
|
82
|
+
var overlap = 0;
|
|
83
|
+
for (var _a = 0, _b = cluster.visited; _a < _b.length; _a++) {
|
|
84
|
+
var idx = _b[_a];
|
|
85
|
+
if (entry.voxelIndices.has(idx))
|
|
86
|
+
overlap++;
|
|
87
|
+
}
|
|
88
|
+
var score = overlap / Math.max(clusterSize, entry.voxelIndices.size);
|
|
89
|
+
if (score > bestScore) {
|
|
90
|
+
bestScore = score;
|
|
91
|
+
best = entry;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return bestScore >= MATCH_THRESHOLD ? best : null;
|
|
95
|
+
}
|
|
96
|
+
/** Persist applied rectangle/ellipse voxels so eraser hits can be tracked per shape. */
|
|
97
|
+
export function registerAppliedShape(nv, draft, _a) {
|
|
98
|
+
var _b, _c;
|
|
99
|
+
var _d = _a === void 0 ? {} : _a, existingId = _d.existingId, _e = _d.eroded, eroded = _e === void 0 ? false : _e;
|
|
100
|
+
var voxelIndices = collectShapeVoxelIndices(nv, draft);
|
|
101
|
+
if (!voxelIndices.size)
|
|
102
|
+
return null;
|
|
103
|
+
nv._cloudMrShapeRegistry = nv._cloudMrShapeRegistry || [];
|
|
104
|
+
var nextId = existingId !== null && existingId !== void 0 ? existingId : (nv._cloudMrShapeNextId = (nv._cloudMrShapeNextId || 0) + 1);
|
|
105
|
+
var entry = {
|
|
106
|
+
id: nextId,
|
|
107
|
+
ptA: __spreadArray([], draft.ptA, true),
|
|
108
|
+
ptB: __spreadArray([], draft.ptB, true),
|
|
109
|
+
penType: draft.penType,
|
|
110
|
+
penValue: draft.penValue,
|
|
111
|
+
axCorSag: draft.axCorSag,
|
|
112
|
+
voxelIndices: voxelIndices,
|
|
113
|
+
eroded: eroded || ((_c = (_b = draft.shapeVoxels) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0) > 0
|
|
114
|
+
};
|
|
115
|
+
var existingIndex = nv._cloudMrShapeRegistry.findIndex(function (e) { return e.id === nextId; });
|
|
116
|
+
if (existingIndex >= 0) {
|
|
117
|
+
nv._cloudMrShapeRegistry[existingIndex] = entry;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
nv._cloudMrShapeRegistry.push(entry);
|
|
121
|
+
}
|
|
122
|
+
return nextId;
|
|
123
|
+
}
|
|
124
|
+
/** Mark shapes whose stored voxels were cleared by the eraser. */
|
|
125
|
+
export function updateShapeRegistryErosionState(nv) {
|
|
126
|
+
var registry = nv._cloudMrShapeRegistry;
|
|
127
|
+
if (!(registry === null || registry === void 0 ? void 0 : registry.length) || !nv.drawBitmap)
|
|
128
|
+
return;
|
|
129
|
+
for (var _i = 0, registry_2 = registry; _i < registry_2.length; _i++) {
|
|
130
|
+
var entry = registry_2[_i];
|
|
131
|
+
if (entry.eroded)
|
|
132
|
+
continue;
|
|
133
|
+
for (var _a = 0, _b = entry.voxelIndices; _a < _b.length; _a++) {
|
|
134
|
+
var idx = _b[_a];
|
|
135
|
+
if (nv.drawBitmap[idx] === 0) {
|
|
136
|
+
entry.eroded = true;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export function removeShapeRegistryEntry(nv, registryId) {
|
|
143
|
+
if (registryId == null || !nv._cloudMrShapeRegistry)
|
|
144
|
+
return;
|
|
145
|
+
nv._cloudMrShapeRegistry = nv._cloudMrShapeRegistry.filter(function (e) { return e.id !== registryId; });
|
|
146
|
+
}
|
|
147
|
+
export function clearShapeRegistry(nv) {
|
|
148
|
+
nv._cloudMrShapeRegistry = [];
|
|
149
|
+
}
|
|
150
|
+
export function paintShapeVoxels(nv, voxels, penValue) {
|
|
151
|
+
var _a;
|
|
152
|
+
var dims = (_a = nv.back) === null || _a === void 0 ? void 0 : _a.dims;
|
|
153
|
+
if (!dims || !(voxels === null || voxels === void 0 ? void 0 : voxels.length))
|
|
154
|
+
return;
|
|
155
|
+
var dx = dims[1];
|
|
156
|
+
var dy = dims[2];
|
|
157
|
+
for (var _i = 0, voxels_1 = voxels; _i < voxels_1.length; _i++) {
|
|
158
|
+
var _b = voxels_1[_i], x = _b[0], y = _b[1], z = _b[2];
|
|
159
|
+
nv.drawBitmap[x + y * dx + z * dx * dy] = penValue;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
33
162
|
export function penTypeToKind(penType) {
|
|
34
163
|
return penType === NI_PEN_TYPE.ELLIPSE ? "ellipse" : "rectangle";
|
|
35
164
|
}
|
|
@@ -88,16 +217,27 @@ export function isDraftTooSmall(ptA, ptB) {
|
|
|
88
217
|
}
|
|
89
218
|
/** @param {ShapeDraft} draft */
|
|
90
219
|
export function redrawDraftShape(nv, draft) {
|
|
220
|
+
var _a;
|
|
91
221
|
if (!(nv === null || nv === void 0 ? void 0 : nv.drawBitmap) || !(draft === null || draft === void 0 ? void 0 : draft.baseBitmap))
|
|
92
222
|
return;
|
|
223
|
+
// Shapes are filled regions — penSize/penBounds must not expand voxels (e.g. after eraser).
|
|
224
|
+
var savedPenBounds = nv.opts.penBounds;
|
|
225
|
+
var savedPenSize = nv.opts.penSize;
|
|
226
|
+
nv.opts.penBounds = 0;
|
|
227
|
+
nv.opts.penSize = 1;
|
|
93
228
|
nv.drawBitmap.set(draft.baseBitmap);
|
|
94
229
|
nv.drawPenAxCorSag = draft.axCorSag;
|
|
95
|
-
if (draft.
|
|
230
|
+
if ((_a = draft.shapeVoxels) === null || _a === void 0 ? void 0 : _a.length) {
|
|
231
|
+
paintShapeVoxels(nv, draft.shapeVoxels, draft.penValue);
|
|
232
|
+
}
|
|
233
|
+
else if (draft.penType === NI_PEN_TYPE.RECTANGLE) {
|
|
96
234
|
nv.drawRectangleMask(draft.ptA, draft.ptB, draft.penValue);
|
|
97
235
|
}
|
|
98
236
|
else {
|
|
99
237
|
nv.drawEllipseMask(draft.ptA, draft.ptB, draft.penValue);
|
|
100
238
|
}
|
|
239
|
+
nv.opts.penBounds = savedPenBounds;
|
|
240
|
+
nv.opts.penSize = savedPenSize;
|
|
101
241
|
nv.refreshDrawing(false, false);
|
|
102
242
|
nv.drawScene();
|
|
103
243
|
}
|
|
@@ -312,8 +452,8 @@ function inferShapePenTypeFromCluster(cluster, axCorSag) {
|
|
|
312
452
|
var bounds = cluster.bounds, voxels = cluster.voxels;
|
|
313
453
|
var x1 = bounds.x1, y1 = bounds.y1, z1 = bounds.z1, x2 = bounds.x2, y2 = bounds.y2, z2 = bounds.z2;
|
|
314
454
|
var filled = new Set();
|
|
315
|
-
for (var _i = 0,
|
|
316
|
-
var _a =
|
|
455
|
+
for (var _i = 0, voxels_2 = voxels; _i < voxels_2.length; _i++) {
|
|
456
|
+
var _a = voxels_2[_i], x = _a[0], y = _a[1], z = _a[2];
|
|
317
457
|
filled.add(sliceKey(axCorSag, x, y, z));
|
|
318
458
|
}
|
|
319
459
|
var uMin;
|
|
@@ -359,6 +499,59 @@ function inferShapePenTypeFromCluster(cluster, axCorSag) {
|
|
|
359
499
|
}
|
|
360
500
|
return ellipseScore > rectScore ? NI_PEN_TYPE.ELLIPSE : NI_PEN_TYPE.RECTANGLE;
|
|
361
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* Geometric completeness check: true when the cluster still fills its bounding
|
|
504
|
+
* shape with no significant gaps (i.e. the eraser has not meaningfully touched it).
|
|
505
|
+
* Used as a fallback when the shape is not in the registry.
|
|
506
|
+
*
|
|
507
|
+
* For rectangles: all bounding-box voxels must be present.
|
|
508
|
+
* For ellipses: we use niivue's actual threshold (≤ 1.0) so boundary pixels
|
|
509
|
+
* that niivue doesn't draw never trigger a false "eroded" result.
|
|
510
|
+
* Additionally we allow up to 5% of interior voxels to be absent
|
|
511
|
+
* to tolerate rendering rounding without masking real erasure.
|
|
512
|
+
*/
|
|
513
|
+
function isClusterCompleteShape(cluster, penType) {
|
|
514
|
+
var bounds = cluster.bounds, voxels = cluster.voxels;
|
|
515
|
+
var x1 = bounds.x1, y1 = bounds.y1, z1 = bounds.z1, x2 = bounds.x2, y2 = bounds.y2, z2 = bounds.z2;
|
|
516
|
+
var filled = new Set(voxels.map(function (_a) {
|
|
517
|
+
var x = _a[0], y = _a[1], z = _a[2];
|
|
518
|
+
return "".concat(x, ",").concat(y, ",").concat(z);
|
|
519
|
+
}));
|
|
520
|
+
if (penType === NI_PEN_TYPE.RECTANGLE) {
|
|
521
|
+
for (var z = z1; z <= z2; z++) {
|
|
522
|
+
for (var y = y1; y <= y2; y++) {
|
|
523
|
+
for (var x = x1; x <= x2; x++) {
|
|
524
|
+
if (!filled.has("".concat(x, ",").concat(y, ",").concat(z)))
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
// Ellipse / ellipsoid: match niivue's exact threshold (≤ 1.0).
|
|
532
|
+
var cx = (x1 + x2) / 2;
|
|
533
|
+
var cy = (y1 + y2) / 2;
|
|
534
|
+
var cz = (z1 + z2) / 2;
|
|
535
|
+
var ru = Math.max(0.5, (x2 - x1) / 2);
|
|
536
|
+
var rv = Math.max(0.5, (y2 - y1) / 2);
|
|
537
|
+
var rw = Math.max(0.5, (z2 - z1) / 2);
|
|
538
|
+
var expected = 0;
|
|
539
|
+
var missing = 0;
|
|
540
|
+
for (var z = z1; z <= z2; z++) {
|
|
541
|
+
for (var y = y1; y <= y2; y++) {
|
|
542
|
+
for (var x = x1; x <= x2; x++) {
|
|
543
|
+
var inEllipse = Math.pow(((x - cx) / ru), 2) + Math.pow(((y - cy) / rv), 2) + Math.pow(((z - cz) / rw), 2) <= 1.0;
|
|
544
|
+
if (inEllipse) {
|
|
545
|
+
expected++;
|
|
546
|
+
if (!filled.has("".concat(x, ",").concat(y, ",").concat(z)))
|
|
547
|
+
missing++;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
// Allow up to 5% missing to handle minor rendering rounding at the boundary.
|
|
553
|
+
return expected === 0 || missing / expected <= 0.05;
|
|
554
|
+
}
|
|
362
555
|
/**
|
|
363
556
|
* When the user clicks on an existing filled ROI while the rectangle/ellipse tool
|
|
364
557
|
* is active (but no draft is currently open), flood-fill the clicked cluster to
|
|
@@ -369,17 +562,21 @@ export function captureShapeDraftFromClick(nv) {
|
|
|
369
562
|
var cluster = floodFillClusterFromVox(nv, seedVox);
|
|
370
563
|
if (!cluster)
|
|
371
564
|
return null;
|
|
372
|
-
var label = cluster.label, visited = cluster.visited, bounds = cluster.bounds;
|
|
565
|
+
var label = cluster.label, visited = cluster.visited, voxels = cluster.voxels, bounds = cluster.bounds;
|
|
373
566
|
var x1 = bounds.x1, y1 = bounds.y1, z1 = bounds.z1, x2 = bounds.x2, y2 = bounds.y2, z2 = bounds.z2;
|
|
374
567
|
var baseBitmap = eraseClusterFromBitmap(nv.drawBitmap, visited);
|
|
375
568
|
var axCorSag = inferAxCorSagFromBounds(x1, y1, z1, x2, y2, z2, nv.drawPenAxCorSag >= 0 ? nv.drawPenAxCorSag : 0);
|
|
376
569
|
var penType = inferShapePenTypeFromCluster(cluster, axCorSag);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
570
|
+
var registryEntry = findShapeRegistryEntry(nv, cluster);
|
|
571
|
+
// A shape is considered modified (partially erased) if:
|
|
572
|
+
// - the registry knows it was eroded, OR
|
|
573
|
+
// - (no registry entry) the geometry check finds gaps
|
|
574
|
+
var isModified = (registryEntry === null || registryEntry === void 0 ? void 0 : registryEntry.eroded) === true ||
|
|
575
|
+
(!registryEntry && !isClusterCompleteShape(cluster, penType));
|
|
576
|
+
return __assign(__assign({ ptA: [x1, y1, z1], ptB: [x2, y2, z2], penValue: label, axCorSag: axCorSag, penType: penType, baseBitmap: baseBitmap }, (registryEntry ? { _registryId: registryEntry.id } : {})), (isModified
|
|
577
|
+
? { shapeVoxels: voxels.map(function (_a) {
|
|
578
|
+
var x = _a[0], y = _a[1], z = _a[2];
|
|
579
|
+
return [x, y, z];
|
|
580
|
+
}) }
|
|
581
|
+
: {}));
|
|
385
582
|
}
|