cloudmr-ux 4.8.0 → 4.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CmrComponents/niivue-viewer/CloudMrNiivuePanel.js +10 -1
- package/dist/CmrComponents/niivue-viewer/CloudMrNiivueViewer.js +72 -48
- package/dist/CmrComponents/niivue-viewer/NiivuePatcher.js +10 -0
- package/dist/CmrComponents/niivue-viewer/ShapeDraftOverlay.js +18 -7
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/EraserPlatte.d.ts +3 -3
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/EraserPlatte.js +2 -2
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/MroDrawToolkit.js +1 -1
- package/dist/CmrComponents/niivue-viewer/shapeDraftUtils.d.ts +32 -0
- package/dist/CmrComponents/niivue-viewer/shapeDraftUtils.js +209 -12
- package/package.json +1 -1
|
@@ -109,7 +109,7 @@ export function CloudMrNiivuePanel(props) {
|
|
|
109
109
|
props.resampleImage();
|
|
110
110
|
}, [histogram]);
|
|
111
111
|
function applyDrawShapeTool(tool) {
|
|
112
|
-
var _a, _b, _c, _d, _e;
|
|
112
|
+
var _a, _b, _c, _d, _e, _f;
|
|
113
113
|
if (props.shapeDraft) {
|
|
114
114
|
// Apply (commit) the current draft rather than discarding it
|
|
115
115
|
(_a = props.onApplyShapeDraftKeepTool) === null || _a === void 0 ? void 0 : _a.call(props);
|
|
@@ -139,6 +139,15 @@ export function CloudMrNiivuePanel(props) {
|
|
|
139
139
|
: tool === "ellipse"
|
|
140
140
|
? NI_PEN_TYPE.ELLIPSE
|
|
141
141
|
: NI_PEN_TYPE.PEN;
|
|
142
|
+
if (tool === "rectangle" || tool === "ellipse") {
|
|
143
|
+
nv.opts.penBounds = 0;
|
|
144
|
+
nv.opts.penSize = 1;
|
|
145
|
+
}
|
|
146
|
+
else if (tool === "pen") {
|
|
147
|
+
var penBrush = (_f = props.drawToolkitProps.brushSize) !== null && _f !== void 0 ? _f : 1;
|
|
148
|
+
nv.opts.penBounds = (penBrush - 1) / 2;
|
|
149
|
+
nv.opts.penSize = penBrush;
|
|
150
|
+
}
|
|
142
151
|
nv.drawScene();
|
|
143
152
|
if ((tool === "rectangle" || tool === "ellipse") && !props.drawToolkitProps.drawingEnabled) {
|
|
144
153
|
props.drawToolkitProps.setDrawingEnabled(true);
|
|
@@ -64,6 +64,7 @@ import { NumberPicker } from './NumberPicker';
|
|
|
64
64
|
import { ColorPicker } from './ColorPicker';
|
|
65
65
|
import { LayersPanel } from './LayersPanel';
|
|
66
66
|
import { applyPenDraft, cancelPenDraft, captureFreehandDraft, fillPolylineDraft, unfillPolylineDraft, polylineDraftFromNv, syncPolylineDraftToNv, registerAppliedPolyline, restoreCommittedPolyline, collectPolylineAppliedVoxelIndices, } from './penDraftUtils';
|
|
67
|
+
import { registerAppliedShape, updateShapeRegistryErosionState, removeShapeRegistryEntry, clearShapeRegistry, } from './shapeDraftUtils';
|
|
67
68
|
import { CloudMrNiivuePanel } from './CloudMrNiivuePanel';
|
|
68
69
|
import { Niivue } from './NiivuePatcher';
|
|
69
70
|
import NVSwitch from './Switch';
|
|
@@ -177,6 +178,9 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
177
178
|
var _25 = useState(0), polylineVertexCount = _25[0], setPolylineVertexCount = _25[1];
|
|
178
179
|
var _26 = useState(null), penDraft = _26[0], setPenDraft = _26[1];
|
|
179
180
|
var _27 = useState(1), brushSize = _27[0], setBrushSize = _27[1];
|
|
181
|
+
var _28 = useState(1), eraserSize = _28[0], setEraserSize = _28[1];
|
|
182
|
+
var brushSizeRef = React.useRef(1);
|
|
183
|
+
var eraserSizeRef = React.useRef(1);
|
|
180
184
|
var shapeDraftRef = React.useRef(null);
|
|
181
185
|
var penDraftRef = React.useRef(null);
|
|
182
186
|
var drawShapeToolRef = React.useRef(null);
|
|
@@ -189,8 +193,8 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
189
193
|
nv.opts.penBounds = 0;
|
|
190
194
|
nv.opts.penSize = 1;
|
|
191
195
|
}, []);
|
|
192
|
-
var
|
|
193
|
-
var
|
|
196
|
+
var _29 = React.useState(1.0), gamma = _29[0], setGamma = _29[1];
|
|
197
|
+
var _30 = React.useState(0), gammaKey = _30[0], setGammaKey = _30[1];
|
|
194
198
|
// Niivue → React bridge so other places (Toolbar) can force the UI to reset
|
|
195
199
|
nv.onResetGamma = function () {
|
|
196
200
|
setGamma(1.0);
|
|
@@ -235,13 +239,13 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
235
239
|
// setLayers([...nv.volumes])
|
|
236
240
|
// }, [])
|
|
237
241
|
// values dualslider
|
|
238
|
-
var
|
|
242
|
+
var _31 = useState(0), rangeKey = _31[0], setRangeKey = _31[1];
|
|
239
243
|
nv.onResetContrast = function () {
|
|
240
244
|
setRangeKey(rangeKey + 1);
|
|
241
245
|
};
|
|
242
|
-
var
|
|
243
|
-
var
|
|
244
|
-
var
|
|
246
|
+
var _32 = useState([0, 0, 0]), boundMins = _32[0], setBoundMins = _32[1];
|
|
247
|
+
var _33 = useState([1, 1, 1]), boundMaxs = _33[0], setBoundMaxs = _33[1];
|
|
248
|
+
var _34 = useState([0.5, 0.5, 0.5]), mms = _34[0], setMMs = _34[1];
|
|
245
249
|
nv.onImageLoaded = function () {
|
|
246
250
|
var _a, _b;
|
|
247
251
|
var oldCrosshairPos = __spreadArray([], nv.scene.crosshairPos, true);
|
|
@@ -437,6 +441,11 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
437
441
|
nv._cloudMrSuppressDrawingChangedMouseUp = false;
|
|
438
442
|
return;
|
|
439
443
|
}
|
|
444
|
+
if (nv.opts.drawingEnabled &&
|
|
445
|
+
nv.opts.penType === NI_PEN_TYPE.PEN &&
|
|
446
|
+
nv.opts.penValue === 0) {
|
|
447
|
+
updateShapeRegistryErosionState(nv);
|
|
448
|
+
}
|
|
440
449
|
if (nv.opts.drawingEnabled) {
|
|
441
450
|
setDrawingChanged(true);
|
|
442
451
|
resampleImage();
|
|
@@ -524,7 +533,7 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
524
533
|
setTextsVisible(true);
|
|
525
534
|
}
|
|
526
535
|
}
|
|
527
|
-
var
|
|
536
|
+
var _35 = useState("pan"), dragMode = _35[0], setDragMode = _35[1];
|
|
528
537
|
function nvSetDragMode(dragMode) {
|
|
529
538
|
switch (dragMode) {
|
|
530
539
|
case "none":
|
|
@@ -561,11 +570,24 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
561
570
|
nv.setDrawingEnabled(enabled);
|
|
562
571
|
nv.drawScene();
|
|
563
572
|
}
|
|
564
|
-
function
|
|
565
|
-
setBrushSize(size);
|
|
573
|
+
function applyNvBrushSize(size) {
|
|
566
574
|
nv.opts.penBounds = (size - 1) / 2;
|
|
567
575
|
nv.opts.penSize = size;
|
|
568
576
|
}
|
|
577
|
+
function nvUpdateBrushSize(size) {
|
|
578
|
+
setBrushSize(size);
|
|
579
|
+
brushSizeRef.current = size;
|
|
580
|
+
if (drawShapeToolRef.current === "pen" && drawPen !== 0 && drawPen !== 8) {
|
|
581
|
+
applyNvBrushSize(size);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function nvUpdateEraserSize(size) {
|
|
585
|
+
setEraserSize(size);
|
|
586
|
+
eraserSizeRef.current = size;
|
|
587
|
+
if (drawPen === 0 || drawPen === 8) {
|
|
588
|
+
applyNvBrushSize(size);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
569
591
|
function nvUpdateDrawPen(a) {
|
|
570
592
|
var _a;
|
|
571
593
|
var raw = Number(a.target.value);
|
|
@@ -583,21 +605,13 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
583
605
|
nv.opts.deferShapeCommit = false;
|
|
584
606
|
nv.opts.polylinePenMode = false;
|
|
585
607
|
(_a = nv.cloudMrResetPolyline) === null || _a === void 0 ? void 0 : _a.call(nv);
|
|
586
|
-
|
|
587
|
-
cancelPenDraft(nv, penDraftRef.current);
|
|
588
|
-
setPenDraft(null);
|
|
589
|
-
penDraftRef.current = null;
|
|
590
|
-
nv._cloudMrPenDraftActive = false;
|
|
591
|
-
setPolylineVertexCount(0);
|
|
592
|
-
}
|
|
593
|
-
if (shapeDraftRef.current) {
|
|
594
|
-
cancelShapeDraft();
|
|
595
|
-
}
|
|
608
|
+
applyActiveDraftIfAny();
|
|
596
609
|
}
|
|
597
610
|
else if (drawShapeToolRef.current === "pen") {
|
|
598
611
|
nv.opts.deferFreehandCommit = false;
|
|
599
612
|
nv.opts.polylinePenMode = penDrawModeRef.current === "polyline";
|
|
600
613
|
nv.opts.isFilledPen = penDrawModeRef.current === "freehand";
|
|
614
|
+
applyNvBrushSize(brushSizeRef.current);
|
|
601
615
|
}
|
|
602
616
|
}
|
|
603
617
|
function deactivateDrawTools() {
|
|
@@ -625,26 +639,17 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
625
639
|
nv.setPenValue(1, false);
|
|
626
640
|
nv.opts.isFilledPen = false;
|
|
627
641
|
}
|
|
642
|
+
applyNvBrushSize(1);
|
|
628
643
|
}
|
|
629
644
|
function activateEraser() {
|
|
630
|
-
|
|
645
|
+
applyActiveDraftIfAny();
|
|
631
646
|
setDrawShapeTool(null);
|
|
632
647
|
nv.opts.penType = NI_PEN_TYPE.PEN;
|
|
633
648
|
nv.opts.deferShapeCommit = false;
|
|
634
649
|
nv.opts.deferFreehandCommit = false;
|
|
635
650
|
nv.opts.polylinePenMode = false;
|
|
636
|
-
if (shapeDraftRef.current) {
|
|
637
|
-
cancelShapeDraft();
|
|
638
|
-
}
|
|
639
|
-
if (penDraftRef.current) {
|
|
640
|
-
cancelPenDraft(nv, penDraftRef.current);
|
|
641
|
-
(_a = nv.cloudMrResetPolyline) === null || _a === void 0 ? void 0 : _a.call(nv);
|
|
642
|
-
setPolylineVertexCount(0);
|
|
643
|
-
setPenDraft(null);
|
|
644
|
-
penDraftRef.current = null;
|
|
645
|
-
nv._cloudMrPenDraftActive = false;
|
|
646
|
-
}
|
|
647
651
|
nvUpdateDrawPen({ target: { value: 8 } });
|
|
652
|
+
applyNvBrushSize(eraserSizeRef.current);
|
|
648
653
|
nvSetDrawingEnabled(true);
|
|
649
654
|
}
|
|
650
655
|
function nvUpdateDrawOpacity(a) {
|
|
@@ -809,7 +814,7 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
809
814
|
nv.opts.crosshairWidth = w;
|
|
810
815
|
nv.drawScene();
|
|
811
816
|
}
|
|
812
|
-
var
|
|
817
|
+
var _36 = useState({}), labelMapping = _36[0], setLabelMapping = _36[1];
|
|
813
818
|
function resampleImage(mapping) {
|
|
814
819
|
if (mapping === void 0) { mapping = labelMapping; }
|
|
815
820
|
var el = typeof document !== "undefined" ? document.getElementById("histoplot") : null;
|
|
@@ -947,6 +952,7 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
947
952
|
nv.onShapeCommitted = function (draft) {
|
|
948
953
|
nv.drawAddUndoBitmap(nv.drawFillOverwrites);
|
|
949
954
|
markShapeVoxelKind(draft);
|
|
955
|
+
registerAppliedShape(nv, draft);
|
|
950
956
|
setDrawingChanged(true);
|
|
951
957
|
resampleImage();
|
|
952
958
|
};
|
|
@@ -1020,6 +1026,7 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1020
1026
|
nv._cloudMrPolylineBaseBitmap = null;
|
|
1021
1027
|
nv._cloudMrPolylineSessionStartBitmap = null;
|
|
1022
1028
|
nv._cloudMrToolKindBitmap = null;
|
|
1029
|
+
clearShapeRegistry(nv);
|
|
1023
1030
|
}
|
|
1024
1031
|
function clearDrawingHandler() {
|
|
1025
1032
|
var _a;
|
|
@@ -1057,6 +1064,9 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1057
1064
|
var draft = shapeDraftRef.current;
|
|
1058
1065
|
if (!(draft === null || draft === void 0 ? void 0 : draft.baseBitmap))
|
|
1059
1066
|
return;
|
|
1067
|
+
if (draft._registryId != null) {
|
|
1068
|
+
removeShapeRegistryEntry(nv, draft._registryId);
|
|
1069
|
+
}
|
|
1060
1070
|
// Clear tool-kind tags for voxels this shape occupied
|
|
1061
1071
|
if (nv._cloudMrToolKindBitmap) {
|
|
1062
1072
|
for (var i = 0; i < nv.drawBitmap.length; i++) {
|
|
@@ -1162,13 +1172,29 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1162
1172
|
}
|
|
1163
1173
|
}
|
|
1164
1174
|
}
|
|
1175
|
+
function applyActiveDraftIfAny() {
|
|
1176
|
+
if (shapeDraftRef.current) {
|
|
1177
|
+
applyShapeDraft();
|
|
1178
|
+
}
|
|
1179
|
+
else if (penDraftRef.current) {
|
|
1180
|
+
applyPenDraftHandler();
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1165
1183
|
function applyShapeDraft(_a) {
|
|
1166
|
-
var _b
|
|
1184
|
+
var _b, _c;
|
|
1185
|
+
var _d = _a === void 0 ? {} : _a, _e = _d.keepTool, keepTool = _e === void 0 ? false : _e;
|
|
1167
1186
|
var draft = shapeDraftRef.current;
|
|
1168
1187
|
if (!draft)
|
|
1169
1188
|
return;
|
|
1170
1189
|
nv.drawAddUndoBitmap(nv.drawFillOverwrites);
|
|
1171
1190
|
markShapeVoxelKind(draft);
|
|
1191
|
+
var registryId = registerAppliedShape(nv, draft, {
|
|
1192
|
+
existingId: draft._registryId,
|
|
1193
|
+
eroded: ((_c = (_b = draft.shapeVoxels) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0) > 0
|
|
1194
|
+
});
|
|
1195
|
+
if (registryId != null) {
|
|
1196
|
+
draft._registryId = registryId;
|
|
1197
|
+
}
|
|
1172
1198
|
setDrawingChanged(true);
|
|
1173
1199
|
setShapeDraft(null);
|
|
1174
1200
|
shapeDraftRef.current = null;
|
|
@@ -1187,6 +1213,7 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1187
1213
|
shapeDraftRef.current = draft;
|
|
1188
1214
|
}
|
|
1189
1215
|
nv.onShapeDraftReady = function (draft) {
|
|
1216
|
+
applyNvBrushSize(1);
|
|
1190
1217
|
setShapeDraft(draft);
|
|
1191
1218
|
shapeDraftRef.current = draft;
|
|
1192
1219
|
nv._cloudMrShapeDraftActive = true;
|
|
@@ -1198,12 +1225,7 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1198
1225
|
nvSetDrawingEnabled(false);
|
|
1199
1226
|
};
|
|
1200
1227
|
nv.onApplyActiveDraft = function () {
|
|
1201
|
-
|
|
1202
|
-
applyShapeDraft();
|
|
1203
|
-
}
|
|
1204
|
-
else if (penDraftRef.current) {
|
|
1205
|
-
applyPenDraftHandler();
|
|
1206
|
-
}
|
|
1228
|
+
applyActiveDraftIfAny();
|
|
1207
1229
|
};
|
|
1208
1230
|
React.useEffect(function () {
|
|
1209
1231
|
nv._cloudMrPenDraftActive = penDraft != null;
|
|
@@ -1259,7 +1281,7 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1259
1281
|
setSelectionBoxColor(__spreadArray(__spreadArray([], rgb01, true), [0.5], false));
|
|
1260
1282
|
nv.setSelectionBoxColor(__spreadArray(__spreadArray([], rgb01, true), [0.5], false));
|
|
1261
1283
|
}
|
|
1262
|
-
var
|
|
1284
|
+
var _37 = React.useState('axial'), sliceType = _37[0], setSliceType = _37[1];
|
|
1263
1285
|
function nvUpdateSliceType(newSliceType) {
|
|
1264
1286
|
setSliceType(newSliceType);
|
|
1265
1287
|
if (newSliceType === 'axial') {
|
|
@@ -1399,14 +1421,14 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1399
1421
|
return [2 /*return*/];
|
|
1400
1422
|
});
|
|
1401
1423
|
}); };
|
|
1402
|
-
var
|
|
1403
|
-
var
|
|
1404
|
-
var
|
|
1405
|
-
}), saveConfirmCallback =
|
|
1406
|
-
var
|
|
1407
|
-
var
|
|
1408
|
-
var
|
|
1409
|
-
var
|
|
1424
|
+
var _38 = useState(''), selectedROI = _38[0], setSelectedDrawingLayer = _38[1];
|
|
1425
|
+
var _39 = useState(false), saveDialogOpen = _39[0], setSaveDialogOpen = _39[1];
|
|
1426
|
+
var _40 = useState(function () {
|
|
1427
|
+
}), saveConfirmCallback = _40[0], setSaveConfirmCallback = _40[1];
|
|
1428
|
+
var _41 = useState(false), confirmationOpen = _41[0], setConfirmationOpen = _41[1];
|
|
1429
|
+
var _42 = useState(function () { }), warningConfirmationCallback = _42[0], setWarningConfirmationCallback = _42[1];
|
|
1430
|
+
var _43 = useState(function () { }), warningCancelCallback = _43[0], setWarningCancelCallback = _43[1];
|
|
1431
|
+
var _44 = useState(false), drawingChanged = _44[0], setDrawingChanged = _44[1];
|
|
1410
1432
|
// When new drawing strokes are made on top of a saved ROI, reset the dropdown
|
|
1411
1433
|
// so it shows the "ROI Layer" placeholder instead of the stale saved name.
|
|
1412
1434
|
React.useEffect(function () {
|
|
@@ -1763,6 +1785,8 @@ export default function CloudMrNiivueViewer(props) {
|
|
|
1763
1785
|
penDraftFilled: (penDraft === null || penDraft === void 0 ? void 0 : penDraft.filled) === true,
|
|
1764
1786
|
brushSize: brushSize,
|
|
1765
1787
|
updateBrushSize: nvUpdateBrushSize,
|
|
1788
|
+
eraserSize: eraserSize,
|
|
1789
|
+
updateEraserSize: nvUpdateEraserSize,
|
|
1766
1790
|
onActivateEraser: activateEraser,
|
|
1767
1791
|
onDeactivateDrawTools: deactivateDrawTools,
|
|
1768
1792
|
onClearDrawing: clearDrawingHandler
|
|
@@ -1673,6 +1673,16 @@ Niivue.prototype.mouseUpListener = function cloudMrMouseUpListener() {
|
|
|
1673
1673
|
}
|
|
1674
1674
|
|
|
1675
1675
|
if (!pendingDraft?.baseBitmap) {
|
|
1676
|
+
// If a draft is open and the user clicked (not dragged) on the canvas,
|
|
1677
|
+
// treat it as an "apply" — same behaviour as right-click or the Apply button.
|
|
1678
|
+
if (
|
|
1679
|
+
(this._cloudMrShapeDraftActive || this._cloudMrPenDraftActive) &&
|
|
1680
|
+
isClickWithoutDrag(this.uiData) &&
|
|
1681
|
+
typeof this.onApplyActiveDraft === "function"
|
|
1682
|
+
) {
|
|
1683
|
+
this.onApplyActiveDraft();
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1676
1686
|
cloudMrTryReopenDraftOnClick(this);
|
|
1677
1687
|
return;
|
|
1678
1688
|
}
|
|
@@ -28,7 +28,7 @@ var HANDLE_SIZE = 10;
|
|
|
28
28
|
* @param {{ nv: any, draft: import('./shapeDraftUtils').ShapeDraft, onDraftChange: (d: any) => void, onApplyDraft?: () => void, overlayKey?: unknown }} props
|
|
29
29
|
*/
|
|
30
30
|
export function ShapeDraftOverlay(_a) {
|
|
31
|
-
var _b;
|
|
31
|
+
var _b, _c, _d;
|
|
32
32
|
var nv = _a.nv, draft = _a.draft, onDraftChange = _a.onDraftChange, onApplyDraft = _a.onApplyDraft, overlayKey = _a.overlayKey;
|
|
33
33
|
var theme = useNiivueViewerTheme();
|
|
34
34
|
var dragRef = useRef(null);
|
|
@@ -38,7 +38,7 @@ export function ShapeDraftOverlay(_a) {
|
|
|
38
38
|
var finishDragRef = useRef(null);
|
|
39
39
|
var onApplyDraftRef = useRef(onApplyDraft);
|
|
40
40
|
onApplyDraftRef.current = onApplyDraft;
|
|
41
|
-
var
|
|
41
|
+
var _e = useState(0), setTick = _e[1];
|
|
42
42
|
var bump = useCallback(function () { return setTick(function (t) { return t + 1; }); }, []);
|
|
43
43
|
useEffect(function () {
|
|
44
44
|
var onMove = function () { return bump(); };
|
|
@@ -82,6 +82,7 @@ export function ShapeDraftOverlay(_a) {
|
|
|
82
82
|
return null;
|
|
83
83
|
return { left: left, top: top, width: width, height: height };
|
|
84
84
|
}, [cornerCss]);
|
|
85
|
+
var resizeDisabled = ((_d = (_c = draft === null || draft === void 0 ? void 0 : draft.shapeVoxels) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) > 0;
|
|
85
86
|
var applyDraft = useCallback(function (nextDraft) {
|
|
86
87
|
redrawDraftShape(nv, nextDraft);
|
|
87
88
|
onDraftChange(nextDraft);
|
|
@@ -101,6 +102,7 @@ export function ShapeDraftOverlay(_a) {
|
|
|
101
102
|
}
|
|
102
103
|
};
|
|
103
104
|
onPointerMoveRef.current = function (event) {
|
|
105
|
+
var _a;
|
|
104
106
|
var drag = dragRef.current;
|
|
105
107
|
var currentDraft = draftRef.current;
|
|
106
108
|
if (!drag || !currentDraft)
|
|
@@ -113,8 +115,11 @@ export function ShapeDraftOverlay(_a) {
|
|
|
113
115
|
return;
|
|
114
116
|
var startCanvas = clientToCanvasPos(canvas, drag.startClientX, drag.startClientY);
|
|
115
117
|
var endCanvas = clientToCanvasPos(canvas, event.clientX, event.clientY);
|
|
116
|
-
var
|
|
117
|
-
var next = __assign(__assign({}, currentDraft), { ptA: translatePt(drag.startPtA,
|
|
118
|
+
var delta_1 = canvasDeltaToVoxDelta(nv, startCanvas, endCanvas);
|
|
119
|
+
var next = __assign(__assign({}, currentDraft), { ptA: translatePt(drag.startPtA, delta_1), ptB: translatePt(drag.startPtB, delta_1), shapeVoxels: (_a = drag.startShapeVoxels) === null || _a === void 0 ? void 0 : _a.map(function (_a) {
|
|
120
|
+
var x = _a[0], y = _a[1], z = _a[2];
|
|
121
|
+
return translatePt([x, y, z], delta_1);
|
|
122
|
+
}) });
|
|
118
123
|
applyDraft(next);
|
|
119
124
|
return;
|
|
120
125
|
}
|
|
@@ -123,10 +128,11 @@ export function ShapeDraftOverlay(_a) {
|
|
|
123
128
|
if (!vox)
|
|
124
129
|
return;
|
|
125
130
|
var next = resizeDraftCorner(nv, currentDraft, drag.cornerIndex, vox);
|
|
126
|
-
applyDraft(next);
|
|
131
|
+
applyDraft(__assign(__assign({}, next), { shapeVoxels: undefined }));
|
|
127
132
|
}
|
|
128
133
|
};
|
|
129
134
|
var startDrag = useCallback(function (event, mode, cornerIndex) {
|
|
135
|
+
var _a;
|
|
130
136
|
if (cornerIndex === void 0) { cornerIndex = -1; }
|
|
131
137
|
event.preventDefault();
|
|
132
138
|
event.stopPropagation();
|
|
@@ -137,7 +143,11 @@ export function ShapeDraftOverlay(_a) {
|
|
|
137
143
|
startClientX: event.clientX,
|
|
138
144
|
startClientY: event.clientY,
|
|
139
145
|
startPtA: __spreadArray([], draft.ptA, true),
|
|
140
|
-
startPtB: __spreadArray([], draft.ptB, true)
|
|
146
|
+
startPtB: __spreadArray([], draft.ptB, true),
|
|
147
|
+
startShapeVoxels: (_a = draft.shapeVoxels) === null || _a === void 0 ? void 0 : _a.map(function (_a) {
|
|
148
|
+
var x = _a[0], y = _a[1], z = _a[2];
|
|
149
|
+
return [x, y, z];
|
|
150
|
+
})
|
|
141
151
|
};
|
|
142
152
|
window.addEventListener("pointermove", onPointerMoveRef.current);
|
|
143
153
|
window.addEventListener("pointerup", finishDragRef.current);
|
|
@@ -181,6 +191,7 @@ export function ShapeDraftOverlay(_a) {
|
|
|
181
191
|
background: theme.accentMutedBgLight,
|
|
182
192
|
boxSizing: "border-box",
|
|
183
193
|
pointerEvents: "none"
|
|
184
|
-
} }), 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
|
|
194
|
+
} }), 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: resizeDisabled ? "Move shape (resize unavailable after erasing)" : "Move shape" })), !resizeDisabled &&
|
|
195
|
+
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: "nwse-resize" }), title: "Resize shape" }, "corner-".concat(i))); })] })));
|
|
185
196
|
}
|
|
186
197
|
export default ShapeDraftOverlay;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export default function EraserPlatte({ expandEraseOptions, updateDrawPen, setDrawingEnabled,
|
|
1
|
+
export default function EraserPlatte({ expandEraseOptions, updateDrawPen, setDrawingEnabled, eraserSize, updateEraserSize, }: {
|
|
2
2
|
expandEraseOptions: any;
|
|
3
3
|
updateDrawPen: any;
|
|
4
4
|
setDrawingEnabled: any;
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
eraserSize?: number | undefined;
|
|
6
|
+
updateEraserSize: any;
|
|
7
7
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -15,7 +15,7 @@ import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
|
|
|
15
15
|
import FiberManualRecordOutlinedIcon from "@mui/icons-material/FiberManualRecordOutlined";
|
|
16
16
|
import { BrushSizeSlider } from "./BrushSizeSlider";
|
|
17
17
|
export default function EraserPlatte(_a) {
|
|
18
|
-
var expandEraseOptions = _a.expandEraseOptions, updateDrawPen = _a.updateDrawPen, setDrawingEnabled = _a.setDrawingEnabled, _b = _a.
|
|
18
|
+
var expandEraseOptions = _a.expandEraseOptions, updateDrawPen = _a.updateDrawPen, setDrawingEnabled = _a.setDrawingEnabled, _b = _a.eraserSize, eraserSize = _b === void 0 ? 1 : _b, updateEraserSize = _a.updateEraserSize;
|
|
19
19
|
var eraseOptions = [
|
|
20
20
|
_jsx(FiberManualRecordIcon, { style: { color: "white" } }, "e0"),
|
|
21
21
|
_jsx(FiberManualRecordOutlinedIcon, { style: { color: "white" } }, "e1"),
|
|
@@ -33,7 +33,7 @@ export default function EraserPlatte(_a) {
|
|
|
33
33
|
borderTopRightRadius: "6pt",
|
|
34
34
|
background: "#333",
|
|
35
35
|
width: 150
|
|
36
|
-
}, direction: "column" }, { children: [
|
|
36
|
+
}, direction: "column" }, { children: [updateEraserSize && (_jsx(BrushSizeSlider, { label: "Eraser size", brushSize: eraserSize, updateBrushSize: updateEraserSize })), _jsx(Stack, __assign({ direction: "row", style: { justifyContent: "center" } }, { children: eraseOptions.map(function (value, index) { return (_jsx(IconButton, __assign({ onClick: function () {
|
|
37
37
|
updateDrawPen({ target: { value: index === 0 ? 8 : 0 } });
|
|
38
38
|
setDrawingEnabled(true);
|
|
39
39
|
} }, { children: value }), index)); }) }))] })));
|
|
@@ -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, penDraftKind: props.penDraftKind, penDraftFilled: props.penDraftFilled, onApplyPenDraft: props.onApplyPenDraft, onDeletePenDraft: props.onDeletePenDraft, 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, onDeleteShapeDraft: props.onDeleteShapeDraft })] })), _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, onDeleteShapeDraft: props.onDeleteShapeDraft })] })), _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 ? theme.selectedToolSx : {})) }, { children: filled || !eraserActive ? (_jsx(EraserIcon, {})) : (_jsx(AutoFixNormalOutlinedIcon, { sx: { color: ICON_COLOR } })) })) })), _jsx(EraserPlatte, { expandEraseOptions: expandedOption === "e", updateDrawPen: props.updateDrawPen, setDrawingEnabled: props.setDrawingEnabled,
|
|
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, onDeletePenDraft: props.onDeletePenDraft, 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, onDeleteShapeDraft: props.onDeleteShapeDraft })] })), _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, onDeleteShapeDraft: props.onDeleteShapeDraft })] })), _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 ? theme.selectedToolSx : {})) }, { children: filled || !eraserActive ? (_jsx(EraserIcon, {})) : (_jsx(AutoFixNormalOutlinedIcon, { sx: { color: ICON_COLOR } })) })) })), _jsx(EraserPlatte, { expandEraseOptions: expandedOption === "e", updateDrawPen: props.updateDrawPen, setDrawingEnabled: props.setDrawingEnabled, 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 () {
|
|
193
193
|
var _a;
|
|
194
194
|
(_a = props.onClearDrawing) === null || _a === void 0 ? void 0 : _a.call(props);
|
|
195
195
|
setExpandedOption("n");
|
|
@@ -7,7 +7,29 @@
|
|
|
7
7
|
* @property {number} axCorSag
|
|
8
8
|
* @property {number} penType
|
|
9
9
|
* @property {Uint8Array} baseBitmap
|
|
10
|
+
* @property {[number, number, number][]} [shapeVoxels] exact voxels when reopening a modified shape
|
|
11
|
+
* @property {number} [_registryId] links to _cloudMrShapeRegistry for erosion tracking
|
|
10
12
|
*/
|
|
13
|
+
export function collectShapeVoxelIndices(nv: any, draft: any): Set<any>;
|
|
14
|
+
export function findShapeRegistryEntryById(nv: any, registryId: any): any;
|
|
15
|
+
/**
|
|
16
|
+
* Match a flood-filled cluster to a registered shape using an IoU-style score:
|
|
17
|
+
* overlap / max(clusterSize, entrySize)
|
|
18
|
+
* This prevents a large eroded rectangle from matching a smaller new shape drawn
|
|
19
|
+
* inside its original footprint, while still correctly identifying the same shape
|
|
20
|
+
* being re-opened (where both sizes are nearly equal and overlap is near 1.0).
|
|
21
|
+
*/
|
|
22
|
+
export function findShapeRegistryEntry(nv: any, cluster: any): any;
|
|
23
|
+
/** Persist applied rectangle/ellipse voxels so eraser hits can be tracked per shape. */
|
|
24
|
+
export function registerAppliedShape(nv: any, draft: any, { existingId, eroded }?: {
|
|
25
|
+
existingId: any;
|
|
26
|
+
eroded?: boolean | undefined;
|
|
27
|
+
}): any;
|
|
28
|
+
/** Mark shapes whose stored voxels were cleared by the eraser. */
|
|
29
|
+
export function updateShapeRegistryErosionState(nv: any): void;
|
|
30
|
+
export function removeShapeRegistryEntry(nv: any, registryId: any): void;
|
|
31
|
+
export function clearShapeRegistry(nv: any): void;
|
|
32
|
+
export function paintShapeVoxels(nv: any, voxels: any, penValue: any): void;
|
|
11
33
|
export function penTypeToKind(penType: any): "rectangle" | "ellipse";
|
|
12
34
|
export function normalizeBounds(ptA: any, ptB: any, dims: any): {
|
|
13
35
|
x1: number;
|
|
@@ -70,6 +92,8 @@ export function eraseClusterFromBitmap(bitmap: any, visited: any): Uint8Array;
|
|
|
70
92
|
* reconstruct a ShapeDraft so the bounding-box overlay reappears for re-editing.
|
|
71
93
|
*/
|
|
72
94
|
export function captureShapeDraftFromClick(nv: any): {
|
|
95
|
+
shapeVoxels?: number[][] | undefined;
|
|
96
|
+
_registryId?: any;
|
|
73
97
|
ptA: any[];
|
|
74
98
|
ptB: any[];
|
|
75
99
|
penValue: number;
|
|
@@ -85,4 +109,12 @@ export type ShapeDraft = {
|
|
|
85
109
|
axCorSag: number;
|
|
86
110
|
penType: number;
|
|
87
111
|
baseBitmap: Uint8Array;
|
|
112
|
+
/**
|
|
113
|
+
* exact voxels when reopening a modified shape
|
|
114
|
+
*/
|
|
115
|
+
shapeVoxels?: [number, number, number][] | undefined;
|
|
116
|
+
/**
|
|
117
|
+
* links to _cloudMrShapeRegistry for erosion tracking
|
|
118
|
+
*/
|
|
119
|
+
_registryId?: number | undefined;
|
|
88
120
|
};
|
|
@@ -29,7 +29,136 @@ import { voxFromMouse } from "./polylinePenUtils";
|
|
|
29
29
|
* @property {number} axCorSag
|
|
30
30
|
* @property {number} penType
|
|
31
31
|
* @property {Uint8Array} baseBitmap
|
|
32
|
+
* @property {[number, number, number][]} [shapeVoxels] exact voxels when reopening a modified shape
|
|
33
|
+
* @property {number} [_registryId] links to _cloudMrShapeRegistry for erosion tracking
|
|
32
34
|
*/
|
|
35
|
+
export function collectShapeVoxelIndices(nv, draft) {
|
|
36
|
+
var _a, _b;
|
|
37
|
+
var indices = new Set();
|
|
38
|
+
if (!(nv === null || nv === void 0 ? void 0 : nv.drawBitmap))
|
|
39
|
+
return indices;
|
|
40
|
+
var dims = (_a = nv.back) === null || _a === void 0 ? void 0 : _a.dims;
|
|
41
|
+
if (((_b = draft.shapeVoxels) === null || _b === void 0 ? void 0 : _b.length) && dims) {
|
|
42
|
+
var dx = dims[1];
|
|
43
|
+
var dy = dims[2];
|
|
44
|
+
for (var _i = 0, _c = draft.shapeVoxels; _i < _c.length; _i++) {
|
|
45
|
+
var _d = _c[_i], x = _d[0], y = _d[1], z = _d[2];
|
|
46
|
+
indices.add(x + y * dx + z * dx * dy);
|
|
47
|
+
}
|
|
48
|
+
return indices;
|
|
49
|
+
}
|
|
50
|
+
if (!draft.baseBitmap)
|
|
51
|
+
return indices;
|
|
52
|
+
for (var i = 0; i < nv.drawBitmap.length; i++) {
|
|
53
|
+
if (nv.drawBitmap[i] === draft.penValue && draft.baseBitmap[i] !== draft.penValue) {
|
|
54
|
+
indices.add(i);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return indices;
|
|
58
|
+
}
|
|
59
|
+
export function findShapeRegistryEntryById(nv, registryId) {
|
|
60
|
+
var _a, _b;
|
|
61
|
+
return (_b = (_a = nv._cloudMrShapeRegistry) === null || _a === void 0 ? void 0 : _a.find(function (entry) { return entry.id === registryId; })) !== null && _b !== void 0 ? _b : null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Match a flood-filled cluster to a registered shape using an IoU-style score:
|
|
65
|
+
* overlap / max(clusterSize, entrySize)
|
|
66
|
+
* This prevents a large eroded rectangle from matching a smaller new shape drawn
|
|
67
|
+
* inside its original footprint, while still correctly identifying the same shape
|
|
68
|
+
* being re-opened (where both sizes are nearly equal and overlap is near 1.0).
|
|
69
|
+
*/
|
|
70
|
+
export function findShapeRegistryEntry(nv, cluster) {
|
|
71
|
+
var registry = nv._cloudMrShapeRegistry;
|
|
72
|
+
if (!(registry === null || registry === void 0 ? void 0 : registry.length) || !(cluster === null || cluster === void 0 ? void 0 : cluster.visited))
|
|
73
|
+
return null;
|
|
74
|
+
var clusterSize = cluster.visited.size;
|
|
75
|
+
if (clusterSize === 0)
|
|
76
|
+
return null;
|
|
77
|
+
var MATCH_THRESHOLD = 0.5;
|
|
78
|
+
var best = null;
|
|
79
|
+
var bestScore = 0;
|
|
80
|
+
for (var _i = 0, registry_1 = registry; _i < registry_1.length; _i++) {
|
|
81
|
+
var entry = registry_1[_i];
|
|
82
|
+
var overlap = 0;
|
|
83
|
+
for (var _a = 0, _b = cluster.visited; _a < _b.length; _a++) {
|
|
84
|
+
var idx = _b[_a];
|
|
85
|
+
if (entry.voxelIndices.has(idx))
|
|
86
|
+
overlap++;
|
|
87
|
+
}
|
|
88
|
+
var score = overlap / Math.max(clusterSize, entry.voxelIndices.size);
|
|
89
|
+
if (score > bestScore) {
|
|
90
|
+
bestScore = score;
|
|
91
|
+
best = entry;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return bestScore >= MATCH_THRESHOLD ? best : null;
|
|
95
|
+
}
|
|
96
|
+
/** Persist applied rectangle/ellipse voxels so eraser hits can be tracked per shape. */
|
|
97
|
+
export function registerAppliedShape(nv, draft, _a) {
|
|
98
|
+
var _b, _c;
|
|
99
|
+
var _d = _a === void 0 ? {} : _a, existingId = _d.existingId, _e = _d.eroded, eroded = _e === void 0 ? false : _e;
|
|
100
|
+
var voxelIndices = collectShapeVoxelIndices(nv, draft);
|
|
101
|
+
if (!voxelIndices.size)
|
|
102
|
+
return null;
|
|
103
|
+
nv._cloudMrShapeRegistry = nv._cloudMrShapeRegistry || [];
|
|
104
|
+
var nextId = existingId !== null && existingId !== void 0 ? existingId : (nv._cloudMrShapeNextId = (nv._cloudMrShapeNextId || 0) + 1);
|
|
105
|
+
var entry = {
|
|
106
|
+
id: nextId,
|
|
107
|
+
ptA: __spreadArray([], draft.ptA, true),
|
|
108
|
+
ptB: __spreadArray([], draft.ptB, true),
|
|
109
|
+
penType: draft.penType,
|
|
110
|
+
penValue: draft.penValue,
|
|
111
|
+
axCorSag: draft.axCorSag,
|
|
112
|
+
voxelIndices: voxelIndices,
|
|
113
|
+
eroded: eroded || ((_c = (_b = draft.shapeVoxels) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0) > 0
|
|
114
|
+
};
|
|
115
|
+
var existingIndex = nv._cloudMrShapeRegistry.findIndex(function (e) { return e.id === nextId; });
|
|
116
|
+
if (existingIndex >= 0) {
|
|
117
|
+
nv._cloudMrShapeRegistry[existingIndex] = entry;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
nv._cloudMrShapeRegistry.push(entry);
|
|
121
|
+
}
|
|
122
|
+
return nextId;
|
|
123
|
+
}
|
|
124
|
+
/** Mark shapes whose stored voxels were cleared by the eraser. */
|
|
125
|
+
export function updateShapeRegistryErosionState(nv) {
|
|
126
|
+
var registry = nv._cloudMrShapeRegistry;
|
|
127
|
+
if (!(registry === null || registry === void 0 ? void 0 : registry.length) || !nv.drawBitmap)
|
|
128
|
+
return;
|
|
129
|
+
for (var _i = 0, registry_2 = registry; _i < registry_2.length; _i++) {
|
|
130
|
+
var entry = registry_2[_i];
|
|
131
|
+
if (entry.eroded)
|
|
132
|
+
continue;
|
|
133
|
+
for (var _a = 0, _b = entry.voxelIndices; _a < _b.length; _a++) {
|
|
134
|
+
var idx = _b[_a];
|
|
135
|
+
if (nv.drawBitmap[idx] === 0) {
|
|
136
|
+
entry.eroded = true;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export function removeShapeRegistryEntry(nv, registryId) {
|
|
143
|
+
if (registryId == null || !nv._cloudMrShapeRegistry)
|
|
144
|
+
return;
|
|
145
|
+
nv._cloudMrShapeRegistry = nv._cloudMrShapeRegistry.filter(function (e) { return e.id !== registryId; });
|
|
146
|
+
}
|
|
147
|
+
export function clearShapeRegistry(nv) {
|
|
148
|
+
nv._cloudMrShapeRegistry = [];
|
|
149
|
+
}
|
|
150
|
+
export function paintShapeVoxels(nv, voxels, penValue) {
|
|
151
|
+
var _a;
|
|
152
|
+
var dims = (_a = nv.back) === null || _a === void 0 ? void 0 : _a.dims;
|
|
153
|
+
if (!dims || !(voxels === null || voxels === void 0 ? void 0 : voxels.length))
|
|
154
|
+
return;
|
|
155
|
+
var dx = dims[1];
|
|
156
|
+
var dy = dims[2];
|
|
157
|
+
for (var _i = 0, voxels_1 = voxels; _i < voxels_1.length; _i++) {
|
|
158
|
+
var _b = voxels_1[_i], x = _b[0], y = _b[1], z = _b[2];
|
|
159
|
+
nv.drawBitmap[x + y * dx + z * dx * dy] = penValue;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
33
162
|
export function penTypeToKind(penType) {
|
|
34
163
|
return penType === NI_PEN_TYPE.ELLIPSE ? "ellipse" : "rectangle";
|
|
35
164
|
}
|
|
@@ -88,16 +217,27 @@ export function isDraftTooSmall(ptA, ptB) {
|
|
|
88
217
|
}
|
|
89
218
|
/** @param {ShapeDraft} draft */
|
|
90
219
|
export function redrawDraftShape(nv, draft) {
|
|
220
|
+
var _a;
|
|
91
221
|
if (!(nv === null || nv === void 0 ? void 0 : nv.drawBitmap) || !(draft === null || draft === void 0 ? void 0 : draft.baseBitmap))
|
|
92
222
|
return;
|
|
223
|
+
// Shapes are filled regions — penSize/penBounds must not expand voxels (e.g. after eraser).
|
|
224
|
+
var savedPenBounds = nv.opts.penBounds;
|
|
225
|
+
var savedPenSize = nv.opts.penSize;
|
|
226
|
+
nv.opts.penBounds = 0;
|
|
227
|
+
nv.opts.penSize = 1;
|
|
93
228
|
nv.drawBitmap.set(draft.baseBitmap);
|
|
94
229
|
nv.drawPenAxCorSag = draft.axCorSag;
|
|
95
|
-
if (draft.
|
|
230
|
+
if ((_a = draft.shapeVoxels) === null || _a === void 0 ? void 0 : _a.length) {
|
|
231
|
+
paintShapeVoxels(nv, draft.shapeVoxels, draft.penValue);
|
|
232
|
+
}
|
|
233
|
+
else if (draft.penType === NI_PEN_TYPE.RECTANGLE) {
|
|
96
234
|
nv.drawRectangleMask(draft.ptA, draft.ptB, draft.penValue);
|
|
97
235
|
}
|
|
98
236
|
else {
|
|
99
237
|
nv.drawEllipseMask(draft.ptA, draft.ptB, draft.penValue);
|
|
100
238
|
}
|
|
239
|
+
nv.opts.penBounds = savedPenBounds;
|
|
240
|
+
nv.opts.penSize = savedPenSize;
|
|
101
241
|
nv.refreshDrawing(false, false);
|
|
102
242
|
nv.drawScene();
|
|
103
243
|
}
|
|
@@ -312,8 +452,8 @@ function inferShapePenTypeFromCluster(cluster, axCorSag) {
|
|
|
312
452
|
var bounds = cluster.bounds, voxels = cluster.voxels;
|
|
313
453
|
var x1 = bounds.x1, y1 = bounds.y1, z1 = bounds.z1, x2 = bounds.x2, y2 = bounds.y2, z2 = bounds.z2;
|
|
314
454
|
var filled = new Set();
|
|
315
|
-
for (var _i = 0,
|
|
316
|
-
var _a =
|
|
455
|
+
for (var _i = 0, voxels_2 = voxels; _i < voxels_2.length; _i++) {
|
|
456
|
+
var _a = voxels_2[_i], x = _a[0], y = _a[1], z = _a[2];
|
|
317
457
|
filled.add(sliceKey(axCorSag, x, y, z));
|
|
318
458
|
}
|
|
319
459
|
var uMin;
|
|
@@ -359,6 +499,59 @@ function inferShapePenTypeFromCluster(cluster, axCorSag) {
|
|
|
359
499
|
}
|
|
360
500
|
return ellipseScore > rectScore ? NI_PEN_TYPE.ELLIPSE : NI_PEN_TYPE.RECTANGLE;
|
|
361
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* Geometric completeness check: true when the cluster still fills its bounding
|
|
504
|
+
* shape with no significant gaps (i.e. the eraser has not meaningfully touched it).
|
|
505
|
+
* Used as a fallback when the shape is not in the registry.
|
|
506
|
+
*
|
|
507
|
+
* For rectangles: all bounding-box voxels must be present.
|
|
508
|
+
* For ellipses: we use niivue's actual threshold (≤ 1.0) so boundary pixels
|
|
509
|
+
* that niivue doesn't draw never trigger a false "eroded" result.
|
|
510
|
+
* Additionally we allow up to 5% of interior voxels to be absent
|
|
511
|
+
* to tolerate rendering rounding without masking real erasure.
|
|
512
|
+
*/
|
|
513
|
+
function isClusterCompleteShape(cluster, penType) {
|
|
514
|
+
var bounds = cluster.bounds, voxels = cluster.voxels;
|
|
515
|
+
var x1 = bounds.x1, y1 = bounds.y1, z1 = bounds.z1, x2 = bounds.x2, y2 = bounds.y2, z2 = bounds.z2;
|
|
516
|
+
var filled = new Set(voxels.map(function (_a) {
|
|
517
|
+
var x = _a[0], y = _a[1], z = _a[2];
|
|
518
|
+
return "".concat(x, ",").concat(y, ",").concat(z);
|
|
519
|
+
}));
|
|
520
|
+
if (penType === NI_PEN_TYPE.RECTANGLE) {
|
|
521
|
+
for (var z = z1; z <= z2; z++) {
|
|
522
|
+
for (var y = y1; y <= y2; y++) {
|
|
523
|
+
for (var x = x1; x <= x2; x++) {
|
|
524
|
+
if (!filled.has("".concat(x, ",").concat(y, ",").concat(z)))
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
// Ellipse / ellipsoid: match niivue's exact threshold (≤ 1.0).
|
|
532
|
+
var cx = (x1 + x2) / 2;
|
|
533
|
+
var cy = (y1 + y2) / 2;
|
|
534
|
+
var cz = (z1 + z2) / 2;
|
|
535
|
+
var ru = Math.max(0.5, (x2 - x1) / 2);
|
|
536
|
+
var rv = Math.max(0.5, (y2 - y1) / 2);
|
|
537
|
+
var rw = Math.max(0.5, (z2 - z1) / 2);
|
|
538
|
+
var expected = 0;
|
|
539
|
+
var missing = 0;
|
|
540
|
+
for (var z = z1; z <= z2; z++) {
|
|
541
|
+
for (var y = y1; y <= y2; y++) {
|
|
542
|
+
for (var x = x1; x <= x2; x++) {
|
|
543
|
+
var inEllipse = Math.pow(((x - cx) / ru), 2) + Math.pow(((y - cy) / rv), 2) + Math.pow(((z - cz) / rw), 2) <= 1.0;
|
|
544
|
+
if (inEllipse) {
|
|
545
|
+
expected++;
|
|
546
|
+
if (!filled.has("".concat(x, ",").concat(y, ",").concat(z)))
|
|
547
|
+
missing++;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
// Allow up to 5% missing to handle minor rendering rounding at the boundary.
|
|
553
|
+
return expected === 0 || missing / expected <= 0.05;
|
|
554
|
+
}
|
|
362
555
|
/**
|
|
363
556
|
* When the user clicks on an existing filled ROI while the rectangle/ellipse tool
|
|
364
557
|
* is active (but no draft is currently open), flood-fill the clicked cluster to
|
|
@@ -369,17 +562,21 @@ export function captureShapeDraftFromClick(nv) {
|
|
|
369
562
|
var cluster = floodFillClusterFromVox(nv, seedVox);
|
|
370
563
|
if (!cluster)
|
|
371
564
|
return null;
|
|
372
|
-
var label = cluster.label, visited = cluster.visited, bounds = cluster.bounds;
|
|
565
|
+
var label = cluster.label, visited = cluster.visited, voxels = cluster.voxels, bounds = cluster.bounds;
|
|
373
566
|
var x1 = bounds.x1, y1 = bounds.y1, z1 = bounds.z1, x2 = bounds.x2, y2 = bounds.y2, z2 = bounds.z2;
|
|
374
567
|
var baseBitmap = eraseClusterFromBitmap(nv.drawBitmap, visited);
|
|
375
568
|
var axCorSag = inferAxCorSagFromBounds(x1, y1, z1, x2, y2, z2, nv.drawPenAxCorSag >= 0 ? nv.drawPenAxCorSag : 0);
|
|
376
569
|
var penType = inferShapePenTypeFromCluster(cluster, axCorSag);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
570
|
+
var registryEntry = findShapeRegistryEntry(nv, cluster);
|
|
571
|
+
// A shape is considered modified (partially erased) if:
|
|
572
|
+
// - the registry knows it was eroded, OR
|
|
573
|
+
// - (no registry entry) the geometry check finds gaps
|
|
574
|
+
var isModified = (registryEntry === null || registryEntry === void 0 ? void 0 : registryEntry.eroded) === true ||
|
|
575
|
+
(!registryEntry && !isClusterCompleteShape(cluster, penType));
|
|
576
|
+
return __assign(__assign({ ptA: [x1, y1, z1], ptB: [x2, y2, z2], penValue: label, axCorSag: axCorSag, penType: penType, baseBitmap: baseBitmap }, (registryEntry ? { _registryId: registryEntry.id } : {})), (isModified
|
|
577
|
+
? { shapeVoxels: voxels.map(function (_a) {
|
|
578
|
+
var x = _a[0], y = _a[1], z = _a[2];
|
|
579
|
+
return [x, y, z];
|
|
580
|
+
}) }
|
|
581
|
+
: {}));
|
|
385
582
|
}
|