@cornerstonejs/tools 5.4.12 → 5.4.14

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.
Files changed (46) hide show
  1. package/dist/esm/eventListeners/segmentation/segmentationDataModifiedEventListener.js +2 -2
  2. package/dist/esm/index.d.ts +2 -2
  3. package/dist/esm/index.js +2 -2
  4. package/dist/esm/stateManagement/segmentation/SegmentationRenderingEngine.d.ts +2 -1
  5. package/dist/esm/stateManagement/segmentation/SegmentationRenderingEngine.js +45 -1
  6. package/dist/esm/stateManagement/segmentation/labelmapModel/labelmapEditTransaction.d.ts +12 -2
  7. package/dist/esm/stateManagement/segmentation/labelmapModel/labelmapEditTransaction.js +21 -1
  8. package/dist/esm/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.d.ts +5 -0
  9. package/dist/esm/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.js +47 -1
  10. package/dist/esm/stateManagement/segmentation/labelmapModel/privateLabelmap.d.ts +5 -1
  11. package/dist/esm/stateManagement/segmentation/labelmapModel/privateLabelmap.js +49 -4
  12. package/dist/esm/tools/annotation/ClickSegmentTool.d.ts +51 -0
  13. package/dist/esm/tools/annotation/ClickSegmentTool.js +748 -0
  14. package/dist/esm/tools/annotation/regionSegmentHoverCursors.d.ts +4 -0
  15. package/dist/esm/tools/annotation/regionSegmentHoverCursors.js +4 -0
  16. package/dist/esm/tools/displayTools/Labelmap/addVolumesAsIndependentComponents.js +42 -11
  17. package/dist/esm/tools/index.d.ts +2 -1
  18. package/dist/esm/tools/index.js +2 -1
  19. package/dist/esm/tools/segmentation/BrushTool.js +1 -0
  20. package/dist/esm/tools/segmentation/strategies/BrushStrategy.js +21 -0
  21. package/dist/esm/tools/segmentation/strategies/utils/crossLayerErase.js +20 -0
  22. package/dist/esm/utilities/segmentation/commitSliceMasksToLabelmap.d.ts +12 -0
  23. package/dist/esm/utilities/segmentation/commitSliceMasksToLabelmap.js +128 -0
  24. package/dist/esm/utilities/segmentation/createEnsureSliceLoadedForVolume.d.ts +2 -0
  25. package/dist/esm/utilities/segmentation/createEnsureSliceLoadedForVolume.js +36 -0
  26. package/dist/esm/utilities/segmentation/createLabelmapMemo.d.ts +7 -0
  27. package/dist/esm/utilities/segmentation/createLabelmapMemo.js +41 -8
  28. package/dist/esm/utilities/segmentation/floodFillIslandRemoval.d.ts +20 -0
  29. package/dist/esm/utilities/segmentation/floodFillIslandRemoval.js +118 -0
  30. package/dist/esm/utilities/segmentation/floodFillSliceLazy.d.ts +33 -0
  31. package/dist/esm/utilities/segmentation/floodFillSliceLazy.js +167 -0
  32. package/dist/esm/utilities/segmentation/growCut/floodFillIntensityRangeTypes.d.ts +51 -0
  33. package/dist/esm/utilities/segmentation/growCut/floodFillIntensityRangeTypes.js +0 -0
  34. package/dist/esm/utilities/segmentation/growCut/getViewportVoiMappingForVolume.d.ts +10 -0
  35. package/dist/esm/utilities/segmentation/growCut/getViewportVoiMappingForVolume.js +21 -0
  36. package/dist/esm/utilities/segmentation/growCut/intensityRange/adaptiveRegionIntensityRange.d.ts +57 -0
  37. package/dist/esm/utilities/segmentation/growCut/intensityRange/adaptiveRegionIntensityRange.js +559 -0
  38. package/dist/esm/utilities/segmentation/growCut/neighborhoodStats.d.ts +10 -0
  39. package/dist/esm/utilities/segmentation/growCut/neighborhoodStats.js +44 -0
  40. package/dist/esm/utilities/segmentation/growCut/runFloodFillSegmentation.d.ts +61 -0
  41. package/dist/esm/utilities/segmentation/growCut/runFloodFillSegmentation.js +524 -0
  42. package/dist/esm/utilities/segmentation/islandRemoval.d.ts +3 -2
  43. package/dist/esm/utilities/segmentation/islandRemoval.js +3 -0
  44. package/dist/esm/version.d.ts +1 -1
  45. package/dist/esm/version.js +1 -1
  46. package/package.json +4 -4
@@ -0,0 +1,4 @@
1
+ export declare const CIRCLE_CURSOR = "url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9'%20fill='none'%20stroke='%2300dc82'%20stroke-width='2'/%3E%3C/svg%3E\") 12 12, crosshair";
2
+ export declare const PLUS_CURSOR = "url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9'%20fill='none'%20stroke='%2300dc82'%20stroke-width='2'/%3E%3Cpath%20d='M12%208v8M8%2012h8'%20stroke='%2300dc82'%20stroke-width='2'%20stroke-linecap='round'/%3E%3C/svg%3E\") 12 12, copy";
3
+ export declare const BLOCKED_CURSOR = "url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9'%20fill='none'%20stroke='%23ff5a5a'%20stroke-width='2'/%3E%3Cpath%20d='M5.7%205.7L18.3%2018.3'%20stroke='%23ff5a5a'%20stroke-width='2'%20stroke-linecap='round'/%3E%3C/svg%3E\") 12 12, not-allowed";
4
+ export declare const PENDING_CURSOR = "url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9'%20fill='none'%20stroke='%23a0a6ad'%20stroke-width='2'%20stroke-dasharray='4%203'/%3E%3C/svg%3E\") 12 12, progress";
@@ -0,0 +1,4 @@
1
+ export const CIRCLE_CURSOR = "url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9'%20fill='none'%20stroke='%2300dc82'%20stroke-width='2'/%3E%3C/svg%3E\") 12 12, crosshair";
2
+ export const PLUS_CURSOR = "url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9'%20fill='none'%20stroke='%2300dc82'%20stroke-width='2'/%3E%3Cpath%20d='M12%208v8M8%2012h8'%20stroke='%2300dc82'%20stroke-width='2'%20stroke-linecap='round'/%3E%3C/svg%3E\") 12 12, copy";
3
+ export const BLOCKED_CURSOR = "url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9'%20fill='none'%20stroke='%23ff5a5a'%20stroke-width='2'/%3E%3Cpath%20d='M5.7%205.7L18.3%2018.3'%20stroke='%23ff5a5a'%20stroke-width='2'%20stroke-linecap='round'/%3E%3C/svg%3E\") 12 12, not-allowed";
4
+ export const PENDING_CURSOR = "url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9'%20fill='none'%20stroke='%23a0a6ad'%20stroke-width='2'%20stroke-dasharray='4%203'/%3E%3C/svg%3E\") 12 12, progress";
@@ -1,4 +1,4 @@
1
- import { cache, Enums, convertMapperToNotSharedMapper, volumeLoader, eventTarget, createVolumeActor, } from '@cornerstonejs/core';
1
+ import { cache, Enums, convertMapperToNotSharedMapper, volumeLoader, eventTarget, } from '@cornerstonejs/core';
2
2
  import { Events, SegmentationRepresentations } from '../../../enums/index.js';
3
3
  import { getSegmentation } from '../../../stateManagement/segmentation/getSegmentation.js';
4
4
  const internalCache = new Map();
@@ -52,6 +52,8 @@ export async function addVolumesAsIndependentComponents({ viewport, volumeInputs
52
52
  }
53
53
  viewport.removeActors([uid]);
54
54
  const oldMapper = actor.getMapper();
55
+ const sharedImageData = oldMapper.getInputData();
56
+ const originalScalars = sharedImageData.getPointData().getScalars() ?? null;
55
57
  const mapper = convertMapperToNotSharedMapper(oldMapper);
56
58
  actor.setMapper(mapper);
57
59
  mapper.setBlendMode(Enums.BlendModes.LABELMAP_EDGE_PROJECTION_BLEND);
@@ -68,9 +70,16 @@ export async function addVolumesAsIndependentComponents({ viewport, volumeInputs
68
70
  .getProperty()
69
71
  .getIndependentComponents();
70
72
  actor.getProperty().setIndependentComponents(true);
73
+ const oldUseLabelOutline = actor.getProperty().getUseLabelOutline();
74
+ const oldLabelOutlineOpacity = actor.getProperty().getLabelOutlineOpacity();
75
+ const oldLabelOutlineThickness = actor
76
+ .getProperty()
77
+ .getLabelOutlineThickness();
78
+ const representationUID = volumeInputArray[0].representationUID ??
79
+ `${segmentationId}-${SegmentationRepresentations.Labelmap}`;
71
80
  viewport.addActor({
72
81
  ...defaultActor,
73
- representationUID: `${segmentationId}-${SegmentationRepresentations.Labelmap}`,
82
+ representationUID,
74
83
  });
75
84
  internalCache.set(uid, {
76
85
  added: true,
@@ -82,7 +91,7 @@ export async function addVolumesAsIndependentComponents({ viewport, volumeInputs
82
91
  preLoad: load,
83
92
  });
84
93
  function onSegmentationDataModified(evt) {
85
- const { segmentationId } = evt.detail;
94
+ const { segmentationId, modifiedSlicesToUse } = evt.detail;
86
95
  const { representationData } = getSegmentation(segmentationId);
87
96
  const { volumeId: segVolumeId } = representationData.Labelmap;
88
97
  if (segVolumeId !== segImageVolume.volumeId) {
@@ -92,19 +101,30 @@ export async function addVolumesAsIndependentComponents({ viewport, volumeInputs
92
101
  const segVoxelManager = segmentationVolume.voxelManager;
93
102
  const imageData = mapper.getInputData();
94
103
  const array = imageData.getPointData().getArray(0);
95
- const baseData = array.getData();
104
+ const combinedData = array.getData();
96
105
  const newComp = 2;
97
106
  const dims = segImageData.getDimensions();
98
- const slices = Array.from({ length: dims[2] }, (_, i) => i);
107
+ const sliceSize = dims[0] * dims[1];
108
+ const slices = modifiedSlicesToUse?.length
109
+ ? modifiedSlicesToUse
110
+ : Array.from({ length: dims[2] }, (_, i) => i);
99
111
  for (const z of slices) {
100
- for (let y = 0; y < dims[1]; ++y) {
101
- for (let x = 0; x < dims[0]; ++x) {
102
- const iTuple = x + dims[0] * (y + dims[1] * z);
103
- baseData[iTuple * newComp + 1] = segVoxelManager.getAtIndex(iTuple);
112
+ const sliceStart = z * sliceSize;
113
+ const sliceImage = cache.getImage(segmentationVolume.imageIds?.[z]);
114
+ const sliceData = sliceImage?.voxelManager?.getScalarData?.();
115
+ if (sliceData?.length === sliceSize) {
116
+ for (let i = 0; i < sliceSize; ++i) {
117
+ combinedData[(sliceStart + i) * newComp + 1] = sliceData[i];
118
+ }
119
+ }
120
+ else {
121
+ for (let i = 0; i < sliceSize; ++i) {
122
+ const iTuple = sliceStart + i;
123
+ combinedData[iTuple * newComp + 1] = segVoxelManager.getAtIndex(iTuple);
104
124
  }
105
125
  }
106
126
  }
107
- array.setData(baseData);
127
+ array.setData(combinedData);
108
128
  imageData.modified();
109
129
  viewport.render();
110
130
  }
@@ -113,7 +133,7 @@ export async function addVolumesAsIndependentComponents({ viewport, volumeInputs
113
133
  if (evt.detail.viewportId !== viewport.id) {
114
134
  return;
115
135
  }
116
- eventTarget.removeEventListener(Events.SEGMENTATION_DATA_MODIFIED, onSegmentationDataModified);
136
+ eventTarget.removeEventListenerDebounced(Events.SEGMENTATION_DATA_MODIFIED, onSegmentationDataModified);
117
137
  eventTarget.removeEventListener(Events.SEGMENTATION_REPRESENTATION_REMOVED, onSegmentationRepresentationRemoved);
118
138
  const actorEntry = viewport.getActor(uid);
119
139
  if (actorEntry) {
@@ -124,11 +144,22 @@ export async function addVolumesAsIndependentComponents({ viewport, volumeInputs
124
144
  return;
125
145
  }
126
146
  actor.setMapper(oldMapper);
147
+ const pointData = sharedImageData.getPointData();
148
+ if (originalScalars) {
149
+ pointData.setScalars(originalScalars);
150
+ }
151
+ else {
152
+ pointData.removeArray('Pixels');
153
+ }
154
+ sharedImageData.modified();
127
155
  actor.getProperty().setColorMixPreset(oldColorMixPreset);
128
156
  actor
129
157
  .getProperty()
130
158
  .setForceNearestInterpolation(1, oldForceNearestInterpolation);
131
159
  actor.getProperty().setIndependentComponents(oldIndependentComponents);
160
+ actor.getProperty().setUseLabelOutline(oldUseLabelOutline);
161
+ actor.getProperty().setLabelOutlineOpacity(oldLabelOutlineOpacity);
162
+ actor.getProperty().setLabelOutlineThickness(oldLabelOutlineThickness);
132
163
  viewport.addActor({
133
164
  ...defaultActor,
134
165
  });
@@ -44,6 +44,7 @@ import KeyImageTool from './annotation/KeyImageTool.js';
44
44
  import AnnotationEraserTool from './AnnotationEraserTool.js';
45
45
  import RegionSegmentTool from './annotation/RegionSegmentTool.js';
46
46
  import RegionSegmentPlusTool from './annotation/RegionSegmentPlusTool.js';
47
+ import ClickSegmentTool from './annotation/ClickSegmentTool.js';
47
48
  import WholeBodySegmentTool from './annotation/WholeBodySegmentTool.js';
48
49
  import LabelmapBaseTool from './segmentation/LabelmapBaseTool.js';
49
50
  import RectangleScissorsTool from './segmentation/RectangleScissorsTool.js';
@@ -61,4 +62,4 @@ import SegmentBidirectionalTool from './segmentation/SegmentBidirectionalTool.js
61
62
  import * as strategies from './segmentation/strategies/index.js';
62
63
  import SegmentLabelTool from './segmentation/SegmentLabelTool.js';
63
64
  import LabelMapEditWithContourTool from './segmentation/LabelmapEditWithContour.js';
64
- export { BaseTool, AnnotationTool, AnnotationDisplayTool, PanTool, TrackballRotateTool, VolumeCroppingTool, VolumeCroppingControlTool, DragProbeTool, WindowLevelTool, WindowLevelRegionTool, StackScrollTool, PlanarRotateTool, ZoomTool, MIPJumpToClickTool, ReferenceCursors, CrosshairsTool, ReferenceLinesTool, OverlayGridTool, SegmentationIntersectionTool, BidirectionalTool, LabelTool, LengthTool, HeightTool, ProbeTool, RectangleROITool, EllipticalROITool, CircleROITool, ETDRSGridTool, SplineROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, LivewireContourTool, LivewireContourSegmentationTool, ArrowAnnotateTool, AngleTool, CobbAngleTool, UltrasoundDirectionalTool, UltrasoundPleuraBLineTool, KeyImageTool, AnnotationEraserTool as EraserTool, RectangleScissorsTool, CircleScissorsTool, SphereScissorsTool, RectangleROIThresholdTool, RectangleROIStartEndThresholdTool, CircleROIStartEndThresholdTool, SplineContourSegmentationTool, BrushTool, MagnifyTool, AdvancedMagnifyTool, PaintFillTool, ScaleOverlayTool, OrientationMarkerTool, OrientationControllerTool, SculptorTool, SegmentSelectTool, VolumeRotateTool, RegionSegmentTool, RegionSegmentPlusTool, WholeBodySegmentTool, LabelmapBaseTool, SegmentBidirectionalTool, SegmentLabelTool, LabelMapEditWithContourTool, strategies, };
65
+ export { BaseTool, AnnotationTool, AnnotationDisplayTool, PanTool, TrackballRotateTool, VolumeCroppingTool, VolumeCroppingControlTool, DragProbeTool, WindowLevelTool, WindowLevelRegionTool, StackScrollTool, PlanarRotateTool, ZoomTool, MIPJumpToClickTool, ReferenceCursors, CrosshairsTool, ReferenceLinesTool, OverlayGridTool, SegmentationIntersectionTool, BidirectionalTool, LabelTool, LengthTool, HeightTool, ProbeTool, RectangleROITool, EllipticalROITool, CircleROITool, ETDRSGridTool, SplineROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, LivewireContourTool, LivewireContourSegmentationTool, ArrowAnnotateTool, AngleTool, CobbAngleTool, UltrasoundDirectionalTool, UltrasoundPleuraBLineTool, KeyImageTool, AnnotationEraserTool as EraserTool, RectangleScissorsTool, CircleScissorsTool, SphereScissorsTool, RectangleROIThresholdTool, RectangleROIStartEndThresholdTool, CircleROIStartEndThresholdTool, SplineContourSegmentationTool, BrushTool, MagnifyTool, AdvancedMagnifyTool, PaintFillTool, ScaleOverlayTool, OrientationMarkerTool, OrientationControllerTool, SculptorTool, SegmentSelectTool, VolumeRotateTool, RegionSegmentTool, RegionSegmentPlusTool, ClickSegmentTool, WholeBodySegmentTool, LabelmapBaseTool, SegmentBidirectionalTool, SegmentLabelTool, LabelMapEditWithContourTool, strategies, };
@@ -44,6 +44,7 @@ import KeyImageTool from './annotation/KeyImageTool.js';
44
44
  import AnnotationEraserTool from './AnnotationEraserTool.js';
45
45
  import RegionSegmentTool from './annotation/RegionSegmentTool.js';
46
46
  import RegionSegmentPlusTool from './annotation/RegionSegmentPlusTool.js';
47
+ import ClickSegmentTool from './annotation/ClickSegmentTool.js';
47
48
  import WholeBodySegmentTool from './annotation/WholeBodySegmentTool.js';
48
49
  import LabelmapBaseTool from './segmentation/LabelmapBaseTool.js';
49
50
  import RectangleScissorsTool from './segmentation/RectangleScissorsTool.js';
@@ -61,4 +62,4 @@ import SegmentBidirectionalTool from './segmentation/SegmentBidirectionalTool.js
61
62
  import * as strategies from './segmentation/strategies/index.js';
62
63
  import SegmentLabelTool from './segmentation/SegmentLabelTool.js';
63
64
  import LabelMapEditWithContourTool from './segmentation/LabelmapEditWithContour.js';
64
- export { BaseTool, AnnotationTool, AnnotationDisplayTool, PanTool, TrackballRotateTool, VolumeCroppingTool, VolumeCroppingControlTool, DragProbeTool, WindowLevelTool, WindowLevelRegionTool, StackScrollTool, PlanarRotateTool, ZoomTool, MIPJumpToClickTool, ReferenceCursors, CrosshairsTool, ReferenceLinesTool, OverlayGridTool, SegmentationIntersectionTool, BidirectionalTool, LabelTool, LengthTool, HeightTool, ProbeTool, RectangleROITool, EllipticalROITool, CircleROITool, ETDRSGridTool, SplineROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, LivewireContourTool, LivewireContourSegmentationTool, ArrowAnnotateTool, AngleTool, CobbAngleTool, UltrasoundDirectionalTool, UltrasoundPleuraBLineTool, KeyImageTool, AnnotationEraserTool as EraserTool, RectangleScissorsTool, CircleScissorsTool, SphereScissorsTool, RectangleROIThresholdTool, RectangleROIStartEndThresholdTool, CircleROIStartEndThresholdTool, SplineContourSegmentationTool, BrushTool, MagnifyTool, AdvancedMagnifyTool, PaintFillTool, ScaleOverlayTool, OrientationMarkerTool, OrientationControllerTool, SculptorTool, SegmentSelectTool, VolumeRotateTool, RegionSegmentTool, RegionSegmentPlusTool, WholeBodySegmentTool, LabelmapBaseTool, SegmentBidirectionalTool, SegmentLabelTool, LabelMapEditWithContourTool, strategies, };
65
+ export { BaseTool, AnnotationTool, AnnotationDisplayTool, PanTool, TrackballRotateTool, VolumeCroppingTool, VolumeCroppingControlTool, DragProbeTool, WindowLevelTool, WindowLevelRegionTool, StackScrollTool, PlanarRotateTool, ZoomTool, MIPJumpToClickTool, ReferenceCursors, CrosshairsTool, ReferenceLinesTool, OverlayGridTool, SegmentationIntersectionTool, BidirectionalTool, LabelTool, LengthTool, HeightTool, ProbeTool, RectangleROITool, EllipticalROITool, CircleROITool, ETDRSGridTool, SplineROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, LivewireContourTool, LivewireContourSegmentationTool, ArrowAnnotateTool, AngleTool, CobbAngleTool, UltrasoundDirectionalTool, UltrasoundPleuraBLineTool, KeyImageTool, AnnotationEraserTool as EraserTool, RectangleScissorsTool, CircleScissorsTool, SphereScissorsTool, RectangleROIThresholdTool, RectangleROIStartEndThresholdTool, CircleROIStartEndThresholdTool, SplineContourSegmentationTool, BrushTool, MagnifyTool, AdvancedMagnifyTool, PaintFillTool, ScaleOverlayTool, OrientationMarkerTool, OrientationControllerTool, SculptorTool, SegmentSelectTool, VolumeRotateTool, RegionSegmentTool, RegionSegmentPlusTool, ClickSegmentTool, WholeBodySegmentTool, LabelmapBaseTool, SegmentBidirectionalTool, SegmentLabelTool, LabelMapEditWithContourTool, strategies, };
@@ -124,6 +124,7 @@ class BrushTool extends LabelmapBaseTool {
124
124
  };
125
125
  this._hoverData = this.createHoverData(element, canvasPoint);
126
126
  if (!this._hoverData) {
127
+ this._editData = null;
127
128
  return false;
128
129
  }
129
130
  this._calculateCursor(element, canvasPoint);
@@ -2,6 +2,7 @@ import { triggerSegmentationDataModified } from '../../../stateManagement/segmen
2
2
  import compositions from './compositions/index.js';
3
3
  import { getStrategyData } from './utils/getStrategyData.js';
4
4
  import { StrategyCallbacks } from '../../../enums/index.js';
5
+ import { memoAsStep } from '../../../utilities/segmentation/createLabelmapMemo.js';
5
6
  import { eraseCrossLayerOverwrites, prepareOverlapOperationData, } from './utils/labelmapOverlap.js';
6
7
  import { shouldUseLazyLabelmapEditing } from '../utils/shouldUseLazyLabelmapEditing.js';
7
8
  class BrushStrategy {
@@ -23,7 +24,27 @@ class BrushStrategy {
23
24
  }
24
25
  if (initializedData.memo?.segmentationVoxelManager !==
25
26
  initializedData.segmentationVoxelManager) {
27
+ const previousMemo = initializedData.memo;
26
28
  initializedData.memo = initializedData.createMemo(initializedData.segmentationId, initializedData.segmentationVoxelManager);
29
+ if (previousMemo &&
30
+ previousMemo !== initializedData.memo &&
31
+ previousMemo.commitMemo?.()) {
32
+ (initializedData.memo.priorSteps ||= []).push(memoAsStep(previousMemo));
33
+ }
34
+ }
35
+ const moveStep = initializedData.labelmapEditTransaction?.moveStep;
36
+ if (moveStep && initializedData.memo) {
37
+ const { segmentationId } = initializedData;
38
+ (initializedData.memo.priorSteps ||= []).push({
39
+ undo: () => {
40
+ moveStep.undo();
41
+ triggerSegmentationDataModified(segmentationId);
42
+ },
43
+ redo: () => {
44
+ moveStep.redo();
45
+ triggerSegmentationDataModified(segmentationId);
46
+ },
47
+ });
27
48
  }
28
49
  if (initializedData.segmentationVoxelManager !==
29
50
  originalSegmentationVoxelManager ||
@@ -1,4 +1,5 @@
1
1
  import { getSegmentation } from '../../../../stateManagement/segmentation/getSegmentation.js';
2
+ import { triggerSegmentationDataModified } from '../../../../stateManagement/segmentation/triggerSegmentationEvents.js';
2
3
  import { collectCrossLayerEraseBindings, eraseLabelmapEditTransactionOverwrites, } from '../../../../stateManagement/segmentation/helpers/labelmapSegmentationState.js';
3
4
  function collectCrossLayerEraseBindingsForOperation(operationData) {
4
5
  const { segmentationId, labelmapId, overwriteSegmentIndices } = operationData;
@@ -12,12 +13,31 @@ function eraseCrossLayerOverwrites(operationData) {
12
13
  if (!segmentation) {
13
14
  return [];
14
15
  }
16
+ const { memo } = operationData;
15
17
  return eraseLabelmapEditTransactionOverwrites(segmentation, operationData.labelmapEditTransaction, {
16
18
  viewport: operationData.viewport,
17
19
  referenceImageData: operationData.segmentationImageData,
18
20
  isInObject: operationData.isInObject,
19
21
  isInObjectBoundsIJK: operationData.isInObjectBoundsIJK,
20
22
  imageId: operationData.imageId,
23
+ crossLayerEraseCallback: memo
24
+ ? ({ voxelManager, labelValue, indices }) => {
25
+ (memo.postSteps ||= []).push({
26
+ undo: () => {
27
+ for (const index of indices) {
28
+ voxelManager.setAtIndex(index, labelValue);
29
+ }
30
+ triggerSegmentationDataModified(operationData.segmentationId);
31
+ },
32
+ redo: () => {
33
+ for (const index of indices) {
34
+ voxelManager.setAtIndex(index, 0);
35
+ }
36
+ triggerSegmentationDataModified(operationData.segmentationId);
37
+ },
38
+ });
39
+ }
40
+ : undefined,
21
41
  });
22
42
  }
23
43
  export { collectCrossLayerEraseBindingsForOperation as collectCrossLayerEraseBindings, eraseCrossLayerOverwrites, };
@@ -0,0 +1,12 @@
1
+ import { type Types } from '@cornerstonejs/core';
2
+ export declare function commitSliceMasksToLabelmapVolume({ labelmapVolume, sliceMasks, width: w, height: h, paintIndex, historyVoxelManager, }: {
3
+ labelmapVolume: Types.IImageVolume;
4
+ sliceMasks: Map<number, Uint8Array>;
5
+ width: number;
6
+ height: number;
7
+ paintIndex: number;
8
+ historyVoxelManager?: Types.IVoxelManager<number>;
9
+ }): {
10
+ floodedPoints: Types.Point3[];
11
+ voxelCount: number;
12
+ };
@@ -0,0 +1,128 @@
1
+ import { cache, utilities } from '@cornerstonejs/core';
2
+ import { FLOOD_SLICE_FLAG_VISITED } from './floodFillSliceLazy.js';
3
+ const { VoxelManager } = utilities;
4
+ export function commitSliceMasksToLabelmapVolume({ labelmapVolume, sliceMasks, width: w, height: h, paintIndex, historyVoxelManager, }) {
5
+ const floodedPoints = [];
6
+ let voxelCount = 0;
7
+ const vm = labelmapVolume.voxelManager;
8
+ const historyWriter = historyVoxelManager;
9
+ const [labelmapWidth, labelmapHeight, depth] = labelmapVolume.dimensions;
10
+ if (labelmapWidth !== w || labelmapHeight !== h) {
11
+ throw new Error(`commitSliceMasksToLabelmapVolume: labelmap in-plane dimensions ` +
12
+ `${labelmapWidth}x${labelmapHeight} do not match mask dimensions ${w}x${h}`);
13
+ }
14
+ const frameSize = w * h;
15
+ const expectedLen = frameSize * depth;
16
+ const zs = Array.from(sliceMasks.keys()).sort((a, b) => a - b);
17
+ for (let zi = 0; zi < zs.length; zi++) {
18
+ const z = zs[zi];
19
+ if (z < 0 || z >= depth) {
20
+ continue;
21
+ }
22
+ const flags = sliceMasks.get(z);
23
+ if (!flags || flags.length !== frameSize) {
24
+ continue;
25
+ }
26
+ let sliceTouched = false;
27
+ let minX = Infinity;
28
+ let minY = Infinity;
29
+ let maxX = -Infinity;
30
+ let maxY = -Infinity;
31
+ const appendRunFlooded = (y, x0, x1) => {
32
+ for (let xi = x0; xi < x1; xi++) {
33
+ floodedPoints.push([xi, y, z]);
34
+ }
35
+ voxelCount += x1 - x0;
36
+ sliceTouched = true;
37
+ minX = Math.min(minX, x0);
38
+ maxX = Math.max(maxX, x1 - 1);
39
+ minY = Math.min(minY, y);
40
+ maxY = Math.max(maxY, y);
41
+ };
42
+ const denseScalar = !historyWriter &&
43
+ vm.scalarData &&
44
+ vm.scalarData.length >= expectedLen &&
45
+ typeof vm.scalarData.fill === 'function';
46
+ if (denseScalar) {
47
+ const base = z * frameSize;
48
+ const data = vm.scalarData;
49
+ for (let y = 0; y < h; y++) {
50
+ const row = y * w;
51
+ for (let x = 0; x < w;) {
52
+ const li = row + x;
53
+ if (!(flags[li] & FLOOD_SLICE_FLAG_VISITED)) {
54
+ x++;
55
+ continue;
56
+ }
57
+ const x0 = x;
58
+ while (x < w && flags[row + x] & FLOOD_SLICE_FLAG_VISITED) {
59
+ x++;
60
+ }
61
+ data.fill(paintIndex, base + row + x0, base + row + x);
62
+ appendRunFlooded(y, x0, x);
63
+ }
64
+ }
65
+ }
66
+ else {
67
+ const imageIds = labelmapVolume.imageIds;
68
+ const image = imageIds?.length && z < imageIds.length
69
+ ? cache.getImage(imageIds[z])
70
+ : null;
71
+ const svm = image?.voxelManager;
72
+ if (!historyWriter &&
73
+ svm?.scalarData &&
74
+ svm.scalarData.length >= frameSize &&
75
+ typeof svm.scalarData.fill === 'function') {
76
+ const data = svm.scalarData;
77
+ for (let y = 0; y < h; y++) {
78
+ const row = y * w;
79
+ for (let x = 0; x < w;) {
80
+ const li = row + x;
81
+ if (!(flags[li] & FLOOD_SLICE_FLAG_VISITED)) {
82
+ x++;
83
+ continue;
84
+ }
85
+ const x0 = x;
86
+ while (x < w && flags[row + x] & FLOOD_SLICE_FLAG_VISITED) {
87
+ x++;
88
+ }
89
+ data.fill(paintIndex, row + x0, row + x);
90
+ appendRunFlooded(y, x0, x);
91
+ }
92
+ }
93
+ svm.modifiedSlices.add(z);
94
+ }
95
+ else {
96
+ const writer = historyWriter ?? vm;
97
+ for (let y = 0; y < h; y++) {
98
+ const row = y * w;
99
+ for (let x = 0; x < w; x++) {
100
+ if (!(flags[row + x] & FLOOD_SLICE_FLAG_VISITED)) {
101
+ continue;
102
+ }
103
+ const index = z * frameSize + row + x;
104
+ writer.setAtIndex(index, paintIndex);
105
+ floodedPoints.push([x, y, z]);
106
+ voxelCount++;
107
+ sliceTouched = true;
108
+ minX = Math.min(minX, x);
109
+ maxX = Math.max(maxX, x);
110
+ minY = Math.min(minY, y);
111
+ maxY = Math.max(maxY, y);
112
+ }
113
+ }
114
+ }
115
+ }
116
+ if (sliceTouched) {
117
+ vm.modifiedSlices.add(z);
118
+ if (Number.isFinite(minX) &&
119
+ Number.isFinite(minY) &&
120
+ Number.isFinite(maxX) &&
121
+ Number.isFinite(maxY)) {
122
+ VoxelManager.addBounds(vm.boundsIJK, [minX, minY, z]);
123
+ VoxelManager.addBounds(vm.boundsIJK, [maxX, maxY, z]);
124
+ }
125
+ }
126
+ }
127
+ return { floodedPoints, voxelCount };
128
+ }
@@ -0,0 +1,2 @@
1
+ import type { Types } from '@cornerstonejs/core';
2
+ export declare function createEnsureSliceLoadedForVolume(volume: Types.IImageVolume): (z: number) => Promise<void>;
@@ -0,0 +1,36 @@
1
+ import { imageLoader } from '@cornerstonejs/core';
2
+ export function createEnsureSliceLoadedForVolume(volume) {
3
+ const numSlices = volume.dimensions[2];
4
+ const imageIds = volume.imageIds;
5
+ if (!imageIds?.length) {
6
+ return async () => undefined;
7
+ }
8
+ const loadedSlices = new Set();
9
+ const inFlight = new Map();
10
+ return async function ensureSliceLoaded(z) {
11
+ if (!Number.isFinite(z) || z < 0 || z >= numSlices) {
12
+ return;
13
+ }
14
+ if (loadedSlices.has(z)) {
15
+ return;
16
+ }
17
+ const existing = inFlight.get(z);
18
+ if (existing) {
19
+ return existing;
20
+ }
21
+ const imageId = imageIds[z];
22
+ if (!imageId) {
23
+ return;
24
+ }
25
+ const promise = imageLoader
26
+ .loadImage(imageId)
27
+ .then(() => {
28
+ loadedSlices.add(z);
29
+ })
30
+ .finally(() => {
31
+ inFlight.delete(z);
32
+ });
33
+ inFlight.set(z, promise);
34
+ return promise;
35
+ };
36
+ }
@@ -1,5 +1,9 @@
1
1
  import { utilities } from '@cornerstonejs/core';
2
2
  import type { Types } from '@cornerstonejs/core';
3
+ export type LabelmapRestoreStep = {
4
+ undo: () => void;
5
+ redo: () => void;
6
+ };
3
7
  export type LabelmapMemo = Types.Memo & {
4
8
  segmentationVoxelManager: Types.IVoxelManager<number>;
5
9
  voxelManager: Types.IVoxelManager<number>;
@@ -7,6 +11,8 @@ export type LabelmapMemo = Types.Memo & {
7
11
  undoVoxelManager?: Types.IVoxelManager<number>;
8
12
  memo?: LabelmapMemo;
9
13
  id: string;
14
+ priorSteps?: LabelmapRestoreStep[];
15
+ postSteps?: LabelmapRestoreStep[];
10
16
  };
11
17
  export declare function createLabelmapMemo<T>(segmentationId: string, segmentationVoxelManager: Types.IVoxelManager<T>): {
12
18
  segmentationId: string;
@@ -17,6 +23,7 @@ export declare function createLabelmapMemo<T>(segmentationId: string, segmentati
17
23
  id: string;
18
24
  operationType: string;
19
25
  };
26
+ export declare function memoAsStep(memo: LabelmapMemo): LabelmapRestoreStep;
20
27
  export declare function restoreMemo(isUndo?: boolean): void;
21
28
  export declare function createRleMemo<T>(segmentationId: string, segmentationVoxelManager: Types.IVoxelManager<T>): {
22
29
  segmentationId: string;
@@ -4,14 +4,46 @@ const { VoxelManager, RLEVoxelMap } = utilities;
4
4
  export function createLabelmapMemo(segmentationId, segmentationVoxelManager) {
5
5
  return createRleMemo(segmentationId, segmentationVoxelManager);
6
6
  }
7
+ export function memoAsStep(memo) {
8
+ return {
9
+ undo: () => memo.restoreMemo(true),
10
+ redo: () => memo.restoreMemo(false),
11
+ };
12
+ }
7
13
  export function restoreMemo(isUndo) {
8
- const { segmentationVoxelManager, undoVoxelManager, redoVoxelManager } = this;
9
- const useVoxelManager = isUndo === false ? redoVoxelManager : undoVoxelManager;
10
- useVoxelManager.forEach(({ value, pointIJK }) => {
11
- segmentationVoxelManager.setAtIJKPoint(pointIJK, value);
12
- });
13
- const slices = useVoxelManager.getArrayOfModifiedSlices();
14
- triggerSegmentationDataModified(this.segmentationId, slices);
14
+ const undo = isUndo !== false;
15
+ const priorSteps = this.priorSteps ?? [];
16
+ const postSteps = this.postSteps ?? [];
17
+ const restoreVoxelChanges = () => {
18
+ const { segmentationVoxelManager, undoVoxelManager, redoVoxelManager } = this;
19
+ const useVoxelManager = undo ? undoVoxelManager : redoVoxelManager;
20
+ if (!useVoxelManager) {
21
+ return;
22
+ }
23
+ useVoxelManager.forEach(({ value, pointIJK }) => {
24
+ segmentationVoxelManager.setAtIJKPoint(pointIJK, value);
25
+ });
26
+ const slices = useVoxelManager.getArrayOfModifiedSlices();
27
+ triggerSegmentationDataModified(this.segmentationId, slices);
28
+ };
29
+ if (undo) {
30
+ for (let i = postSteps.length - 1; i >= 0; i--) {
31
+ postSteps[i].undo();
32
+ }
33
+ restoreVoxelChanges();
34
+ for (let i = priorSteps.length - 1; i >= 0; i--) {
35
+ priorSteps[i].undo();
36
+ }
37
+ }
38
+ else {
39
+ for (const step of priorSteps) {
40
+ step.redo();
41
+ }
42
+ restoreVoxelChanges();
43
+ for (const step of postSteps) {
44
+ step.redo();
45
+ }
46
+ }
15
47
  }
16
48
  export function createRleMemo(segmentationId, segmentationVoxelManager) {
17
49
  const voxelManager = VoxelManager.createRLEHistoryVoxelManager(segmentationVoxelManager);
@@ -30,8 +62,9 @@ function commitMemo() {
30
62
  if (this.redoVoxelManager) {
31
63
  return true;
32
64
  }
65
+ const hasSteps = !!(this.priorSteps?.length || this.postSteps?.length);
33
66
  if (!this.voxelManager.modifiedSlices.size) {
34
- return false;
67
+ return hasSteps;
35
68
  }
36
69
  const { segmentationVoxelManager } = this;
37
70
  const undoVoxelManager = VoxelManager.createRLEHistoryVoxelManager(segmentationVoxelManager);
@@ -0,0 +1,20 @@
1
+ import type { Types } from '@cornerstonejs/core';
2
+ import IslandRemoval, { SegmentationEnum } from './islandRemoval.js';
3
+ export { SegmentationEnum };
4
+ export default class FloodFillIslandRemoval extends IslandRemoval {
5
+ private verboseLogging;
6
+ private usingPreviewLayer;
7
+ private internalFilledPoints;
8
+ constructor(options?: {
9
+ maxInternalRemove?: number;
10
+ fillInternalEdge?: boolean;
11
+ verboseLogging?: boolean;
12
+ });
13
+ initialize(viewport: any, segmentationVoxels: any, options: any): boolean;
14
+ private static countRleValueVolume;
15
+ floodFillSegmentIsland(): number;
16
+ removeExternalIslands(): number;
17
+ removeInternalIslands(): number[];
18
+ protected onInternalPointFilled(point: Types.Point3): void;
19
+ getInternalFilledPoints(): Types.Point3[];
20
+ }