cloudmr-ux 4.7.2 → 4.7.4

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 = penMode === "freehand";
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, polylineDraftFromNv, syncPolylineDraftToNv, } from './penDraftUtils';
66
+ import { applyPenDraft, cancelPenDraft, 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';
@@ -837,7 +837,11 @@ export default function CloudMrNiivueViewer(props) {
837
837
  var draft = penDraftRef.current;
838
838
  if (!draft)
839
839
  return;
840
+ var registryId = draft._registryId;
840
841
  cancelPenDraft(nv, draft);
842
+ if (registryId && draft.kind === "polyline") {
843
+ restoreCommittedPolyline(nv, registryId);
844
+ }
841
845
  if (draft.kind === "polyline") {
842
846
  (_a = nv.cloudMrResetPolyline) === null || _a === void 0 ? void 0 : _a.call(nv);
843
847
  setPolylineVertexCount(0);
@@ -850,22 +854,41 @@ export default function CloudMrNiivueViewer(props) {
850
854
  }
851
855
  }
852
856
  function applyPenDraftHandler(_a) {
853
- var _b;
854
- var _c = _a === void 0 ? {} : _a, _d = _c.keepTool, keepTool = _d === void 0 ? false : _d;
857
+ var _b, _c, _d;
858
+ var _e = _a === void 0 ? {} : _a, _f = _e.keepTool, keepTool = _f === void 0 ? false : _f;
855
859
  var draft = penDraftRef.current;
856
860
  if (!draft)
857
861
  return;
858
- applyPenDraft(nv, draft);
862
+ if (draft.kind === "polyline" && ((_b = nv._cloudMrPolylineVertices) === null || _b === void 0 ? void 0 : _b.length) >= 2) {
863
+ var fresh = polylineDraftFromNv(nv, { filled: !!draft.filled });
864
+ if (fresh) {
865
+ draft = fresh;
866
+ penDraftRef.current = draft;
867
+ }
868
+ }
869
+ draft = (_c = applyPenDraft(nv, draft)) !== null && _c !== void 0 ? _c : draft;
870
+ if (draft.kind === "polyline" && nv._cloudMrPolylineSessionStartBitmap) {
871
+ draft = __assign(__assign({}, draft), { baseBitmap: new Uint8Array(nv._cloudMrPolylineSessionStartBitmap) });
872
+ }
873
+ penDraftRef.current = draft;
859
874
  markPenVoxelKind(draft, draft.kind === "polyline" ? 3 : 2);
860
875
  if (draft.kind === "polyline") {
861
- (_b = nv.cloudMrResetPolyline) === null || _b === void 0 ? void 0 : _b.call(nv);
876
+ registerAppliedPolyline(nv, draft, draft._registryId);
877
+ (_d = nv.cloudMrResetPolyline) === null || _d === void 0 ? void 0 : _d.call(nv);
862
878
  setPolylineVertexCount(0);
863
879
  }
864
880
  setPenDraft(null);
865
881
  penDraftRef.current = null;
866
882
  nv._cloudMrPenDraftActive = false;
867
883
  setDrawingChanged(true);
868
- if (!keepTool) {
884
+ if (keepTool) {
885
+ var mode = penDrawModeRef.current;
886
+ nv.opts.polylinePenMode = mode === "polyline";
887
+ nv.opts.isFilledPen = mode === "freehand";
888
+ nv.opts.deferFreehandCommit = mode === "freehand";
889
+ nvSetDrawingEnabled(true);
890
+ }
891
+ else {
869
892
  // Deactivate tool entirely — palette closes, user re-enters edit by clicking the ROI
870
893
  setDrawShapeTool(null);
871
894
  nv.opts.deferFreehandCommit = false;
@@ -878,7 +901,9 @@ export default function CloudMrNiivueViewer(props) {
878
901
  var draft = penDraftRef.current;
879
902
  if (!draft || draft.kind !== "polyline" || draft.vertices.length < 3)
880
903
  return;
881
- var next = fillPolylineDraft(nv, draft);
904
+ var next = draft.filled
905
+ ? unfillPolylineDraft(nv, draft)
906
+ : fillPolylineDraft(nv, draft);
882
907
  onPenDraftChange(next);
883
908
  }
884
909
  function onPenDraftChange(draft) {
@@ -900,14 +925,18 @@ export default function CloudMrNiivueViewer(props) {
900
925
  // Called by NiivuePatcher when the user clicks an applied pen ROI to re-edit it.
901
926
  // penKind: 2=freehand, 3=polyline
902
927
  nv.onPenDraftReopenReady = function (draft, penKind) {
928
+ var _a, _b;
903
929
  setPenDraft(draft);
904
930
  penDraftRef.current = draft;
905
931
  nv._cloudMrPenDraftActive = true;
906
932
  // Auto-select pen tool so the palette opens
907
933
  setDrawShapeTool("pen");
908
- var mode = penKind === 3 ? "polyline" : "freehand";
934
+ var mode = draft.kind === "polyline" ? "polyline" : "freehand";
909
935
  setPenDrawMode(mode);
910
936
  penDrawModeRef.current = mode;
937
+ if (draft.kind === "polyline") {
938
+ setPolylineVertexCount((_b = (_a = draft.vertices) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0);
939
+ }
911
940
  nv.opts.deferFreehandCommit = false;
912
941
  nv.opts.polylinePenMode = false;
913
942
  nvSetDrawingEnabled(false);
@@ -1010,11 +1039,18 @@ export default function CloudMrNiivueViewer(props) {
1010
1039
  ensureToolKindBitmap();
1011
1040
  var tkb = nv._cloudMrToolKindBitmap;
1012
1041
  var dims = (_a = nv.back) === null || _a === void 0 ? void 0 : _a.dims;
1042
+ if (draft.kind === "polyline") {
1043
+ for (var _i = 0, _b = collectPolylineAppliedVoxelIndices(nv, draft); _i < _b.length; _i++) {
1044
+ var idx = _b[_i];
1045
+ tkb[idx] = kind;
1046
+ }
1047
+ return;
1048
+ }
1013
1049
  if (draft.kind === "freehand" && draft.strokeVoxels && dims) {
1014
1050
  var dx = dims[1];
1015
1051
  var dy = dims[2];
1016
- for (var _i = 0, _b = draft.strokeVoxels; _i < _b.length; _i++) {
1017
- var _c = _b[_i], x = _c[0], y = _c[1], z = _c[2];
1052
+ for (var _c = 0, _d = draft.strokeVoxels; _c < _d.length; _c++) {
1053
+ var _e = _d[_c], x = _e[0], y = _e[1], z = _e[2];
1018
1054
  tkb[x + y * dx + z * dx * dy] = kind;
1019
1055
  }
1020
1056
  }
@@ -1621,6 +1657,7 @@ export default function CloudMrNiivueViewer(props) {
1621
1657
  onCancelPenDraft: cancelPenDraftHandler,
1622
1658
  onFillPenDraft: fillPolylineDraftHandler,
1623
1659
  penDraftActive: penDraft != null,
1660
+ penDraftFilled: (penDraft === null || penDraft === void 0 ? void 0 : penDraft.filled) === true,
1624
1661
  brushSize: brushSize,
1625
1662
  updateBrushSize: nvUpdateBrushSize,
1626
1663
  onActivateEraser: activateEraser,
@@ -24,8 +24,12 @@ import {
24
24
  import {
25
25
  captureFreehandDraft,
26
26
  capturePenDraftFromClick,
27
+ capturePolylineDraftFromClick,
28
+ isRegisteredPolylineClick,
27
29
  redrawFreehandDraft,
30
+ redrawPolylineDraft,
28
31
  shouldDeferFreehandCommit,
32
+ syncPolylineDraftToNv,
29
33
  } from "./penDraftUtils.js";
30
34
 
31
35
  /*
@@ -1513,6 +1517,7 @@ function cloudMrTryReopenShapeDraftOnClick(nv) {
1513
1517
  // If we know this voxel was drawn with the pen, skip shape reopen
1514
1518
  const kind = clickedVoxelToolKind(nv);
1515
1519
  if (kind === 2 || kind === 3) return false;
1520
+ if (isRegisteredPolylineClick(nv)) return false;
1516
1521
 
1517
1522
  const reopenDraft = captureShapeDraftFromClick(nv);
1518
1523
  if (!reopenDraft) return false;
@@ -1537,14 +1542,21 @@ function cloudMrTryReopenPenDraftOnClick(nv) {
1537
1542
  const kind = clickedVoxelToolKind(nv);
1538
1543
  if (kind === 1) return false;
1539
1544
 
1540
- const draft = capturePenDraftFromClick(nv);
1541
- if (!draft) return false;
1545
+ let draft = capturePolylineDraftFromClick(nv);
1546
+ let penKind = draft ? 3 : kind === 3 ? 3 : 2;
1542
1547
 
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;
1548
+ if (!draft) {
1549
+ draft = capturePenDraftFromClick(nv);
1550
+ if (!draft) return false;
1551
+ if (kind !== 3) penKind = 2;
1552
+ }
1546
1553
 
1547
- redrawFreehandDraft(nv, draft);
1554
+ if (draft.kind === "polyline") {
1555
+ redrawPolylineDraft(nv, draft);
1556
+ syncPolylineDraftToNv(nv, draft);
1557
+ } else {
1558
+ redrawFreehandDraft(nv, draft);
1559
+ }
1548
1560
  nv._cloudMrSuppressDrawingChangedMouseUp = true;
1549
1561
  if (typeof nv.onPenDraftReopenReady === "function") {
1550
1562
  nv.onPenDraftReopenReady(draft, penKind);
@@ -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, penDraftFilled, onApplyPenDraft, onFillPenDraft, brushSize, updateBrushSize, shapeDraftActive, onApplyShapeDraft, }: {
2
2
  expanded: any;
3
3
  updateDrawPen: any;
4
4
  setDrawingEnabled: any;
@@ -7,6 +7,7 @@ export default function DrawColorPlatte({ expanded, updateDrawPen, setDrawingEna
7
7
  onPenDrawModeChange: any;
8
8
  polylineVertexCount?: number | undefined;
9
9
  penDraftActive?: boolean | undefined;
10
+ penDraftFilled?: boolean | undefined;
10
11
  onApplyPenDraft: any;
11
12
  onFillPenDraft: any;
12
13
  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, _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,14 +53,16 @@ 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 && (_jsx(Tooltip, __assign({ title: "Fill interior (keeps outline editable until Apply)" }, { children: _jsx(Button, __assign({ size: "small", "aria-label": "fill polyline", onClick: function () { return onFillPenDraft === null || onFillPenDraft === void 0 ? void 0 : onFillPenDraft(); }, sx: {
57
- 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 && 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 && (_jsx(Tooltip, __assign({ title: penDraftFilled
57
+ ? "Remove fill (keeps outline editable)"
58
+ : "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: {
59
+ color: penDraftFilled ? "#ffb74d" : "#c9a0e8",
58
60
  fontSize: ACTION_FONT_SIZE,
59
61
  textTransform: "none",
60
62
  minWidth: 0,
61
63
  py: 0.25,
62
64
  px: 0.75
63
- } }, { children: "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
+ } }, { 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: {
64
66
  color: "#c9a0e8",
65
67
  fontSize: ACTION_FONT_SIZE,
66
68
  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, 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,15 +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 {{ x1: number, y1: number, x2: number, y2: number, z1: number, z2: number }} [bounds]
11
- * @property {boolean} [filled]
12
- */
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;
13
9
  export function isEraserActive(nv: any): any;
14
10
  export function isFreehandPenActive(nv: any): any;
15
11
  export function shouldDeferFreehandCommit(nv: any): any;
@@ -48,7 +44,22 @@ export function polylineDraftFromNv(nv: any, { filled }?: {
48
44
  } | null;
49
45
  /** Fill polyline interior without closing the outline or committing the draft. */
50
46
  export function fillPolylineDraft(nv: any, draft: any): any;
51
- 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;
52
63
  /**
53
64
  * Flood-fill from the clicked voxel to reconstruct a freehand PenDraft for re-editing.
54
65
  * Returns null if the click didn't land on a labeled voxel.
@@ -79,4 +90,5 @@ export type PenDraft = {
79
90
  z2: number;
80
91
  } | undefined;
81
92
  filled?: boolean | undefined;
93
+ _registryId?: number | undefined;
82
94
  };
@@ -32,7 +32,127 @@ import { voxFromMouse } from "./polylinePenUtils";
32
32
  * @property {[number, number, number][]} [strokeVoxels]
33
33
  * @property {{ x1: number, y1: number, x2: number, y2: number, z1: number, z2: number }} [bounds]
34
34
  * @property {boolean} [filled]
35
+ * @property {number} [_registryId]
35
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
+ }
36
156
  export function isEraserActive(nv) {
37
157
  return (nv.opts.drawingEnabled &&
38
158
  nv.opts.penType === NI_PEN_TYPE.PEN &&
@@ -130,10 +250,16 @@ export function redrawFreehandDraft(nv, draft) {
130
250
  if (!((_a = draft === null || draft === void 0 ? void 0 : draft.strokeVoxels) === null || _a === void 0 ? void 0 : _a.length) || !draft.baseBitmap)
131
251
  return;
132
252
  nv.drawBitmap.set(draft.baseBitmap);
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;
133
258
  for (var _i = 0, _b = draft.strokeVoxels; _i < _b.length; _i++) {
134
259
  var _c = _b[_i], x = _c[0], y = _c[1], z = _c[2];
135
260
  nv.drawPt(x, y, z, draft.penValue);
136
261
  }
262
+ nv.opts.penBounds = savedPenBounds;
137
263
  nv.refreshDrawing(false, false);
138
264
  nv.drawScene();
139
265
  }
@@ -254,9 +380,35 @@ export function fillPolylineDraft(nv, draft) {
254
380
  syncPolylineDraftToNv(nv, next);
255
381
  return next;
256
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
+ }
257
394
  export function applyPenDraft(nv, draft) {
395
+ var _a;
258
396
  if (draft.kind === "polyline") {
259
- 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
+ }
260
412
  }
261
413
  else {
262
414
  redrawFreehandDraft(nv, draft);
@@ -265,6 +417,27 @@ export function applyPenDraft(nv, draft) {
265
417
  if (typeof nv.onDrawingChanged === "function") {
266
418
  nv.onDrawingChanged("draw");
267
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
+ };
268
441
  }
269
442
  /**
270
443
  * Flood-fill from the clicked voxel to reconstruct a freehand PenDraft for re-editing.
@@ -272,7 +445,7 @@ export function applyPenDraft(nv, draft) {
272
445
  */
273
446
  export function capturePenDraftFromClick(nv) {
274
447
  var seedVox = voxFromMouse(nv);
275
- var cluster = floodFillClusterFromVox(nv, seedVox);
448
+ var cluster = floodFillClusterFromVox(nv, seedVox, { connectivity: 26 });
276
449
  if (!cluster)
277
450
  return null;
278
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.2",
3
+ "version": "4.7.4",
4
4
  "author": "erosmontin@gmail.com",
5
5
  "license": "MIT",
6
6
  "repository": "erosmontin/cloudmr-ux",