cloudmr-ux 4.6.0 → 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.
@@ -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
- function cloudMrTryApplyDraftOnRightClick(nv, event) {
1475
- if (!cloudMrHasApplyableDraft(nv)) {
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
- event.preventDefault();
1479
- event.stopPropagation();
1480
- if (typeof nv.onApplyActiveDraft === "function") {
1481
- nv.onApplyActiveDraft();
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
- overlayKey: any;
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, onApplyPenDraft, onCancelPenDraft, onFillPenDraft, brushSize, updateBrushSize, shapeDraftActive, onApplyShapeDraft, onCancelShapeDraft, }: {
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 Apply/Cancel while adjusting.
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, onApplyPenDraft = _a.onApplyPenDraft, 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, onApplyShapeDraft = _a.onApplyShapeDraft, onCancelShapeDraft = _a.onCancelShapeDraft;
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" })) })), _jsxs(Stack, __assign({ direction: "row", alignItems: "center", spacing: 1 }, { children: [penDrawMode === "polyline" && polylineVertexCount >= 3 && (_jsx(Tooltip, __assign({ title: "Fill interior (keeps outline editable until Apply)" }, { children: _jsx(Button, __assign({ size: "small", "aria-label": "fill polyline", onClick: function () { return onFillPenDraft === null || onFillPenDraft === void 0 ? void 0 : onFillPenDraft(); }, sx: {
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: "Apply" })) }))] })))] })));
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudmr-ux",
3
- "version": "4.6.0",
3
+ "version": "4.6.1",
4
4
  "author": "erosmontin@gmail.com",
5
5
  "license": "MIT",
6
6
  "repository": "erosmontin/cloudmr-ux",