cloudmr-ux 4.8.3 → 4.8.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.
- package/dist/CmrComponents/niivue-viewer/CloudMrNiivueViewer.js +104 -31
- package/dist/CmrComponents/niivue-viewer/NiivuePatcher.js +9 -8
- package/dist/CmrComponents/niivue-viewer/PenDraftOverlay.js +10 -3
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/DrawColorPlatte.d.ts +2 -1
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/DrawColorPlatte.js +61 -29
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/EraserPlatte.d.ts +2 -3
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/EraserPlatte.js +6 -14
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/MroDrawToolkit.js +5 -7
- package/dist/CmrComponents/niivue-viewer/penDraftUtils.d.ts +20 -4
- package/dist/CmrComponents/niivue-viewer/penDraftUtils.js +41 -6
- package/dist/CmrComponents/niivue-viewer/polylinePenUtils.d.ts +6 -1
- package/dist/CmrComponents/niivue-viewer/polylinePenUtils.js +18 -0
- package/package.json +1 -1
|
@@ -63,8 +63,8 @@ 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, captureFreehandDraft, fillPolylineDraft, unfillPolylineDraft, polylineDraftFromNv, syncPolylineDraftToNv, registerAppliedPolyline,
|
|
67
|
-
import { registerAppliedShape, updateShapeRegistryErosionState, removeShapeRegistryEntry, clearShapeRegistry, } from './shapeDraftUtils';
|
|
66
|
+
import { applyPenDraft, cancelPenDraft, captureFreehandDraft, fillPolylineDraft, unfillPolylineDraft, polylineDraftFromNv, redrawPolylineDraft, redrawFreehandDraft, syncPolylineDraftToNv, registerAppliedPolyline, collectPolylineAppliedVoxelIndices, } from './penDraftUtils';
|
|
67
|
+
import { registerAppliedShape, updateShapeRegistryErosionState, removeShapeRegistryEntry, clearShapeRegistry, redrawDraftShape, } from './shapeDraftUtils';
|
|
68
68
|
import { CloudMrNiivuePanel } from './CloudMrNiivuePanel';
|
|
69
69
|
import { Niivue } from './NiivuePatcher';
|
|
70
70
|
import NVSwitch from './Switch';
|
|
@@ -578,22 +578,22 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
578
578
|
setBrushSize(size);
|
|
579
579
|
brushSizeRef.current = size;
|
|
580
580
|
var tool = drawShapeToolRef.current;
|
|
581
|
-
if ((tool === "pen" || tool === "polyline") && drawPen !==
|
|
581
|
+
if ((tool === "pen" || tool === "polyline") && drawPen !== 8) {
|
|
582
582
|
applyNvBrushSize(size);
|
|
583
583
|
}
|
|
584
584
|
}
|
|
585
585
|
function nvUpdateEraserSize(size) {
|
|
586
586
|
setEraserSize(size);
|
|
587
587
|
eraserSizeRef.current = size;
|
|
588
|
-
if (drawPen ===
|
|
588
|
+
if (drawPen === 8) {
|
|
589
589
|
applyNvBrushSize(size);
|
|
590
590
|
}
|
|
591
591
|
}
|
|
592
592
|
function nvUpdateDrawPen(a) {
|
|
593
|
-
var _a;
|
|
593
|
+
var _a, _b, _c;
|
|
594
594
|
var raw = Number(a.target.value);
|
|
595
595
|
setDrawPen(raw);
|
|
596
|
-
var isEraser = raw ===
|
|
596
|
+
var isEraser = raw === 8;
|
|
597
597
|
if (raw === 8) {
|
|
598
598
|
nv.setPenValue(0, true);
|
|
599
599
|
}
|
|
@@ -620,6 +620,43 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
620
620
|
nv.opts.isFilledPen = false;
|
|
621
621
|
applyNvBrushSize(brushSizeRef.current);
|
|
622
622
|
}
|
|
623
|
+
// Immediately repaint any active draft in the new color so the user sees the change.
|
|
624
|
+
if (!isEraser) {
|
|
625
|
+
var colorValue = raw & 7;
|
|
626
|
+
if (shapeDraftRef.current) {
|
|
627
|
+
var next = __assign(__assign({}, shapeDraftRef.current), { penValue: colorValue });
|
|
628
|
+
shapeDraftRef.current = next;
|
|
629
|
+
setShapeDraft(next);
|
|
630
|
+
redrawDraftShape(nv, next);
|
|
631
|
+
}
|
|
632
|
+
else if (penDraftRef.current) {
|
|
633
|
+
var next = __assign(__assign({}, penDraftRef.current), { penValue: colorValue });
|
|
634
|
+
penDraftRef.current = next;
|
|
635
|
+
setPenDraft(next);
|
|
636
|
+
if (next.kind === "polyline") {
|
|
637
|
+
var count = (_c = (_b = next.vertices) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0;
|
|
638
|
+
if (count >= 2) {
|
|
639
|
+
// Redraw segments in the new color, then update the base so the
|
|
640
|
+
// rubber-band preview also uses the new color.
|
|
641
|
+
redrawPolylineDraft(nv, __assign(__assign({}, next), { filled: false }));
|
|
642
|
+
if (nv.drawBitmap)
|
|
643
|
+
nv._cloudMrPolylineBaseBitmap = nv.drawBitmap.slice();
|
|
644
|
+
// Re-apply fill on top when polygon is closeable (base stays segments-only).
|
|
645
|
+
if (count >= 3) {
|
|
646
|
+
nv.drawPenAxCorSag = next.axCorSag;
|
|
647
|
+
nv.drawPenFillPts = next.vertices.map(function (v) { return __spreadArray([], v, true); });
|
|
648
|
+
nv._cloudMrSkipNextUndoBitmap = true;
|
|
649
|
+
nv.drawPenFilled();
|
|
650
|
+
nv.refreshDrawing(false, false);
|
|
651
|
+
nv.drawScene();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
else if (next.kind === "freehand") {
|
|
656
|
+
redrawFreehandDraft(nv, next);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
623
660
|
}
|
|
624
661
|
function deactivateDrawTools() {
|
|
625
662
|
var _a, _b;
|
|
@@ -873,17 +910,28 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
873
910
|
nv.opts.deferFreehandCommit = false;
|
|
874
911
|
}
|
|
875
912
|
function cancelPenDraftHandler() {
|
|
876
|
-
var _a;
|
|
913
|
+
var _a, _b;
|
|
877
914
|
var draft = penDraftRef.current;
|
|
878
915
|
if (!draft)
|
|
879
916
|
return;
|
|
880
917
|
var registryId = draft._registryId;
|
|
881
918
|
cancelPenDraft(nv, draft);
|
|
882
|
-
if (registryId
|
|
883
|
-
|
|
919
|
+
if (registryId) {
|
|
920
|
+
// Restore the exact committed voxels from the registry (works for both the
|
|
921
|
+
// legacy polyline-draft case and the new freehand-converted case).
|
|
922
|
+
var entry_1 = (_a = nv._cloudMrPolylineRegistry) === null || _a === void 0 ? void 0 : _a.find(function (e) { return e.id === registryId; });
|
|
923
|
+
if (entry_1) {
|
|
924
|
+
var bitmap_1 = nv.drawBitmap;
|
|
925
|
+
entry_1.voxelIndices.forEach(function (idx) {
|
|
926
|
+
if (idx < bitmap_1.length)
|
|
927
|
+
bitmap_1[idx] = entry_1.penValue;
|
|
928
|
+
});
|
|
929
|
+
nv.refreshDrawing(true, false);
|
|
930
|
+
nv.drawScene();
|
|
931
|
+
}
|
|
884
932
|
}
|
|
885
933
|
if (draft.kind === "polyline") {
|
|
886
|
-
(
|
|
934
|
+
(_b = nv.cloudMrResetPolyline) === null || _b === void 0 ? void 0 : _b.call(nv);
|
|
887
935
|
setPolylineVertexCount(0);
|
|
888
936
|
}
|
|
889
937
|
setPenDraft(null);
|
|
@@ -895,19 +943,25 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
895
943
|
}
|
|
896
944
|
}
|
|
897
945
|
function applyPenDraftHandler(_a) {
|
|
898
|
-
var _b, _c, _d;
|
|
899
|
-
var
|
|
946
|
+
var _b, _c, _d, _e, _f;
|
|
947
|
+
var _g = _a === void 0 ? {} : _a, _h = _g.keepTool, keepTool = _h === void 0 ? false : _h;
|
|
900
948
|
var draft = penDraftRef.current;
|
|
901
949
|
if (!draft)
|
|
902
950
|
return;
|
|
903
951
|
if (draft.kind === "polyline" && ((_b = nv._cloudMrPolylineVertices) === null || _b === void 0 ? void 0 : _b.length) >= 2) {
|
|
904
|
-
var fresh = polylineDraftFromNv(nv, { filled:
|
|
952
|
+
var fresh = polylineDraftFromNv(nv, { filled: ((_c = draft.vertices) === null || _c === void 0 ? void 0 : _c.length) >= 3 });
|
|
905
953
|
if (fresh) {
|
|
906
954
|
draft = fresh;
|
|
907
955
|
penDraftRef.current = draft;
|
|
908
956
|
}
|
|
909
957
|
}
|
|
910
|
-
|
|
958
|
+
// Auto-fill polyline on apply — fillPolylineDraft updates _cloudMrPolylineBaseBitmap
|
|
959
|
+
// to the filled state so applyPenDraft commits the correct filled bitmap.
|
|
960
|
+
if (draft.kind === "polyline" && ((_d = draft.vertices) === null || _d === void 0 ? void 0 : _d.length) >= 3) {
|
|
961
|
+
draft = fillPolylineDraft(nv, draft);
|
|
962
|
+
penDraftRef.current = draft;
|
|
963
|
+
}
|
|
964
|
+
draft = (_e = applyPenDraft(nv, draft)) !== null && _e !== void 0 ? _e : draft;
|
|
911
965
|
if (draft.kind === "polyline" && nv._cloudMrPolylineSessionStartBitmap) {
|
|
912
966
|
draft = __assign(__assign({}, draft), { baseBitmap: new Uint8Array(nv._cloudMrPolylineSessionStartBitmap) });
|
|
913
967
|
}
|
|
@@ -915,9 +969,14 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
915
969
|
markPenVoxelKind(draft, draft.kind === "polyline" ? 3 : 2);
|
|
916
970
|
if (draft.kind === "polyline") {
|
|
917
971
|
registerAppliedPolyline(nv, draft, draft._registryId);
|
|
918
|
-
(
|
|
972
|
+
(_f = nv.cloudMrResetPolyline) === null || _f === void 0 ? void 0 : _f.call(nv);
|
|
919
973
|
setPolylineVertexCount(0);
|
|
920
974
|
}
|
|
975
|
+
else if (draft._registryId && nv._cloudMrPolylineRegistry) {
|
|
976
|
+
// Was a committed polyline reopened as a freehand draft (move-only).
|
|
977
|
+
// Remove the stale registry entry so it is not matched on future clicks.
|
|
978
|
+
nv._cloudMrPolylineRegistry = nv._cloudMrPolylineRegistry.filter(function (e) { return e.id !== draft._registryId; });
|
|
979
|
+
}
|
|
921
980
|
setPenDraft(null);
|
|
922
981
|
penDraftRef.current = null;
|
|
923
982
|
nv._cloudMrPenDraftActive = false;
|
|
@@ -997,12 +1056,17 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
997
1056
|
var _a, _b;
|
|
998
1057
|
setPolylineVertexCount(count);
|
|
999
1058
|
if (drawShapeToolRef.current === "polyline" && count >= 2) {
|
|
1000
|
-
var
|
|
1001
|
-
var preserveFill = (prev === null || prev === void 0 ? void 0 : prev.kind) === "polyline" &&
|
|
1002
|
-
prev.filled &&
|
|
1003
|
-
((_a = prev.vertices) === null || _a === void 0 ? void 0 : _a.length) === count;
|
|
1004
|
-
var draft = polylineDraftFromNv(nv, { filled: preserveFill });
|
|
1059
|
+
var draft = polylineDraftFromNv(nv, { filled: count >= 3 });
|
|
1005
1060
|
if (draft) {
|
|
1061
|
+
if (count >= 3) {
|
|
1062
|
+
// Show fill live on canvas after each vertex commit.
|
|
1063
|
+
// Save/restore _cloudMrPolylineBaseBitmap (segments-only) so the
|
|
1064
|
+
// rubber-band preview still works correctly on mouse move.
|
|
1065
|
+
var savedBase = (_a = nv._cloudMrPolylineBaseBitmap) === null || _a === void 0 ? void 0 : _a.slice();
|
|
1066
|
+
redrawPolylineDraft(nv, draft);
|
|
1067
|
+
if (savedBase)
|
|
1068
|
+
nv._cloudMrPolylineBaseBitmap = savedBase;
|
|
1069
|
+
}
|
|
1006
1070
|
setPenDraft(draft);
|
|
1007
1071
|
penDraftRef.current = draft;
|
|
1008
1072
|
}
|
|
@@ -1245,25 +1309,34 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1245
1309
|
return undefined;
|
|
1246
1310
|
}
|
|
1247
1311
|
var canvas = document.getElementById("niiCanvas");
|
|
1248
|
-
if (!canvas) {
|
|
1249
|
-
return undefined;
|
|
1250
|
-
}
|
|
1251
1312
|
var applyOnRightClick = function (event) {
|
|
1252
1313
|
var _a;
|
|
1253
1314
|
event.preventDefault();
|
|
1254
1315
|
event.stopPropagation();
|
|
1255
1316
|
(_a = nv.onApplyActiveDraft) === null || _a === void 0 ? void 0 : _a.call(nv);
|
|
1256
1317
|
};
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1318
|
+
// Right-click on canvas: apply via Niivue's own handler.
|
|
1319
|
+
if (canvas) {
|
|
1320
|
+
canvas.addEventListener("contextmenu", applyOnRightClick, true);
|
|
1321
|
+
}
|
|
1322
|
+
// Any mousedown outside the canvas exits editing mode.
|
|
1323
|
+
// Clicks on the canvas are handled by Niivue's own mouseUpListener,
|
|
1324
|
+
// so we skip them here to avoid a double-apply race.
|
|
1325
|
+
var onDocMouseDown = function (event) {
|
|
1326
|
+
var _a;
|
|
1327
|
+
var target = event.target;
|
|
1328
|
+
var onCanvas = target === canvas || (canvas && canvas.contains(target));
|
|
1329
|
+
if (onCanvas)
|
|
1330
|
+
return;
|
|
1331
|
+
// Left or right click anywhere outside the canvas → apply draft.
|
|
1332
|
+
(_a = nv.onApplyActiveDraft) === null || _a === void 0 ? void 0 : _a.call(nv);
|
|
1261
1333
|
};
|
|
1262
|
-
|
|
1263
|
-
canvas.addEventListener("contextmenu", applyOnRightClick, true);
|
|
1334
|
+
document.addEventListener("mousedown", onDocMouseDown, true);
|
|
1264
1335
|
return function () {
|
|
1265
|
-
canvas
|
|
1266
|
-
|
|
1336
|
+
if (canvas) {
|
|
1337
|
+
canvas.removeEventListener("contextmenu", applyOnRightClick, true);
|
|
1338
|
+
}
|
|
1339
|
+
document.removeEventListener("mousedown", onDocMouseDown, true);
|
|
1267
1340
|
};
|
|
1268
1341
|
}, [shapeDraft, penDraft]);
|
|
1269
1342
|
React.useEffect(function () {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { NI_PEN_TYPE } from "./niivuePenType.js";
|
|
13
13
|
import {
|
|
14
14
|
addPolylineVertex,
|
|
15
|
+
POLYLINE_CLOSE,
|
|
15
16
|
axCorSagFromMouse,
|
|
16
17
|
cancelPolyline,
|
|
17
18
|
finishPolyline,
|
|
@@ -30,7 +31,6 @@ import {
|
|
|
30
31
|
redrawFreehandDraft,
|
|
31
32
|
redrawPolylineDraft,
|
|
32
33
|
shouldDeferFreehandCommit,
|
|
33
|
-
syncPolylineDraftToNv,
|
|
34
34
|
} from "./penDraftUtils.js";
|
|
35
35
|
|
|
36
36
|
/*
|
|
@@ -1587,12 +1587,9 @@ function cloudMrTryReopenPenDraftOnClick(nv) {
|
|
|
1587
1587
|
if (kind !== 3) penKind = 2;
|
|
1588
1588
|
}
|
|
1589
1589
|
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
} else {
|
|
1594
|
-
redrawFreehandDraft(nv, draft);
|
|
1595
|
-
}
|
|
1590
|
+
// capturePolylineDraftFromClick always returns a freehand draft now (exact stored
|
|
1591
|
+
// voxels, no vertex state, no re-flood-fill), so the redraw path is uniform.
|
|
1592
|
+
redrawFreehandDraft(nv, draft);
|
|
1596
1593
|
nv._cloudMrSuppressDrawingChangedMouseUp = true;
|
|
1597
1594
|
if (typeof nv.onPenDraftReopenReady === "function") {
|
|
1598
1595
|
nv.onPenDraftReopenReady(draft, penKind);
|
|
@@ -1668,7 +1665,11 @@ Niivue.prototype.mouseUpListener = function cloudMrMouseUpListener() {
|
|
|
1668
1665
|
_mouseUpListener.call(this);
|
|
1669
1666
|
|
|
1670
1667
|
if (polylineClick) {
|
|
1671
|
-
addPolylineVertex(this);
|
|
1668
|
+
const result = addPolylineVertex(this);
|
|
1669
|
+
if (result === POLYLINE_CLOSE && typeof this.onApplyActiveDraft === "function") {
|
|
1670
|
+
// Double-click detected — connect last vertex back to first, fill, and commit.
|
|
1671
|
+
this.onApplyActiveDraft();
|
|
1672
|
+
}
|
|
1672
1673
|
return;
|
|
1673
1674
|
}
|
|
1674
1675
|
|
|
@@ -24,6 +24,8 @@ import { boundsToFreehandCorners, redrawFreehandDraft, redrawPolylineDraft, sync
|
|
|
24
24
|
import { canvasDeltaToVoxDelta, clientToCanvasPos, voxToOverlayPos, voxUnderClient, } from "./shapeDraftUtils";
|
|
25
25
|
import { useNiivueViewerTheme } from "./NiivueViewerThemeContext";
|
|
26
26
|
var HANDLE_SIZE = 10;
|
|
27
|
+
/** Extra space (px) added around the polyline bounding box so it's easy to see and click. */
|
|
28
|
+
var POLYLINE_BOX_PADDING = 14;
|
|
27
29
|
function cloneFreehandDraft(draft) {
|
|
28
30
|
return __assign(__assign({}, draft), { strokeVoxels: draft.strokeVoxels.map(function (v) { return __spreadArray([], v, true); }), bounds: draft.bounds ? __assign({}, draft.bounds) : draft.bounds });
|
|
29
31
|
}
|
|
@@ -100,7 +102,13 @@ export function PenDraftOverlay(_a) {
|
|
|
100
102
|
var height = Math.max.apply(Math, ys) - top;
|
|
101
103
|
if (width < 2 && height < 2)
|
|
102
104
|
return null;
|
|
103
|
-
|
|
105
|
+
var pad = isPolyline ? POLYLINE_BOX_PADDING : 0;
|
|
106
|
+
return {
|
|
107
|
+
left: left - pad,
|
|
108
|
+
top: top - pad,
|
|
109
|
+
width: Math.max(width, 2) + pad * 2,
|
|
110
|
+
height: Math.max(height, 2) + pad * 2
|
|
111
|
+
};
|
|
104
112
|
}, [cornerCss, draft === null || draft === void 0 ? void 0 : draft.axCorSag, freehandCorners, isPolyline, nv, overlayKey]);
|
|
105
113
|
var applyDraft = useCallback(function (nextDraft) {
|
|
106
114
|
if (nextDraft.kind === "polyline") {
|
|
@@ -211,7 +219,6 @@ export function PenDraftOverlay(_a) {
|
|
|
211
219
|
background: theme.accentMutedBgLight,
|
|
212
220
|
boxSizing: "border-box",
|
|
213
221
|
pointerEvents: "none"
|
|
214
|
-
} }), centerCss && (_jsx("div", { role: "presentation", onPointerDown: function (e) { return startDrag(e, "move"); }, style: __assign(__assign({}, handleStyle), { left: centerCss.x, top: centerCss.y, cursor: "move", borderRadius: 2, width: 12, height: 12, marginLeft: -6, marginTop: -6 }), title: "Move shape" }))
|
|
215
|
-
cornerCss.map(function (pos, i) { return (_jsx("div", { role: "presentation", onPointerDown: function (e) { return startDrag(e, "corner", i); }, style: __assign(__assign({}, handleStyle), { left: pos.x, top: pos.y, cursor: "grab" }), title: "Move vertex" }, "handle-".concat(i))); })] })));
|
|
222
|
+
} }), centerCss && (_jsx("div", { role: "presentation", onPointerDown: function (e) { return startDrag(e, "move"); }, style: __assign(__assign({}, handleStyle), { left: centerCss.x, top: centerCss.y, cursor: "move", borderRadius: 2, width: 12, height: 12, marginLeft: -6, marginTop: -6 }), title: "Move shape" }))] })));
|
|
216
223
|
}
|
|
217
224
|
export default PenDraftOverlay;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export default function DrawColorPlatte({ expanded, updateDrawPen, setDrawingEnabled, penToolKind, polylineVertexCount, penDraftActive, penDraftKind, penDraftFilled, onApplyPenDraft, onDeletePenDraft, onFillPenDraft, brushSize, updateBrushSize, shapeDraftActive, onApplyShapeDraft, onDeleteShapeDraft, }: {
|
|
1
|
+
export default function DrawColorPlatte({ expanded, updateDrawPen, setDrawingEnabled, penToolKind, currentPenValue, polylineVertexCount, penDraftActive, penDraftKind, penDraftFilled, onApplyPenDraft, onDeletePenDraft, onFillPenDraft, brushSize, updateBrushSize, shapeDraftActive, onApplyShapeDraft, onDeleteShapeDraft, }: {
|
|
2
2
|
expanded: any;
|
|
3
3
|
updateDrawPen: any;
|
|
4
4
|
setDrawingEnabled: any;
|
|
5
5
|
penToolKind?: null | undefined;
|
|
6
|
+
currentPenValue?: number | undefined;
|
|
6
7
|
polylineVertexCount?: number | undefined;
|
|
7
8
|
penDraftActive?: boolean | undefined;
|
|
8
9
|
penDraftKind: any;
|
|
@@ -20,13 +20,24 @@ import CheckIcon from "@mui/icons-material/Check";
|
|
|
20
20
|
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
|
21
21
|
import { BrushSizeSlider } from "./BrushSizeSlider";
|
|
22
22
|
var FILLED_COLORS = [
|
|
23
|
-
{ sx: { color: "red" } },
|
|
24
|
-
{ sx: { color: "green" } },
|
|
25
|
-
{ sx: { color: "blue" } },
|
|
26
|
-
{ sx: { color: "yellow" } },
|
|
27
|
-
{ sx: { color: "cyan" } },
|
|
28
|
-
{ sx: { color: "#e81ce8" } },
|
|
23
|
+
{ sx: { color: "red" }, glow: "rgba(255, 72, 72, 0.55)" },
|
|
24
|
+
{ sx: { color: "green" }, glow: "rgba(72, 200, 72, 0.55)" },
|
|
25
|
+
{ sx: { color: "blue" }, glow: "rgba(72, 140, 255, 0.55)" },
|
|
26
|
+
{ sx: { color: "yellow" }, glow: "rgba(255, 220, 72, 0.5)" },
|
|
27
|
+
{ sx: { color: "cyan" }, glow: "rgba(72, 220, 255, 0.55)" },
|
|
28
|
+
{ sx: { color: "#e81ce8" }, glow: "rgba(232, 28, 232, 0.5)" },
|
|
29
29
|
];
|
|
30
|
+
/** Soft halo that hugs the circular icon shape (drop-shadow, not box-shadow). */
|
|
31
|
+
function swatchGlowFilter(glow, _a) {
|
|
32
|
+
var _b = _a === void 0 ? {} : _a, _c = _b.active, active = _c === void 0 ? false : _c, _d = _b.hover, hover = _d === void 0 ? false : _d;
|
|
33
|
+
if (active) {
|
|
34
|
+
return "drop-shadow(0 0 1px ".concat(glow, ") drop-shadow(0 0 4px ").concat(glow, ") drop-shadow(0 0 8px rgba(255, 255, 255, 0.12))");
|
|
35
|
+
}
|
|
36
|
+
if (hover) {
|
|
37
|
+
return "drop-shadow(0 0 2px rgba(255, 255, 255, 0.28))";
|
|
38
|
+
}
|
|
39
|
+
return "none";
|
|
40
|
+
}
|
|
30
41
|
var ACTION_FONT_SIZE = "0.75rem";
|
|
31
42
|
var ACTION_ICON_SIZE = "0.875rem";
|
|
32
43
|
export default function DrawColorPlatte(_a) {
|
|
@@ -34,7 +45,11 @@ export default function DrawColorPlatte(_a) {
|
|
|
34
45
|
/** @type {"freehand" | "polyline" | null} */
|
|
35
46
|
_b = _a.penToolKind,
|
|
36
47
|
/** @type {"freehand" | "polyline" | null} */
|
|
37
|
-
penToolKind = _b === void 0 ? null : _b,
|
|
48
|
+
penToolKind = _b === void 0 ? null : _b,
|
|
49
|
+
/** Currently active pen value (1–6); used to highlight the selected swatch. */
|
|
50
|
+
_c = _a.currentPenValue,
|
|
51
|
+
/** Currently active pen value (1–6); used to highlight the selected swatch. */
|
|
52
|
+
currentPenValue = _c === void 0 ? 1 : _c, _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, onDeletePenDraft = _a.onDeletePenDraft, 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, onDeleteShapeDraft = _a.onDeleteShapeDraft;
|
|
38
53
|
var isFreehandTool = penToolKind === "freehand";
|
|
39
54
|
var isPolylineTool = penToolKind === "polyline";
|
|
40
55
|
return (_jsxs(Stack, __assign({ style: {
|
|
@@ -50,10 +65,36 @@ export default function DrawColorPlatte(_a) {
|
|
|
50
65
|
borderTopLeftRadius: "6pt",
|
|
51
66
|
borderTopRightRadius: "6pt",
|
|
52
67
|
background: "#333"
|
|
53
|
-
}, direction: "column", spacing: 0.5, sx: { py: expanded ? 0.5 : 0 } }, { children: [(isFreehandTool || isPolylineTool) && expanded && updateBrushSize && (_jsx(BrushSizeSlider, { label: "Line thickness", brushSize: brushSize, updateBrushSize: updateBrushSize })), _jsx(Stack, __assign({ direction: "row" }, { children: FILLED_COLORS.map(function (color, index) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
}, direction: "column", spacing: 0.5, sx: { py: expanded ? 0.5 : 0 } }, { children: [(isFreehandTool || isPolylineTool) && expanded && updateBrushSize && (_jsx(BrushSizeSlider, { label: "Line thickness", brushSize: brushSize, updateBrushSize: updateBrushSize })), _jsx(Stack, __assign({ direction: "row", sx: { px: 0.25 } }, { children: FILLED_COLORS.map(function (color, index) {
|
|
69
|
+
var penValue = index + 1;
|
|
70
|
+
var isActive = penValue === (currentPenValue & 7);
|
|
71
|
+
return (_jsx(IconButton, __assign({ "aria-label": "Color ".concat(penValue), "aria-pressed": isActive, onClick: function () {
|
|
72
|
+
updateDrawPen({ target: { value: penValue } });
|
|
73
|
+
setDrawingEnabled(true);
|
|
74
|
+
}, sx: {
|
|
75
|
+
p: "6px",
|
|
76
|
+
borderRadius: "50%",
|
|
77
|
+
boxShadow: "none",
|
|
78
|
+
transition: "background-color 0.15s ease",
|
|
79
|
+
"&:hover": {
|
|
80
|
+
backgroundColor: "rgba(255, 255, 255, 0.06)"
|
|
81
|
+
},
|
|
82
|
+
"& .color-swatch": {
|
|
83
|
+
fontSize: "1.2rem",
|
|
84
|
+
display: "block",
|
|
85
|
+
transition: "filter 0.18s ease",
|
|
86
|
+
filter: swatchGlowFilter(color.glow, { active: isActive })
|
|
87
|
+
},
|
|
88
|
+
"&:hover .color-swatch": {
|
|
89
|
+
filter: swatchGlowFilter(color.glow, {
|
|
90
|
+
active: isActive,
|
|
91
|
+
hover: !isActive
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
} }, { children: _jsx(FiberManualRecordIcon, { className: "color-swatch", sx: color.sx }) }), index));
|
|
95
|
+
}) })), isPolylineTool && expanded && (_jsx(Typography, __assign({ sx: { px: 1, pb: 0.5, fontSize: "0.68rem", color: "#aaa", userSelect: "none" } }, { children: polylineVertexCount >= 3
|
|
96
|
+
? "Double-click to close & fill · Right-click to cancel"
|
|
97
|
+
: "Click to add vertices" }))), penDraftActive &&
|
|
57
98
|
expanded &&
|
|
58
99
|
((isFreehandTool && penDraftKind === "freehand") ||
|
|
59
100
|
(isPolylineTool && penDraftKind === "polyline")) && (_jsxs(Stack, __assign({ direction: "row", alignItems: "center", justifyContent: "space-between", sx: { px: 1, py: 0.5, borderTop: "1px solid #555", width: "100%" } }, { children: [_jsx(Tooltip, __assign({ title: "Delete this ROI drawing" }, { children: _jsx(Button, __assign({ size: "small", "aria-label": "delete pen draft", onClick: function () { return onDeletePenDraft === null || onDeletePenDraft === void 0 ? void 0 : onDeletePenDraft(); }, startIcon: _jsx(DeleteOutlineIcon, { sx: { fontSize: ACTION_ICON_SIZE } }), sx: {
|
|
@@ -64,24 +105,15 @@ export default function DrawColorPlatte(_a) {
|
|
|
64
105
|
py: 0.25,
|
|
65
106
|
px: 0.75,
|
|
66
107
|
"& .MuiButton-startIcon": { mr: 0.5, ml: 0 }
|
|
67
|
-
} }, { children: "Delete" })) })), isPolylineTool && (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
} }, { children: penDraftFilled ? "Undo Fill" : "Fill" })) }))), _jsx(Tooltip, __assign({ title: "Apply polyline (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: {
|
|
77
|
-
color: "#c9a0e8",
|
|
78
|
-
fontSize: ACTION_FONT_SIZE,
|
|
79
|
-
textTransform: "none",
|
|
80
|
-
minWidth: 0,
|
|
81
|
-
py: 0.25,
|
|
82
|
-
px: 0.75,
|
|
83
|
-
"& .MuiButton-startIcon": { mr: 0.5, ml: 0 }
|
|
84
|
-
} }, { children: "Apply" })) }))] })))] }))), shapeDraftActive && expanded && (_jsx(Stack, __assign({ direction: "row", alignItems: "center", justifyContent: "flex-start", sx: { px: 1, py: 0.5, borderTop: "1px solid #555", width: "100%" } }, { children: _jsx(Tooltip, __assign({ title: "Delete this ROI drawing" }, { children: _jsx(Button, __assign({ size: "small", "aria-label": "delete shape draft", onClick: function () { return onDeleteShapeDraft === null || onDeleteShapeDraft === void 0 ? void 0 : onDeleteShapeDraft(); }, startIcon: _jsx(DeleteOutlineIcon, { sx: { fontSize: ACTION_ICON_SIZE } }), sx: {
|
|
108
|
+
} }, { children: "Delete" })) })), isPolylineTool && (_jsx(Tooltip, __assign({ title: "Apply polyline (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: {
|
|
109
|
+
color: "#c9a0e8",
|
|
110
|
+
fontSize: ACTION_FONT_SIZE,
|
|
111
|
+
textTransform: "none",
|
|
112
|
+
minWidth: 0,
|
|
113
|
+
py: 0.25,
|
|
114
|
+
px: 0.75,
|
|
115
|
+
"& .MuiButton-startIcon": { mr: 0.5, ml: 0 }
|
|
116
|
+
} }, { children: "Apply" })) })))] }))), shapeDraftActive && expanded && (_jsx(Stack, __assign({ direction: "row", alignItems: "center", justifyContent: "flex-start", sx: { px: 1, py: 0.5, borderTop: "1px solid #555", width: "100%" } }, { children: _jsx(Tooltip, __assign({ title: "Delete this ROI drawing" }, { children: _jsx(Button, __assign({ size: "small", "aria-label": "delete shape draft", onClick: function () { return onDeleteShapeDraft === null || onDeleteShapeDraft === void 0 ? void 0 : onDeleteShapeDraft(); }, startIcon: _jsx(DeleteOutlineIcon, { sx: { fontSize: ACTION_ICON_SIZE } }), sx: {
|
|
85
117
|
color: "#f44336",
|
|
86
118
|
fontSize: ACTION_FONT_SIZE,
|
|
87
119
|
textTransform: "none",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/** Eraser palette — size control only (filled/region eraser is always used). */
|
|
2
|
+
export default function EraserPlatte({ expandEraseOptions, eraserSize, updateEraserSize, }: {
|
|
2
3
|
expandEraseOptions: any;
|
|
3
|
-
updateDrawPen: any;
|
|
4
|
-
setDrawingEnabled: any;
|
|
5
4
|
eraserSize?: number | undefined;
|
|
6
5
|
updateEraserSize: any;
|
|
7
6
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -9,18 +9,13 @@ var __assign = (this && this.__assign) || function () {
|
|
|
9
9
|
};
|
|
10
10
|
return __assign.apply(this, arguments);
|
|
11
11
|
};
|
|
12
|
-
import { jsx as _jsx
|
|
13
|
-
import { Stack
|
|
14
|
-
import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
|
|
15
|
-
import FiberManualRecordOutlinedIcon from "@mui/icons-material/FiberManualRecordOutlined";
|
|
12
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
|
+
import { Stack } from "@mui/material";
|
|
16
14
|
import { BrushSizeSlider } from "./BrushSizeSlider";
|
|
15
|
+
/** Eraser palette — size control only (filled/region eraser is always used). */
|
|
17
16
|
export default function EraserPlatte(_a) {
|
|
18
|
-
var expandEraseOptions = _a.expandEraseOptions,
|
|
19
|
-
|
|
20
|
-
_jsx(FiberManualRecordIcon, { style: { color: "white" } }, "e0"),
|
|
21
|
-
_jsx(FiberManualRecordOutlinedIcon, { style: { color: "white" } }, "e1"),
|
|
22
|
-
];
|
|
23
|
-
return (_jsxs(Stack, __assign({ style: {
|
|
17
|
+
var expandEraseOptions = _a.expandEraseOptions, _b = _a.eraserSize, eraserSize = _b === void 0 ? 1 : _b, updateEraserSize = _a.updateEraserSize;
|
|
18
|
+
return (_jsx(Stack, __assign({ style: {
|
|
24
19
|
position: "absolute",
|
|
25
20
|
top: "100%",
|
|
26
21
|
left: 0,
|
|
@@ -33,8 +28,5 @@ export default function EraserPlatte(_a) {
|
|
|
33
28
|
borderTopRightRadius: "6pt",
|
|
34
29
|
background: "#333",
|
|
35
30
|
width: 150
|
|
36
|
-
}, direction: "column" }, { children:
|
|
37
|
-
updateDrawPen({ target: { value: index === 0 ? 8 : 0 } });
|
|
38
|
-
setDrawingEnabled(true);
|
|
39
|
-
} }, { children: value }), index)); }) }))] })));
|
|
31
|
+
}, direction: "column" }, { children: updateEraserSize && (_jsx(BrushSizeSlider, { label: "Eraser size", brushSize: eraserSize, updateBrushSize: updateEraserSize })) })));
|
|
40
32
|
}
|
|
@@ -20,10 +20,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
20
20
|
import React, { useState, useEffect } from "react";
|
|
21
21
|
import { Box, Card, CardContent, IconButton, Slider, Stack, Tooltip, Typography, } from "@mui/material";
|
|
22
22
|
import DrawIcon from "@mui/icons-material/Draw";
|
|
23
|
-
import
|
|
23
|
+
import PolylineIcon from "@mui/icons-material/Polyline";
|
|
24
24
|
import CropSquareOutlinedIcon from "@mui/icons-material/CropSquareOutlined";
|
|
25
25
|
import CircleOutlinedIcon from "@mui/icons-material/CircleOutlined";
|
|
26
|
-
import AutoFixNormalOutlinedIcon from "@mui/icons-material/AutoFixNormalOutlined";
|
|
27
26
|
import ReplyIcon from "@mui/icons-material/Reply";
|
|
28
27
|
import CameraAltIcon from "@mui/icons-material/CameraAlt";
|
|
29
28
|
import SvgIcon from "@mui/material/SvgIcon";
|
|
@@ -80,9 +79,8 @@ export function MroDrawToolkit(props) {
|
|
|
80
79
|
var _b = useState("n"), expandedOption = _b[0], setExpandedOption = _b[1];
|
|
81
80
|
var _c = useState(false), expandOpacityOptions = _c[0], setExpandOpacityOptions = _c[1];
|
|
82
81
|
var _d = useState(undefined), setMaskColor = _d[1];
|
|
83
|
-
var filled = props.drawPen > 7;
|
|
84
82
|
useEffect(function () {
|
|
85
|
-
var eraserSelected = props.drawPen ===
|
|
83
|
+
var eraserSelected = props.drawPen === 8;
|
|
86
84
|
// Close palette when tool is deactivated programmatically (e.g. after Apply).
|
|
87
85
|
// Keep eraser palette open when switching from shape/pen edit → eraser in one click.
|
|
88
86
|
if (drawShapeTool === null && !props.shapeDraftActive && !props.penDraftActive) {
|
|
@@ -110,7 +108,7 @@ export function MroDrawToolkit(props) {
|
|
|
110
108
|
var eraserActive = expandedOption === "e";
|
|
111
109
|
function leaveEraserIfActive() {
|
|
112
110
|
var _a;
|
|
113
|
-
if (!eraserActive && props.drawPen !==
|
|
111
|
+
if (!eraserActive && props.drawPen !== 8) {
|
|
114
112
|
return;
|
|
115
113
|
}
|
|
116
114
|
(_a = props.onDeactivateDrawTools) === null || _a === void 0 ? void 0 : _a.call(props);
|
|
@@ -228,7 +226,7 @@ export function MroDrawToolkit(props) {
|
|
|
228
226
|
alignItems: "center",
|
|
229
227
|
gap: 4,
|
|
230
228
|
overflow: "visible"
|
|
231
|
-
} }, { children: [_jsxs(ToolClickAway, __assign({ active: expandedOption === "d", onClickAway: handleDrawToolClickAway, sx: __assign(__assign({}, toolGroupSx), { zIndex: expandedOption === "d" ? 1600 : "auto" }) }, { children: [_jsx(Tooltip, __assign({ title: "Freehand" }, { children: _jsx(IconButton, __assign({ "aria-label": "freehand", 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, penToolKind: "freehand", penDraftActive: props.penDraftActive, penDraftKind: props.penDraftKind, onDeletePenDraft: props.onDeletePenDraft, brushSize: props.brushSize, updateBrushSize: props.updateBrushSize })] })), _jsxs(ToolClickAway, __assign({ active: expandedOption === "p", onClickAway: handleDrawToolClickAway, sx: __assign(__assign({}, toolGroupSx), { zIndex: expandedOption === "p" ? 1600 : "auto" }) }, { children: [_jsx(Tooltip, __assign({ title: "Polyline" }, { children: _jsx(IconButton, __assign({ "aria-label": "polyline", size: "small", onClick: clickPolyline, sx: __assign(__assign({}, toolBtnSx), shapeSelectedSx("polyline")) }, { children: _jsx(
|
|
229
|
+
} }, { children: [_jsxs(ToolClickAway, __assign({ active: expandedOption === "d", onClickAway: handleDrawToolClickAway, sx: __assign(__assign({}, toolGroupSx), { zIndex: expandedOption === "d" ? 1600 : "auto" }) }, { children: [_jsx(Tooltip, __assign({ title: "Freehand" }, { children: _jsx(IconButton, __assign({ "aria-label": "freehand", 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, penToolKind: "freehand", currentPenValue: props.drawPen, penDraftActive: props.penDraftActive, penDraftKind: props.penDraftKind, onDeletePenDraft: props.onDeletePenDraft, brushSize: props.brushSize, updateBrushSize: props.updateBrushSize })] })), _jsxs(ToolClickAway, __assign({ active: expandedOption === "p", onClickAway: handleDrawToolClickAway, sx: __assign(__assign({}, toolGroupSx), { zIndex: expandedOption === "p" ? 1600 : "auto" }) }, { children: [_jsx(Tooltip, __assign({ title: "Polyline" }, { children: _jsx(IconButton, __assign({ "aria-label": "polyline", size: "small", onClick: clickPolyline, sx: __assign(__assign({}, toolBtnSx), shapeSelectedSx("polyline")) }, { children: _jsx(PolylineIcon, { sx: { color: "inherit" } }) })) })), _jsx(DrawColorPlatte, { expanded: expandedOption === "p", updateDrawPen: props.updateDrawPen, setDrawingEnabled: props.setDrawingEnabled, penToolKind: "polyline", currentPenValue: props.drawPen, polylineVertexCount: props.polylineVertexCount, penDraftActive: props.penDraftActive, penDraftKind: props.penDraftKind, penDraftFilled: props.penDraftFilled, onApplyPenDraft: props.onApplyPenDraft, onDeletePenDraft: props.onDeletePenDraft, onFillPenDraft: props.onFillPenDraft, brushSize: props.brushSize, updateBrushSize: props.updateBrushSize })] })), _jsxs(ToolClickAway, __assign({ active: expandedOption === "r", onClickAway: handleDrawToolClickAway, sx: __assign(__assign({}, toolGroupSx), { zIndex: expandedOption === "r" ? 1600 : "auto" }) }, { 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, currentPenValue: props.drawPen, shapeDraftActive: props.shapeDraftActive && drawShapeTool === "rectangle", onApplyShapeDraft: props.onApplyShapeDraft, onDeleteShapeDraft: props.onDeleteShapeDraft })] })), _jsxs(ToolClickAway, __assign({ active: expandedOption === "l", onClickAway: handleDrawToolClickAway, sx: __assign(__assign({}, toolGroupSx), { zIndex: expandedOption === "l" ? 1600 : "auto" }) }, { 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, currentPenValue: props.drawPen, shapeDraftActive: props.shapeDraftActive && drawShapeTool === "ellipse", onApplyShapeDraft: props.onApplyShapeDraft, onDeleteShapeDraft: props.onDeleteShapeDraft })] })), _jsxs(ToolClickAway, __assign({ active: expandedOption === "e", onClickAway: handleDrawToolClickAway, sx: __assign(__assign({}, toolGroupSx), { zIndex: expandedOption === "e" ? 1600 : "auto" }) }, { children: [_jsx(Tooltip, __assign({ title: "Eraser" }, { children: _jsx(IconButton, __assign({ "aria-label": "erase", size: "small", onClick: clickEraser, sx: __assign(__assign({}, toolBtnSx), (eraserActive ? theme.selectedToolSx : {})) }, { children: _jsx(EraserIcon, {}) })) })), _jsx(EraserPlatte, { expandEraseOptions: expandedOption === "e", eraserSize: props.eraserSize, updateEraserSize: props.updateEraserSize })] })), _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 () {
|
|
232
230
|
var _a;
|
|
233
231
|
(_a = props.onClearDrawing) === null || _a === void 0 ? void 0 : _a.call(props);
|
|
234
232
|
setExpandedOption("n");
|
|
@@ -239,7 +237,7 @@ export function MroDrawToolkit(props) {
|
|
|
239
237
|
display: "inline-flex",
|
|
240
238
|
alignItems: "center",
|
|
241
239
|
color: ICON_COLOR
|
|
242
|
-
} }, { children: [_jsx(Tooltip, __assign({ title: "
|
|
240
|
+
} }, { children: [_jsx(Tooltip, __assign({ title: "ROI opacity" }, { children: _jsx(IconButton, __assign({ "aria-label": "opaque", size: "small", onClick: function () { return setExpandOpacityOptions(!expandOpacityOptions); }, sx: toolBtnSx }, { children: _jsx(OpacityIcon, { sx: { color: ICON_COLOR } }) })) })), _jsx(Typography, __assign({ component: "span", sx: { fontSize: "0.7rem", mr: 0.25, userSelect: "none", color: ICON_COLOR } }, { children: props.drawingOpacity.toFixed(2) })), _jsx(OpacityPlatte, { drawingOpacity: props.drawingOpacity, setDrawingOpacity: props.setDrawingOpacity, expanded: expandOpacityOptions })] })), _jsxs(Box, __assign({ sx: { position: "relative", zIndex: expandedOption === "m" ? 1600 : "auto", display: "inline-flex", alignItems: "center" } }, { children: [_jsx(Tooltip, __assign({ title: "Mask by intensity range" }, { children: _jsx(IconButton, __assign({ "aria-label": "fill", size: "small", onClick: clickMask, sx: toolBtnSx }, { children: _jsx(FormatColorFillIcon, { sx: { color: ICON_COLOR } }) })) })), _jsx(MaskPlatte, { resampleImage: function () {
|
|
243
241
|
props.resampleImage();
|
|
244
242
|
props.setDrawingChanged(true);
|
|
245
243
|
}, expanded: expandedOption === "m", nv: props.nv, setMaskColor: setMaskColor, unfocus: function () { return setExpandedOption("n"); } })] }))] })) })) })) }))] })));
|
|
@@ -48,16 +48,32 @@ export function fillPolylineDraft(nv: any, draft: any): any;
|
|
|
48
48
|
export function unfillPolylineDraft(nv: any, draft: any): any;
|
|
49
49
|
export function applyPenDraft(nv: any, draft: any): any;
|
|
50
50
|
/**
|
|
51
|
-
* Reconstruct a
|
|
52
|
-
*
|
|
51
|
+
* Reconstruct a PenDraft from a registered polyline entry, returned as a
|
|
52
|
+
* **freehand** draft so it follows the same simple move-only edit flow.
|
|
53
|
+
*
|
|
54
|
+
* Using the exact stored voxel set instead of re-running drawPenFilled avoids
|
|
55
|
+
* two bugs:
|
|
56
|
+
* 1. Re-running the flood-fill on complex/self-intersecting shapes can produce
|
|
57
|
+
* a different pixel set (winding-rule differences, sub-voxel rounding).
|
|
58
|
+
* 2. syncPolylineDraftToNv was planting old vertices back into
|
|
59
|
+
* _cloudMrPolylineVertices; a React re-render could then re-enable
|
|
60
|
+
* polylinePenMode and the next canvas click would append to the stale
|
|
61
|
+
* vertices, visually "recreating" the old shape.
|
|
53
62
|
*/
|
|
54
63
|
export function capturePolylineDraftFromClick(nv: any): {
|
|
55
64
|
kind: string;
|
|
56
65
|
baseBitmap: Uint8Array;
|
|
57
66
|
axCorSag: any;
|
|
58
67
|
penValue: any;
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
strokeVoxels: any[];
|
|
69
|
+
bounds: {
|
|
70
|
+
x1: number;
|
|
71
|
+
y1: number;
|
|
72
|
+
z1: number;
|
|
73
|
+
x2: number;
|
|
74
|
+
y2: number;
|
|
75
|
+
z2: number;
|
|
76
|
+
};
|
|
61
77
|
_registryId: any;
|
|
62
78
|
} | null;
|
|
63
79
|
/**
|
|
@@ -420,22 +420,57 @@ export function applyPenDraft(nv, draft) {
|
|
|
420
420
|
return draft;
|
|
421
421
|
}
|
|
422
422
|
/**
|
|
423
|
-
* Reconstruct a
|
|
424
|
-
*
|
|
423
|
+
* Reconstruct a PenDraft from a registered polyline entry, returned as a
|
|
424
|
+
* **freehand** draft so it follows the same simple move-only edit flow.
|
|
425
|
+
*
|
|
426
|
+
* Using the exact stored voxel set instead of re-running drawPenFilled avoids
|
|
427
|
+
* two bugs:
|
|
428
|
+
* 1. Re-running the flood-fill on complex/self-intersecting shapes can produce
|
|
429
|
+
* a different pixel set (winding-rule differences, sub-voxel rounding).
|
|
430
|
+
* 2. syncPolylineDraftToNv was planting old vertices back into
|
|
431
|
+
* _cloudMrPolylineVertices; a React re-render could then re-enable
|
|
432
|
+
* polylinePenMode and the next canvas click would append to the stale
|
|
433
|
+
* vertices, visually "recreating" the old shape.
|
|
425
434
|
*/
|
|
426
435
|
export function capturePolylineDraftFromClick(nv) {
|
|
436
|
+
var _a;
|
|
427
437
|
var seedVox = voxFromMouse(nv);
|
|
428
438
|
var entry = findPolylineRegistryEntry(nv, seedVox);
|
|
429
|
-
if (!entry)
|
|
439
|
+
if (!entry || !((_a = nv.back) === null || _a === void 0 ? void 0 : _a.dims))
|
|
430
440
|
return null;
|
|
441
|
+
var dx = nv.back.dims[1];
|
|
442
|
+
var dy = nv.back.dims[2];
|
|
443
|
+
// Decode stored linear indices back to [x, y, z] triples.
|
|
444
|
+
var strokeVoxels = [];
|
|
445
|
+
var x1 = Infinity, y1 = Infinity, z1 = Infinity;
|
|
446
|
+
var x2 = -Infinity, y2 = -Infinity, z2 = -Infinity;
|
|
447
|
+
entry.voxelIndices.forEach(function (idx) {
|
|
448
|
+
var z = Math.floor(idx / (dx * dy));
|
|
449
|
+
var rem = idx - z * dx * dy;
|
|
450
|
+
var y = Math.floor(rem / dx);
|
|
451
|
+
var x = rem % dx;
|
|
452
|
+
strokeVoxels.push([x, y, z]);
|
|
453
|
+
if (x < x1)
|
|
454
|
+
x1 = x;
|
|
455
|
+
if (x > x2)
|
|
456
|
+
x2 = x;
|
|
457
|
+
if (y < y1)
|
|
458
|
+
y1 = y;
|
|
459
|
+
if (y > y2)
|
|
460
|
+
y2 = y;
|
|
461
|
+
if (z < z1)
|
|
462
|
+
z1 = z;
|
|
463
|
+
if (z > z2)
|
|
464
|
+
z2 = z;
|
|
465
|
+
});
|
|
431
466
|
var baseBitmap = eraseClusterFromBitmap(nv.drawBitmap, entry.voxelIndices);
|
|
432
467
|
return {
|
|
433
|
-
kind: "
|
|
468
|
+
kind: "freehand",
|
|
434
469
|
baseBitmap: baseBitmap,
|
|
435
470
|
axCorSag: entry.axCorSag,
|
|
436
471
|
penValue: entry.penValue,
|
|
437
|
-
|
|
438
|
-
|
|
472
|
+
strokeVoxels: strokeVoxels,
|
|
473
|
+
bounds: { x1: x1, y1: y1, z1: z1, x2: x2, y2: y2, z2: z2 },
|
|
439
474
|
_registryId: entry.id
|
|
440
475
|
};
|
|
441
476
|
}
|
|
@@ -6,7 +6,12 @@ export function notifyPolylineChange(nv: any, count: any): void;
|
|
|
6
6
|
export function resetPolylineState(nv: any): void;
|
|
7
7
|
export function cancelPolyline(nv: any): void;
|
|
8
8
|
export function previewPolylineSegment(nv: any): void;
|
|
9
|
-
export function addPolylineVertex(nv: any): boolean;
|
|
9
|
+
export function addPolylineVertex(nv: any): boolean | typeof POLYLINE_CLOSE;
|
|
10
10
|
export function finishPolyline(nv: any, { fillClosed }?: {
|
|
11
11
|
fillClosed?: boolean | undefined;
|
|
12
12
|
}): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Sentinel returned by addPolylineVertex when a double-click is detected with
|
|
15
|
+
* ≥3 existing vertices. The caller should close the polygon and auto-commit.
|
|
16
|
+
*/
|
|
17
|
+
export const POLYLINE_CLOSE: unique symbol;
|
|
@@ -46,6 +46,7 @@ export function resetPolylineState(nv) {
|
|
|
46
46
|
nv._cloudMrPolylineBaseBitmap = null;
|
|
47
47
|
nv._cloudMrPolylineSessionStartBitmap = null;
|
|
48
48
|
nv._cloudMrPolylineAxCorSag = -1;
|
|
49
|
+
nv._cloudMrLastPolylineClickMs = null;
|
|
49
50
|
notifyPolylineChange(nv, 0);
|
|
50
51
|
}
|
|
51
52
|
export function cancelPolyline(nv) {
|
|
@@ -69,12 +70,29 @@ export function previewPolylineSegment(nv) {
|
|
|
69
70
|
nv.refreshDrawing(false, false);
|
|
70
71
|
nv.drawScene();
|
|
71
72
|
}
|
|
73
|
+
/** Max ms between two clicks to be considered a double-click. */
|
|
74
|
+
var DOUBLE_CLICK_MS = 400;
|
|
75
|
+
/**
|
|
76
|
+
* Sentinel returned by addPolylineVertex when a double-click is detected with
|
|
77
|
+
* ≥3 existing vertices. The caller should close the polygon and auto-commit.
|
|
78
|
+
*/
|
|
79
|
+
export var POLYLINE_CLOSE = Symbol("POLYLINE_CLOSE");
|
|
72
80
|
export function addPolylineVertex(nv) {
|
|
73
81
|
var pt = voxFromMouse(nv);
|
|
74
82
|
if (!pt)
|
|
75
83
|
return false;
|
|
76
84
|
var penValue = nv.opts.penValue;
|
|
77
85
|
var verts = nv._cloudMrPolylineVertices || (nv._cloudMrPolylineVertices = []);
|
|
86
|
+
// Double-click detection: two clicks within DOUBLE_CLICK_MS on ≥3 vertices.
|
|
87
|
+
var now = Date.now();
|
|
88
|
+
var isDoubleClick = verts.length >= 3 &&
|
|
89
|
+
nv._cloudMrLastPolylineClickMs != null &&
|
|
90
|
+
now - nv._cloudMrLastPolylineClickMs <= DOUBLE_CLICK_MS;
|
|
91
|
+
nv._cloudMrLastPolylineClickMs = now;
|
|
92
|
+
if (isDoubleClick) {
|
|
93
|
+
// Don't add this click as a vertex — signal close instead.
|
|
94
|
+
return POLYLINE_CLOSE;
|
|
95
|
+
}
|
|
78
96
|
if (verts.length === 0) {
|
|
79
97
|
nv._cloudMrPolylineSessionStartBitmap = nv.drawBitmap.slice();
|
|
80
98
|
nv._cloudMrPolylineBaseBitmap = nv._cloudMrPolylineSessionStartBitmap.slice();
|