cloudmr-ux 4.6.0 → 4.6.2

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%"
@@ -832,6 +832,14 @@ export default function CloudMrNiivueViewer(props) {
832
832
  (_a = nv.cloudMrCancelPolyline) === null || _a === void 0 ? void 0 : _a.call(nv);
833
833
  }
834
834
  }
835
+ function syncActiveDraftRefs() {
836
+ nv._cloudMrActiveShapeDraft = shapeDraftRef.current;
837
+ nv._cloudMrActivePenDraft = penDraftRef.current;
838
+ }
839
+ function clearActiveDraftRefs() {
840
+ nv._cloudMrActiveShapeDraft = null;
841
+ nv._cloudMrActivePenDraft = null;
842
+ }
835
843
  function cancelPenDraftHandler() {
836
844
  var _a;
837
845
  var draft = penDraftRef.current;
@@ -845,6 +853,7 @@ export default function CloudMrNiivueViewer(props) {
845
853
  setPenDraft(null);
846
854
  penDraftRef.current = null;
847
855
  nv._cloudMrPenDraftActive = false;
856
+ clearActiveDraftRefs();
848
857
  if (drawShapeToolRef.current === "pen") {
849
858
  nvSetDrawingEnabled(true);
850
859
  }
@@ -862,6 +871,7 @@ export default function CloudMrNiivueViewer(props) {
862
871
  setPenDraft(null);
863
872
  penDraftRef.current = null;
864
873
  nv._cloudMrPenDraftActive = false;
874
+ clearActiveDraftRefs();
865
875
  setDrawingChanged(true);
866
876
  if (drawShapeToolRef.current === "pen") {
867
877
  nvSetDrawingEnabled(true);
@@ -878,6 +888,7 @@ export default function CloudMrNiivueViewer(props) {
878
888
  function onPenDraftChange(draft) {
879
889
  setPenDraft(draft);
880
890
  penDraftRef.current = draft;
891
+ syncActiveDraftRefs();
881
892
  if (draft.kind === "polyline") {
882
893
  syncPolylineDraftToNv(nv, draft);
883
894
  }
@@ -889,6 +900,7 @@ export default function CloudMrNiivueViewer(props) {
889
900
  setPenDraft(draft);
890
901
  penDraftRef.current = draft;
891
902
  nv._cloudMrPenDraftActive = true;
903
+ syncActiveDraftRefs();
892
904
  nvSetDrawingEnabled(false);
893
905
  };
894
906
  nv.onPolylineChange = function (count) {
@@ -903,11 +915,13 @@ export default function CloudMrNiivueViewer(props) {
903
915
  if (draft) {
904
916
  setPenDraft(draft);
905
917
  penDraftRef.current = draft;
918
+ syncActiveDraftRefs();
906
919
  }
907
920
  }
908
921
  else if (((_b = penDraftRef.current) === null || _b === void 0 ? void 0 : _b.kind) === "polyline") {
909
922
  setPenDraft(null);
910
923
  penDraftRef.current = null;
924
+ syncActiveDraftRefs();
911
925
  }
912
926
  };
913
927
  function cancelShapeDraft() {
@@ -920,6 +934,7 @@ export default function CloudMrNiivueViewer(props) {
920
934
  setShapeDraft(null);
921
935
  shapeDraftRef.current = null;
922
936
  nv._cloudMrShapeDraftActive = false;
937
+ clearActiveDraftRefs();
923
938
  if (drawShapeToolRef.current) {
924
939
  nvSetDrawingEnabled(true);
925
940
  }
@@ -932,6 +947,7 @@ export default function CloudMrNiivueViewer(props) {
932
947
  setShapeDraft(null);
933
948
  shapeDraftRef.current = null;
934
949
  nv._cloudMrShapeDraftActive = false;
950
+ clearActiveDraftRefs();
935
951
  if (drawShapeToolRef.current) {
936
952
  nvSetDrawingEnabled(true);
937
953
  }
@@ -940,11 +956,13 @@ export default function CloudMrNiivueViewer(props) {
940
956
  function onShapeDraftChange(draft) {
941
957
  setShapeDraft(draft);
942
958
  shapeDraftRef.current = draft;
959
+ syncActiveDraftRefs();
943
960
  }
944
961
  nv.onShapeDraftReady = function (draft) {
945
962
  setShapeDraft(draft);
946
963
  shapeDraftRef.current = draft;
947
964
  nv._cloudMrShapeDraftActive = true;
965
+ syncActiveDraftRefs();
948
966
  nvSetDrawingEnabled(false);
949
967
  };
950
968
  nv.onApplyActiveDraft = function () {
@@ -961,32 +979,6 @@ export default function CloudMrNiivueViewer(props) {
961
979
  React.useEffect(function () {
962
980
  nv._cloudMrShapeDraftActive = shapeDraft != null;
963
981
  }, [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
982
  React.useEffect(function () {
991
983
  if (!shapeDraft && !penDraft) {
992
984
  return undefined;
@@ -1000,16 +992,6 @@ export default function CloudMrNiivueViewer(props) {
1000
992
  else if (penDraft) {
1001
993
  cancelPenDraftHandler();
1002
994
  }
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
995
  }
1014
996
  };
1015
997
  window.addEventListener("keydown", onKeyDown);
@@ -4,9 +4,13 @@
4
4
  import { Niivue, NVImage, NVImageFromUrlOptions } from "@niivue/niivue";
5
5
  import {
6
6
  captureDeferredShapeDraft,
7
+ captureShapeDraftFromClick,
7
8
  isDraftTooSmall,
9
+ isVoxelPartOfDraft,
10
+ redrawDraftShape,
8
11
  shouldDeferShapeCommit,
9
12
  } from "./shapeDraftUtils.js";
13
+ import { NI_PEN_TYPE } from "./niivuePenType.js";
10
14
  import {
11
15
  addPolylineVertex,
12
16
  axCorSagFromMouse,
@@ -19,6 +23,9 @@ import {
19
23
  } from "./polylinePenUtils.js";
20
24
  import {
21
25
  captureFreehandDraft,
26
+ capturePenDraftFromClick,
27
+ isPenDrawToolActive,
28
+ redrawFreehandDraft,
22
29
  shouldDeferFreehandCommit,
23
30
  } from "./penDraftUtils.js";
24
31
 
@@ -1465,29 +1472,93 @@ Niivue.prototype.cloudMrResetPolyline = function cloudMrResetPolyline() {
1465
1472
  resetPolylineState(this);
1466
1473
  };
1467
1474
 
1468
- const RIGHT_MOUSE_BUTTON = 2;
1469
-
1470
1475
  function cloudMrHasApplyableDraft(nv) {
1471
1476
  return !!(nv._cloudMrShapeDraftActive || nv._cloudMrPenDraftActive);
1472
1477
  }
1473
1478
 
1474
- function cloudMrTryApplyDraftOnRightClick(nv, event) {
1475
- if (!cloudMrHasApplyableDraft(nv)) {
1479
+ function getActiveDraft(nv) {
1480
+ if (nv._cloudMrPenDraftActive && nv._cloudMrActivePenDraft) {
1481
+ return nv._cloudMrActivePenDraft;
1482
+ }
1483
+ if (nv._cloudMrShapeDraftActive && nv._cloudMrActiveShapeDraft) {
1484
+ return nv._cloudMrActiveShapeDraft;
1485
+ }
1486
+ return null;
1487
+ }
1488
+
1489
+ function isClickOnActiveDraft(nv) {
1490
+ const draft = getActiveDraft(nv);
1491
+ if (!draft) return false;
1492
+ const vox = voxFromMouse(nv);
1493
+ if (!vox) return false;
1494
+ return isVoxelPartOfDraft(nv, draft, vox);
1495
+ }
1496
+
1497
+ /** Commit the active draft when the user clicks outside the ROI being edited. */
1498
+ function cloudMrTryApplyDraftOnClickAway(nv) {
1499
+ if (!cloudMrHasApplyableDraft(nv) || !isClickWithoutDrag(nv.uiData)) {
1500
+ return false;
1501
+ }
1502
+ if (isClickOnActiveDraft(nv)) {
1476
1503
  return false;
1477
1504
  }
1478
- event.preventDefault();
1479
- event.stopPropagation();
1480
1505
  if (typeof nv.onApplyActiveDraft === "function") {
1506
+ nv._cloudMrSuppressDrawingChangedMouseUp = true;
1481
1507
  nv.onApplyActiveDraft();
1482
1508
  }
1483
1509
  return true;
1484
1510
  }
1485
1511
 
1512
+ /**
1513
+ * When no draft is active and the user clicks an existing ROI, reconstruct a
1514
+ * shape or pen draft so the bounding-box overlay reappears for re-editing.
1515
+ */
1516
+ function cloudMrTryReopenDraftOnClick(nv) {
1517
+ if (
1518
+ nv._cloudMrShapeDraftActive ||
1519
+ nv._cloudMrPenDraftActive ||
1520
+ !isClickWithoutDrag(nv.uiData)
1521
+ ) {
1522
+ return false;
1523
+ }
1524
+
1525
+ const penType = nv.opts.penType;
1526
+ const isShapeTool =
1527
+ nv.opts.deferShapeCommit &&
1528
+ nv.opts.drawingEnabled &&
1529
+ (penType === NI_PEN_TYPE.RECTANGLE || penType === NI_PEN_TYPE.ELLIPSE);
1530
+ const isPenTool = isPenDrawToolActive(nv);
1531
+
1532
+ if (!isShapeTool && !isPenTool) {
1533
+ return false;
1534
+ }
1535
+
1536
+ if (isShapeTool) {
1537
+ const reopenDraft = captureShapeDraftFromClick(nv);
1538
+ if (!reopenDraft) return false;
1539
+ redrawDraftShape(nv, reopenDraft);
1540
+ nv._cloudMrSuppressDrawingChangedMouseUp = true;
1541
+ if (typeof nv.onShapeDraftReady === "function") {
1542
+ nv.onShapeDraftReady(reopenDraft);
1543
+ }
1544
+ return true;
1545
+ }
1546
+
1547
+ const reopenDraft = capturePenDraftFromClick(nv);
1548
+ if (!reopenDraft) return false;
1549
+ redrawFreehandDraft(nv, reopenDraft);
1550
+ nv._cloudMrSuppressDrawingChangedMouseUp = true;
1551
+ if (typeof nv.onPenDraftReady === "function") {
1552
+ nv.onPenDraftReady(reopenDraft);
1553
+ }
1554
+ if (nv.opts.polylinePenMode) {
1555
+ resetPolylineState(nv);
1556
+ }
1557
+ return true;
1558
+ }
1559
+
1486
1560
  const _mouseDownListener = Niivue.prototype.mouseDownListener;
1487
1561
  Niivue.prototype.mouseDownListener = function cloudMrMouseDownListener(e) {
1488
- if (e.button === RIGHT_MOUSE_BUTTON && cloudMrTryApplyDraftOnRightClick(this, e)) {
1489
- return;
1490
- }
1491
1562
  if (shouldDeferFreehandCommit(this) && this.drawBitmap) {
1492
1563
  this._cloudMrFreehandSessionStartBitmap = this.drawBitmap.slice();
1493
1564
  this._cloudMrFreehandAxCorSag = -1;
@@ -1551,17 +1622,30 @@ Niivue.prototype.mouseUpListener = function cloudMrMouseUpListener() {
1551
1622
  _mouseUpListener.call(this);
1552
1623
 
1553
1624
  if (polylineClick) {
1625
+ if (cloudMrTryReopenDraftOnClick(this)) return;
1554
1626
  addPolylineVertex(this);
1555
1627
  return;
1556
1628
  }
1557
1629
 
1630
+ if (cloudMrHasApplyableDraft(this) && isClickWithoutDrag(this.uiData)) {
1631
+ if (cloudMrTryApplyDraftOnClickAway(this)) {
1632
+ cloudMrTryReopenDraftOnClick(this);
1633
+ return;
1634
+ }
1635
+ if (isClickOnActiveDraft(this)) {
1636
+ return;
1637
+ }
1638
+ }
1639
+
1558
1640
  if (!pendingDraft?.baseBitmap) {
1641
+ cloudMrTryReopenDraftOnClick(this);
1559
1642
  return;
1560
1643
  }
1561
1644
  if (isDraftTooSmall(pendingDraft.ptA, pendingDraft.ptB)) {
1562
1645
  this.drawBitmap.set(pendingDraft.baseBitmap);
1563
1646
  this.refreshDrawing(true, false);
1564
1647
  this.drawScene();
1648
+ cloudMrTryReopenDraftOnClick(this);
1565
1649
  return;
1566
1650
  }
1567
1651
  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
  }
@@ -13,6 +13,27 @@
13
13
  export function isEraserActive(nv: any): any;
14
14
  export function isFreehandPenActive(nv: any): any;
15
15
  export function shouldDeferFreehandCommit(nv: any): any;
16
+ export function isPenDrawToolActive(nv: any): any;
17
+ /**
18
+ * Re-enter pen edit mode when clicking an existing freehand/polyline ROI.
19
+ * Reopens as a freehand draft (move via bounding box) since vertex data is
20
+ * not stored in the bitmap alone.
21
+ */
22
+ export function capturePenDraftFromClick(nv: any): {
23
+ kind: string;
24
+ baseBitmap: Uint8Array;
25
+ axCorSag: any;
26
+ penValue: number;
27
+ strokeVoxels: [number, number, number][];
28
+ bounds: {
29
+ x1: any;
30
+ y1: any;
31
+ z1: any;
32
+ x2: any;
33
+ y2: any;
34
+ z2: any;
35
+ };
36
+ } | null;
16
37
  export function redrawPolylineDraft(nv: any, draft: any): void;
17
38
  export function translatePolylineVertices(vertices: any, delta: any): any;
18
39
  export function updatePolylineVertex(vertices: any, index: any, newVox: any): any;
@@ -18,7 +18,8 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
18
18
  }
19
19
  return to.concat(ar || Array.prototype.slice.call(from));
20
20
  };
21
- import { translatePt } from "./shapeDraftUtils";
21
+ import { translatePt, floodFillClusterFromVox, eraseClusterFromBitmap, } from "./shapeDraftUtils";
22
+ import { axCorSagFromMouse, voxFromMouse } from "./polylinePenUtils";
22
23
  import { NI_PEN_TYPE } from "./niivuePenType";
23
24
  /** @typedef {'polyline' | 'freehand'} PenDraftKind */
24
25
  /**
@@ -47,6 +48,34 @@ export function isFreehandPenActive(nv) {
47
48
  export function shouldDeferFreehandCommit(nv) {
48
49
  return !!nv.opts.deferFreehandCommit && isFreehandPenActive(nv);
49
50
  }
51
+ export function isPenDrawToolActive(nv) {
52
+ return (nv.opts.drawingEnabled &&
53
+ nv.opts.penType === NI_PEN_TYPE.PEN &&
54
+ nv.opts.penValue > 0 &&
55
+ (nv.opts.deferFreehandCommit || nv.opts.polylinePenMode));
56
+ }
57
+ /**
58
+ * Re-enter pen edit mode when clicking an existing freehand/polyline ROI.
59
+ * Reopens as a freehand draft (move via bounding box) since vertex data is
60
+ * not stored in the bitmap alone.
61
+ */
62
+ export function capturePenDraftFromClick(nv) {
63
+ var seedVox = voxFromMouse(nv);
64
+ var cluster = floodFillClusterFromVox(nv, seedVox);
65
+ if (!cluster)
66
+ return null;
67
+ var label = cluster.label, visited = cluster.visited, voxels = cluster.voxels, bounds = cluster.bounds;
68
+ var x1 = bounds.x1, y1 = bounds.y1, z1 = bounds.z1, x2 = bounds.x2, y2 = bounds.y2, z2 = bounds.z2;
69
+ var axCorSag = nv.drawPenAxCorSag >= 0 ? nv.drawPenAxCorSag : axCorSagFromMouse(nv);
70
+ return {
71
+ kind: "freehand",
72
+ baseBitmap: eraseClusterFromBitmap(nv.drawBitmap, visited),
73
+ axCorSag: axCorSag,
74
+ penValue: label,
75
+ strokeVoxels: voxels,
76
+ bounds: { x1: x1, y1: y1, z1: z1, x2: x2, y2: y2, z2: z2 }
77
+ };
78
+ }
50
79
  export function redrawPolylineDraft(nv, draft) {
51
80
  var _a;
52
81
  if (!((_a = draft === null || draft === void 0 ? void 0 : draft.vertices) === null || _a === void 0 ? void 0 : _a.length) || !draft.baseBitmap)
@@ -49,6 +49,34 @@ export function captureDeferredShapeDraft(nv: any): {
49
49
  baseBitmap: Uint8Array | null;
50
50
  };
51
51
  export function shouldDeferShapeCommit(nv: any): any;
52
+ /**
53
+ * Flood-fill a connected voxel cluster from a seed.
54
+ * @returns {{ label: number, visited: Set<number>, voxels: [number,number,number][], bounds: object } | null}
55
+ */
56
+ export function floodFillClusterFromVox(nv: any, seedVox: any): {
57
+ label: number;
58
+ visited: Set<number>;
59
+ voxels: [number, number, number][];
60
+ bounds: object;
61
+ } | null;
62
+ /** True when a voxel belongs to the live draft overlay (drawn on top of baseBitmap). */
63
+ export function isVoxelPartOfDraft(nv: any, draft: any, seedVox: any): boolean;
64
+ export function eraseClusterFromBitmap(bitmap: any, visited: any): Uint8Array;
65
+ /**
66
+ * When the user clicks on an existing filled ROI while the rectangle/ellipse tool
67
+ * is active (but no draft is currently open), flood-fill the clicked cluster to
68
+ * reconstruct a ShapeDraft so the bounding-box overlay reappears for re-editing.
69
+ *
70
+ * Returns null if the click didn't land on a labeled voxel.
71
+ */
72
+ export function captureShapeDraftFromClick(nv: any): {
73
+ ptA: any[];
74
+ ptB: any[];
75
+ penValue: number;
76
+ axCorSag: number;
77
+ penType: any;
78
+ baseBitmap: Uint8Array;
79
+ } | null;
52
80
  export type ShapeDraftKind = 'rectangle' | 'ellipse';
53
81
  export type ShapeDraft = {
54
82
  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,136 @@ 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;
207
+ if (spanY <= spanX && spanY <= spanZ)
208
+ return 1;
209
+ if (spanX <= spanY && spanX <= spanZ)
210
+ return 2;
211
+ return fallback;
212
+ }
213
+ /**
214
+ * Flood-fill a connected voxel cluster from a seed.
215
+ * @returns {{ label: number, visited: Set<number>, voxels: [number,number,number][], bounds: object } | null}
216
+ */
217
+ export function floodFillClusterFromVox(nv, seedVox) {
218
+ var _a;
219
+ var dims = (_a = nv.back) === null || _a === void 0 ? void 0 : _a.dims;
220
+ if (!dims || !nv.drawBitmap || !seedVox)
221
+ return null;
222
+ var dx = dims[1];
223
+ var dy = dims[2];
224
+ var dz = dims[3];
225
+ var seedIdx = voxelIndex(seedVox[0], seedVox[1], seedVox[2], dx, dy);
226
+ var label = nv.drawBitmap[seedIdx];
227
+ if (!label)
228
+ return null;
229
+ var visited = new Set();
230
+ var queue = [seedIdx];
231
+ visited.add(seedIdx);
232
+ var voxels = [];
233
+ var x1 = Infinity;
234
+ var y1 = Infinity;
235
+ var z1 = Infinity;
236
+ var x2 = -Infinity;
237
+ var y2 = -Infinity;
238
+ var z2 = -Infinity;
239
+ while (queue.length > 0) {
240
+ var idx = queue.shift();
241
+ var _b = decodeVoxelIndex(idx, dx, dy), x = _b[0], y = _b[1], z = _b[2];
242
+ voxels.push([x, y, z]);
243
+ x1 = Math.min(x1, x);
244
+ y1 = Math.min(y1, y);
245
+ z1 = Math.min(z1, z);
246
+ x2 = Math.max(x2, x);
247
+ y2 = Math.max(y2, y);
248
+ z2 = Math.max(z2, z);
249
+ var neighbors = [
250
+ [x + 1, y, z],
251
+ [x - 1, y, z],
252
+ [x, y + 1, z],
253
+ [x, y - 1, z],
254
+ [x, y, z + 1],
255
+ [x, y, z - 1],
256
+ ];
257
+ for (var _i = 0, neighbors_1 = neighbors; _i < neighbors_1.length; _i++) {
258
+ var _c = neighbors_1[_i], nx = _c[0], ny = _c[1], nz = _c[2];
259
+ if (nx < 0 || ny < 0 || nz < 0 || nx >= dx || ny >= dy || nz >= dz)
260
+ continue;
261
+ var nIdx = voxelIndex(nx, ny, nz, dx, dy);
262
+ if (visited.has(nIdx) || nv.drawBitmap[nIdx] !== label)
263
+ continue;
264
+ visited.add(nIdx);
265
+ queue.push(nIdx);
266
+ }
267
+ }
268
+ if (!Number.isFinite(x1))
269
+ return null;
270
+ return {
271
+ label: label,
272
+ visited: visited,
273
+ voxels: voxels,
274
+ bounds: { x1: x1, y1: y1, z1: z1, x2: x2, y2: y2, z2: z2 }
275
+ };
276
+ }
277
+ /** True when a voxel belongs to the live draft overlay (drawn on top of baseBitmap). */
278
+ export function isVoxelPartOfDraft(nv, draft, seedVox) {
279
+ var _a;
280
+ if (!(draft === null || draft === void 0 ? void 0 : draft.baseBitmap) || !(nv === null || nv === void 0 ? void 0 : nv.drawBitmap) || !seedVox)
281
+ return false;
282
+ var dims = (_a = nv.back) === null || _a === void 0 ? void 0 : _a.dims;
283
+ if (!dims)
284
+ return false;
285
+ var dx = dims[1];
286
+ var dy = dims[2];
287
+ var idx = voxelIndex(seedVox[0], seedVox[1], seedVox[2], dx, dy);
288
+ return (nv.drawBitmap[idx] === draft.penValue &&
289
+ draft.baseBitmap[idx] !== draft.penValue);
290
+ }
291
+ export function eraseClusterFromBitmap(bitmap, visited) {
292
+ var next = new Uint8Array(bitmap);
293
+ visited.forEach(function (idx) {
294
+ next[idx] = 0;
295
+ });
296
+ return next;
297
+ }
298
+ /**
299
+ * When the user clicks on an existing filled ROI while the rectangle/ellipse tool
300
+ * is active (but no draft is currently open), flood-fill the clicked cluster to
301
+ * reconstruct a ShapeDraft so the bounding-box overlay reappears for re-editing.
302
+ *
303
+ * Returns null if the click didn't land on a labeled voxel.
304
+ */
305
+ export function captureShapeDraftFromClick(nv) {
306
+ var seedVox = voxFromMouse(nv);
307
+ var cluster = floodFillClusterFromVox(nv, seedVox);
308
+ if (!cluster)
309
+ return null;
310
+ var label = cluster.label, visited = cluster.visited, bounds = cluster.bounds;
311
+ var x1 = bounds.x1, y1 = bounds.y1, z1 = bounds.z1, x2 = bounds.x2, y2 = bounds.y2, z2 = bounds.z2;
312
+ var baseBitmap = eraseClusterFromBitmap(nv.drawBitmap, visited);
313
+ var axCorSag = inferAxCorSagFromBounds(x1, y1, z1, x2, y2, z2, nv.drawPenAxCorSag >= 0 ? nv.drawPenAxCorSag : 0);
314
+ return {
315
+ ptA: [x1, y1, z1],
316
+ ptB: [x2, y2, z2],
317
+ penValue: label,
318
+ axCorSag: axCorSag,
319
+ penType: nv.opts.penType,
320
+ baseBitmap: baseBitmap
321
+ };
322
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudmr-ux",
3
- "version": "4.6.0",
3
+ "version": "4.6.2",
4
4
  "author": "erosmontin@gmail.com",
5
5
  "license": "MIT",
6
6
  "repository": "erosmontin/cloudmr-ux",