cloudmr-ux 4.8.2 → 4.8.4

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.
@@ -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, _d, _e, _f;
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 = penMode === "polyline";
129
- nv.opts.isFilledPen = penMode === "freehand";
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 {
@@ -143,13 +143,14 @@ export function CloudMrNiivuePanel(props) {
143
143
  nv.opts.penBounds = 0;
144
144
  nv.opts.penSize = 1;
145
145
  }
146
- else if (tool === "pen") {
147
- var penBrush = (_f = props.drawToolkitProps.brushSize) !== null && _f !== void 0 ? _f : 1;
146
+ else if (tool === "pen" || tool === "polyline") {
147
+ var penBrush = (_c = props.drawToolkitProps.brushSize) !== null && _c !== void 0 ? _c : 1;
148
148
  nv.opts.penBounds = (penBrush - 1) / 2;
149
149
  nv.opts.penSize = penBrush;
150
150
  }
151
151
  nv.drawScene();
152
- if ((tool === "rectangle" || tool === "ellipse") && !props.drawToolkitProps.drawingEnabled) {
152
+ if ((tool === "rectangle" || tool === "ellipse" || tool === "pen" || tool === "polyline") &&
153
+ !props.drawToolkitProps.drawingEnabled) {
153
154
  props.drawToolkitProps.setDrawingEnabled(true);
154
155
  }
155
156
  }
@@ -63,8 +63,8 @@ import { SettingsPanel } from './SettingsPanel';
63
63
  import { NumberPicker } from './NumberPicker';
64
64
  import { ColorPicker } from './ColorPicker';
65
65
  import { LayersPanel } from './LayersPanel';
66
- import { applyPenDraft, cancelPenDraft, captureFreehandDraft, fillPolylineDraft, unfillPolylineDraft, polylineDraftFromNv, syncPolylineDraftToNv, registerAppliedPolyline, restoreCommittedPolyline, collectPolylineAppliedVoxelIndices, } from './penDraftUtils';
67
- import { registerAppliedShape, updateShapeRegistryErosionState, removeShapeRegistryEntry, clearShapeRegistry, } from './shapeDraftUtils';
66
+ import { applyPenDraft, cancelPenDraft, captureFreehandDraft, fillPolylineDraft, unfillPolylineDraft, polylineDraftFromNv, redrawPolylineDraft, redrawFreehandDraft, syncPolylineDraftToNv, registerAppliedPolyline, collectPolylineAppliedVoxelIndices, } from './penDraftUtils';
67
+ import { registerAppliedShape, updateShapeRegistryErosionState, removeShapeRegistryEntry, clearShapeRegistry, redrawDraftShape, } from './shapeDraftUtils';
68
68
  import { CloudMrNiivuePanel } from './CloudMrNiivuePanel';
69
69
  import { Niivue } from './NiivuePatcher';
70
70
  import NVSwitch from './Switch';
@@ -577,22 +577,23 @@ export default function CloudMrNiivueViewer(props) {
577
577
  function nvUpdateBrushSize(size) {
578
578
  setBrushSize(size);
579
579
  brushSizeRef.current = size;
580
- if (drawShapeToolRef.current === "pen" && drawPen !== 0 && drawPen !== 8) {
580
+ var tool = drawShapeToolRef.current;
581
+ if ((tool === "pen" || tool === "polyline") && drawPen !== 8) {
581
582
  applyNvBrushSize(size);
582
583
  }
583
584
  }
584
585
  function nvUpdateEraserSize(size) {
585
586
  setEraserSize(size);
586
587
  eraserSizeRef.current = size;
587
- if (drawPen === 0 || drawPen === 8) {
588
+ if (drawPen === 8) {
588
589
  applyNvBrushSize(size);
589
590
  }
590
591
  }
591
592
  function nvUpdateDrawPen(a) {
592
- var _a;
593
+ var _a, _b, _c;
593
594
  var raw = Number(a.target.value);
594
595
  setDrawPen(raw);
595
- var isEraser = raw === 0 || raw === 8;
596
+ var isEraser = raw === 8;
596
597
  if (raw === 8) {
597
598
  nv.setPenValue(0, true);
598
599
  }
@@ -609,10 +610,53 @@ export default function CloudMrNiivueViewer(props) {
609
610
  }
610
611
  else if (drawShapeToolRef.current === "pen") {
611
612
  nv.opts.deferFreehandCommit = false;
612
- nv.opts.polylinePenMode = penDrawModeRef.current === "polyline";
613
- nv.opts.isFilledPen = penDrawModeRef.current === "freehand";
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;
614
621
  applyNvBrushSize(brushSizeRef.current);
615
622
  }
623
+ // Immediately repaint any active draft in the new color so the user sees the change.
624
+ if (!isEraser) {
625
+ var colorValue = raw & 7;
626
+ if (shapeDraftRef.current) {
627
+ var next = __assign(__assign({}, shapeDraftRef.current), { penValue: colorValue });
628
+ shapeDraftRef.current = next;
629
+ setShapeDraft(next);
630
+ redrawDraftShape(nv, next);
631
+ }
632
+ else if (penDraftRef.current) {
633
+ var next = __assign(__assign({}, penDraftRef.current), { penValue: colorValue });
634
+ penDraftRef.current = next;
635
+ setPenDraft(next);
636
+ if (next.kind === "polyline") {
637
+ var count = (_c = (_b = next.vertices) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0;
638
+ if (count >= 2) {
639
+ // Redraw segments in the new color, then update the base so the
640
+ // rubber-band preview also uses the new color.
641
+ redrawPolylineDraft(nv, __assign(__assign({}, next), { filled: false }));
642
+ if (nv.drawBitmap)
643
+ nv._cloudMrPolylineBaseBitmap = nv.drawBitmap.slice();
644
+ // Re-apply fill on top when polygon is closeable (base stays segments-only).
645
+ if (count >= 3) {
646
+ nv.drawPenAxCorSag = next.axCorSag;
647
+ nv.drawPenFillPts = next.vertices.map(function (v) { return __spreadArray([], v, true); });
648
+ nv._cloudMrSkipNextUndoBitmap = true;
649
+ nv.drawPenFilled();
650
+ nv.refreshDrawing(false, false);
651
+ nv.drawScene();
652
+ }
653
+ }
654
+ }
655
+ else if (next.kind === "freehand") {
656
+ redrawFreehandDraft(nv, next);
657
+ }
658
+ }
659
+ }
616
660
  }
617
661
  function deactivateDrawTools() {
618
662
  var _a, _b;
@@ -866,40 +910,58 @@ export default function CloudMrNiivueViewer(props) {
866
910
  nv.opts.deferFreehandCommit = false;
867
911
  }
868
912
  function cancelPenDraftHandler() {
869
- var _a;
913
+ var _a, _b;
870
914
  var draft = penDraftRef.current;
871
915
  if (!draft)
872
916
  return;
873
917
  var registryId = draft._registryId;
874
918
  cancelPenDraft(nv, draft);
875
- if (registryId && draft.kind === "polyline") {
876
- restoreCommittedPolyline(nv, registryId);
919
+ if (registryId) {
920
+ // Restore the exact committed voxels from the registry (works for both the
921
+ // legacy polyline-draft case and the new freehand-converted case).
922
+ var entry_1 = (_a = nv._cloudMrPolylineRegistry) === null || _a === void 0 ? void 0 : _a.find(function (e) { return e.id === registryId; });
923
+ if (entry_1) {
924
+ var bitmap_1 = nv.drawBitmap;
925
+ entry_1.voxelIndices.forEach(function (idx) {
926
+ if (idx < bitmap_1.length)
927
+ bitmap_1[idx] = entry_1.penValue;
928
+ });
929
+ nv.refreshDrawing(true, false);
930
+ nv.drawScene();
931
+ }
877
932
  }
878
933
  if (draft.kind === "polyline") {
879
- (_a = nv.cloudMrResetPolyline) === null || _a === void 0 ? void 0 : _a.call(nv);
934
+ (_b = nv.cloudMrResetPolyline) === null || _b === void 0 ? void 0 : _b.call(nv);
880
935
  setPolylineVertexCount(0);
881
936
  }
882
937
  setPenDraft(null);
883
938
  penDraftRef.current = null;
884
939
  nv._cloudMrPenDraftActive = false;
885
- if (drawShapeToolRef.current === "pen") {
940
+ var tool = drawShapeToolRef.current;
941
+ if (tool === "pen" || tool === "polyline") {
886
942
  nvSetDrawingEnabled(true);
887
943
  }
888
944
  }
889
945
  function applyPenDraftHandler(_a) {
890
- var _b, _c, _d;
891
- var _e = _a === void 0 ? {} : _a, _f = _e.keepTool, keepTool = _f === void 0 ? false : _f;
946
+ var _b, _c, _d, _e, _f;
947
+ var _g = _a === void 0 ? {} : _a, _h = _g.keepTool, keepTool = _h === void 0 ? false : _h;
892
948
  var draft = penDraftRef.current;
893
949
  if (!draft)
894
950
  return;
895
951
  if (draft.kind === "polyline" && ((_b = nv._cloudMrPolylineVertices) === null || _b === void 0 ? void 0 : _b.length) >= 2) {
896
- var fresh = polylineDraftFromNv(nv, { filled: !!draft.filled });
952
+ var fresh = polylineDraftFromNv(nv, { filled: ((_c = draft.vertices) === null || _c === void 0 ? void 0 : _c.length) >= 3 });
897
953
  if (fresh) {
898
954
  draft = fresh;
899
955
  penDraftRef.current = draft;
900
956
  }
901
957
  }
902
- draft = (_c = applyPenDraft(nv, draft)) !== null && _c !== void 0 ? _c : draft;
958
+ // Auto-fill polyline on apply fillPolylineDraft updates _cloudMrPolylineBaseBitmap
959
+ // to the filled state so applyPenDraft commits the correct filled bitmap.
960
+ if (draft.kind === "polyline" && ((_d = draft.vertices) === null || _d === void 0 ? void 0 : _d.length) >= 3) {
961
+ draft = fillPolylineDraft(nv, draft);
962
+ penDraftRef.current = draft;
963
+ }
964
+ draft = (_e = applyPenDraft(nv, draft)) !== null && _e !== void 0 ? _e : draft;
903
965
  if (draft.kind === "polyline" && nv._cloudMrPolylineSessionStartBitmap) {
904
966
  draft = __assign(__assign({}, draft), { baseBitmap: new Uint8Array(nv._cloudMrPolylineSessionStartBitmap) });
905
967
  }
@@ -907,17 +969,22 @@ export default function CloudMrNiivueViewer(props) {
907
969
  markPenVoxelKind(draft, draft.kind === "polyline" ? 3 : 2);
908
970
  if (draft.kind === "polyline") {
909
971
  registerAppliedPolyline(nv, draft, draft._registryId);
910
- (_d = nv.cloudMrResetPolyline) === null || _d === void 0 ? void 0 : _d.call(nv);
972
+ (_f = nv.cloudMrResetPolyline) === null || _f === void 0 ? void 0 : _f.call(nv);
911
973
  setPolylineVertexCount(0);
912
974
  }
975
+ else if (draft._registryId && nv._cloudMrPolylineRegistry) {
976
+ // Was a committed polyline reopened as a freehand draft (move-only).
977
+ // Remove the stale registry entry so it is not matched on future clicks.
978
+ nv._cloudMrPolylineRegistry = nv._cloudMrPolylineRegistry.filter(function (e) { return e.id !== draft._registryId; });
979
+ }
913
980
  setPenDraft(null);
914
981
  penDraftRef.current = null;
915
982
  nv._cloudMrPenDraftActive = false;
916
983
  setDrawingChanged(true);
917
984
  if (keepTool) {
918
- var mode = penDrawModeRef.current;
919
- nv.opts.polylinePenMode = mode === "polyline";
920
- nv.opts.isFilledPen = mode === "freehand";
985
+ var tool = drawShapeToolRef.current;
986
+ nv.opts.polylinePenMode = tool === "polyline";
987
+ nv.opts.isFilledPen = tool === "pen";
921
988
  nv.opts.deferFreehandCommit = false;
922
989
  nvSetDrawingEnabled(true);
923
990
  }
@@ -974,11 +1041,10 @@ export default function CloudMrNiivueViewer(props) {
974
1041
  setPenDraft(draft);
975
1042
  penDraftRef.current = draft;
976
1043
  nv._cloudMrPenDraftActive = true;
977
- // Auto-select pen tool so the palette opens
978
- setDrawShapeTool("pen");
979
- var mode = draft.kind === "polyline" ? "polyline" : "freehand";
980
- setPenDrawMode(mode);
981
- penDrawModeRef.current = mode;
1044
+ // Auto-select the correct pen tool so the right palette opens
1045
+ var tool = draft.kind === "polyline" ? "polyline" : "pen";
1046
+ setDrawShapeTool(tool);
1047
+ penDrawModeRef.current = draft.kind === "polyline" ? "polyline" : "freehand";
982
1048
  if (draft.kind === "polyline") {
983
1049
  setPolylineVertexCount((_b = (_a = draft.vertices) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0);
984
1050
  }
@@ -989,13 +1055,18 @@ export default function CloudMrNiivueViewer(props) {
989
1055
  nv.onPolylineChange = function (count) {
990
1056
  var _a, _b;
991
1057
  setPolylineVertexCount(count);
992
- if (penDrawModeRef.current === "polyline" && count >= 2) {
993
- var prev = penDraftRef.current;
994
- var preserveFill = (prev === null || prev === void 0 ? void 0 : prev.kind) === "polyline" &&
995
- prev.filled &&
996
- ((_a = prev.vertices) === null || _a === void 0 ? void 0 : _a.length) === count;
997
- var draft = polylineDraftFromNv(nv, { filled: preserveFill });
1058
+ if (drawShapeToolRef.current === "polyline" && count >= 2) {
1059
+ var draft = polylineDraftFromNv(nv, { filled: count >= 3 });
998
1060
  if (draft) {
1061
+ if (count >= 3) {
1062
+ // Show fill live on canvas after each vertex commit.
1063
+ // Save/restore _cloudMrPolylineBaseBitmap (segments-only) so the
1064
+ // rubber-band preview still works correctly on mouse move.
1065
+ var savedBase = (_a = nv._cloudMrPolylineBaseBitmap) === null || _a === void 0 ? void 0 : _a.slice();
1066
+ redrawPolylineDraft(nv, draft);
1067
+ if (savedBase)
1068
+ nv._cloudMrPolylineBaseBitmap = savedBase;
1069
+ }
999
1070
  setPenDraft(draft);
1000
1071
  penDraftRef.current = draft;
1001
1072
  }
@@ -30,7 +30,6 @@ import {
30
30
  redrawFreehandDraft,
31
31
  redrawPolylineDraft,
32
32
  shouldDeferFreehandCommit,
33
- syncPolylineDraftToNv,
34
33
  } from "./penDraftUtils.js";
35
34
 
36
35
  /*
@@ -1587,12 +1586,9 @@ function cloudMrTryReopenPenDraftOnClick(nv) {
1587
1586
  if (kind !== 3) penKind = 2;
1588
1587
  }
1589
1588
 
1590
- if (draft.kind === "polyline") {
1591
- redrawPolylineDraft(nv, draft);
1592
- syncPolylineDraftToNv(nv, draft);
1593
- } else {
1594
- redrawFreehandDraft(nv, draft);
1595
- }
1589
+ // capturePolylineDraftFromClick always returns a freehand draft now (exact stored
1590
+ // voxels, no vertex state, no re-flood-fill), so the redraw path is uniform.
1591
+ redrawFreehandDraft(nv, draft);
1596
1592
  nv._cloudMrSuppressDrawingChangedMouseUp = true;
1597
1593
  if (typeof nv.onPenDraftReopenReady === "function") {
1598
1594
  nv.onPenDraftReopenReady(draft, penKind);
@@ -24,6 +24,8 @@ import { boundsToFreehandCorners, redrawFreehandDraft, redrawPolylineDraft, sync
24
24
  import { canvasDeltaToVoxDelta, clientToCanvasPos, voxToOverlayPos, voxUnderClient, } from "./shapeDraftUtils";
25
25
  import { useNiivueViewerTheme } from "./NiivueViewerThemeContext";
26
26
  var HANDLE_SIZE = 10;
27
+ /** Extra space (px) added around the polyline bounding box so it's easy to see and click. */
28
+ var POLYLINE_BOX_PADDING = 14;
27
29
  function cloneFreehandDraft(draft) {
28
30
  return __assign(__assign({}, draft), { strokeVoxels: draft.strokeVoxels.map(function (v) { return __spreadArray([], v, true); }), bounds: draft.bounds ? __assign({}, draft.bounds) : draft.bounds });
29
31
  }
@@ -100,7 +102,13 @@ export function PenDraftOverlay(_a) {
100
102
  var height = Math.max.apply(Math, ys) - top;
101
103
  if (width < 2 && height < 2)
102
104
  return null;
103
- return { left: left, top: top, width: Math.max(width, 2), height: Math.max(height, 2) };
105
+ var pad = isPolyline ? POLYLINE_BOX_PADDING : 0;
106
+ return {
107
+ left: left - pad,
108
+ top: top - pad,
109
+ width: Math.max(width, 2) + pad * 2,
110
+ height: Math.max(height, 2) + pad * 2
111
+ };
104
112
  }, [cornerCss, draft === null || draft === void 0 ? void 0 : draft.axCorSag, freehandCorners, isPolyline, nv, overlayKey]);
105
113
  var applyDraft = useCallback(function (nextDraft) {
106
114
  if (nextDraft.kind === "polyline") {
@@ -211,7 +219,6 @@ export function PenDraftOverlay(_a) {
211
219
  background: theme.accentMutedBgLight,
212
220
  boxSizing: "border-box",
213
221
  pointerEvents: "none"
214
- } }), 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" })), isPolyline &&
215
- 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: "grab" }), title: "Move vertex" }, "handle-".concat(i))); })] })));
222
+ } }), 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" }))] })));
216
223
  }
217
224
  export default PenDraftOverlay;
@@ -1,10 +1,9 @@
1
- export default function DrawColorPlatte({ expanded, updateDrawPen, setDrawingEnabled, showPenModes, penDrawMode, onPenDrawModeChange, polylineVertexCount, penDraftActive, penDraftKind, penDraftFilled, onApplyPenDraft, onDeletePenDraft, onFillPenDraft, brushSize, updateBrushSize, shapeDraftActive, onApplyShapeDraft, onDeleteShapeDraft, }: {
1
+ export default function DrawColorPlatte({ expanded, updateDrawPen, setDrawingEnabled, penToolKind, currentPenValue, 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
- showPenModes?: boolean | undefined;
6
- penDrawMode?: string | undefined;
7
- onPenDrawModeChange: any;
5
+ penToolKind?: null | undefined;
6
+ currentPenValue?: number | undefined;
8
7
  polylineVertexCount?: number | undefined;
9
8
  penDraftActive?: boolean | undefined;
10
9
  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
- * Pen palette adds freehand vs polyline mode; pen/shape drafts show Apply while adjusting.
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";
@@ -19,25 +20,38 @@ import CheckIcon from "@mui/icons-material/Check";
19
20
  import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
20
21
  import { BrushSizeSlider } from "./BrushSizeSlider";
21
22
  var FILLED_COLORS = [
22
- { sx: { color: "red" } },
23
- { sx: { color: "green" } },
24
- { sx: { color: "blue" } },
25
- { sx: { color: "yellow" } },
26
- { sx: { color: "cyan" } },
27
- { sx: { color: "#e81ce8" } },
23
+ { sx: { color: "red" }, glow: "rgba(255, 72, 72, 0.55)" },
24
+ { sx: { color: "green" }, glow: "rgba(72, 200, 72, 0.55)" },
25
+ { sx: { color: "blue" }, glow: "rgba(72, 140, 255, 0.55)" },
26
+ { sx: { color: "yellow" }, glow: "rgba(255, 220, 72, 0.5)" },
27
+ { sx: { color: "cyan" }, glow: "rgba(72, 220, 255, 0.55)" },
28
+ { sx: { color: "#e81ce8" }, glow: "rgba(232, 28, 232, 0.5)" },
28
29
  ];
30
+ /** Soft halo that hugs the circular icon shape (drop-shadow, not box-shadow). */
31
+ function swatchGlowFilter(glow, _a) {
32
+ var _b = _a === void 0 ? {} : _a, _c = _b.active, active = _c === void 0 ? false : _c, _d = _b.hover, hover = _d === void 0 ? false : _d;
33
+ if (active) {
34
+ return "drop-shadow(0 0 1px ".concat(glow, ") drop-shadow(0 0 4px ").concat(glow, ") drop-shadow(0 0 8px rgba(255, 255, 255, 0.12))");
35
+ }
36
+ if (hover) {
37
+ return "drop-shadow(0 0 2px rgba(255, 255, 255, 0.28))";
38
+ }
39
+ return "none";
40
+ }
29
41
  var ACTION_FONT_SIZE = "0.75rem";
30
42
  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
43
  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, penDraftKind = _a.penDraftKind, _f = _a.penDraftFilled, penDraftFilled = _f === void 0 ? false : _f, onApplyPenDraft = _a.onApplyPenDraft, onDeletePenDraft = _a.onDeletePenDraft, onFillPenDraft = _a.onFillPenDraft, _g = _a.brushSize, brushSize = _g === void 0 ? 1 : _g, updateBrushSize = _a.updateBrushSize, _h = _a.shapeDraftActive, shapeDraftActive = _h === void 0 ? false : _h, onApplyShapeDraft = _a.onApplyShapeDraft, onDeleteShapeDraft = _a.onDeleteShapeDraft;
44
+ var expanded = _a.expanded, updateDrawPen = _a.updateDrawPen, setDrawingEnabled = _a.setDrawingEnabled,
45
+ /** @type {"freehand" | "polyline" | null} */
46
+ _b = _a.penToolKind,
47
+ /** @type {"freehand" | "polyline" | null} */
48
+ penToolKind = _b === void 0 ? null : _b,
49
+ /** Currently active pen value (1–6); used to highlight the selected swatch. */
50
+ _c = _a.currentPenValue,
51
+ /** Currently active pen value (1–6); used to highlight the selected swatch. */
52
+ currentPenValue = _c === void 0 ? 1 : _c, _d = _a.polylineVertexCount, polylineVertexCount = _d === void 0 ? 0 : _d, _e = _a.penDraftActive, penDraftActive = _e === void 0 ? false : _e, penDraftKind = _a.penDraftKind, _f = _a.penDraftFilled, penDraftFilled = _f === void 0 ? false : _f, onApplyPenDraft = _a.onApplyPenDraft, onDeletePenDraft = _a.onDeletePenDraft, onFillPenDraft = _a.onFillPenDraft, _g = _a.brushSize, brushSize = _g === void 0 ? 1 : _g, updateBrushSize = _a.updateBrushSize, _h = _a.shapeDraftActive, shapeDraftActive = _h === void 0 ? false : _h, onApplyShapeDraft = _a.onApplyShapeDraft, onDeleteShapeDraft = _a.onDeleteShapeDraft;
53
+ var isFreehandTool = penToolKind === "freehand";
54
+ var isPolylineTool = penToolKind === "polyline";
41
55
  return (_jsxs(Stack, __assign({ style: {
42
56
  position: "absolute",
43
57
  top: "100%",
@@ -51,13 +65,37 @@ export default function DrawColorPlatte(_a) {
51
65
  borderTopLeftRadius: "6pt",
52
66
  borderTopRightRadius: "6pt",
53
67
  background: "#333"
54
- }, direction: "column", spacing: 0.5, sx: { py: expanded ? 0.5 : 0 } }, { children: [showPenModes && expanded && (_jsxs(Stack, __assign({ direction: "row", alignItems: "center", spacing: 0.5, sx: { px: 0.75, pt: 0.25 } }, { children: [_jsx(Button, __assign({ size: "small", onClick: function () { return onPenDrawModeChange === null || onPenDrawModeChange === void 0 ? void 0 : onPenDrawModeChange("freehand"); }, sx: modeBtnSx(penDrawMode === "freehand") }, { children: "Freehand" })), _jsx(Button, __assign({ size: "small", onClick: function () { return onPenDrawModeChange === null || onPenDrawModeChange === void 0 ? void 0 : onPenDrawModeChange("polyline"); }, sx: modeBtnSx(penDrawMode === "polyline") }, { children: "Polyline" }))] }))), showPenModes && 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
- updateDrawPen({ target: { value: index + 1 } });
56
- setDrawingEnabled(true);
57
- } }, { children: _jsx(FiberManualRecordIcon, { sx: color.sx }) }), index)); }) })), showPenModes && penDrawMode === "polyline" && 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" }))), showPenModes &&
58
- penDraftActive &&
59
- (penDrawMode === "polyline" || penDraftKind === "freehand") &&
60
- 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: "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: {
68
+ }, 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", sx: { px: 0.25 } }, { children: FILLED_COLORS.map(function (color, index) {
69
+ var penValue = index + 1;
70
+ var isActive = penValue === (currentPenValue & 7);
71
+ return (_jsx(IconButton, __assign({ "aria-label": "Color ".concat(penValue), "aria-pressed": isActive, onClick: function () {
72
+ updateDrawPen({ target: { value: penValue } });
73
+ setDrawingEnabled(true);
74
+ }, sx: {
75
+ p: "6px",
76
+ borderRadius: "50%",
77
+ boxShadow: "none",
78
+ transition: "background-color 0.15s ease",
79
+ "&:hover": {
80
+ backgroundColor: "rgba(255, 255, 255, 0.06)"
81
+ },
82
+ "& .color-swatch": {
83
+ fontSize: "1.2rem",
84
+ display: "block",
85
+ transition: "filter 0.18s ease",
86
+ filter: swatchGlowFilter(color.glow, { active: isActive })
87
+ },
88
+ "&:hover .color-swatch": {
89
+ filter: swatchGlowFilter(color.glow, {
90
+ active: isActive,
91
+ hover: !isActive
92
+ })
93
+ }
94
+ } }, { children: _jsx(FiberManualRecordIcon, { className: "color-swatch", sx: color.sx }) }), index));
95
+ }) })), isPolylineTool && expanded && (_jsx(Typography, __assign({ sx: { px: 1, pb: 0.5, fontSize: "0.68rem", color: "#aaa", userSelect: "none" } }, { children: "Click to add vertices \u00B7 Right-click to fill" }))), penDraftActive &&
96
+ expanded &&
97
+ ((isFreehandTool && penDraftKind === "freehand") ||
98
+ (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
99
  color: "#f44336",
62
100
  fontSize: ACTION_FONT_SIZE,
63
101
  textTransform: "none",
@@ -65,24 +103,15 @@ export default function DrawColorPlatte(_a) {
65
103
  py: 0.25,
66
104
  px: 0.75,
67
105
  "& .MuiButton-startIcon": { mr: 0.5, ml: 0 }
68
- } }, { children: "Delete" })) })), _jsxs(Stack, __assign({ direction: "row", spacing: 1, alignItems: "center" }, { children: [penDrawMode === "polyline" && polylineVertexCount >= 3 && (_jsx(Tooltip, __assign({ title: penDraftFilled
69
- ? "Remove fill (keeps outline editable)"
70
- : "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
- color: penDraftFilled ? "#ffb74d" : "#c9a0e8",
72
- fontSize: ACTION_FONT_SIZE,
73
- textTransform: "none",
74
- minWidth: 0,
75
- py: 0.25,
76
- px: 0.75
77
- } }, { children: penDraftFilled ? "Undo Fill" : "Fill" })) }))), penDrawMode === "polyline" && (_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
- color: "#c9a0e8",
79
- fontSize: ACTION_FONT_SIZE,
80
- textTransform: "none",
81
- minWidth: 0,
82
- py: 0.25,
83
- px: 0.75,
84
- "& .MuiButton-startIcon": { mr: 0.5, ml: 0 }
85
- } }, { 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: {
106
+ } }, { children: "Delete" })) })), isPolylineTool && (_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: {
107
+ color: "#c9a0e8",
108
+ fontSize: ACTION_FONT_SIZE,
109
+ textTransform: "none",
110
+ minWidth: 0,
111
+ py: 0.25,
112
+ px: 0.75,
113
+ "& .MuiButton-startIcon": { mr: 0.5, ml: 0 }
114
+ } }, { 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
115
  color: "#f44336",
87
116
  fontSize: ACTION_FONT_SIZE,
88
117
  textTransform: "none",
@@ -1,7 +1,6 @@
1
- export default function EraserPlatte({ expandEraseOptions, updateDrawPen, setDrawingEnabled, eraserSize, updateEraserSize, }: {
1
+ /** Eraser palette size control only (filled/region eraser is always used). */
2
+ export default function EraserPlatte({ expandEraseOptions, eraserSize, updateEraserSize, }: {
2
3
  expandEraseOptions: any;
3
- updateDrawPen: any;
4
- setDrawingEnabled: any;
5
4
  eraserSize?: number | undefined;
6
5
  updateEraserSize: any;
7
6
  }): import("react/jsx-runtime").JSX.Element;
@@ -9,18 +9,13 @@ var __assign = (this && this.__assign) || function () {
9
9
  };
10
10
  return __assign.apply(this, arguments);
11
11
  };
12
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
- import { Stack, IconButton } from "@mui/material";
14
- import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
15
- import FiberManualRecordOutlinedIcon from "@mui/icons-material/FiberManualRecordOutlined";
12
+ import { jsx as _jsx } from "react/jsx-runtime";
13
+ import { Stack } from "@mui/material";
16
14
  import { BrushSizeSlider } from "./BrushSizeSlider";
15
+ /** Eraser palette — size control only (filled/region eraser is always used). */
17
16
  export default function EraserPlatte(_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
- var eraseOptions = [
20
- _jsx(FiberManualRecordIcon, { style: { color: "white" } }, "e0"),
21
- _jsx(FiberManualRecordOutlinedIcon, { style: { color: "white" } }, "e1"),
22
- ];
23
- return (_jsxs(Stack, __assign({ style: {
17
+ var expandEraseOptions = _a.expandEraseOptions, _b = _a.eraserSize, eraserSize = _b === void 0 ? 1 : _b, updateEraserSize = _a.updateEraserSize;
18
+ return (_jsx(Stack, __assign({ style: {
24
19
  position: "absolute",
25
20
  top: "100%",
26
21
  left: 0,
@@ -33,8 +28,5 @@ export default function EraserPlatte(_a) {
33
28
  borderTopRightRadius: "6pt",
34
29
  background: "#333",
35
30
  width: 150
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
- updateDrawPen({ target: { value: index === 0 ? 8 : 0 } });
38
- setDrawingEnabled(true);
39
- } }, { children: value }), index)); }) }))] })));
31
+ }, direction: "column" }, { children: updateEraserSize && (_jsx(BrushSizeSlider, { label: "Eraser size", brushSize: eraserSize, updateBrushSize: updateEraserSize })) })));
40
32
  }
@@ -20,9 +20,9 @@ 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 PolylineIcon from "@mui/icons-material/Polyline";
23
24
  import CropSquareOutlinedIcon from "@mui/icons-material/CropSquareOutlined";
24
25
  import CircleOutlinedIcon from "@mui/icons-material/CircleOutlined";
25
- import AutoFixNormalOutlinedIcon from "@mui/icons-material/AutoFixNormalOutlined";
26
26
  import ReplyIcon from "@mui/icons-material/Reply";
27
27
  import CameraAltIcon from "@mui/icons-material/CameraAlt";
28
28
  import SvgIcon from "@mui/material/SvgIcon";
@@ -42,6 +42,14 @@ function clickTargetIsNiivueCanvas(target) {
42
42
  return false;
43
43
  return !!target.closest("#niiCanvas");
44
44
  }
45
+ /** Click-away scoped to one tool button + its dropdown palette. */
46
+ function ToolClickAway(_a) {
47
+ var active = _a.active, onClickAway = _a.onClickAway, sx = _a.sx, children = _a.children;
48
+ var box = _jsx(Box, __assign({ sx: sx }, { children: children }));
49
+ if (!active)
50
+ return box;
51
+ return _jsx(ClickAwayListener, __assign({ onClickAway: onClickAway }, { children: box }));
52
+ }
45
53
  /** Same icon tone as typical MUI on-paper controls (matches slice panel body). */
46
54
  var ICON_COLOR = "#212121";
47
55
  function EraserIcon(props) {
@@ -71,11 +79,12 @@ export function MroDrawToolkit(props) {
71
79
  var _b = useState("n"), expandedOption = _b[0], setExpandedOption = _b[1];
72
80
  var _c = useState(false), expandOpacityOptions = _c[0], setExpandOpacityOptions = _c[1];
73
81
  var _d = useState(undefined), setMaskColor = _d[1];
74
- var filled = props.drawPen > 7;
75
82
  useEffect(function () {
76
- // Close palette when tool is deactivated programmatically (e.g. after Apply)
83
+ var eraserSelected = props.drawPen === 8;
84
+ // Close palette when tool is deactivated programmatically (e.g. after Apply).
85
+ // Keep eraser palette open when switching from shape/pen edit → eraser in one click.
77
86
  if (drawShapeTool === null && !props.shapeDraftActive && !props.penDraftActive) {
78
- setExpandedOption("n");
87
+ setExpandedOption(eraserSelected ? "e" : "n");
79
88
  return;
80
89
  }
81
90
  if (!props.shapeDraftActive && !props.penDraftActive)
@@ -86,14 +95,20 @@ export function MroDrawToolkit(props) {
86
95
  setExpandedOption("l");
87
96
  else if (drawShapeTool === "pen")
88
97
  setExpandedOption("d");
89
- }, [props.shapeDraftActive, props.penDraftActive, drawShapeTool]);
98
+ else if (drawShapeTool === "polyline")
99
+ setExpandedOption("p");
100
+ }, [props.shapeDraftActive, props.penDraftActive, drawShapeTool, props.drawPen]);
90
101
  var shapeSelectedSx = function (shape) {
102
+ if (shape === "pen")
103
+ return (drawShapeTool === "pen") ? theme.selectedToolSx : {};
104
+ if (shape === "polyline")
105
+ return (drawShapeTool === "polyline") ? theme.selectedToolSx : {};
91
106
  return drawShapeTool === shape ? theme.selectedToolSx : {};
92
107
  };
93
108
  var eraserActive = expandedOption === "e";
94
109
  function leaveEraserIfActive() {
95
110
  var _a;
96
- if (!eraserActive && props.drawPen !== 0 && props.drawPen !== 8) {
111
+ if (!eraserActive && props.drawPen !== 8) {
97
112
  return;
98
113
  }
99
114
  (_a = props.onDeactivateDrawTools) === null || _a === void 0 ? void 0 : _a.call(props);
@@ -112,6 +127,19 @@ export function MroDrawToolkit(props) {
112
127
  props.setDrawingEnabled(true);
113
128
  }
114
129
  }
130
+ function clickPolyline() {
131
+ leaveEraserIfActive();
132
+ onDrawShapeToolChange === null || onDrawShapeToolChange === void 0 ? void 0 : onDrawShapeToolChange("polyline");
133
+ if (expandedOption === "p") {
134
+ setExpandedOption("n");
135
+ props.setDrawingEnabled(false);
136
+ }
137
+ else {
138
+ setExpandedOption("p");
139
+ setExpandOpacityOptions(false);
140
+ props.setDrawingEnabled(true);
141
+ }
142
+ }
115
143
  function clickRectangle() {
116
144
  leaveEraserIfActive();
117
145
  onDrawShapeToolChange === null || onDrawShapeToolChange === void 0 ? void 0 : onDrawShapeToolChange("rectangle");
@@ -165,43 +193,52 @@ export function MroDrawToolkit(props) {
165
193
  "&:hover": { backgroundColor: "rgba(0,0,0,0.04)" },
166
194
  "&.Mui-disabled": { color: "rgba(0,0,0,0.26)" }
167
195
  };
168
- return (_jsx(ClickAwayListener, __assign({ onClickAway: function (event) {
169
- var _a;
170
- if (clickTargetIsNiivueCanvas(event.target))
171
- return;
172
- if (props.shapeDraftActive || props.penDraftActive)
173
- return;
174
- setExpandedOption("n");
175
- setExpandOpacityOptions(false);
176
- (_a = props.onDeactivateDrawTools) === null || _a === void 0 ? void 0 : _a.call(props);
177
- } }, { children: _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: {
178
- mb: 2,
179
- borderTopLeftRadius: 0,
180
- borderTopRightRadius: 0,
181
- overflow: "visible"
182
- } }, { children: _jsx(CardContent, __assign({ sx: {
183
- overflow: "visible",
184
- "&:last-child": { paddingBottom: 2 }
185
- } }, { children: _jsx("div", __assign({ style: { display: "flex", flexDirection: "column", overflow: "visible" } }, { children: _jsxs("div", __assign({ style: {
186
- display: "flex",
187
- flexDirection: "row",
188
- flexWrap: "wrap",
189
- alignItems: "center",
190
- gap: 4,
191
- overflow: "visible"
192
- } }, { children: [_jsxs(Box, __assign({ sx: { position: "relative", zIndex: expandedOption === "d" ? 1600 : "auto", display: "inline-flex", alignItems: "center" } }, { children: [_jsx(Tooltip, __assign({ title: "Pen" }, { children: _jsx(IconButton, __assign({ "aria-label": "pen", 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, showPenModes: true, penDrawMode: props.penDrawMode, onPenDrawModeChange: props.onPenDrawModeChange, polylineVertexCount: props.polylineVertexCount, onCancelPolyline: props.onCancelPolyline, 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(Box, __assign({ sx: { position: "relative", zIndex: expandedOption === "r" ? 1600 : "auto", display: "inline-flex", alignItems: "center" } }, { 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(Box, __assign({ sx: { position: "relative", zIndex: expandedOption === "l" ? 1600 : "auto", display: "inline-flex", alignItems: "center" } }, { 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(Box, __assign({ sx: { position: "relative", zIndex: expandedOption === "e" ? 1600 : "auto", display: "inline-flex", alignItems: "center" } }, { 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 () {
193
- var _a;
194
- (_a = props.onClearDrawing) === null || _a === void 0 ? void 0 : _a.call(props);
195
- setExpandedOption("n");
196
- setExpandOpacityOptions(false);
197
- }, 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: {
198
- position: "relative",
199
- zIndex: expandOpacityOptions ? 1600 : "auto",
200
- display: "inline-flex",
201
- alignItems: "center",
202
- color: ICON_COLOR
203
- } }, { 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 () {
204
- props.resampleImage();
205
- props.setDrawingChanged(true);
206
- }, expanded: expandedOption === "m", nv: props.nv, setMaskColor: setMaskColor, unfocus: function () { return setExpandedOption("n"); } })] }))] })) })) })) }))] })) })));
196
+ function shouldIgnoreToolClickAway(event) {
197
+ if (clickTargetIsNiivueCanvas(event.target))
198
+ return true;
199
+ if (props.shapeDraftActive || props.penDraftActive)
200
+ return true;
201
+ return false;
202
+ }
203
+ function handleDrawToolClickAway(event) {
204
+ var _a;
205
+ if (shouldIgnoreToolClickAway(event))
206
+ return;
207
+ if (expandedOption === "n" || expandedOption === "m")
208
+ return;
209
+ setExpandedOption("n");
210
+ setExpandOpacityOptions(false);
211
+ (_a = props.onDeactivateDrawTools) === null || _a === void 0 ? void 0 : _a.call(props);
212
+ }
213
+ var toolGroupSx = { position: "relative", display: "inline-flex", alignItems: "center" };
214
+ 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: {
215
+ mb: 2,
216
+ borderTopLeftRadius: 0,
217
+ borderTopRightRadius: 0,
218
+ overflow: "visible"
219
+ } }, { children: _jsx(CardContent, __assign({ sx: {
220
+ overflow: "visible",
221
+ "&:last-child": { paddingBottom: 2 }
222
+ } }, { children: _jsx("div", __assign({ style: { display: "flex", flexDirection: "column", overflow: "visible" } }, { children: _jsxs("div", __assign({ style: {
223
+ display: "flex",
224
+ flexDirection: "row",
225
+ flexWrap: "wrap",
226
+ alignItems: "center",
227
+ gap: 4,
228
+ overflow: "visible"
229
+ } }, { 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", currentPenValue: props.drawPen, 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(PolylineIcon, { sx: { color: "inherit" } }) })) })), _jsx(DrawColorPlatte, { expanded: expandedOption === "p", updateDrawPen: props.updateDrawPen, setDrawingEnabled: props.setDrawingEnabled, penToolKind: "polyline", currentPenValue: props.drawPen, 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, currentPenValue: props.drawPen, 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, currentPenValue: props.drawPen, 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: _jsx(EraserIcon, {}) })) })), _jsx(EraserPlatte, { expandEraseOptions: expandedOption === "e", 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 () {
230
+ var _a;
231
+ (_a = props.onClearDrawing) === null || _a === void 0 ? void 0 : _a.call(props);
232
+ setExpandedOption("n");
233
+ setExpandOpacityOptions(false);
234
+ }, 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: {
235
+ position: "relative",
236
+ zIndex: expandOpacityOptions ? 1600 : "auto",
237
+ display: "inline-flex",
238
+ alignItems: "center",
239
+ color: ICON_COLOR
240
+ } }, { children: [_jsx(Tooltip, __assign({ title: "ROI 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 () {
241
+ props.resampleImage();
242
+ props.setDrawingChanged(true);
243
+ }, expanded: expandedOption === "m", nv: props.nv, setMaskColor: setMaskColor, unfocus: function () { return setExpandedOption("n"); } })] }))] })) })) })) }))] })));
207
244
  }
@@ -48,16 +48,32 @@ export function fillPolylineDraft(nv: any, draft: any): any;
48
48
  export function unfillPolylineDraft(nv: any, draft: any): any;
49
49
  export function applyPenDraft(nv: any, draft: any): any;
50
50
  /**
51
- * Reconstruct a polyline PenDraft from stored vertices (not flood-fill).
52
- * Returns null if the click didn't land on a registered polyline.
51
+ * Reconstruct a PenDraft from a registered polyline entry, returned as a
52
+ * **freehand** draft so it follows the same simple move-only edit flow.
53
+ *
54
+ * Using the exact stored voxel set instead of re-running drawPenFilled avoids
55
+ * two bugs:
56
+ * 1. Re-running the flood-fill on complex/self-intersecting shapes can produce
57
+ * a different pixel set (winding-rule differences, sub-voxel rounding).
58
+ * 2. syncPolylineDraftToNv was planting old vertices back into
59
+ * _cloudMrPolylineVertices; a React re-render could then re-enable
60
+ * polylinePenMode and the next canvas click would append to the stale
61
+ * vertices, visually "recreating" the old shape.
53
62
  */
54
63
  export function capturePolylineDraftFromClick(nv: any): {
55
64
  kind: string;
56
65
  baseBitmap: Uint8Array;
57
66
  axCorSag: any;
58
67
  penValue: any;
59
- vertices: any;
60
- filled: any;
68
+ strokeVoxels: any[];
69
+ bounds: {
70
+ x1: number;
71
+ y1: number;
72
+ z1: number;
73
+ x2: number;
74
+ y2: number;
75
+ z2: number;
76
+ };
61
77
  _registryId: any;
62
78
  } | null;
63
79
  /**
@@ -420,22 +420,57 @@ export function applyPenDraft(nv, draft) {
420
420
  return draft;
421
421
  }
422
422
  /**
423
- * Reconstruct a polyline PenDraft from stored vertices (not flood-fill).
424
- * Returns null if the click didn't land on a registered polyline.
423
+ * Reconstruct a PenDraft from a registered polyline entry, returned as a
424
+ * **freehand** draft so it follows the same simple move-only edit flow.
425
+ *
426
+ * Using the exact stored voxel set instead of re-running drawPenFilled avoids
427
+ * two bugs:
428
+ * 1. Re-running the flood-fill on complex/self-intersecting shapes can produce
429
+ * a different pixel set (winding-rule differences, sub-voxel rounding).
430
+ * 2. syncPolylineDraftToNv was planting old vertices back into
431
+ * _cloudMrPolylineVertices; a React re-render could then re-enable
432
+ * polylinePenMode and the next canvas click would append to the stale
433
+ * vertices, visually "recreating" the old shape.
425
434
  */
426
435
  export function capturePolylineDraftFromClick(nv) {
436
+ var _a;
427
437
  var seedVox = voxFromMouse(nv);
428
438
  var entry = findPolylineRegistryEntry(nv, seedVox);
429
- if (!entry)
439
+ if (!entry || !((_a = nv.back) === null || _a === void 0 ? void 0 : _a.dims))
430
440
  return null;
441
+ var dx = nv.back.dims[1];
442
+ var dy = nv.back.dims[2];
443
+ // Decode stored linear indices back to [x, y, z] triples.
444
+ var strokeVoxels = [];
445
+ var x1 = Infinity, y1 = Infinity, z1 = Infinity;
446
+ var x2 = -Infinity, y2 = -Infinity, z2 = -Infinity;
447
+ entry.voxelIndices.forEach(function (idx) {
448
+ var z = Math.floor(idx / (dx * dy));
449
+ var rem = idx - z * dx * dy;
450
+ var y = Math.floor(rem / dx);
451
+ var x = rem % dx;
452
+ strokeVoxels.push([x, y, z]);
453
+ if (x < x1)
454
+ x1 = x;
455
+ if (x > x2)
456
+ x2 = x;
457
+ if (y < y1)
458
+ y1 = y;
459
+ if (y > y2)
460
+ y2 = y;
461
+ if (z < z1)
462
+ z1 = z;
463
+ if (z > z2)
464
+ z2 = z;
465
+ });
431
466
  var baseBitmap = eraseClusterFromBitmap(nv.drawBitmap, entry.voxelIndices);
432
467
  return {
433
- kind: "polyline",
468
+ kind: "freehand",
434
469
  baseBitmap: baseBitmap,
435
470
  axCorSag: entry.axCorSag,
436
471
  penValue: entry.penValue,
437
- vertices: entry.vertices.map(function (v) { return __spreadArray([], v, true); }),
438
- filled: entry.filled,
472
+ strokeVoxels: strokeVoxels,
473
+ bounds: { x1: x1, y1: y1, z1: z1, x2: x2, y2: y2, z2: z2 },
439
474
  _registryId: entry.id
440
475
  };
441
476
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudmr-ux",
3
- "version": "4.8.2",
3
+ "version": "4.8.4",
4
4
  "author": "erosmontin@gmail.com",
5
5
  "license": "MIT",
6
6
  "repository": "erosmontin/cloudmr-ux",