cloudmr-ux 4.6.1 → 4.6.2

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