cloudmr-ux 4.6.6 → 4.6.8

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.
@@ -51,6 +51,7 @@ export interface CloudMrNiivuePanelProps {
51
51
  shapeDraft: import("./shapeDraftUtils").ShapeDraft | null;
52
52
  onShapeDraftChange: (draft: import("./shapeDraftUtils").ShapeDraft) => void;
53
53
  onApplyShapeDraft: () => void;
54
+ onApplyShapeDraftKeepTool?: () => void;
54
55
  onCancelShapeDraft: () => void;
55
56
  penDraft: {
56
57
  kind: "polyline" | "freehand";
@@ -71,6 +72,7 @@ export interface CloudMrNiivuePanelProps {
71
72
  } | null;
72
73
  onPenDraftChange: (draft: NonNullable<CloudMrNiivuePanelProps["penDraft"]>) => void;
73
74
  onApplyPenDraft: () => void;
75
+ onApplyPenDraftKeepTool?: () => void;
74
76
  onCancelPenDraft: () => void;
75
77
  }
76
78
  export declare function CloudMrNiivuePanel(props: CloudMrNiivuePanelProps): import("react/jsx-runtime").JSX.Element;
@@ -107,15 +107,16 @@ export function CloudMrNiivuePanel(props) {
107
107
  props.resampleImage();
108
108
  }, [histogram]);
109
109
  function applyDrawShapeTool(tool) {
110
- var _a, _b;
110
+ var _a, _b, _c, _d;
111
111
  if (props.shapeDraft) {
112
- props.onCancelShapeDraft();
112
+ // Apply (commit) the current draft rather than discarding it
113
+ (_a = props.onApplyShapeDraftKeepTool) === null || _a === void 0 ? void 0 : _a.call(props);
113
114
  }
114
115
  if (props.penDraft) {
115
- props.onCancelPenDraft();
116
+ (_b = props.onApplyPenDraftKeepTool) === null || _b === void 0 ? void 0 : _b.call(props);
116
117
  }
117
118
  if (tool !== "pen") {
118
- (_b = (_a = props.drawToolkitProps).onPenDrawModeChange) === null || _b === void 0 ? void 0 : _b.call(_a, "freehand");
119
+ (_d = (_c = props.drawToolkitProps).onPenDrawModeChange) === null || _d === void 0 ? void 0 : _d.call(_c, "freehand");
119
120
  }
120
121
  props.setDrawShapeTool(tool);
121
122
  var nv = props.nv;
@@ -849,22 +849,28 @@ export default function CloudMrNiivueViewer(props) {
849
849
  nvSetDrawingEnabled(true);
850
850
  }
851
851
  }
852
- function applyPenDraftHandler() {
853
- var _a;
852
+ function applyPenDraftHandler(_a) {
853
+ var _b;
854
+ var _c = _a === void 0 ? {} : _a, _d = _c.keepTool, keepTool = _d === void 0 ? false : _d;
854
855
  var draft = penDraftRef.current;
855
856
  if (!draft)
856
857
  return;
857
858
  applyPenDraft(nv, draft);
859
+ markPenVoxelKind(draft, draft.kind === "polyline" ? 3 : 2);
858
860
  if (draft.kind === "polyline") {
859
- (_a = nv.cloudMrResetPolyline) === null || _a === void 0 ? void 0 : _a.call(nv);
861
+ (_b = nv.cloudMrResetPolyline) === null || _b === void 0 ? void 0 : _b.call(nv);
860
862
  setPolylineVertexCount(0);
861
863
  }
862
864
  setPenDraft(null);
863
865
  penDraftRef.current = null;
864
866
  nv._cloudMrPenDraftActive = false;
865
867
  setDrawingChanged(true);
866
- if (drawShapeToolRef.current === "pen") {
867
- nvSetDrawingEnabled(true);
868
+ if (!keepTool) {
869
+ // Deactivate tool entirely — palette closes, user re-enters edit by clicking the ROI
870
+ setDrawShapeTool(null);
871
+ nv.opts.deferFreehandCommit = false;
872
+ nv.opts.polylinePenMode = false;
873
+ nvSetDrawingEnabled(false);
868
874
  }
869
875
  resampleImage();
870
876
  }
@@ -891,6 +897,21 @@ export default function CloudMrNiivueViewer(props) {
891
897
  nv._cloudMrPenDraftActive = true;
892
898
  nvSetDrawingEnabled(false);
893
899
  };
900
+ // Called by NiivuePatcher when the user clicks an applied pen ROI to re-edit it.
901
+ // penKind: 2=freehand, 3=polyline
902
+ nv.onPenDraftReopenReady = function (draft, penKind) {
903
+ setPenDraft(draft);
904
+ penDraftRef.current = draft;
905
+ nv._cloudMrPenDraftActive = true;
906
+ // Auto-select pen tool so the palette opens
907
+ setDrawShapeTool("pen");
908
+ var mode = penKind === 3 ? "polyline" : "freehand";
909
+ setPenDrawMode(mode);
910
+ penDrawModeRef.current = mode;
911
+ nv.opts.deferFreehandCommit = false;
912
+ nv.opts.polylinePenMode = false;
913
+ nvSetDrawingEnabled(false);
914
+ };
894
915
  nv.onPolylineChange = function (count) {
895
916
  var _a, _b;
896
917
  setPolylineVertexCount(count);
@@ -924,16 +945,64 @@ export default function CloudMrNiivueViewer(props) {
924
945
  nvSetDrawingEnabled(true);
925
946
  }
926
947
  }
927
- function applyShapeDraft() {
928
- if (!shapeDraftRef.current)
948
+ function ensureToolKindBitmap() {
949
+ if (!nv.drawBitmap)
950
+ return;
951
+ if (!nv._cloudMrToolKindBitmap || nv._cloudMrToolKindBitmap.length !== nv.drawBitmap.length) {
952
+ nv._cloudMrToolKindBitmap = new Uint8Array(nv.drawBitmap.length);
953
+ }
954
+ }
955
+ function markShapeVoxelKind(draft) {
956
+ if (!nv.drawBitmap || !(draft === null || draft === void 0 ? void 0 : draft.baseBitmap))
957
+ return;
958
+ ensureToolKindBitmap();
959
+ var tkb = nv._cloudMrToolKindBitmap;
960
+ for (var i = 0; i < nv.drawBitmap.length; i++) {
961
+ if (nv.drawBitmap[i] === draft.penValue && draft.baseBitmap[i] !== draft.penValue) {
962
+ tkb[i] = 1; // shape
963
+ }
964
+ }
965
+ }
966
+ function markPenVoxelKind(draft, kind) {
967
+ var _a;
968
+ if (!nv.drawBitmap)
969
+ return;
970
+ ensureToolKindBitmap();
971
+ var tkb = nv._cloudMrToolKindBitmap;
972
+ var dims = (_a = nv.back) === null || _a === void 0 ? void 0 : _a.dims;
973
+ if (draft.kind === "freehand" && draft.strokeVoxels && dims) {
974
+ var dx = dims[1];
975
+ var dy = dims[2];
976
+ for (var _i = 0, _b = draft.strokeVoxels; _i < _b.length; _i++) {
977
+ var _c = _b[_i], x = _c[0], y = _c[1], z = _c[2];
978
+ tkb[x + y * dx + z * dx * dy] = kind;
979
+ }
980
+ }
981
+ else if (draft.baseBitmap) {
982
+ for (var i = 0; i < nv.drawBitmap.length; i++) {
983
+ if (nv.drawBitmap[i] === draft.penValue && draft.baseBitmap[i] !== draft.penValue) {
984
+ tkb[i] = kind;
985
+ }
986
+ }
987
+ }
988
+ }
989
+ function applyShapeDraft(_a) {
990
+ var _b = _a === void 0 ? {} : _a, _c = _b.keepTool, keepTool = _c === void 0 ? false : _c;
991
+ var draft = shapeDraftRef.current;
992
+ if (!draft)
929
993
  return;
930
994
  nv.drawAddUndoBitmap(nv.drawFillOverwrites);
995
+ markShapeVoxelKind(draft);
931
996
  setDrawingChanged(true);
932
997
  setShapeDraft(null);
933
998
  shapeDraftRef.current = null;
934
999
  nv._cloudMrShapeDraftActive = false;
935
- if (drawShapeToolRef.current) {
936
- nvSetDrawingEnabled(true);
1000
+ if (!keepTool) {
1001
+ // Deactivate tool entirely — palette closes, user re-enters edit by clicking the ROI
1002
+ setDrawShapeTool(null);
1003
+ nv.opts.deferShapeCommit = false;
1004
+ nv.opts.penType = NI_PEN_TYPE.PEN;
1005
+ nvSetDrawingEnabled(false);
937
1006
  }
938
1007
  resampleImage();
939
1008
  }
@@ -945,6 +1014,11 @@ export default function CloudMrNiivueViewer(props) {
945
1014
  setShapeDraft(draft);
946
1015
  shapeDraftRef.current = draft;
947
1016
  nv._cloudMrShapeDraftActive = true;
1017
+ // Auto-select the correct tool so the palette opens on re-entry via ROI click
1018
+ var tool = draft.penType === NI_PEN_TYPE.ELLIPSE ? "ellipse" : "rectangle";
1019
+ setDrawShapeTool(tool);
1020
+ nv.opts.penType = draft.penType;
1021
+ nv.opts.deferShapeCommit = true;
948
1022
  nvSetDrawingEnabled(false);
949
1023
  };
950
1024
  nv.onApplyActiveDraft = function () {
@@ -1536,7 +1610,7 @@ export default function CloudMrNiivueViewer(props) {
1536
1610
  props.rois[selectedROI].filename : undefined) }), props.niis[selectedVolume] != undefined && _jsx(CloudMrNiivuePanel, { nv: nv, transformFactors: transformFactors, decimalPrecision: decimalPrecision, locationData: locationData, locationTableVisible: locationTableVisible, pipelineID: props.pipelineID, resampleImage: resampleImage, rois: rois, drawToolkitProps: drawToolkitProps, drawShapeTool: drawShapeTool, setDrawShapeTool: setDrawShapeTool, mins: boundMins, maxs: boundMaxs, mms: mms, min: min, max: max, setMin: setMin, setMax: setMax, unzipAndRenderROI: unpackROI, zipAndSendROI: zipAndSendDrawingLayer, setLabelAlias: setLabelAlias, onAfterRoiUpload: function () {
1537
1611
  var _a;
1538
1612
  void ((_a = props.refreshPipelineRois) === null || _a === void 0 ? void 0 : _a.call(props));
1539
- }, gamma: gamma, gammaKey: gammaKey, setGamma: setGamma, shapeDraft: shapeDraft, onShapeDraftChange: onShapeDraftChange, onApplyShapeDraft: applyShapeDraft, onCancelShapeDraft: cancelShapeDraft, penDraft: penDraft, onPenDraftChange: onPenDraftChange, onApplyPenDraft: applyPenDraftHandler, onCancelPenDraft: cancelPenDraftHandler }, "".concat(selectedVolume))] })));
1613
+ }, gamma: gamma, gammaKey: gammaKey, setGamma: setGamma, shapeDraft: shapeDraft, onShapeDraftChange: onShapeDraftChange, onApplyShapeDraft: function () { return applyShapeDraft(); }, onApplyShapeDraftKeepTool: function () { return applyShapeDraft({ keepTool: true }); }, onCancelShapeDraft: cancelShapeDraft, penDraft: penDraft, onPenDraftChange: onPenDraftChange, onApplyPenDraft: function () { return applyPenDraftHandler(); }, onApplyPenDraftKeepTool: function () { return applyPenDraftHandler({ keepTool: true }); }, onCancelPenDraft: cancelPenDraftHandler }, "".concat(selectedVolume))] })));
1540
1614
  }
1541
1615
  function niiToVolume(nii) {
1542
1616
  return {
@@ -19,9 +19,12 @@ import {
19
19
  isPolylinePenActive,
20
20
  previewPolylineSegment,
21
21
  resetPolylineState,
22
+ voxFromMouse,
22
23
  } from "./polylinePenUtils.js";
23
24
  import {
24
25
  captureFreehandDraft,
26
+ capturePenDraftFromClick,
27
+ redrawFreehandDraft,
25
28
  shouldDeferFreehandCommit,
26
29
  } from "./penDraftUtils.js";
27
30
 
@@ -1486,21 +1489,30 @@ function cloudMrTryApplyDraftOnRightClick(nv, event) {
1486
1489
  return true;
1487
1490
  }
1488
1491
 
1492
+ /** Returns the tool-kind code stored for the voxel under the mouse (0=unknown,1=shape,2=pen freehand,3=pen polyline). */
1493
+ function clickedVoxelToolKind(nv) {
1494
+ const bitmap = nv._cloudMrToolKindBitmap;
1495
+ if (!bitmap || !nv.back?.dims) return 0;
1496
+ const seedVox = voxFromMouse(nv);
1497
+ if (!seedVox) return 0;
1498
+ const dims = nv.back.dims;
1499
+ const dx = dims[1];
1500
+ const dy = dims[2];
1501
+ const idx = seedVox[0] + seedVox[1] * dx + seedVox[2] * dx * dy;
1502
+ return bitmap[idx] || 0;
1503
+ }
1504
+
1489
1505
  /**
1490
- * Re-enter rectangle/ellipse edit mode when clicking an existing applied ROI.
1506
+ * Re-enter rectangle/ellipse edit mode when clicking an existing applied shape ROI.
1507
+ * Skips voxels that were drawn with the pen tool (those are handled by pen reopen).
1491
1508
  */
1492
1509
  function cloudMrTryReopenShapeDraftOnClick(nv) {
1493
- const penType = nv.opts.penType;
1494
- if (
1495
- !nv.opts.deferShapeCommit ||
1496
- !nv.opts.drawingEnabled ||
1497
- nv._cloudMrShapeDraftActive ||
1498
- nv._cloudMrPenDraftActive ||
1499
- !isClickWithoutDrag(nv.uiData) ||
1500
- (penType !== NI_PEN_TYPE.RECTANGLE && penType !== NI_PEN_TYPE.ELLIPSE)
1501
- ) {
1502
- return false;
1503
- }
1510
+ if (nv._cloudMrShapeDraftActive || nv._cloudMrPenDraftActive) return false;
1511
+ if (!isClickWithoutDrag(nv.uiData)) return false;
1512
+
1513
+ // If we know this voxel was drawn with the pen, skip shape reopen
1514
+ const kind = clickedVoxelToolKind(nv);
1515
+ if (kind === 2 || kind === 3) return false;
1504
1516
 
1505
1517
  const reopenDraft = captureShapeDraftFromClick(nv);
1506
1518
  if (!reopenDraft) return false;
@@ -1513,6 +1525,33 @@ function cloudMrTryReopenShapeDraftOnClick(nv) {
1513
1525
  return true;
1514
1526
  }
1515
1527
 
1528
+ /**
1529
+ * Re-enter pen (freehand) edit mode when clicking an existing applied pen ROI.
1530
+ * Skips voxels that were drawn with a shape tool.
1531
+ */
1532
+ function cloudMrTryReopenPenDraftOnClick(nv) {
1533
+ if (nv._cloudMrShapeDraftActive || nv._cloudMrPenDraftActive) return false;
1534
+ if (!isClickWithoutDrag(nv.uiData)) return false;
1535
+
1536
+ // Only handle pen voxels — skip if this was drawn with a shape tool
1537
+ const kind = clickedVoxelToolKind(nv);
1538
+ if (kind === 1) return false;
1539
+
1540
+ const draft = capturePenDraftFromClick(nv);
1541
+ if (!draft) return false;
1542
+
1543
+ // Look up pen sub-mode (freehand=2 or polyline=3) from the kind bitmap
1544
+ let penKind = kind; // 2 or 3; if 0 (unknown) default to freehand (2)
1545
+ if (penKind !== 3) penKind = 2;
1546
+
1547
+ redrawFreehandDraft(nv, draft);
1548
+ nv._cloudMrSuppressDrawingChangedMouseUp = true;
1549
+ if (typeof nv.onPenDraftReopenReady === "function") {
1550
+ nv.onPenDraftReopenReady(draft, penKind);
1551
+ }
1552
+ return true;
1553
+ }
1554
+
1516
1555
  const _mouseDownListener = Niivue.prototype.mouseDownListener;
1517
1556
  Niivue.prototype.mouseDownListener = function cloudMrMouseDownListener(e) {
1518
1557
  if (e.button === RIGHT_MOUSE_BUTTON && cloudMrTryApplyDraftOnRightClick(this, e)) {
@@ -1586,14 +1625,18 @@ Niivue.prototype.mouseUpListener = function cloudMrMouseUpListener() {
1586
1625
  }
1587
1626
 
1588
1627
  if (!pendingDraft?.baseBitmap) {
1589
- cloudMrTryReopenShapeDraftOnClick(this);
1628
+ if (!cloudMrTryReopenShapeDraftOnClick(this)) {
1629
+ cloudMrTryReopenPenDraftOnClick(this);
1630
+ }
1590
1631
  return;
1591
1632
  }
1592
1633
  if (isDraftTooSmall(pendingDraft.ptA, pendingDraft.ptB)) {
1593
1634
  this.drawBitmap.set(pendingDraft.baseBitmap);
1594
1635
  this.refreshDrawing(true, false);
1595
1636
  this.drawScene();
1596
- cloudMrTryReopenShapeDraftOnClick(this);
1637
+ if (!cloudMrTryReopenShapeDraftOnClick(this)) {
1638
+ cloudMrTryReopenPenDraftOnClick(this);
1639
+ }
1597
1640
  return;
1598
1641
  }
1599
1642
  this._cloudMrSuppressDrawingChangedMouseUp = true;
@@ -71,6 +71,11 @@ export function MroDrawToolkit(props) {
71
71
  var _d = useState(undefined), setMaskColor = _d[1];
72
72
  var filled = props.drawPen > 7;
73
73
  useEffect(function () {
74
+ // Close palette when tool is deactivated programmatically (e.g. after Apply)
75
+ if (drawShapeTool === null && !props.shapeDraftActive && !props.penDraftActive) {
76
+ setExpandedOption("n");
77
+ return;
78
+ }
74
79
  if (!props.shapeDraftActive && !props.penDraftActive)
75
80
  return;
76
81
  if (drawShapeTool === "rectangle")
@@ -49,6 +49,18 @@ export function polylineDraftFromNv(nv: any, { filled }?: {
49
49
  /** Fill polyline interior without closing the outline or committing the draft. */
50
50
  export function fillPolylineDraft(nv: any, draft: any): any;
51
51
  export function applyPenDraft(nv: any, draft: any): void;
52
+ /**
53
+ * Flood-fill from the clicked voxel to reconstruct a freehand PenDraft for re-editing.
54
+ * Returns null if the click didn't land on a labeled voxel.
55
+ */
56
+ export function capturePenDraftFromClick(nv: any): {
57
+ kind: string;
58
+ baseBitmap: Uint8Array;
59
+ axCorSag: number;
60
+ penValue: number;
61
+ strokeVoxels: [number, number, number][];
62
+ bounds: object;
63
+ } | null;
52
64
  export function cancelPenDraft(nv: any, draft: any): void;
53
65
  export type PenDraftKind = 'polyline' | 'freehand';
54
66
  export type PenDraft = {
@@ -18,8 +18,9 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
18
18
  }
19
19
  return to.concat(ar || Array.prototype.slice.call(from));
20
20
  };
21
- import { translatePt } from "./shapeDraftUtils";
21
+ import { translatePt, floodFillClusterFromVox, eraseClusterFromBitmap, inferAxCorSagFromBounds, } from "./shapeDraftUtils";
22
22
  import { NI_PEN_TYPE } from "./niivuePenType";
23
+ import { voxFromMouse } from "./polylinePenUtils";
23
24
  /** @typedef {'polyline' | 'freehand'} PenDraftKind */
24
25
  /**
25
26
  * @typedef {Object} PenDraft
@@ -265,6 +266,28 @@ export function applyPenDraft(nv, draft) {
265
266
  nv.onDrawingChanged("draw");
266
267
  }
267
268
  }
269
+ /**
270
+ * Flood-fill from the clicked voxel to reconstruct a freehand PenDraft for re-editing.
271
+ * Returns null if the click didn't land on a labeled voxel.
272
+ */
273
+ export function capturePenDraftFromClick(nv) {
274
+ var seedVox = voxFromMouse(nv);
275
+ var cluster = floodFillClusterFromVox(nv, seedVox);
276
+ if (!cluster)
277
+ return null;
278
+ var label = cluster.label, visited = cluster.visited, voxels = cluster.voxels, bounds = cluster.bounds;
279
+ var x1 = bounds.x1, y1 = bounds.y1, z1 = bounds.z1, x2 = bounds.x2, y2 = bounds.y2, z2 = bounds.z2;
280
+ var baseBitmap = eraseClusterFromBitmap(nv.drawBitmap, visited);
281
+ var axCorSag = inferAxCorSagFromBounds(x1, y1, z1, x2, y2, z2, nv.drawPenAxCorSag >= 0 ? nv.drawPenAxCorSag : 0);
282
+ return {
283
+ kind: "freehand",
284
+ baseBitmap: baseBitmap,
285
+ axCorSag: axCorSag,
286
+ penValue: label,
287
+ strokeVoxels: voxels,
288
+ bounds: bounds
289
+ };
290
+ }
268
291
  export function cancelPenDraft(nv, draft) {
269
292
  if (draft === null || draft === void 0 ? void 0 : draft.baseBitmap) {
270
293
  nv.drawBitmap.set(draft.baseBitmap);
@@ -49,6 +49,18 @@ export function captureDeferredShapeDraft(nv: any): {
49
49
  baseBitmap: Uint8Array | null;
50
50
  };
51
51
  export function shouldDeferShapeCommit(nv: any): any;
52
+ export function inferAxCorSagFromBounds(x1: any, y1: any, z1: any, x2: any, y2: any, z2: any, fallback?: number): number;
53
+ /**
54
+ * Flood-fill a connected voxel cluster from a seed.
55
+ * @returns {{ label: number, visited: Set<number>, voxels: [number,number,number][], bounds: object } | null}
56
+ */
57
+ export function floodFillClusterFromVox(nv: any, seedVox: any): {
58
+ label: number;
59
+ visited: Set<number>;
60
+ voxels: [number, number, number][];
61
+ bounds: object;
62
+ } | null;
63
+ export function eraseClusterFromBitmap(bitmap: any, visited: any): Uint8Array;
52
64
  /**
53
65
  * When the user clicks on an existing filled ROI while the rectangle/ellipse tool
54
66
  * is active (but no draft is currently open), flood-fill the clicked cluster to
@@ -197,7 +197,7 @@ function decodeVoxelIndex(idx, dx, dy) {
197
197
  var x = rem % dx;
198
198
  return [x, y, z];
199
199
  }
200
- function inferAxCorSagFromBounds(x1, y1, z1, x2, y2, z2, fallback) {
200
+ export function inferAxCorSagFromBounds(x1, y1, z1, x2, y2, z2, fallback) {
201
201
  if (fallback === void 0) { fallback = 0; }
202
202
  var spanX = x2 - x1;
203
203
  var spanY = y2 - y1;
@@ -221,7 +221,7 @@ function sliceKey(axCorSag, x, y, z) {
221
221
  * Flood-fill a connected voxel cluster from a seed.
222
222
  * @returns {{ label: number, visited: Set<number>, voxels: [number,number,number][], bounds: object } | null}
223
223
  */
224
- function floodFillClusterFromVox(nv, seedVox) {
224
+ export function floodFillClusterFromVox(nv, seedVox) {
225
225
  var _a;
226
226
  var dims = (_a = nv.back) === null || _a === void 0 ? void 0 : _a.dims;
227
227
  if (!dims || !nv.drawBitmap || !seedVox)
@@ -281,7 +281,7 @@ function floodFillClusterFromVox(nv, seedVox) {
281
281
  bounds: { x1: x1, y1: y1, z1: z1, x2: x2, y2: y2, z2: z2 }
282
282
  };
283
283
  }
284
- function eraseClusterFromBitmap(bitmap, visited) {
284
+ export function eraseClusterFromBitmap(bitmap, visited) {
285
285
  var next = new Uint8Array(bitmap);
286
286
  visited.forEach(function (idx) {
287
287
  next[idx] = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudmr-ux",
3
- "version": "4.6.6",
3
+ "version": "4.6.8",
4
4
  "author": "erosmontin@gmail.com",
5
5
  "license": "MIT",
6
6
  "repository": "erosmontin/cloudmr-ux",