cloudmr-ux 4.6.7 → 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,19 +945,65 @@ 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
- // Deactivate tool entirely — palette closes, user re-enters edit by clicking the ROI
936
- setDrawShapeTool(null);
937
- nv.opts.deferShapeCommit = false;
938
- nv.opts.penType = NI_PEN_TYPE.PEN;
939
- nvSetDrawingEnabled(false);
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);
1006
+ }
940
1007
  resampleImage();
941
1008
  }
942
1009
  function onShapeDraftChange(draft) {
@@ -1543,7 +1610,7 @@ export default function CloudMrNiivueViewer(props) {
1543
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 () {
1544
1611
  var _a;
1545
1612
  void ((_a = props.refreshPipelineRois) === null || _a === void 0 ? void 0 : _a.call(props));
1546
- }, 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))] })));
1547
1614
  }
1548
1615
  function niiToVolume(nii) {
1549
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,18 +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.
1491
- * Works regardless of which tool is currently active (or whether any tool is active),
1492
- * so clicking an ROI after apply always re-enters edit mode.
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).
1493
1508
  */
1494
1509
  function cloudMrTryReopenShapeDraftOnClick(nv) {
1495
- if (nv._cloudMrShapeDraftActive || nv._cloudMrPenDraftActive) {
1496
- return false;
1497
- }
1498
- if (!isClickWithoutDrag(nv.uiData)) {
1499
- return false;
1500
- }
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;
1501
1516
 
1502
1517
  const reopenDraft = captureShapeDraftFromClick(nv);
1503
1518
  if (!reopenDraft) return false;
@@ -1510,6 +1525,33 @@ function cloudMrTryReopenShapeDraftOnClick(nv) {
1510
1525
  return true;
1511
1526
  }
1512
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
+
1513
1555
  const _mouseDownListener = Niivue.prototype.mouseDownListener;
1514
1556
  Niivue.prototype.mouseDownListener = function cloudMrMouseDownListener(e) {
1515
1557
  if (e.button === RIGHT_MOUSE_BUTTON && cloudMrTryApplyDraftOnRightClick(this, e)) {
@@ -1583,14 +1625,18 @@ Niivue.prototype.mouseUpListener = function cloudMrMouseUpListener() {
1583
1625
  }
1584
1626
 
1585
1627
  if (!pendingDraft?.baseBitmap) {
1586
- cloudMrTryReopenShapeDraftOnClick(this);
1628
+ if (!cloudMrTryReopenShapeDraftOnClick(this)) {
1629
+ cloudMrTryReopenPenDraftOnClick(this);
1630
+ }
1587
1631
  return;
1588
1632
  }
1589
1633
  if (isDraftTooSmall(pendingDraft.ptA, pendingDraft.ptB)) {
1590
1634
  this.drawBitmap.set(pendingDraft.baseBitmap);
1591
1635
  this.refreshDrawing(true, false);
1592
1636
  this.drawScene();
1593
- cloudMrTryReopenShapeDraftOnClick(this);
1637
+ if (!cloudMrTryReopenShapeDraftOnClick(this)) {
1638
+ cloudMrTryReopenPenDraftOnClick(this);
1639
+ }
1594
1640
  return;
1595
1641
  }
1596
1642
  this._cloudMrSuppressDrawingChangedMouseUp = true;
@@ -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.7",
3
+ "version": "4.6.8",
4
4
  "author": "erosmontin@gmail.com",
5
5
  "license": "MIT",
6
6
  "repository": "erosmontin/cloudmr-ux",