cloudmr-ux 4.8.0 → 4.8.3

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