cloudmr-ux 4.7.3 → 4.7.5

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.
@@ -107,7 +107,7 @@ export function CloudMrNiivuePanel(props) {
107
107
  props.resampleImage();
108
108
  }, [histogram]);
109
109
  function applyDrawShapeTool(tool) {
110
- var _a, _b, _c, _d;
110
+ var _a, _b, _c, _d, _e;
111
111
  if (props.shapeDraft) {
112
112
  // Apply (commit) the current draft rather than discarding it
113
113
  (_a = props.onApplyShapeDraftKeepTool) === null || _a === void 0 ? void 0 : _a.call(props);
@@ -120,9 +120,17 @@ export function CloudMrNiivuePanel(props) {
120
120
  }
121
121
  props.setDrawShapeTool(tool);
122
122
  var nv = props.nv;
123
+ var penMode = (_e = props.drawToolkitProps.penDrawMode) !== null && _e !== void 0 ? _e : "freehand";
123
124
  nv.opts.deferShapeCommit = tool === "rectangle" || tool === "ellipse";
124
- nv.opts.deferFreehandCommit =
125
- tool === "pen" && props.drawToolkitProps.penDrawMode === "freehand";
125
+ if (tool === "pen") {
126
+ nv.opts.polylinePenMode = penMode === "polyline";
127
+ nv.opts.isFilledPen = penMode === "freehand";
128
+ nv.opts.deferFreehandCommit = false;
129
+ }
130
+ else {
131
+ nv.opts.polylinePenMode = false;
132
+ nv.opts.deferFreehandCommit = false;
133
+ }
126
134
  nv.opts.penType =
127
135
  tool === "rectangle"
128
136
  ? NI_PEN_TYPE.RECTANGLE
@@ -63,7 +63,7 @@ import { SettingsPanel } from './SettingsPanel';
63
63
  import { NumberPicker } from './NumberPicker';
64
64
  import { ColorPicker } from './ColorPicker';
65
65
  import { LayersPanel } from './LayersPanel';
66
- import { applyPenDraft, cancelPenDraft, fillPolylineDraft, fillFreehandDraft, polylineDraftFromNv, syncPolylineDraftToNv, } from './penDraftUtils';
66
+ import { applyPenDraft, cancelPenDraft, captureFreehandDraft, fillPolylineDraft, unfillPolylineDraft, polylineDraftFromNv, syncPolylineDraftToNv, registerAppliedPolyline, restoreCommittedPolyline, collectPolylineAppliedVoxelIndices, } from './penDraftUtils';
67
67
  import { CloudMrNiivuePanel } from './CloudMrNiivuePanel';
68
68
  import { Niivue } from './NiivuePatcher';
69
69
  import NVSwitch from './Switch';
@@ -104,7 +104,7 @@ export var nv = new Niivue({
104
104
  crosshairColor: [1, 1, 0],
105
105
  fontColor: [0.00, 0.94, 0.37, 1],
106
106
  isNearestInterpolation: true,
107
- isFilledPen: false,
107
+ isFilledPen: true,
108
108
  penValue: 1,
109
109
  penType: NI_PEN_TYPE.PEN
110
110
  });
@@ -593,9 +593,9 @@ export default function CloudMrNiivueViewer(props) {
593
593
  }
594
594
  }
595
595
  else if (drawShapeToolRef.current === "pen") {
596
- nv.opts.deferFreehandCommit = penDrawModeRef.current === "freehand";
596
+ nv.opts.deferFreehandCommit = false;
597
597
  nv.opts.polylinePenMode = penDrawModeRef.current === "polyline";
598
- nv.opts.isFilledPen = false;
598
+ nv.opts.isFilledPen = penDrawModeRef.current === "freehand";
599
599
  }
600
600
  }
601
601
  function deactivateDrawTools() {
@@ -821,23 +821,53 @@ export default function CloudMrNiivueViewer(props) {
821
821
  }
822
822
  }
823
823
  function syncPenDrawMode(mode) {
824
- var _a;
825
- setPenDrawMode(mode);
826
- penDrawModeRef.current = mode;
827
- nv.opts.polylinePenMode = mode === "polyline";
828
- nv.opts.isFilledPen = false;
829
- nv.opts.deferFreehandCommit =
830
- drawShapeToolRef.current === "pen" && mode === "freehand";
824
+ var _a, _b;
825
+ var prevMode = penDrawModeRef.current;
826
+ // Commit an in-progress freehand before switching to polyline so it stays
827
+ // marked as pen (kind 2) and is not misidentified as a shape on click.
828
+ if (prevMode === "freehand" && mode === "polyline") {
829
+ var draft = penDraftRef.current;
830
+ if ((draft === null || draft === void 0 ? void 0 : draft.kind) === "freehand") {
831
+ applyPenDraft(nv, draft);
832
+ markPenVoxelKind(draft, 2);
833
+ setPenDraft(null);
834
+ penDraftRef.current = null;
835
+ nv._cloudMrPenDraftActive = false;
836
+ }
837
+ else if (nv._cloudMrFreehandSessionStartBitmap) {
838
+ var axCorSag = nv._cloudMrFreehandAxCorSag >= 0 ? nv._cloudMrFreehandAxCorSag : nv.drawPenAxCorSag;
839
+ var captured = captureFreehandDraft(nv, nv._cloudMrFreehandSessionStartBitmap, axCorSag);
840
+ nv._cloudMrFreehandSessionStartBitmap = null;
841
+ if (captured)
842
+ markPenVoxelKind(captured, 2);
843
+ }
844
+ }
831
845
  if (mode === "freehand") {
832
846
  (_a = nv.cloudMrCancelPolyline) === null || _a === void 0 ? void 0 : _a.call(nv);
847
+ if (((_b = penDraftRef.current) === null || _b === void 0 ? void 0 : _b.kind) === "polyline") {
848
+ cancelPenDraft(nv, penDraftRef.current);
849
+ setPenDraft(null);
850
+ penDraftRef.current = null;
851
+ nv._cloudMrPenDraftActive = false;
852
+ setPolylineVertexCount(0);
853
+ }
833
854
  }
855
+ setPenDrawMode(mode);
856
+ penDrawModeRef.current = mode;
857
+ nv.opts.polylinePenMode = mode === "polyline";
858
+ nv.opts.isFilledPen = mode === "freehand";
859
+ nv.opts.deferFreehandCommit = false;
834
860
  }
835
861
  function cancelPenDraftHandler() {
836
862
  var _a;
837
863
  var draft = penDraftRef.current;
838
864
  if (!draft)
839
865
  return;
866
+ var registryId = draft._registryId;
840
867
  cancelPenDraft(nv, draft);
868
+ if (registryId && draft.kind === "polyline") {
869
+ restoreCommittedPolyline(nv, registryId);
870
+ }
841
871
  if (draft.kind === "polyline") {
842
872
  (_a = nv.cloudMrResetPolyline) === null || _a === void 0 ? void 0 : _a.call(nv);
843
873
  setPolylineVertexCount(0);
@@ -850,22 +880,41 @@ export default function CloudMrNiivueViewer(props) {
850
880
  }
851
881
  }
852
882
  function applyPenDraftHandler(_a) {
853
- var _b;
854
- var _c = _a === void 0 ? {} : _a, _d = _c.keepTool, keepTool = _d === void 0 ? false : _d;
883
+ var _b, _c, _d;
884
+ var _e = _a === void 0 ? {} : _a, _f = _e.keepTool, keepTool = _f === void 0 ? false : _f;
855
885
  var draft = penDraftRef.current;
856
886
  if (!draft)
857
887
  return;
858
- applyPenDraft(nv, draft);
888
+ if (draft.kind === "polyline" && ((_b = nv._cloudMrPolylineVertices) === null || _b === void 0 ? void 0 : _b.length) >= 2) {
889
+ var fresh = polylineDraftFromNv(nv, { filled: !!draft.filled });
890
+ if (fresh) {
891
+ draft = fresh;
892
+ penDraftRef.current = draft;
893
+ }
894
+ }
895
+ draft = (_c = applyPenDraft(nv, draft)) !== null && _c !== void 0 ? _c : draft;
896
+ if (draft.kind === "polyline" && nv._cloudMrPolylineSessionStartBitmap) {
897
+ draft = __assign(__assign({}, draft), { baseBitmap: new Uint8Array(nv._cloudMrPolylineSessionStartBitmap) });
898
+ }
899
+ penDraftRef.current = draft;
859
900
  markPenVoxelKind(draft, draft.kind === "polyline" ? 3 : 2);
860
901
  if (draft.kind === "polyline") {
861
- (_b = nv.cloudMrResetPolyline) === null || _b === void 0 ? void 0 : _b.call(nv);
902
+ registerAppliedPolyline(nv, draft, draft._registryId);
903
+ (_d = nv.cloudMrResetPolyline) === null || _d === void 0 ? void 0 : _d.call(nv);
862
904
  setPolylineVertexCount(0);
863
905
  }
864
906
  setPenDraft(null);
865
907
  penDraftRef.current = null;
866
908
  nv._cloudMrPenDraftActive = false;
867
909
  setDrawingChanged(true);
868
- if (!keepTool) {
910
+ if (keepTool) {
911
+ var mode = penDrawModeRef.current;
912
+ nv.opts.polylinePenMode = mode === "polyline";
913
+ nv.opts.isFilledPen = mode === "freehand";
914
+ nv.opts.deferFreehandCommit = false;
915
+ nvSetDrawingEnabled(true);
916
+ }
917
+ else {
869
918
  // Deactivate tool entirely — palette closes, user re-enters edit by clicking the ROI
870
919
  setDrawShapeTool(null);
871
920
  nv.opts.deferFreehandCommit = false;
@@ -874,20 +923,14 @@ export default function CloudMrNiivueViewer(props) {
874
923
  }
875
924
  resampleImage();
876
925
  }
877
- function fillPenDraftHandler() {
926
+ function fillPolylineDraftHandler() {
878
927
  var draft = penDraftRef.current;
879
- if (!draft)
928
+ if (!draft || draft.kind !== "polyline" || draft.vertices.length < 3)
880
929
  return;
881
- if (draft.kind === "polyline") {
882
- if (draft.vertices.length < 3)
883
- return;
884
- onPenDraftChange(fillPolylineDraft(nv, draft));
885
- }
886
- else if (draft.kind === "freehand") {
887
- if (!draft.pathVertices || draft.pathVertices.length < 3)
888
- return;
889
- onPenDraftChange(fillFreehandDraft(nv, draft));
890
- }
930
+ var next = draft.filled
931
+ ? unfillPolylineDraft(nv, draft)
932
+ : fillPolylineDraft(nv, draft);
933
+ onPenDraftChange(next);
891
934
  }
892
935
  function onPenDraftChange(draft) {
893
936
  setPenDraft(draft);
@@ -899,6 +942,17 @@ export default function CloudMrNiivueViewer(props) {
899
942
  function cancelPolylineDraft() {
900
943
  cancelPenDraftHandler();
901
944
  }
945
+ nv.onShapeCommitted = function (draft) {
946
+ nv.drawAddUndoBitmap(nv.drawFillOverwrites);
947
+ markShapeVoxelKind(draft);
948
+ setDrawingChanged(true);
949
+ resampleImage();
950
+ };
951
+ nv.onFreehandCommitted = function (draft) {
952
+ markPenVoxelKind(draft, 2);
953
+ setDrawingChanged(true);
954
+ resampleImage();
955
+ };
902
956
  nv.onPenDraftReady = function (draft) {
903
957
  setPenDraft(draft);
904
958
  penDraftRef.current = draft;
@@ -908,14 +962,18 @@ export default function CloudMrNiivueViewer(props) {
908
962
  // Called by NiivuePatcher when the user clicks an applied pen ROI to re-edit it.
909
963
  // penKind: 2=freehand, 3=polyline
910
964
  nv.onPenDraftReopenReady = function (draft, penKind) {
965
+ var _a, _b;
911
966
  setPenDraft(draft);
912
967
  penDraftRef.current = draft;
913
968
  nv._cloudMrPenDraftActive = true;
914
969
  // Auto-select pen tool so the palette opens
915
970
  setDrawShapeTool("pen");
916
- var mode = penKind === 3 ? "polyline" : "freehand";
971
+ var mode = draft.kind === "polyline" ? "polyline" : "freehand";
917
972
  setPenDrawMode(mode);
918
973
  penDrawModeRef.current = mode;
974
+ if (draft.kind === "polyline") {
975
+ setPolylineVertexCount((_b = (_a = draft.vertices) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0);
976
+ }
919
977
  nv.opts.deferFreehandCommit = false;
920
978
  nv.opts.polylinePenMode = false;
921
979
  nvSetDrawingEnabled(false);
@@ -959,7 +1017,6 @@ export default function CloudMrNiivueViewer(props) {
959
1017
  nv._cloudMrPolylineVertices = null;
960
1018
  nv._cloudMrPolylineBaseBitmap = null;
961
1019
  nv._cloudMrPolylineSessionStartBitmap = null;
962
- nv._cloudMrFreehandPath = null;
963
1020
  nv._cloudMrToolKindBitmap = null;
964
1021
  }
965
1022
  function clearDrawingHandler() {
@@ -1019,11 +1076,18 @@ export default function CloudMrNiivueViewer(props) {
1019
1076
  ensureToolKindBitmap();
1020
1077
  var tkb = nv._cloudMrToolKindBitmap;
1021
1078
  var dims = (_a = nv.back) === null || _a === void 0 ? void 0 : _a.dims;
1079
+ if (draft.kind === "polyline") {
1080
+ for (var _i = 0, _b = collectPolylineAppliedVoxelIndices(nv, draft); _i < _b.length; _i++) {
1081
+ var idx = _b[_i];
1082
+ tkb[idx] = kind;
1083
+ }
1084
+ return;
1085
+ }
1022
1086
  if (draft.kind === "freehand" && draft.strokeVoxels && dims) {
1023
1087
  var dx = dims[1];
1024
1088
  var dy = dims[2];
1025
- for (var _i = 0, _b = draft.strokeVoxels; _i < _b.length; _i++) {
1026
- var _c = _b[_i], x = _c[0], y = _c[1], z = _c[2];
1089
+ for (var _c = 0, _d = draft.strokeVoxels; _c < _d.length; _c++) {
1090
+ var _e = _d[_c], x = _e[0], y = _e[1], z = _e[2];
1027
1091
  tkb[x + y * dx + z * dx * dy] = kind;
1028
1092
  }
1029
1093
  }
@@ -1628,8 +1692,10 @@ export default function CloudMrNiivueViewer(props) {
1628
1692
  onCancelPolyline: cancelPolylineDraft,
1629
1693
  onApplyPenDraft: applyPenDraftHandler,
1630
1694
  onCancelPenDraft: cancelPenDraftHandler,
1631
- onFillPenDraft: fillPenDraftHandler,
1695
+ onFillPenDraft: fillPolylineDraftHandler,
1632
1696
  penDraftActive: penDraft != null,
1697
+ penDraftKind: penDraft === null || penDraft === void 0 ? void 0 : penDraft.kind,
1698
+ penDraftFilled: (penDraft === null || penDraft === void 0 ? void 0 : penDraft.filled) === true,
1633
1699
  brushSize: brushSize,
1634
1700
  updateBrushSize: nvUpdateBrushSize,
1635
1701
  onActivateEraser: activateEraser,
@@ -24,8 +24,13 @@ import {
24
24
  import {
25
25
  captureFreehandDraft,
26
26
  capturePenDraftFromClick,
27
+ capturePolylineDraftFromClick,
28
+ isFreehandPenActive,
29
+ isRegisteredPolylineClick,
27
30
  redrawFreehandDraft,
31
+ redrawPolylineDraft,
28
32
  shouldDeferFreehandCommit,
33
+ syncPolylineDraftToNv,
29
34
  } from "./penDraftUtils.js";
30
35
 
31
36
  /*
@@ -533,6 +538,19 @@ Niivue.prototype.drawPenFilled = function () {
533
538
  return;
534
539
  }
535
540
  this.drawAddUndoBitmap()
541
+ if (this._cloudMrFreehandSessionStartBitmap && isFreehandPenActive(this)) {
542
+ const axCorSag =
543
+ this.drawPenAxCorSag >= 0 ? this.drawPenAxCorSag : this._cloudMrFreehandAxCorSag;
544
+ const draft = captureFreehandDraft(
545
+ this,
546
+ this._cloudMrFreehandSessionStartBitmap,
547
+ axCorSag,
548
+ );
549
+ this._cloudMrFreehandSessionStartBitmap = null;
550
+ if (draft && typeof this.onFreehandCommitted === "function") {
551
+ this.onFreehandCommitted(draft);
552
+ }
553
+ }
536
554
  // Post-processing to hide hidden voxels
537
555
  this.hiddenBitmap = new Uint8Array(this.drawBitmap.length);
538
556
  for (let i = 0; i < this.drawBitmap.length; i++) {
@@ -1502,6 +1520,28 @@ function clickedVoxelToolKind(nv) {
1502
1520
  return bitmap[idx] || 0;
1503
1521
  }
1504
1522
 
1523
+ /**
1524
+ * Re-open an applied ROI for editing based on stored (or inferred) tool kind.
1525
+ * Unmarked pen strokes (kind 0) must try pen reopen before shape inference —
1526
+ * otherwise freehand blobs get misclassified as ellipse/rectangle shapes.
1527
+ */
1528
+ function cloudMrTryReopenDraftOnClick(nv) {
1529
+ if (nv._cloudMrShapeDraftActive || nv._cloudMrPenDraftActive) return;
1530
+ if (!isClickWithoutDrag(nv.uiData)) return;
1531
+
1532
+ const kind = clickedVoxelToolKind(nv);
1533
+ if (kind === 1) {
1534
+ cloudMrTryReopenShapeDraftOnClick(nv);
1535
+ return;
1536
+ }
1537
+ if (kind === 2 || kind === 3) {
1538
+ cloudMrTryReopenPenDraftOnClick(nv);
1539
+ return;
1540
+ }
1541
+ if (cloudMrTryReopenPenDraftOnClick(nv)) return;
1542
+ cloudMrTryReopenShapeDraftOnClick(nv);
1543
+ }
1544
+
1505
1545
  /**
1506
1546
  * Re-enter rectangle/ellipse edit mode when clicking an existing applied shape ROI.
1507
1547
  * Skips voxels that were drawn with the pen tool (those are handled by pen reopen).
@@ -1513,6 +1553,7 @@ function cloudMrTryReopenShapeDraftOnClick(nv) {
1513
1553
  // If we know this voxel was drawn with the pen, skip shape reopen
1514
1554
  const kind = clickedVoxelToolKind(nv);
1515
1555
  if (kind === 2 || kind === 3) return false;
1556
+ if (isRegisteredPolylineClick(nv)) return false;
1516
1557
 
1517
1558
  const reopenDraft = captureShapeDraftFromClick(nv);
1518
1559
  if (!reopenDraft) return false;
@@ -1537,14 +1578,21 @@ function cloudMrTryReopenPenDraftOnClick(nv) {
1537
1578
  const kind = clickedVoxelToolKind(nv);
1538
1579
  if (kind === 1) return false;
1539
1580
 
1540
- const draft = capturePenDraftFromClick(nv);
1541
- if (!draft) return false;
1581
+ let draft = capturePolylineDraftFromClick(nv);
1582
+ let penKind = draft ? 3 : kind === 3 ? 3 : 2;
1542
1583
 
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;
1584
+ if (!draft) {
1585
+ draft = capturePenDraftFromClick(nv);
1586
+ if (!draft) return false;
1587
+ if (kind !== 3) penKind = 2;
1588
+ }
1546
1589
 
1547
- redrawFreehandDraft(nv, draft);
1590
+ if (draft.kind === "polyline") {
1591
+ redrawPolylineDraft(nv, draft);
1592
+ syncPolylineDraftToNv(nv, draft);
1593
+ } else {
1594
+ redrawFreehandDraft(nv, draft);
1595
+ }
1548
1596
  nv._cloudMrSuppressDrawingChangedMouseUp = true;
1549
1597
  if (typeof nv.onPenDraftReopenReady === "function") {
1550
1598
  nv.onPenDraftReopenReady(draft, penKind);
@@ -1557,21 +1605,16 @@ Niivue.prototype.mouseDownListener = function cloudMrMouseDownListener(e) {
1557
1605
  if (e.button === RIGHT_MOUSE_BUTTON && cloudMrTryApplyDraftOnRightClick(this, e)) {
1558
1606
  return;
1559
1607
  }
1560
- if (shouldDeferFreehandCommit(this) && this.drawBitmap) {
1608
+ if (isFreehandPenActive(this) && this.drawBitmap) {
1561
1609
  this._cloudMrFreehandSessionStartBitmap = this.drawBitmap.slice();
1562
1610
  this._cloudMrFreehandAxCorSag = -1;
1563
- this._cloudMrFreehandPath = [];
1564
1611
  }
1565
1612
  _mouseDownListener.call(this, e);
1566
- if (shouldDeferFreehandCommit(this) && this._cloudMrFreehandSessionStartBitmap) {
1613
+ if (isFreehandPenActive(this) && this._cloudMrFreehandSessionStartBitmap) {
1567
1614
  const axCorSag = axCorSagFromMouse(this);
1568
1615
  if (axCorSag >= 0) {
1569
1616
  this._cloudMrFreehandAxCorSag = axCorSag;
1570
1617
  }
1571
- const vox = voxFromMouse(this);
1572
- if (vox && this._cloudMrFreehandPath) {
1573
- this._cloudMrFreehandPath.push([vox[0], vox[1], vox[2]]);
1574
- }
1575
1618
  }
1576
1619
  };
1577
1620
 
@@ -1598,19 +1641,6 @@ Niivue.prototype.mouseClick = function cloudMrMouseClick(...args) {
1598
1641
  const _mouseMoveListener = Niivue.prototype.mouseMoveListener;
1599
1642
  Niivue.prototype.mouseMoveListener = function cloudMrMouseMoveListener(event) {
1600
1643
  const result = _mouseMoveListener.call(this, event);
1601
- if (shouldDeferFreehandCommit(this) && this.uiData?.isDragging) {
1602
- const vox = voxFromMouse(this);
1603
- if (vox) {
1604
- if (!this._cloudMrFreehandPath) {
1605
- this._cloudMrFreehandPath = [];
1606
- }
1607
- const path = this._cloudMrFreehandPath;
1608
- const last = path[path.length - 1];
1609
- if (!last || last[0] !== vox[0] || last[1] !== vox[1] || last[2] !== vox[2]) {
1610
- path.push([vox[0], vox[1], vox[2]]);
1611
- }
1612
- }
1613
- }
1614
1644
  if (isPolylinePenActive(this) && this._cloudMrPolylineVertices?.length > 0) {
1615
1645
  previewPolylineSegment(this);
1616
1646
  }
@@ -1643,22 +1673,20 @@ Niivue.prototype.mouseUpListener = function cloudMrMouseUpListener() {
1643
1673
  }
1644
1674
 
1645
1675
  if (!pendingDraft?.baseBitmap) {
1646
- if (!cloudMrTryReopenShapeDraftOnClick(this)) {
1647
- cloudMrTryReopenPenDraftOnClick(this);
1648
- }
1676
+ cloudMrTryReopenDraftOnClick(this);
1649
1677
  return;
1650
1678
  }
1651
1679
  if (isDraftTooSmall(pendingDraft.ptA, pendingDraft.ptB)) {
1652
1680
  this.drawBitmap.set(pendingDraft.baseBitmap);
1653
1681
  this.refreshDrawing(true, false);
1654
1682
  this.drawScene();
1655
- if (!cloudMrTryReopenShapeDraftOnClick(this)) {
1656
- cloudMrTryReopenPenDraftOnClick(this);
1657
- }
1683
+ cloudMrTryReopenDraftOnClick(this);
1658
1684
  return;
1659
1685
  }
1660
1686
  this._cloudMrSuppressDrawingChangedMouseUp = true;
1661
- if (typeof this.onShapeDraftReady === "function") {
1687
+ if (typeof this.onShapeCommitted === "function") {
1688
+ this.onShapeCommitted(pendingDraft);
1689
+ } else if (typeof this.onShapeDraftReady === "function") {
1662
1690
  this.onShapeDraftReady(pendingDraft);
1663
1691
  }
1664
1692
  };
@@ -1,4 +1,4 @@
1
- export default function DrawColorPlatte({ expanded, updateDrawPen, setDrawingEnabled, showPenModes, penDrawMode, onPenDrawModeChange, polylineVertexCount, penDraftActive, onApplyPenDraft, onFillPenDraft, brushSize, updateBrushSize, shapeDraftActive, onApplyShapeDraft, }: {
1
+ export default function DrawColorPlatte({ expanded, updateDrawPen, setDrawingEnabled, showPenModes, penDrawMode, onPenDrawModeChange, polylineVertexCount, penDraftActive, penDraftKind, penDraftFilled, onApplyPenDraft, onFillPenDraft, brushSize, updateBrushSize, shapeDraftActive, onApplyShapeDraft, }: {
2
2
  expanded: any;
3
3
  updateDrawPen: any;
4
4
  setDrawingEnabled: any;
@@ -7,6 +7,8 @@ export default function DrawColorPlatte({ expanded, updateDrawPen, setDrawingEna
7
7
  onPenDrawModeChange: any;
8
8
  polylineVertexCount?: number | undefined;
9
9
  penDraftActive?: boolean | undefined;
10
+ penDraftKind: any;
11
+ penDraftFilled?: boolean | undefined;
10
12
  onApplyPenDraft: any;
11
13
  onFillPenDraft: any;
12
14
  brushSize?: number | undefined;
@@ -36,7 +36,7 @@ var modeBtnSx = function (active) { return ({
36
36
  px: 0.75
37
37
  }); };
38
38
  export default function DrawColorPlatte(_a) {
39
- 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, onApplyPenDraft = _a.onApplyPenDraft, 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;
39
+ 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, 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;
40
40
  return (_jsxs(Stack, __assign({ style: {
41
41
  position: "absolute",
42
42
  top: "100%",
@@ -53,15 +53,19 @@ export default function DrawColorPlatte(_a) {
53
53
  }, 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 () {
54
54
  updateDrawPen({ target: { value: index + 1 } });
55
55
  setDrawingEnabled(true);
56
- } }, { 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 && penDraftActive && expanded && (_jsxs(Stack, __assign({ direction: "row", alignItems: "center", justifyContent: "flex-end", spacing: 1, sx: { px: 1, py: 0.5, borderTop: "1px solid #555", width: "100%" } }, { children: [(penDrawMode === "polyline" && polylineVertexCount >= 3) ||
57
- (penDrawMode === "freehand" && penDraftActive) ? (_jsx(Tooltip, __assign({ title: "Fill interior (keeps outline editable until Apply)" }, { children: _jsx(Button, __assign({ size: "small", "aria-label": "fill pen draft", onClick: function () { return onFillPenDraft === null || onFillPenDraft === void 0 ? void 0 : onFillPenDraft(); }, sx: {
58
- color: "#c9a0e8",
56
+ } }, { 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 &&
57
+ penDraftActive &&
58
+ (penDrawMode === "polyline" || penDraftKind === "freehand") &&
59
+ expanded && (_jsxs(Stack, __assign({ direction: "row", alignItems: "center", justifyContent: "flex-end", spacing: 1, sx: { px: 1, py: 0.5, borderTop: "1px solid #555", width: "100%" } }, { children: [penDrawMode === "polyline" && polylineVertexCount >= 3 && (_jsx(Tooltip, __assign({ title: penDraftFilled
60
+ ? "Remove fill (keeps outline editable)"
61
+ : "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: {
62
+ color: penDraftFilled ? "#ffb74d" : "#c9a0e8",
59
63
  fontSize: ACTION_FONT_SIZE,
60
64
  textTransform: "none",
61
65
  minWidth: 0,
62
66
  py: 0.25,
63
67
  px: 0.75
64
- } }, { children: "Fill" })) }))) : null, _jsx(Tooltip, __assign({ title: "Apply shape (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: {
68
+ } }, { children: penDraftFilled ? "Undo Fill" : "Fill" })) }))), _jsx(Tooltip, __assign({ title: "Apply shape (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: {
65
69
  color: "#c9a0e8",
66
70
  fontSize: ACTION_FONT_SIZE,
67
71
  textTransform: "none",
@@ -189,7 +189,7 @@ export function MroDrawToolkit(props) {
189
189
  alignItems: "center",
190
190
  gap: 4,
191
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, onApplyPenDraft: props.onApplyPenDraft, 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 })] })), _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 })] })), _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
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, 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 })] })), _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 })] })), _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
193
193
  ? { backgroundColor: "rgba(88, 15, 139, 0.12)", color: "#580f8b" }
194
194
  : {})) }, { 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 () {
195
195
  var _a;
@@ -1,16 +1,11 @@
1
- /** @typedef {'polyline' | 'freehand'} PenDraftKind */
2
- /**
3
- * @typedef {Object} PenDraft
4
- * @property {PenDraftKind} kind
5
- * @property {Uint8Array} baseBitmap
6
- * @property {number} axCorSag
7
- * @property {number} penValue
8
- * @property {[number, number, number][]} [vertices]
9
- * @property {[number, number, number][]} [strokeVoxels]
10
- * @property {[number, number, number][]} [pathVertices]
11
- * @property {{ x1: number, y1: number, x2: number, y2: number, z1: number, z2: number }} [bounds]
12
- * @property {boolean} [filled]
13
- */
1
+ /** Voxel indices added by a pen draft relative to its base bitmap. */
2
+ export function collectPenDraftVoxelIndices(nv: any, draft: any): Set<any>;
3
+ /** Collect applied polyline voxels using session baseline and vertex redraw fallbacks. */
4
+ export function collectPolylineAppliedVoxelIndices(nv: any, draft: any): Set<any>;
5
+ export function isRegisteredPolylineClick(nv: any, seedVox?: any[] | null): boolean;
6
+ /** Persist applied polyline vertices so reopen can restore the full line. */
7
+ export function registerAppliedPolyline(nv: any, draft: any, existingId: any): any;
8
+ export function restoreCommittedPolyline(nv: any, registryId: any): void;
14
9
  export function isEraserActive(nv: any): any;
15
10
  export function isFreehandPenActive(nv: any): any;
16
11
  export function shouldDeferFreehandCommit(nv: any): any;
@@ -24,7 +19,6 @@ export function captureFreehandDraft(nv: any, sessionStartBitmap: any, axCorSag:
24
19
  axCorSag: any;
25
20
  penValue: any;
26
21
  strokeVoxels: number[][];
27
- pathVertices: any;
28
22
  bounds: {
29
23
  x1: number;
30
24
  y1: number;
@@ -48,11 +42,24 @@ export function polylineDraftFromNv(nv: any, { filled }?: {
48
42
  penValue: any;
49
43
  filled: boolean;
50
44
  } | null;
51
- /** Fill freehand interior from the traced stroke path (outline stays editable until Apply). */
52
- export function fillFreehandDraft(nv: any, draft: any): any;
53
45
  /** Fill polyline interior without closing the outline or committing the draft. */
54
46
  export function fillPolylineDraft(nv: any, draft: any): any;
55
- export function applyPenDraft(nv: any, draft: any): void;
47
+ /** Undo a previous fill — revert to outline-only without committing the draft. */
48
+ export function unfillPolylineDraft(nv: any, draft: any): any;
49
+ export function applyPenDraft(nv: any, draft: any): any;
50
+ /**
51
+ * Reconstruct a polyline PenDraft from stored vertices (not flood-fill).
52
+ * Returns null if the click didn't land on a registered polyline.
53
+ */
54
+ export function capturePolylineDraftFromClick(nv: any): {
55
+ kind: string;
56
+ baseBitmap: Uint8Array;
57
+ axCorSag: any;
58
+ penValue: any;
59
+ vertices: any;
60
+ filled: any;
61
+ _registryId: any;
62
+ } | null;
56
63
  /**
57
64
  * Flood-fill from the clicked voxel to reconstruct a freehand PenDraft for re-editing.
58
65
  * Returns null if the click didn't land on a labeled voxel.
@@ -74,7 +81,6 @@ export type PenDraft = {
74
81
  penValue: number;
75
82
  vertices?: [number, number, number][] | undefined;
76
83
  strokeVoxels?: [number, number, number][] | undefined;
77
- pathVertices?: [number, number, number][] | undefined;
78
84
  bounds?: {
79
85
  x1: number;
80
86
  y1: number;
@@ -84,4 +90,5 @@ export type PenDraft = {
84
90
  z2: number;
85
91
  } | undefined;
86
92
  filled?: boolean | undefined;
93
+ _registryId?: number | undefined;
87
94
  };
@@ -30,17 +30,137 @@ import { voxFromMouse } from "./polylinePenUtils";
30
30
  * @property {number} penValue
31
31
  * @property {[number, number, number][]} [vertices]
32
32
  * @property {[number, number, number][]} [strokeVoxels]
33
- * @property {[number, number, number][]} [pathVertices]
34
33
  * @property {{ x1: number, y1: number, x2: number, y2: number, z1: number, z2: number }} [bounds]
35
34
  * @property {boolean} [filled]
35
+ * @property {number} [_registryId]
36
36
  */
37
+ function voxelIndexFromVox(vox, dx, dy) {
38
+ return vox[0] + vox[1] * dx + vox[2] * dx * dy;
39
+ }
40
+ /** Voxel indices added by a pen draft relative to its base bitmap. */
41
+ export function collectPenDraftVoxelIndices(nv, draft) {
42
+ var indices = new Set();
43
+ if (!nv.drawBitmap || !(draft === null || draft === void 0 ? void 0 : draft.baseBitmap))
44
+ return indices;
45
+ var penValue = draft.penValue;
46
+ for (var i = 0; i < nv.drawBitmap.length; i++) {
47
+ if (nv.drawBitmap[i] === penValue && draft.baseBitmap[i] !== penValue) {
48
+ indices.add(i);
49
+ }
50
+ }
51
+ return indices;
52
+ }
53
+ /** Collect applied polyline voxels using session baseline and vertex redraw fallbacks. */
54
+ export function collectPolylineAppliedVoxelIndices(nv, draft) {
55
+ var _a;
56
+ var indices = collectPenDraftVoxelIndices(nv, draft);
57
+ if (indices.size)
58
+ return indices;
59
+ var session = nv._cloudMrPolylineSessionStartBitmap;
60
+ if (session && nv.drawBitmap && (draft === null || draft === void 0 ? void 0 : draft.penValue) != null) {
61
+ indices = new Set();
62
+ var penValue = draft.penValue;
63
+ for (var i = 0; i < nv.drawBitmap.length; i++) {
64
+ if (nv.drawBitmap[i] === penValue && session[i] !== penValue) {
65
+ indices.add(i);
66
+ }
67
+ }
68
+ if (indices.size)
69
+ return indices;
70
+ }
71
+ if (((_a = draft === null || draft === void 0 ? void 0 : draft.vertices) === null || _a === void 0 ? void 0 : _a.length) >= 2 && session) {
72
+ var baseBitmap = new Uint8Array(session);
73
+ redrawPolylineDraft(nv, __assign(__assign({}, draft), { baseBitmap: baseBitmap, filled: !!draft.filled }));
74
+ indices = collectPenDraftVoxelIndices(nv, __assign(__assign({}, draft), { baseBitmap: baseBitmap }));
75
+ }
76
+ return indices;
77
+ }
78
+ export function isRegisteredPolylineClick(nv, seedVox) {
79
+ if (seedVox === void 0) { seedVox = voxFromMouse(nv); }
80
+ return !!findPolylineRegistryEntry(nv, seedVox);
81
+ }
82
+ function findPolylineRegistryEntryById(nv, registryId) {
83
+ var _a, _b;
84
+ return (_b = (_a = nv._cloudMrPolylineRegistry) === null || _a === void 0 ? void 0 : _a.find(function (entry) { return entry.id === registryId; })) !== null && _b !== void 0 ? _b : null;
85
+ }
86
+ function findPolylineRegistryEntry(nv, seedVox) {
87
+ var _a;
88
+ var registry = nv._cloudMrPolylineRegistry;
89
+ if (!(registry === null || registry === void 0 ? void 0 : registry.length) || !seedVox || !((_a = nv.back) === null || _a === void 0 ? void 0 : _a.dims))
90
+ return null;
91
+ var dx = nv.back.dims[1];
92
+ var dy = nv.back.dims[2];
93
+ var seedIdx = voxelIndexFromVox(seedVox, dx, dy);
94
+ var direct = registry.find(function (entry) { return entry.voxelIndices.has(seedIdx); });
95
+ if (direct)
96
+ return direct;
97
+ var cluster = floodFillClusterFromVox(nv, seedVox, { connectivity: 26 });
98
+ if (!cluster)
99
+ return null;
100
+ var best = null;
101
+ var bestOverlap = 0;
102
+ for (var _i = 0, registry_1 = registry; _i < registry_1.length; _i++) {
103
+ var entry = registry_1[_i];
104
+ var overlap = 0;
105
+ for (var _b = 0, _c = cluster.visited; _b < _c.length; _b++) {
106
+ var idx = _c[_b];
107
+ if (entry.voxelIndices.has(idx))
108
+ overlap++;
109
+ }
110
+ if (overlap > bestOverlap) {
111
+ bestOverlap = overlap;
112
+ best = entry;
113
+ }
114
+ }
115
+ return bestOverlap > 0 ? best : null;
116
+ }
117
+ /** Persist applied polyline vertices so reopen can restore the full line. */
118
+ export function registerAppliedPolyline(nv, draft, existingId) {
119
+ var _a;
120
+ var voxelIndices = collectPolylineAppliedVoxelIndices(nv, draft);
121
+ if (!voxelIndices.size || !((_a = draft.vertices) === null || _a === void 0 ? void 0 : _a.length))
122
+ return null;
123
+ nv._cloudMrPolylineRegistry = nv._cloudMrPolylineRegistry || [];
124
+ var nextId = existingId !== null && existingId !== void 0 ? existingId : ((nv._cloudMrPolylineNextId = (nv._cloudMrPolylineNextId || 0) + 1));
125
+ var entry = {
126
+ id: nextId,
127
+ vertices: draft.vertices.map(function (v) { return __spreadArray([], v, true); }),
128
+ axCorSag: draft.axCorSag,
129
+ penValue: draft.penValue,
130
+ filled: !!draft.filled,
131
+ voxelIndices: voxelIndices
132
+ };
133
+ var existingIndex = nv._cloudMrPolylineRegistry.findIndex(function (e) { return e.id === nextId; });
134
+ if (existingIndex >= 0) {
135
+ nv._cloudMrPolylineRegistry[existingIndex] = entry;
136
+ }
137
+ else {
138
+ nv._cloudMrPolylineRegistry.push(entry);
139
+ }
140
+ return nextId;
141
+ }
142
+ export function restoreCommittedPolyline(nv, registryId) {
143
+ var entry = findPolylineRegistryEntryById(nv, registryId);
144
+ if (!entry || !nv.drawBitmap)
145
+ return;
146
+ var draft = {
147
+ kind: "polyline",
148
+ vertices: entry.vertices.map(function (v) { return __spreadArray([], v, true); }),
149
+ baseBitmap: nv.drawBitmap.slice(),
150
+ axCorSag: entry.axCorSag,
151
+ penValue: entry.penValue,
152
+ filled: entry.filled
153
+ };
154
+ redrawPolylineDraft(nv, draft);
155
+ }
37
156
  export function isEraserActive(nv) {
38
157
  return (nv.opts.drawingEnabled &&
39
158
  nv.opts.penType === NI_PEN_TYPE.PEN &&
40
159
  nv.opts.penValue === 0);
41
160
  }
42
161
  export function isFreehandPenActive(nv) {
43
- return (nv.opts.drawingEnabled &&
162
+ return (nv.opts.isFilledPen &&
163
+ nv.opts.drawingEnabled &&
44
164
  nv.opts.penType === NI_PEN_TYPE.PEN &&
45
165
  !nv.opts.polylinePenMode &&
46
166
  nv.opts.penValue > 0);
@@ -82,7 +202,7 @@ export function syncPolylineDraftToNv(nv, draft) {
82
202
  nv._cloudMrPolylineAxCorSag = draft.axCorSag;
83
203
  }
84
204
  export function captureFreehandDraft(nv, sessionStartBitmap, axCorSag) {
85
- var _a, _b;
205
+ var _a;
86
206
  if (!nv.drawBitmap || !sessionStartBitmap)
87
207
  return null;
88
208
  var penValue = nv.opts.penValue;
@@ -108,7 +228,7 @@ export function captureFreehandDraft(nv, sessionStartBitmap, axCorSag) {
108
228
  var y2 = -Infinity;
109
229
  var z2 = -Infinity;
110
230
  for (var _i = 0, strokeVoxels_1 = strokeVoxels; _i < strokeVoxels_1.length; _i++) {
111
- var _c = strokeVoxels_1[_i], x = _c[0], y = _c[1], z = _c[2];
231
+ var _b = strokeVoxels_1[_i], x = _b[0], y = _b[1], z = _b[2];
112
232
  x1 = Math.min(x1, x);
113
233
  y1 = Math.min(y1, y);
114
234
  z1 = Math.min(z1, z);
@@ -116,87 +236,30 @@ export function captureFreehandDraft(nv, sessionStartBitmap, axCorSag) {
116
236
  y2 = Math.max(y2, y);
117
237
  z2 = Math.max(z2, z);
118
238
  }
119
- var pathVertices = ((_b = nv._cloudMrFreehandPath) === null || _b === void 0 ? void 0 : _b.length)
120
- ? nv._cloudMrFreehandPath.map(function (v) { return __spreadArray([], v, true); })
121
- : undefined;
122
- nv._cloudMrFreehandPath = [];
123
239
  return {
124
240
  kind: "freehand",
125
241
  baseBitmap: new Uint8Array(sessionStartBitmap),
126
242
  axCorSag: axCorSag,
127
243
  penValue: penValue,
128
244
  strokeVoxels: strokeVoxels,
129
- pathVertices: pathVertices,
130
245
  bounds: { x1: x1, y1: y1, z1: z1, x2: x2, y2: y2, z2: z2 }
131
246
  };
132
247
  }
133
- function collectStrokeVoxelsFromBitmap(nv, draft) {
134
- var _a;
135
- var dims = (_a = nv.back) === null || _a === void 0 ? void 0 : _a.dims;
136
- if (!dims || !nv.drawBitmap || !(draft === null || draft === void 0 ? void 0 : draft.baseBitmap))
137
- return draft.strokeVoxels || [];
138
- var strokeVoxels = [];
139
- for (var i = 0; i < nv.drawBitmap.length; i++) {
140
- if (nv.drawBitmap[i] === draft.penValue && draft.baseBitmap[i] !== draft.penValue) {
141
- var z = Math.floor(i / (dims[1] * dims[2]));
142
- var rem = i - z * dims[1] * dims[2];
143
- var y = Math.floor(rem / dims[1]);
144
- var x = rem % dims[1];
145
- strokeVoxels.push([x, y, z]);
146
- }
147
- }
148
- return strokeVoxels;
149
- }
150
- function boundsFromVoxels(voxels) {
151
- var x1 = Infinity;
152
- var y1 = Infinity;
153
- var z1 = Infinity;
154
- var x2 = -Infinity;
155
- var y2 = -Infinity;
156
- var z2 = -Infinity;
157
- for (var _i = 0, voxels_1 = voxels; _i < voxels_1.length; _i++) {
158
- var _a = voxels_1[_i], x = _a[0], y = _a[1], z = _a[2];
159
- x1 = Math.min(x1, x);
160
- y1 = Math.min(y1, y);
161
- z1 = Math.min(z1, z);
162
- x2 = Math.max(x2, x);
163
- y2 = Math.max(y2, y);
164
- z2 = Math.max(z2, z);
165
- }
166
- return { x1: x1, y1: y1, z1: z1, x2: x2, y2: y2, z2: z2 };
167
- }
168
248
  export function redrawFreehandDraft(nv, draft) {
169
- var _a, _b, _c;
170
- if (!(draft === null || draft === void 0 ? void 0 : draft.baseBitmap))
249
+ var _a;
250
+ if (!((_a = draft === null || draft === void 0 ? void 0 : draft.strokeVoxels) === null || _a === void 0 ? void 0 : _a.length) || !draft.baseBitmap)
171
251
  return;
172
252
  nv.drawBitmap.set(draft.baseBitmap);
173
- nv.drawPenAxCorSag = draft.axCorSag;
174
- if (!draft.filled) {
175
- if (!((_a = draft.strokeVoxels) === null || _a === void 0 ? void 0 : _a.length))
176
- return;
177
- for (var _i = 0, _d = draft.strokeVoxels; _i < _d.length; _i++) {
178
- var _e = _d[_i], x = _e[0], y = _e[1], z = _e[2];
179
- nv.drawPt(x, y, z, draft.penValue);
180
- }
181
- }
182
- else if (((_b = draft.pathVertices) === null || _b === void 0 ? void 0 : _b.length) >= 3) {
183
- for (var _f = 0, _g = draft.strokeVoxels || []; _f < _g.length; _f++) {
184
- var _h = _g[_f], x = _h[0], y = _h[1], z = _h[2];
185
- nv.drawPt(x, y, z, draft.penValue);
186
- }
187
- nv.drawPenFillPts = draft.pathVertices.map(function (v) { return __spreadArray([], v, true); });
188
- nv._cloudMrSkipNextUndoBitmap = true;
189
- nv.drawPenFilled();
190
- }
191
- else if ((_c = draft.strokeVoxels) === null || _c === void 0 ? void 0 : _c.length) {
192
- for (var _j = 0, _k = draft.strokeVoxels; _j < _k.length; _j++) {
193
- var _l = _k[_j], x = _l[0], y = _l[1], z = _l[2];
194
- nv.drawPt(x, y, z, draft.penValue);
195
- }
196
- }
197
- else {
198
- return;
253
+ // strokeVoxels already contains every expanded/thickened voxel of the original
254
+ // stroke. Replaying them with penBounds=0 stamps each voxel directly without
255
+ // re-expanding, preventing thickness from accumulating across edits.
256
+ var savedPenBounds = nv.opts.penBounds;
257
+ nv.opts.penBounds = 0;
258
+ for (var _i = 0, _b = draft.strokeVoxels; _i < _b.length; _i++) {
259
+ var _c = _b[_i], x = _c[0], y = _c[1], z = _c[2];
260
+ nv.drawPt(x, y, z, draft.penValue);
199
261
  }
262
+ nv.opts.penBounds = savedPenBounds;
200
263
  nv.refreshDrawing(false, false);
201
264
  nv.drawScene();
202
265
  }
@@ -302,21 +365,6 @@ export function polylineDraftFromNv(nv, _a) {
302
365
  filled: filled
303
366
  };
304
367
  }
305
- /** Fill freehand interior from the traced stroke path (outline stays editable until Apply). */
306
- export function fillFreehandDraft(nv, draft) {
307
- var path = draft.pathVertices;
308
- if (!path || path.length < 3)
309
- return draft;
310
- redrawFreehandDraft(nv, __assign(__assign({}, draft), { filled: false }));
311
- nv.drawPenAxCorSag = draft.axCorSag;
312
- nv.drawPenFillPts = path.map(function (v) { return __spreadArray([], v, true); });
313
- nv._cloudMrSkipNextUndoBitmap = true;
314
- nv.drawPenFilled();
315
- nv.refreshDrawing(false, false);
316
- nv.drawScene();
317
- var strokeVoxels = collectStrokeVoxelsFromBitmap(nv, draft);
318
- return __assign(__assign({}, draft), { filled: true, strokeVoxels: strokeVoxels, bounds: boundsFromVoxels(strokeVoxels) });
319
- }
320
368
  /** Fill polyline interior without closing the outline or committing the draft. */
321
369
  export function fillPolylineDraft(nv, draft) {
322
370
  if (!(draft === null || draft === void 0 ? void 0 : draft.vertices) || draft.vertices.length < 3)
@@ -332,9 +380,35 @@ export function fillPolylineDraft(nv, draft) {
332
380
  syncPolylineDraftToNv(nv, next);
333
381
  return next;
334
382
  }
383
+ /** Undo a previous fill — revert to outline-only without committing the draft. */
384
+ export function unfillPolylineDraft(nv, draft) {
385
+ if (!(draft === null || draft === void 0 ? void 0 : draft.vertices))
386
+ return draft;
387
+ var next = __assign(__assign({}, draft), { filled: false });
388
+ redrawPolylineDraft(nv, next);
389
+ nv.refreshDrawing(false, false);
390
+ nv.drawScene();
391
+ syncPolylineDraftToNv(nv, next);
392
+ return next;
393
+ }
335
394
  export function applyPenDraft(nv, draft) {
395
+ var _a;
336
396
  if (draft.kind === "polyline") {
337
- redrawPolylineDraft(nv, draft);
397
+ // Prefer live vertex state; draft snapshots can lag behind the canvas.
398
+ if (((_a = nv._cloudMrPolylineVertices) === null || _a === void 0 ? void 0 : _a.length) >= 2) {
399
+ var fresh = polylineDraftFromNv(nv, { filled: !!draft.filled });
400
+ if (fresh)
401
+ draft = fresh;
402
+ }
403
+ // Commit the incrementally drawn bitmap (drops rubber-band preview to cursor).
404
+ if (nv._cloudMrPolylineBaseBitmap) {
405
+ nv.drawBitmap.set(nv._cloudMrPolylineBaseBitmap);
406
+ nv.refreshDrawing(false, false);
407
+ nv.drawScene();
408
+ }
409
+ else {
410
+ redrawPolylineDraft(nv, draft);
411
+ }
338
412
  }
339
413
  else {
340
414
  redrawFreehandDraft(nv, draft);
@@ -343,6 +417,27 @@ export function applyPenDraft(nv, draft) {
343
417
  if (typeof nv.onDrawingChanged === "function") {
344
418
  nv.onDrawingChanged("draw");
345
419
  }
420
+ return draft;
421
+ }
422
+ /**
423
+ * Reconstruct a polyline PenDraft from stored vertices (not flood-fill).
424
+ * Returns null if the click didn't land on a registered polyline.
425
+ */
426
+ export function capturePolylineDraftFromClick(nv) {
427
+ var seedVox = voxFromMouse(nv);
428
+ var entry = findPolylineRegistryEntry(nv, seedVox);
429
+ if (!entry)
430
+ return null;
431
+ var baseBitmap = eraseClusterFromBitmap(nv.drawBitmap, entry.voxelIndices);
432
+ return {
433
+ kind: "polyline",
434
+ baseBitmap: baseBitmap,
435
+ axCorSag: entry.axCorSag,
436
+ penValue: entry.penValue,
437
+ vertices: entry.vertices.map(function (v) { return __spreadArray([], v, true); }),
438
+ filled: entry.filled,
439
+ _registryId: entry.id
440
+ };
346
441
  }
347
442
  /**
348
443
  * Flood-fill from the clicked voxel to reconstruct a freehand PenDraft for re-editing.
@@ -350,7 +445,7 @@ export function applyPenDraft(nv, draft) {
350
445
  */
351
446
  export function capturePenDraftFromClick(nv) {
352
447
  var seedVox = voxFromMouse(nv);
353
- var cluster = floodFillClusterFromVox(nv, seedVox);
448
+ var cluster = floodFillClusterFromVox(nv, seedVox, { connectivity: 26 });
354
449
  if (!cluster)
355
450
  return null;
356
451
  var label = cluster.label, visited = cluster.visited, voxels = cluster.voxels, bounds = cluster.bounds;
@@ -52,9 +52,12 @@ export function shouldDeferShapeCommit(nv: any): any;
52
52
  export function inferAxCorSagFromBounds(x1: any, y1: any, z1: any, x2: any, y2: any, z2: any, fallback?: number): number;
53
53
  /**
54
54
  * Flood-fill a connected voxel cluster from a seed.
55
+ * @param {{ connectivity?: 6 | 26 }} [options]
55
56
  * @returns {{ label: number, visited: Set<number>, voxels: [number,number,number][], bounds: object } | null}
56
57
  */
57
- export function floodFillClusterFromVox(nv: any, seedVox: any): {
58
+ export function floodFillClusterFromVox(nv: any, seedVox: any, { connectivity }?: {
59
+ connectivity?: number | undefined;
60
+ }): {
58
61
  label: number;
59
62
  visited: Set<number>;
60
63
  voxels: [number, number, number][];
@@ -217,13 +217,36 @@ function sliceKey(axCorSag, x, y, z) {
217
217
  return "".concat(x, ",").concat(z);
218
218
  return "".concat(y, ",").concat(z);
219
219
  }
220
+ var NEIGHBORS_6 = [
221
+ [1, 0, 0],
222
+ [-1, 0, 0],
223
+ [0, 1, 0],
224
+ [0, -1, 0],
225
+ [0, 0, 1],
226
+ [0, 0, -1],
227
+ ];
228
+ var NEIGHBORS_26 = (function () {
229
+ var out = [];
230
+ for (var dx = -1; dx <= 1; dx++) {
231
+ for (var dy = -1; dy <= 1; dy++) {
232
+ for (var dz = -1; dz <= 1; dz++) {
233
+ if (dx === 0 && dy === 0 && dz === 0)
234
+ continue;
235
+ out.push([dx, dy, dz]);
236
+ }
237
+ }
238
+ }
239
+ return out;
240
+ })();
220
241
  /**
221
242
  * Flood-fill a connected voxel cluster from a seed.
243
+ * @param {{ connectivity?: 6 | 26 }} [options]
222
244
  * @returns {{ label: number, visited: Set<number>, voxels: [number,number,number][], bounds: object } | null}
223
245
  */
224
- export function floodFillClusterFromVox(nv, seedVox) {
225
- var _a;
226
- var dims = (_a = nv.back) === null || _a === void 0 ? void 0 : _a.dims;
246
+ export function floodFillClusterFromVox(nv, seedVox, _a) {
247
+ var _b;
248
+ var _c = _a === void 0 ? {} : _a, _d = _c.connectivity, connectivity = _d === void 0 ? 6 : _d;
249
+ var dims = (_b = nv.back) === null || _b === void 0 ? void 0 : _b.dims;
227
250
  if (!dims || !nv.drawBitmap || !seedVox)
228
251
  return null;
229
252
  var dx = dims[1];
@@ -233,6 +256,7 @@ export function floodFillClusterFromVox(nv, seedVox) {
233
256
  var label = nv.drawBitmap[seedIdx];
234
257
  if (!label)
235
258
  return null;
259
+ var neighborOffsets = connectivity === 26 ? NEIGHBORS_26 : NEIGHBORS_6;
236
260
  var visited = new Set();
237
261
  var queue = [seedIdx];
238
262
  visited.add(seedIdx);
@@ -245,7 +269,7 @@ export function floodFillClusterFromVox(nv, seedVox) {
245
269
  var z2 = -Infinity;
246
270
  while (queue.length > 0) {
247
271
  var idx = queue.shift();
248
- var _b = decodeVoxelIndex(idx, dx, dy), x = _b[0], y = _b[1], z = _b[2];
272
+ var _e = decodeVoxelIndex(idx, dx, dy), x = _e[0], y = _e[1], z = _e[2];
249
273
  voxels.push([x, y, z]);
250
274
  x1 = Math.min(x1, x);
251
275
  y1 = Math.min(y1, y);
@@ -253,16 +277,11 @@ export function floodFillClusterFromVox(nv, seedVox) {
253
277
  x2 = Math.max(x2, x);
254
278
  y2 = Math.max(y2, y);
255
279
  z2 = Math.max(z2, z);
256
- var neighbors = [
257
- [x + 1, y, z],
258
- [x - 1, y, z],
259
- [x, y + 1, z],
260
- [x, y - 1, z],
261
- [x, y, z + 1],
262
- [x, y, z - 1],
263
- ];
264
- for (var _i = 0, neighbors_1 = neighbors; _i < neighbors_1.length; _i++) {
265
- var _c = neighbors_1[_i], nx = _c[0], ny = _c[1], nz = _c[2];
280
+ for (var _i = 0, neighborOffsets_1 = neighborOffsets; _i < neighborOffsets_1.length; _i++) {
281
+ var _f = neighborOffsets_1[_i], ox = _f[0], oy = _f[1], oz = _f[2];
282
+ var nx = x + ox;
283
+ var ny = y + oy;
284
+ var nz = z + oz;
266
285
  if (nx < 0 || ny < 0 || nz < 0 || nx >= dx || ny >= dy || nz >= dz)
267
286
  continue;
268
287
  var nIdx = voxelIndex(nx, ny, nz, dx, dy);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudmr-ux",
3
- "version": "4.7.3",
3
+ "version": "4.7.5",
4
4
  "author": "erosmontin@gmail.com",
5
5
  "license": "MIT",
6
6
  "repository": "erosmontin/cloudmr-ux",