cloudmr-ux 4.5.9 → 4.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CmrComponents/niivue-roi-histogram/resampleNiivueRoiHistogram.js +1 -1
- package/dist/CmrComponents/niivue-viewer/CloudMrNiivuePanel.js +1 -1
- package/dist/CmrComponents/niivue-viewer/CloudMrNiivueViewer.js +0 -36
- package/dist/CmrComponents/niivue-viewer/NiivuePatcher.js +30 -11
- package/dist/CmrComponents/niivue-viewer/PenDraftOverlay.d.ts +5 -3
- package/dist/CmrComponents/niivue-viewer/PenDraftOverlay.js +7 -1
- package/dist/CmrComponents/niivue-viewer/ShapeDraftOverlay.d.ts +3 -2
- package/dist/CmrComponents/niivue-viewer/ShapeDraftOverlay.js +7 -2
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/DrawColorPlatte.d.ts +1 -3
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/DrawColorPlatte.js +15 -28
- package/dist/CmrComponents/niivue-viewer/shapeDraftUtils.d.ts +15 -0
- package/dist/CmrComponents/niivue-viewer/shapeDraftUtils.js +92 -0
- package/dist/core/common/components/NiivueTools/Niivue.js +1 -1
- package/package.json +1 -1
|
@@ -186,7 +186,7 @@ export function CloudMrNiivuePanel(props) {
|
|
|
186
186
|
left: 0,
|
|
187
187
|
width: "100%",
|
|
188
188
|
height: "100%"
|
|
189
|
-
} }), props.shapeDraft && (_jsx(ShapeDraftOverlay, { nv: props.nv, draft: props.shapeDraft, onDraftChange: props.onShapeDraftChange, overlayKey: props.mms })), props.penDraft && (_jsx(PenDraftOverlay, { nv: props.nv, draft: props.penDraft, onDraftChange: props.onPenDraftChange, overlayKey: props.mms }))] }))] })), _jsxs(Box, __assign({ sx: {
|
|
189
|
+
} }), props.shapeDraft && (_jsx(ShapeDraftOverlay, { nv: props.nv, draft: props.shapeDraft, onDraftChange: props.onShapeDraftChange, onApplyDraft: props.onApplyShapeDraft, overlayKey: props.mms })), props.penDraft && (_jsx(PenDraftOverlay, { nv: props.nv, draft: props.penDraft, onDraftChange: props.onPenDraftChange, onApplyDraft: props.onApplyPenDraft, overlayKey: props.mms }))] }))] })), _jsxs(Box, __assign({ sx: {
|
|
190
190
|
width: {
|
|
191
191
|
xs: "100%",
|
|
192
192
|
md: "35%"
|
|
@@ -961,32 +961,6 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
961
961
|
React.useEffect(function () {
|
|
962
962
|
nv._cloudMrShapeDraftActive = shapeDraft != null;
|
|
963
963
|
}, [shapeDraft]);
|
|
964
|
-
React.useEffect(function () {
|
|
965
|
-
if (!shapeDraft && !penDraft) {
|
|
966
|
-
return undefined;
|
|
967
|
-
}
|
|
968
|
-
var canvas = document.getElementById("niiCanvas");
|
|
969
|
-
if (!canvas) {
|
|
970
|
-
return undefined;
|
|
971
|
-
}
|
|
972
|
-
var applyOnRightClick = function (event) {
|
|
973
|
-
var _a;
|
|
974
|
-
event.preventDefault();
|
|
975
|
-
event.stopPropagation();
|
|
976
|
-
(_a = nv.onApplyActiveDraft) === null || _a === void 0 ? void 0 : _a.call(nv);
|
|
977
|
-
};
|
|
978
|
-
var onMouseDown = function (event) {
|
|
979
|
-
if (event.button === 2) {
|
|
980
|
-
applyOnRightClick(event);
|
|
981
|
-
}
|
|
982
|
-
};
|
|
983
|
-
canvas.addEventListener("mousedown", onMouseDown, true);
|
|
984
|
-
canvas.addEventListener("contextmenu", applyOnRightClick, true);
|
|
985
|
-
return function () {
|
|
986
|
-
canvas.removeEventListener("mousedown", onMouseDown, true);
|
|
987
|
-
canvas.removeEventListener("contextmenu", applyOnRightClick, true);
|
|
988
|
-
};
|
|
989
|
-
}, [shapeDraft, penDraft]);
|
|
990
964
|
React.useEffect(function () {
|
|
991
965
|
if (!shapeDraft && !penDraft) {
|
|
992
966
|
return undefined;
|
|
@@ -1000,16 +974,6 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1000
974
|
else if (penDraft) {
|
|
1001
975
|
cancelPenDraftHandler();
|
|
1002
976
|
}
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
if (event.key === "Enter") {
|
|
1006
|
-
event.preventDefault();
|
|
1007
|
-
if (shapeDraft) {
|
|
1008
|
-
applyShapeDraft();
|
|
1009
|
-
}
|
|
1010
|
-
else if (penDraft) {
|
|
1011
|
-
applyPenDraftHandler();
|
|
1012
|
-
}
|
|
1013
977
|
}
|
|
1014
978
|
};
|
|
1015
979
|
window.addEventListener("keydown", onKeyDown);
|
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
import { Niivue, NVImage, NVImageFromUrlOptions } from "@niivue/niivue";
|
|
5
5
|
import {
|
|
6
6
|
captureDeferredShapeDraft,
|
|
7
|
+
captureShapeDraftFromClick,
|
|
7
8
|
isDraftTooSmall,
|
|
9
|
+
redrawDraftShape,
|
|
8
10
|
shouldDeferShapeCommit,
|
|
9
11
|
} from "./shapeDraftUtils.js";
|
|
12
|
+
import { NI_PEN_TYPE } from "./niivuePenType.js";
|
|
10
13
|
import {
|
|
11
14
|
addPolylineVertex,
|
|
12
15
|
axCorSagFromMouse,
|
|
@@ -1465,29 +1468,41 @@ Niivue.prototype.cloudMrResetPolyline = function cloudMrResetPolyline() {
|
|
|
1465
1468
|
resetPolylineState(this);
|
|
1466
1469
|
};
|
|
1467
1470
|
|
|
1468
|
-
const RIGHT_MOUSE_BUTTON = 2;
|
|
1469
|
-
|
|
1470
1471
|
function cloudMrHasApplyableDraft(nv) {
|
|
1471
1472
|
return !!(nv._cloudMrShapeDraftActive || nv._cloudMrPenDraftActive);
|
|
1472
1473
|
}
|
|
1473
1474
|
|
|
1474
|
-
|
|
1475
|
-
|
|
1475
|
+
/**
|
|
1476
|
+
* When no draft is active and the user clicks an existing ROI voxel while the
|
|
1477
|
+
* rectangle or ellipse tool is selected, reconstruct a ShapeDraft from the
|
|
1478
|
+
* clicked cluster so the bounding-box overlay reappears for re-editing.
|
|
1479
|
+
*/
|
|
1480
|
+
function cloudMrTryReopenShapeDraftOnClick(nv) {
|
|
1481
|
+
const penType = nv.opts.penType;
|
|
1482
|
+
if (
|
|
1483
|
+
!nv.opts.deferShapeCommit ||
|
|
1484
|
+
!nv.opts.drawingEnabled ||
|
|
1485
|
+
nv._cloudMrShapeDraftActive ||
|
|
1486
|
+
nv._cloudMrPenDraftActive ||
|
|
1487
|
+
!isClickWithoutDrag(nv.uiData) ||
|
|
1488
|
+
(penType !== NI_PEN_TYPE.RECTANGLE && penType !== NI_PEN_TYPE.ELLIPSE)
|
|
1489
|
+
) {
|
|
1476
1490
|
return false;
|
|
1477
1491
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
if (
|
|
1481
|
-
|
|
1492
|
+
|
|
1493
|
+
const reopenDraft = captureShapeDraftFromClick(nv);
|
|
1494
|
+
if (!reopenDraft) return false;
|
|
1495
|
+
|
|
1496
|
+
redrawDraftShape(nv, reopenDraft);
|
|
1497
|
+
nv._cloudMrSuppressDrawingChangedMouseUp = true;
|
|
1498
|
+
if (typeof nv.onShapeDraftReady === "function") {
|
|
1499
|
+
nv.onShapeDraftReady(reopenDraft);
|
|
1482
1500
|
}
|
|
1483
1501
|
return true;
|
|
1484
1502
|
}
|
|
1485
1503
|
|
|
1486
1504
|
const _mouseDownListener = Niivue.prototype.mouseDownListener;
|
|
1487
1505
|
Niivue.prototype.mouseDownListener = function cloudMrMouseDownListener(e) {
|
|
1488
|
-
if (e.button === RIGHT_MOUSE_BUTTON && cloudMrTryApplyDraftOnRightClick(this, e)) {
|
|
1489
|
-
return;
|
|
1490
|
-
}
|
|
1491
1506
|
if (shouldDeferFreehandCommit(this) && this.drawBitmap) {
|
|
1492
1507
|
this._cloudMrFreehandSessionStartBitmap = this.drawBitmap.slice();
|
|
1493
1508
|
this._cloudMrFreehandAxCorSag = -1;
|
|
@@ -1556,12 +1571,16 @@ Niivue.prototype.mouseUpListener = function cloudMrMouseUpListener() {
|
|
|
1556
1571
|
}
|
|
1557
1572
|
|
|
1558
1573
|
if (!pendingDraft?.baseBitmap) {
|
|
1574
|
+
// No new draft drawn — check if user clicked an existing ROI to re-edit it
|
|
1575
|
+
cloudMrTryReopenShapeDraftOnClick(this);
|
|
1559
1576
|
return;
|
|
1560
1577
|
}
|
|
1561
1578
|
if (isDraftTooSmall(pendingDraft.ptA, pendingDraft.ptB)) {
|
|
1562
1579
|
this.drawBitmap.set(pendingDraft.baseBitmap);
|
|
1563
1580
|
this.refreshDrawing(true, false);
|
|
1564
1581
|
this.drawScene();
|
|
1582
|
+
// Tiny drag treated as a click — also try to reopen an existing ROI
|
|
1583
|
+
cloudMrTryReopenShapeDraftOnClick(this);
|
|
1565
1584
|
return;
|
|
1566
1585
|
}
|
|
1567
1586
|
this._cloudMrSuppressDrawingChangedMouseUp = true;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Adjust handles for polyline (vertex drag) or freehand (move only) drafts.
|
|
3
|
+
* @param {{ nv: any, draft: any, onDraftChange: (d: any) => void, onApplyDraft?: () => void, overlayKey?: unknown }} props
|
|
3
4
|
*/
|
|
4
|
-
export function PenDraftOverlay({ nv, draft, onDraftChange, overlayKey }: {
|
|
5
|
+
export function PenDraftOverlay({ nv, draft, onDraftChange, onApplyDraft, overlayKey }: {
|
|
5
6
|
nv: any;
|
|
6
7
|
draft: any;
|
|
7
|
-
onDraftChange: any;
|
|
8
|
-
|
|
8
|
+
onDraftChange: (d: any) => void;
|
|
9
|
+
onApplyDraft?: (() => void) | undefined;
|
|
10
|
+
overlayKey?: unknown;
|
|
9
11
|
}): import("react/jsx-runtime").JSX.Element | null;
|
|
10
12
|
export default PenDraftOverlay;
|
|
@@ -29,9 +29,10 @@ function cloneFreehandDraft(draft) {
|
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
31
|
* Adjust handles for polyline (vertex drag) or freehand (move only) drafts.
|
|
32
|
+
* @param {{ nv: any, draft: any, onDraftChange: (d: any) => void, onApplyDraft?: () => void, overlayKey?: unknown }} props
|
|
32
33
|
*/
|
|
33
34
|
export function PenDraftOverlay(_a) {
|
|
34
|
-
var nv = _a.nv, draft = _a.draft, onDraftChange = _a.onDraftChange, overlayKey = _a.overlayKey;
|
|
35
|
+
var nv = _a.nv, draft = _a.draft, onDraftChange = _a.onDraftChange, onApplyDraft = _a.onApplyDraft, overlayKey = _a.overlayKey;
|
|
35
36
|
var dragRef = useRef(null);
|
|
36
37
|
var draftRef = useRef(draft);
|
|
37
38
|
draftRef.current = draft;
|
|
@@ -109,6 +110,7 @@ export function PenDraftOverlay(_a) {
|
|
|
109
110
|
onDraftChange(nextDraft);
|
|
110
111
|
}, [nv, onDraftChange]);
|
|
111
112
|
finishDragRef.current = function () {
|
|
113
|
+
var hadDrag = dragRef.current != null;
|
|
112
114
|
dragRef.current = null;
|
|
113
115
|
if (onPointerMoveRef.current) {
|
|
114
116
|
window.removeEventListener("pointermove", onPointerMoveRef.current);
|
|
@@ -116,6 +118,10 @@ export function PenDraftOverlay(_a) {
|
|
|
116
118
|
if (finishDragRef.current) {
|
|
117
119
|
window.removeEventListener("pointerup", finishDragRef.current);
|
|
118
120
|
}
|
|
121
|
+
// Auto-apply as soon as the user releases the handle after a move/resize
|
|
122
|
+
if (hadDrag && onApplyDraft) {
|
|
123
|
+
onApplyDraft();
|
|
124
|
+
}
|
|
119
125
|
};
|
|
120
126
|
onPointerMoveRef.current = function (event) {
|
|
121
127
|
var drag = dragRef.current;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Overlay handles for adjusting a rectangle/ellipse draft before commit.
|
|
3
|
-
* @param {{ nv: any, draft: import('./shapeDraftUtils').ShapeDraft, onDraftChange: (d: any) => void, overlayKey?: unknown }} props
|
|
3
|
+
* @param {{ nv: any, draft: import('./shapeDraftUtils').ShapeDraft, onDraftChange: (d: any) => void, onApplyDraft?: () => void, overlayKey?: unknown }} props
|
|
4
4
|
*/
|
|
5
|
-
export function ShapeDraftOverlay({ nv, draft, onDraftChange, overlayKey }: {
|
|
5
|
+
export function ShapeDraftOverlay({ nv, draft, onDraftChange, onApplyDraft, overlayKey }: {
|
|
6
6
|
nv: any;
|
|
7
7
|
draft: import('./shapeDraftUtils').ShapeDraft;
|
|
8
8
|
onDraftChange: (d: any) => void;
|
|
9
|
+
onApplyDraft?: (() => void) | undefined;
|
|
9
10
|
overlayKey?: unknown;
|
|
10
11
|
}): import("react/jsx-runtime").JSX.Element | null;
|
|
11
12
|
export default ShapeDraftOverlay;
|
|
@@ -25,11 +25,11 @@ var HANDLE_SIZE = 10;
|
|
|
25
25
|
var ACCENT = "#580f8b";
|
|
26
26
|
/**
|
|
27
27
|
* Overlay handles for adjusting a rectangle/ellipse draft before commit.
|
|
28
|
-
* @param {{ nv: any, draft: import('./shapeDraftUtils').ShapeDraft, onDraftChange: (d: any) => void, overlayKey?: unknown }} props
|
|
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
31
|
var _b;
|
|
32
|
-
var nv = _a.nv, draft = _a.draft, onDraftChange = _a.onDraftChange, overlayKey = _a.overlayKey;
|
|
32
|
+
var nv = _a.nv, draft = _a.draft, onDraftChange = _a.onDraftChange, onApplyDraft = _a.onApplyDraft, overlayKey = _a.overlayKey;
|
|
33
33
|
var dragRef = useRef(null);
|
|
34
34
|
var draftRef = useRef(draft);
|
|
35
35
|
draftRef.current = draft;
|
|
@@ -84,6 +84,7 @@ export function ShapeDraftOverlay(_a) {
|
|
|
84
84
|
onDraftChange(nextDraft);
|
|
85
85
|
}, [nv, onDraftChange]);
|
|
86
86
|
finishDragRef.current = function () {
|
|
87
|
+
var hadDrag = dragRef.current != null;
|
|
87
88
|
dragRef.current = null;
|
|
88
89
|
if (onPointerMoveRef.current) {
|
|
89
90
|
window.removeEventListener("pointermove", onPointerMoveRef.current);
|
|
@@ -91,6 +92,10 @@ export function ShapeDraftOverlay(_a) {
|
|
|
91
92
|
if (finishDragRef.current) {
|
|
92
93
|
window.removeEventListener("pointerup", finishDragRef.current);
|
|
93
94
|
}
|
|
95
|
+
// Auto-apply as soon as the user releases the handle after a move/resize
|
|
96
|
+
if (hadDrag && onApplyDraft) {
|
|
97
|
+
onApplyDraft();
|
|
98
|
+
}
|
|
94
99
|
};
|
|
95
100
|
onPointerMoveRef.current = function (event) {
|
|
96
101
|
var drag = dragRef.current;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export default function DrawColorPlatte({ expanded, updateDrawPen, setDrawingEnabled, showPenModes, penDrawMode, onPenDrawModeChange, polylineVertexCount, penDraftActive,
|
|
1
|
+
export default function DrawColorPlatte({ expanded, updateDrawPen, setDrawingEnabled, showPenModes, penDrawMode, onPenDrawModeChange, polylineVertexCount, penDraftActive, onCancelPenDraft, onFillPenDraft, brushSize, updateBrushSize, shapeDraftActive, onCancelShapeDraft, }: {
|
|
2
2
|
expanded: any;
|
|
3
3
|
updateDrawPen: any;
|
|
4
4
|
setDrawingEnabled: any;
|
|
@@ -7,12 +7,10 @@ export default function DrawColorPlatte({ expanded, updateDrawPen, setDrawingEna
|
|
|
7
7
|
onPenDrawModeChange: any;
|
|
8
8
|
polylineVertexCount?: number | undefined;
|
|
9
9
|
penDraftActive?: boolean | undefined;
|
|
10
|
-
onApplyPenDraft: any;
|
|
11
10
|
onCancelPenDraft: any;
|
|
12
11
|
onFillPenDraft: any;
|
|
13
12
|
brushSize?: number | undefined;
|
|
14
13
|
updateBrushSize: any;
|
|
15
14
|
shapeDraftActive?: boolean | undefined;
|
|
16
|
-
onApplyShapeDraft: any;
|
|
17
15
|
onCancelShapeDraft: any;
|
|
18
16
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -11,12 +11,13 @@ var __assign = (this && this.__assign) || function () {
|
|
|
11
11
|
};
|
|
12
12
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
13
|
/**
|
|
14
|
-
* Pen palette adds freehand vs polyline mode; pen/shape drafts show
|
|
14
|
+
* Pen palette adds freehand vs polyline mode; pen/shape drafts show Cancel while adjusting.
|
|
15
|
+
* Shapes are applied automatically on mouse release.
|
|
15
16
|
*/
|
|
16
17
|
import { Stack, IconButton, Button, Tooltip, Typography } from "@mui/material";
|
|
17
18
|
import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
|
|
18
|
-
import CheckIcon from "@mui/icons-material/Check";
|
|
19
19
|
import CloseIcon from "@mui/icons-material/Close";
|
|
20
|
+
import FormatColorFillIcon from "@mui/icons-material/FormatColorFill";
|
|
20
21
|
import { BrushSizeSlider } from "./BrushSizeSlider";
|
|
21
22
|
var FILLED_COLORS = [
|
|
22
23
|
{ sx: { color: "red" } },
|
|
@@ -37,7 +38,7 @@ var modeBtnSx = function (active) { return ({
|
|
|
37
38
|
px: 0.75
|
|
38
39
|
}); };
|
|
39
40
|
export default function DrawColorPlatte(_a) {
|
|
40
|
-
var expanded = _a.expanded, updateDrawPen = _a.updateDrawPen, setDrawingEnabled = _a.setDrawingEnabled, _b = _a.showPenModes, showPenModes = _b === void 0 ? false : _b, _c = _a.penDrawMode, penDrawMode = _c === void 0 ? "freehand" : _c, onPenDrawModeChange = _a.onPenDrawModeChange, _d = _a.polylineVertexCount, polylineVertexCount = _d === void 0 ? 0 : _d, _e = _a.penDraftActive, penDraftActive = _e === void 0 ? false : _e,
|
|
41
|
+
var expanded = _a.expanded, updateDrawPen = _a.updateDrawPen, setDrawingEnabled = _a.setDrawingEnabled, _b = _a.showPenModes, showPenModes = _b === void 0 ? false : _b, _c = _a.penDrawMode, penDrawMode = _c === void 0 ? "freehand" : _c, onPenDrawModeChange = _a.onPenDrawModeChange, _d = _a.polylineVertexCount, polylineVertexCount = _d === void 0 ? 0 : _d, _e = _a.penDraftActive, penDraftActive = _e === void 0 ? false : _e, onCancelPenDraft = _a.onCancelPenDraft, onFillPenDraft = _a.onFillPenDraft, _f = _a.brushSize, brushSize = _f === void 0 ? 1 : _f, updateBrushSize = _a.updateBrushSize, _g = _a.shapeDraftActive, shapeDraftActive = _g === void 0 ? false : _g, onCancelShapeDraft = _a.onCancelShapeDraft;
|
|
41
42
|
return (_jsxs(Stack, __assign({ style: {
|
|
42
43
|
position: "absolute",
|
|
43
44
|
top: "100%",
|
|
@@ -62,36 +63,22 @@ export default function DrawColorPlatte(_a) {
|
|
|
62
63
|
py: 0.25,
|
|
63
64
|
px: 0.75,
|
|
64
65
|
"& .MuiButton-startIcon": { mr: 0.5, ml: 0 }
|
|
65
|
-
} }, { children: "Cancel" })) })),
|
|
66
|
-
color: "#c9a0e8",
|
|
67
|
-
fontSize: ACTION_FONT_SIZE,
|
|
68
|
-
textTransform: "none",
|
|
69
|
-
minWidth: 0,
|
|
70
|
-
py: 0.25,
|
|
71
|
-
px: 0.75
|
|
72
|
-
} }, { children: "Fill" })) }))), _jsx(Tooltip, __assign({ title: "Apply shape (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: {
|
|
73
|
-
color: "#c9a0e8",
|
|
74
|
-
fontSize: ACTION_FONT_SIZE,
|
|
75
|
-
textTransform: "none",
|
|
76
|
-
minWidth: 0,
|
|
77
|
-
py: 0.25,
|
|
78
|
-
px: 0.75,
|
|
79
|
-
"& .MuiButton-startIcon": { mr: 0.5, ml: 0 }
|
|
80
|
-
} }, { children: "Apply" })) }))] }))] }))), shapeDraftActive && expanded && (_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: "Cancel shape (Esc)" }, { children: _jsx(Button, __assign({ size: "small", "aria-label": "cancel shape", onClick: function () { return onCancelShapeDraft === null || onCancelShapeDraft === void 0 ? void 0 : onCancelShapeDraft(); }, startIcon: _jsx(CloseIcon, { sx: { fontSize: ACTION_ICON_SIZE } }), sx: {
|
|
81
|
-
color: "#ccc",
|
|
82
|
-
fontSize: ACTION_FONT_SIZE,
|
|
83
|
-
textTransform: "none",
|
|
84
|
-
minWidth: 0,
|
|
85
|
-
py: 0.25,
|
|
86
|
-
px: 0.75,
|
|
87
|
-
"& .MuiButton-startIcon": { mr: 0.5, ml: 0 }
|
|
88
|
-
} }, { children: "Cancel" })) })), _jsx(Tooltip, __assign({ title: "Apply shape (enter or right-click)" }, { children: _jsx(Button, __assign({ size: "small", "aria-label": "apply shape", onClick: function () { return onApplyShapeDraft === null || onApplyShapeDraft === void 0 ? void 0 : onApplyShapeDraft(); }, startIcon: _jsx(CheckIcon, { sx: { fontSize: ACTION_ICON_SIZE } }), sx: {
|
|
66
|
+
} }, { children: "Cancel" })) })), penDrawMode === "polyline" && polylineVertexCount >= 3 && (_jsx(Tooltip, __assign({ title: "Fill interior (keeps outline editable until release)" }, { children: _jsx(Button, __assign({ size: "small", "aria-label": "fill polyline", onClick: function () { return onFillPenDraft === null || onFillPenDraft === void 0 ? void 0 : onFillPenDraft(); }, startIcon: _jsx(FormatColorFillIcon, { sx: { fontSize: ACTION_ICON_SIZE } }), sx: {
|
|
89
67
|
color: "#c9a0e8",
|
|
90
68
|
fontSize: ACTION_FONT_SIZE,
|
|
91
69
|
textTransform: "none",
|
|
92
70
|
minWidth: 0,
|
|
93
71
|
py: 0.25,
|
|
94
72
|
px: 0.75,
|
|
73
|
+
ml: "auto",
|
|
95
74
|
"& .MuiButton-startIcon": { mr: 0.5, ml: 0 }
|
|
96
|
-
} }, { children: "
|
|
75
|
+
} }, { children: "Fill" })) })))] }))), shapeDraftActive && expanded && (_jsx(Stack, __assign({ direction: "row", alignItems: "center", sx: { px: 1, py: 0.5, borderTop: "1px solid #555", width: "100%" } }, { children: _jsx(Tooltip, __assign({ title: "Cancel shape (Esc)" }, { children: _jsx(Button, __assign({ size: "small", "aria-label": "cancel shape", onClick: function () { return onCancelShapeDraft === null || onCancelShapeDraft === void 0 ? void 0 : onCancelShapeDraft(); }, startIcon: _jsx(CloseIcon, { sx: { fontSize: ACTION_ICON_SIZE } }), sx: {
|
|
76
|
+
color: "#ccc",
|
|
77
|
+
fontSize: ACTION_FONT_SIZE,
|
|
78
|
+
textTransform: "none",
|
|
79
|
+
minWidth: 0,
|
|
80
|
+
py: 0.25,
|
|
81
|
+
px: 0.75,
|
|
82
|
+
"& .MuiButton-startIcon": { mr: 0.5, ml: 0 }
|
|
83
|
+
} }, { children: "Cancel" })) })) })))] })));
|
|
97
84
|
}
|
|
@@ -49,6 +49,21 @@ export function captureDeferredShapeDraft(nv: any): {
|
|
|
49
49
|
baseBitmap: Uint8Array | null;
|
|
50
50
|
};
|
|
51
51
|
export function shouldDeferShapeCommit(nv: any): any;
|
|
52
|
+
/**
|
|
53
|
+
* When the user clicks on an existing filled ROI while the rectangle/ellipse tool
|
|
54
|
+
* is active (but no draft is currently open), flood-fill the clicked cluster to
|
|
55
|
+
* reconstruct a ShapeDraft so the bounding-box overlay reappears for re-editing.
|
|
56
|
+
*
|
|
57
|
+
* Returns null if the click didn't land on a labeled voxel.
|
|
58
|
+
*/
|
|
59
|
+
export function captureShapeDraftFromClick(nv: any): {
|
|
60
|
+
ptA: number[];
|
|
61
|
+
ptB: number[];
|
|
62
|
+
penValue: any;
|
|
63
|
+
axCorSag: number;
|
|
64
|
+
penType: any;
|
|
65
|
+
baseBitmap: Uint8Array;
|
|
66
|
+
} | null;
|
|
52
67
|
export type ShapeDraftKind = 'rectangle' | 'ellipse';
|
|
53
68
|
export type ShapeDraft = {
|
|
54
69
|
ptA: [number, number, number];
|
|
@@ -19,6 +19,7 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
|
19
19
|
return to.concat(ar || Array.prototype.slice.call(from));
|
|
20
20
|
};
|
|
21
21
|
import { NI_PEN_TYPE } from "./niivuePenType";
|
|
22
|
+
import { voxFromMouse } from "./polylinePenUtils";
|
|
22
23
|
/** @typedef {'rectangle' | 'ellipse'} ShapeDraftKind */
|
|
23
24
|
/**
|
|
24
25
|
* @typedef {Object} ShapeDraft
|
|
@@ -186,3 +187,94 @@ export function shouldDeferShapeCommit(nv) {
|
|
|
186
187
|
(penType === NI_PEN_TYPE.RECTANGLE || penType === NI_PEN_TYPE.ELLIPSE) &&
|
|
187
188
|
nv.drawShapePreviewBitmap);
|
|
188
189
|
}
|
|
190
|
+
function voxelIndex(x, y, z, dx, dy) {
|
|
191
|
+
return x + y * dx + z * dx * dy;
|
|
192
|
+
}
|
|
193
|
+
function decodeVoxelIndex(idx, dx, dy) {
|
|
194
|
+
var z = Math.floor(idx / (dx * dy));
|
|
195
|
+
var rem = idx - z * dx * dy;
|
|
196
|
+
var y = Math.floor(rem / dx);
|
|
197
|
+
var x = rem % dx;
|
|
198
|
+
return [x, y, z];
|
|
199
|
+
}
|
|
200
|
+
function inferAxCorSagFromBounds(x1, y1, z1, x2, y2, z2, fallback) {
|
|
201
|
+
if (fallback === void 0) { fallback = 0; }
|
|
202
|
+
var spanX = x2 - x1;
|
|
203
|
+
var spanY = y2 - y1;
|
|
204
|
+
var spanZ = z2 - z1;
|
|
205
|
+
if (spanZ <= spanX && spanZ <= spanY)
|
|
206
|
+
return 0; // axial — flat in Z
|
|
207
|
+
if (spanY <= spanX && spanY <= spanZ)
|
|
208
|
+
return 1; // coronal — flat in Y
|
|
209
|
+
if (spanX <= spanY && spanX <= spanZ)
|
|
210
|
+
return 2; // sagittal — flat in X
|
|
211
|
+
return fallback;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* When the user clicks on an existing filled ROI while the rectangle/ellipse tool
|
|
215
|
+
* is active (but no draft is currently open), flood-fill the clicked cluster to
|
|
216
|
+
* reconstruct a ShapeDraft so the bounding-box overlay reappears for re-editing.
|
|
217
|
+
*
|
|
218
|
+
* Returns null if the click didn't land on a labeled voxel.
|
|
219
|
+
*/
|
|
220
|
+
export function captureShapeDraftFromClick(nv) {
|
|
221
|
+
var _a;
|
|
222
|
+
var dims = (_a = nv.back) === null || _a === void 0 ? void 0 : _a.dims;
|
|
223
|
+
if (!dims || !nv.drawBitmap)
|
|
224
|
+
return null;
|
|
225
|
+
var seedVox = voxFromMouse(nv);
|
|
226
|
+
if (!seedVox)
|
|
227
|
+
return null;
|
|
228
|
+
var dx = dims[1];
|
|
229
|
+
var dy = dims[2];
|
|
230
|
+
var dz = dims[3];
|
|
231
|
+
var seedIdx = voxelIndex(seedVox[0], seedVox[1], seedVox[2], dx, dy);
|
|
232
|
+
var label = nv.drawBitmap[seedIdx];
|
|
233
|
+
if (!label)
|
|
234
|
+
return null;
|
|
235
|
+
// BFS flood-fill to find connected cluster of the same label
|
|
236
|
+
var visited = new Set();
|
|
237
|
+
var queue = [seedIdx];
|
|
238
|
+
visited.add(seedIdx);
|
|
239
|
+
var x1 = Infinity, y1 = Infinity, z1 = Infinity;
|
|
240
|
+
var x2 = -Infinity, y2 = -Infinity, z2 = -Infinity;
|
|
241
|
+
while (queue.length > 0) {
|
|
242
|
+
var idx = queue.shift();
|
|
243
|
+
var _b = decodeVoxelIndex(idx, dx, dy), x = _b[0], y = _b[1], z = _b[2];
|
|
244
|
+
x1 = Math.min(x1, x);
|
|
245
|
+
y1 = Math.min(y1, y);
|
|
246
|
+
z1 = Math.min(z1, z);
|
|
247
|
+
x2 = Math.max(x2, x);
|
|
248
|
+
y2 = Math.max(y2, y);
|
|
249
|
+
z2 = Math.max(z2, z);
|
|
250
|
+
var neighbors = [
|
|
251
|
+
[x + 1, y, z], [x - 1, y, z],
|
|
252
|
+
[x, y + 1, z], [x, y - 1, z],
|
|
253
|
+
[x, y, z + 1], [x, y, z - 1],
|
|
254
|
+
];
|
|
255
|
+
for (var _i = 0, neighbors_1 = neighbors; _i < neighbors_1.length; _i++) {
|
|
256
|
+
var _c = neighbors_1[_i], nx = _c[0], ny = _c[1], nz = _c[2];
|
|
257
|
+
if (nx < 0 || ny < 0 || nz < 0 || nx >= dx || ny >= dy || nz >= dz)
|
|
258
|
+
continue;
|
|
259
|
+
var nIdx = voxelIndex(nx, ny, nz, dx, dy);
|
|
260
|
+
if (visited.has(nIdx) || nv.drawBitmap[nIdx] !== label)
|
|
261
|
+
continue;
|
|
262
|
+
visited.add(nIdx);
|
|
263
|
+
queue.push(nIdx);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (!Number.isFinite(x1))
|
|
267
|
+
return null;
|
|
268
|
+
// baseBitmap is the bitmap with this cluster erased (so re-applying restores it)
|
|
269
|
+
var baseBitmap = new Uint8Array(nv.drawBitmap);
|
|
270
|
+
visited.forEach(function (idx) { baseBitmap[idx] = 0; });
|
|
271
|
+
var axCorSag = inferAxCorSagFromBounds(x1, y1, z1, x2, y2, z2, nv.drawPenAxCorSag >= 0 ? nv.drawPenAxCorSag : 0);
|
|
272
|
+
return {
|
|
273
|
+
ptA: [x1, y1, z1],
|
|
274
|
+
ptB: [x2, y2, z2],
|
|
275
|
+
penValue: label,
|
|
276
|
+
axCorSag: axCorSag,
|
|
277
|
+
penType: nv.opts.penType,
|
|
278
|
+
baseBitmap: baseBitmap
|
|
279
|
+
};
|
|
280
|
+
}
|