@cornerstonejs/tools 2.3.3 → 2.5.0

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 (24) hide show
  1. package/dist/esm/tools/PanTool.d.ts +1 -0
  2. package/dist/esm/tools/PanTool.js +5 -0
  3. package/dist/esm/tools/ZoomTool.js +2 -0
  4. package/dist/esm/tools/annotation/CircleROITool.js +1 -1
  5. package/dist/esm/tools/annotation/LengthTool.js +10 -0
  6. package/dist/esm/tools/annotation/LivewireContourTool.d.ts +1 -1
  7. package/dist/esm/tools/annotation/LivewireContourTool.js +4 -3
  8. package/dist/esm/tools/base/BaseTool.d.ts +7 -0
  9. package/dist/esm/tools/base/BaseTool.js +27 -0
  10. package/dist/esm/tools/segmentation/PaintFillTool.js +1 -1
  11. package/dist/esm/tools/segmentation/strategies/BrushStrategy.js +1 -3
  12. package/dist/esm/tools/segmentation/strategies/compositions/dynamicThreshold.js +18 -3
  13. package/dist/esm/tools/segmentation/strategies/compositions/islandRemoval.d.ts +9 -0
  14. package/dist/esm/tools/segmentation/strategies/compositions/islandRemoval.js +138 -104
  15. package/dist/esm/tools/segmentation/strategies/compositions/preview.js +3 -1
  16. package/dist/esm/tools/segmentation/strategies/compositions/regionFill.js +1 -1
  17. package/dist/esm/tools/segmentation/strategies/compositions/setValue.js +0 -1
  18. package/dist/esm/tools/segmentation/strategies/fillSphere.js +1 -1
  19. package/dist/esm/tools/segmentation/strategies/utils/normalizeViewportPlane.d.ts +19 -0
  20. package/dist/esm/tools/segmentation/strategies/utils/normalizeViewportPlane.js +35 -0
  21. package/dist/esm/types/FloodFillTypes.d.ts +2 -1
  22. package/dist/esm/utilities/segmentation/VolumetricCalculator.js +3 -1
  23. package/dist/esm/utilities/segmentation/floodFill.js +9 -11
  24. package/package.json +3 -3
@@ -5,6 +5,7 @@ declare class PanTool extends BaseTool {
5
5
  constructor(toolProps?: PublicToolProps, defaultToolProps?: ToolProps);
6
6
  touchDragCallback(evt: EventTypes.InteractionEventType): void;
7
7
  mouseDragCallback(evt: EventTypes.InteractionEventType): void;
8
+ preMouseDownCallback: (evt: EventTypes.InteractionEventType) => boolean;
8
9
  _dragCallback(evt: EventTypes.InteractionEventType): void;
9
10
  }
10
11
  export default PanTool;
@@ -5,6 +5,10 @@ class PanTool extends BaseTool {
5
5
  supportedInteractionTypes: ['Mouse', 'Touch'],
6
6
  }) {
7
7
  super(toolProps, defaultToolProps);
8
+ this.preMouseDownCallback = (evt) => {
9
+ this.memo = null;
10
+ return false;
11
+ };
8
12
  }
9
13
  touchDragCallback(evt) {
10
14
  this._dragCallback(evt);
@@ -15,6 +19,7 @@ class PanTool extends BaseTool {
15
19
  _dragCallback(evt) {
16
20
  const { element, deltaPoints } = evt.detail;
17
21
  const enabledElement = getEnabledElement(element);
22
+ this.memo ||= PanTool.createZoomPanMemo(enabledElement.viewport);
18
23
  const deltaPointsWorld = deltaPoints.world;
19
24
  if (deltaPointsWorld[0] === 0 &&
20
25
  deltaPointsWorld[1] === 0 &&
@@ -23,6 +23,7 @@ class ZoomTool extends BaseTool {
23
23
  const camera = enabledElement.viewport.getCamera();
24
24
  const { focalPoint } = camera;
25
25
  this.initialMousePosWorld = worldPos;
26
+ this.memo = null;
26
27
  let dirVec = vec3.fromValues(focalPoint[0] - worldPos[0], focalPoint[1] - worldPos[1], focalPoint[2] - worldPos[2]);
27
28
  dirVec = vec3.normalize(vec3.create(), dirVec);
28
29
  this.dirVec = dirVec;
@@ -145,6 +146,7 @@ class ZoomTool extends BaseTool {
145
146
  const enabledElement = getEnabledElement(element);
146
147
  const { viewport } = enabledElement;
147
148
  const camera = viewport.getCamera();
149
+ this.memo ||= ZoomTool.createZoomPanMemo(viewport);
148
150
  if (camera.parallelProjection) {
149
151
  this._dragParallelProjection(evt, viewport, camera);
150
152
  }
@@ -529,9 +529,9 @@ class CircleROITool extends AnnotationTool {
529
529
  area,
530
530
  mean: stats.mean?.value,
531
531
  max: stats.max?.value,
532
+ pointsInShape,
532
533
  stdDev: stats.stdDev?.value,
533
534
  statsArray: stats.array,
534
- pointsInShape: pointsInShape,
535
535
  isEmptyArea,
536
536
  areaUnit,
537
537
  radius: worldWidth / 2 / scale,
@@ -21,6 +21,16 @@ class LengthTool extends AnnotationTool {
21
21
  configuration: {
22
22
  preventHandleOutsideImage: false,
23
23
  getTextLines: defaultGetTextLines,
24
+ actions: {
25
+ undo: {
26
+ method: 'undo',
27
+ bindings: [{ key: 'z' }],
28
+ },
29
+ redo: {
30
+ method: 'redo',
31
+ bindings: [{ key: 'y' }],
32
+ },
33
+ },
24
34
  },
25
35
  }) {
26
36
  super(toolProps, defaultToolProps);
@@ -51,7 +51,7 @@ declare class LivewireContourTool extends ContourSegmentationBaseTool {
51
51
  renderAnnotation(enabledElement: Types.IEnabledElement, svgDrawingHelper: SVGDrawingHelper): boolean;
52
52
  protected isContourSegmentationTool(): boolean;
53
53
  protected createAnnotation(evt: EventTypes.InteractionEventType): import("../../types").ContourAnnotation;
54
- undo(element: any, config: any, evt: any): void;
54
+ cancelInProgress(element: any, config: any, evt: any): void;
55
55
  protected renderAnnotationInstance(renderContext: {
56
56
  enabledElement: Types.IEnabledElement;
57
57
  targetId: string;
@@ -37,8 +37,8 @@ class LivewireContourTool extends ContourSegmentationBaseTool {
37
37
  epsilon: 0.1,
38
38
  },
39
39
  actions: {
40
- undo: {
41
- method: 'undo',
40
+ cancelInProgress: {
41
+ method: 'cancelInProgress',
42
42
  bindings: [
43
43
  {
44
44
  key: 'Escape',
@@ -576,8 +576,9 @@ class LivewireContourTool extends ContourSegmentationBaseTool {
576
576
  });
577
577
  return annotation;
578
578
  }
579
- undo(element, config, evt) {
579
+ cancelInProgress(element, config, evt) {
580
580
  if (!this.editData) {
581
+ this.undo();
581
582
  return;
582
583
  }
583
584
  this._endCallback(evt, true);
@@ -1,3 +1,4 @@
1
+ import { utilities } from '@cornerstonejs/core';
1
2
  import type { Types } from '@cornerstonejs/core';
2
3
  import ToolModes from '../../enums/ToolModes';
3
4
  import type StrategyCallbacks from '../../enums/StrategyCallbacks';
@@ -8,6 +9,7 @@ declare abstract class BaseTool {
8
9
  configuration: Record<string, any>;
9
10
  toolGroupId: string;
10
11
  mode: ToolModes;
12
+ protected memo: utilities.HistoryMemo.Memo;
11
13
  constructor(toolProps: PublicToolProps, defaultToolProps: ToolProps);
12
14
  getToolName(): string;
13
15
  applyActiveStrategy(enabledElement: Types.IEnabledElement, operationData: unknown): any;
@@ -16,5 +18,10 @@ declare abstract class BaseTool {
16
18
  setActiveStrategy(strategyName: string): void;
17
19
  protected getTargetImageData(targetId: string): Types.IImageData | Types.CPUIImageData;
18
20
  protected getTargetId(viewport: Types.IViewport): string | undefined;
21
+ undo(): void;
22
+ redo(): void;
23
+ static createZoomPanMemo(viewport: any): {
24
+ restoreMemo: () => void;
25
+ };
19
26
  }
20
27
  export default BaseTool;
@@ -1,5 +1,6 @@
1
1
  import { utilities, BaseVolumeViewport } from '@cornerstonejs/core';
2
2
  import ToolModes from '../../enums/ToolModes';
3
+ const { DefaultHistoryMemo } = utilities.HistoryMemo;
3
4
  class BaseTool {
4
5
  constructor(toolProps, defaultToolProps) {
5
6
  const initialProps = utilities.deepMerge(defaultToolProps, toolProps);
@@ -78,6 +79,32 @@ class BaseTool {
78
79
  }
79
80
  throw new Error('getTargetId: viewport must have a getViewReferenceId method');
80
81
  }
82
+ undo() {
83
+ DefaultHistoryMemo.undo();
84
+ this.memo = null;
85
+ }
86
+ redo() {
87
+ DefaultHistoryMemo.redo();
88
+ }
89
+ static createZoomPanMemo(viewport) {
90
+ const state = {
91
+ pan: viewport.getPan(),
92
+ zoom: viewport.getZoom(),
93
+ };
94
+ const zoomPanMemo = {
95
+ restoreMemo: () => {
96
+ const currentPan = viewport.getPan();
97
+ const currentZoom = viewport.getZoom();
98
+ viewport.setZoom(state.zoom);
99
+ viewport.setPan(state.pan);
100
+ viewport.render();
101
+ state.pan = currentPan;
102
+ state.zoom = currentZoom;
103
+ },
104
+ };
105
+ DefaultHistoryMemo.push(zoomPanMemo);
106
+ return zoomPanMemo;
107
+ }
81
108
  }
82
109
  BaseTool.toolName = 'BaseTool';
83
110
  export default BaseTool;
@@ -80,7 +80,7 @@ class PaintFillTool extends BaseTool {
80
80
  return true;
81
81
  };
82
82
  this.getFramesModified = (fixedDimension, fixedDimensionValue, floodFillResult) => {
83
- const { boundaries } = floodFillResult;
83
+ const { flooded: boundaries } = floodFillResult;
84
84
  if (fixedDimension === 2) {
85
85
  return [fixedDimensionValue];
86
86
  }
@@ -90,9 +90,7 @@ export default class BrushStrategy {
90
90
  const segmentationVoxelManagerToUse = operationData.override?.voxelManager || segmentationVoxelManager;
91
91
  const segmentationImageDataToUse = operationData.override?.imageData || segmentationImageData;
92
92
  const previewVoxelManager = operationData.preview?.previewVoxelManager ||
93
- VoxelManager.createHistoryVoxelManager({
94
- sourceVoxelManager: segmentationVoxelManagerToUse,
95
- });
93
+ VoxelManager.createRLEHistoryVoxelManager(segmentationVoxelManager);
96
94
  const previewEnabled = !!operationData.previewColors;
97
95
  const previewSegmentIndex = previewEnabled ? 255 : undefined;
98
96
  const initializedData = {
@@ -2,7 +2,7 @@ import { vec3 } from 'gl-matrix';
2
2
  import StrategyCallbacks from '../../../../enums/StrategyCallbacks';
3
3
  export default {
4
4
  [StrategyCallbacks.Initialize]: (operationData) => {
5
- const { operationName, centerIJK, strategySpecificConfiguration, segmentationVoxelManager, imageVoxelManager, segmentIndex, } = operationData;
5
+ const { operationName, centerIJK, strategySpecificConfiguration, segmentationVoxelManager, imageVoxelManager, segmentIndex, viewport, } = operationData;
6
6
  const { THRESHOLD } = strategySpecificConfiguration;
7
7
  if (!THRESHOLD?.isDynamic || !centerIJK || !segmentIndex) {
8
8
  return;
@@ -14,6 +14,7 @@ export default {
14
14
  const boundsIJK = segmentationVoxelManager.getBoundsIJK();
15
15
  const { threshold: oldThreshold, dynamicRadius = 0 } = THRESHOLD;
16
16
  const useDelta = oldThreshold ? 0 : dynamicRadius;
17
+ const { viewPlaneNormal } = viewport.getCamera();
17
18
  const nestedBounds = boundsIJK.map((ijk, idx) => {
18
19
  const [min, max] = ijk;
19
20
  return [
@@ -21,8 +22,22 @@ export default {
21
22
  Math.min(max, centerIJK[idx] + useDelta),
22
23
  ];
23
24
  });
25
+ if (Math.abs(viewPlaneNormal[0]) > 0.8) {
26
+ nestedBounds[0] = [centerIJK[0], centerIJK[0]];
27
+ }
28
+ else if (Math.abs(viewPlaneNormal[1]) > 0.8) {
29
+ nestedBounds[1] = [centerIJK[1], centerIJK[1]];
30
+ }
31
+ else if (Math.abs(viewPlaneNormal[2]) > 0.8) {
32
+ nestedBounds[2] = [centerIJK[2], centerIJK[2]];
33
+ }
24
34
  const threshold = oldThreshold || [Infinity, -Infinity];
25
- const callback = ({ value }) => {
35
+ const useDeltaSqr = useDelta * useDelta;
36
+ const callback = ({ value, pointIJK }) => {
37
+ const distance = vec3.sqrDist(centerIJK, pointIJK);
38
+ if (distance > useDeltaSqr) {
39
+ return;
40
+ }
26
41
  const gray = Array.isArray(value) ? vec3.len(value) : value;
27
42
  threshold[0] = Math.min(gray, threshold[0]);
28
43
  threshold[1] = Math.max(gray, threshold[1]);
@@ -62,6 +77,6 @@ export default {
62
77
  strategySpecificConfiguration[activeStrategy] = {};
63
78
  }
64
79
  strategySpecificConfiguration[activeStrategy].dynamicRadiusInCanvas =
65
- dynamicRadiusInCanvas;
80
+ 3 + dynamicRadiusInCanvas;
66
81
  },
67
82
  };
@@ -1,5 +1,14 @@
1
+ import { utilities } from '@cornerstonejs/core';
1
2
  import type { InitializedOperationData } from '../BrushStrategy';
3
+ export declare enum SegmentationEnum {
4
+ SEGMENT = 1,
5
+ ISLAND = 2,
6
+ INTERIOR = 3,
7
+ EXTERIOR = 4
8
+ }
2
9
  declare const _default: {
3
10
  onInteractionEnd: (operationData: InitializedOperationData) => void;
4
11
  };
5
12
  export default _default;
13
+ export declare function createSegmentSet(operationData: InitializedOperationData): utilities.RLEVoxelMap<SegmentationEnum>;
14
+ export declare function covers(rle: any, row: any): boolean;
@@ -1,123 +1,157 @@
1
- import floodFill from '../../../../utilities/segmentation/floodFill';
1
+ import { utilities } from '@cornerstonejs/core';
2
2
  import { triggerSegmentationDataModified } from '../../../../stateManagement/segmentation/triggerSegmentationEvents';
3
3
  import StrategyCallbacks from '../../../../enums/StrategyCallbacks';
4
+ import normalizeViewportPlane from '../utils/normalizeViewportPlane';
5
+ const { RLEVoxelMap } = utilities;
6
+ const MAX_IMAGE_SIZE = 65535;
7
+ export var SegmentationEnum;
8
+ (function (SegmentationEnum) {
9
+ SegmentationEnum[SegmentationEnum["SEGMENT"] = 1] = "SEGMENT";
10
+ SegmentationEnum[SegmentationEnum["ISLAND"] = 2] = "ISLAND";
11
+ SegmentationEnum[SegmentationEnum["INTERIOR"] = 3] = "INTERIOR";
12
+ SegmentationEnum[SegmentationEnum["EXTERIOR"] = 4] = "EXTERIOR";
13
+ })(SegmentationEnum || (SegmentationEnum = {}));
4
14
  export default {
5
15
  [StrategyCallbacks.OnInteractionEnd]: (operationData) => {
6
- const { previewVoxelManager: previewVoxelManager, segmentationVoxelManager, strategySpecificConfiguration, previewSegmentIndex, segmentIndex, } = operationData;
7
- if (!strategySpecificConfiguration.THRESHOLD || segmentIndex === null) {
16
+ const { strategySpecificConfiguration, previewSegmentIndex, segmentIndex } = operationData;
17
+ if (!strategySpecificConfiguration.THRESHOLD ||
18
+ segmentIndex === null ||
19
+ previewSegmentIndex === undefined) {
8
20
  return;
9
21
  }
10
- const clickedPoints = previewVoxelManager.getPoints();
11
- if (!clickedPoints?.length) {
22
+ const segmentSet = createSegmentSet(operationData);
23
+ if (!segmentSet) {
12
24
  return;
13
25
  }
14
- if (previewSegmentIndex === undefined) {
26
+ const externalRemoved = removeExternalIslands(operationData, segmentSet);
27
+ if (externalRemoved === undefined) {
15
28
  return;
16
29
  }
17
- const boundsIJK = previewVoxelManager
18
- .getBoundsIJK()
19
- .map((bound, i) => [
20
- Math.min(bound[0], ...clickedPoints.map((point) => point[i])),
21
- Math.max(bound[1], ...clickedPoints.map((point) => point[i])),
22
- ]);
23
- if (boundsIJK.find((it) => it[0] < 0 || it[1] > 65535)) {
30
+ const arrayOfSlices = removeInternalIslands(operationData, segmentSet);
31
+ if (!arrayOfSlices) {
24
32
  return;
25
33
  }
26
- const floodedSet = new Set();
27
- const getter = (i, j, k) => {
28
- if (i < boundsIJK[0][0] ||
29
- i > boundsIJK[0][1] ||
30
- j < boundsIJK[1][0] ||
31
- j > boundsIJK[1][1] ||
32
- k < boundsIJK[2][0] ||
33
- k > boundsIJK[2][1]) {
34
- return -1;
35
- }
36
- const index = segmentationVoxelManager.toIndex([i, j, k]);
37
- if (floodedSet.has(index)) {
38
- return -2;
39
- }
40
- const oldVal = segmentationVoxelManager.getAtIndex(index);
41
- const isIn = oldVal === previewSegmentIndex || oldVal === segmentIndex ? 1 : 0;
42
- if (!isIn) {
43
- segmentationVoxelManager.addPoint(index);
44
- }
45
- return isIn;
46
- };
47
- let floodedCount = 0;
48
- const onFlood = (i, j, k) => {
49
- const index = segmentationVoxelManager.toIndex([i, j, k]);
50
- if (floodedSet.has(index)) {
51
- return;
52
- }
53
- previewVoxelManager.setAtIJK(i, j, k, previewSegmentIndex);
54
- floodedSet.add(index);
55
- floodedCount++;
56
- };
57
- clickedPoints.forEach((clickedPoint) => {
58
- if (getter(...clickedPoint) === 1) {
59
- floodFill(getter, clickedPoint, {
60
- onFlood,
61
- diagonals: true,
62
- });
63
- }
64
- });
65
- let clearedCount = 0;
66
- let previewCount = 0;
67
- const callback = ({ index, pointIJK, value: trackValue }) => {
68
- const value = segmentationVoxelManager.getAtIndex(index);
69
- if (floodedSet.has(index)) {
70
- previewCount++;
71
- const newValue = trackValue === segmentIndex ? segmentIndex : previewSegmentIndex;
72
- previewVoxelManager.setAtIJKPoint(pointIJK, newValue);
73
- }
74
- else if (value === previewSegmentIndex) {
75
- clearedCount++;
76
- const newValue = trackValue ?? 0;
77
- previewVoxelManager.setAtIJKPoint(pointIJK, newValue);
78
- }
79
- };
80
- previewVoxelManager.forEach(callback, {});
81
- if (floodedCount - previewCount !== 0) {
82
- console.warn('There were flooded=', floodedCount, 'cleared=', clearedCount, 'preview count=', previewCount, 'not handled', floodedCount - previewCount);
34
+ triggerSegmentationDataModified(operationData.segmentationId, arrayOfSlices, previewSegmentIndex);
35
+ },
36
+ };
37
+ export function createSegmentSet(operationData) {
38
+ const { segmentationVoxelManager, previewSegmentIndex, previewVoxelManager, segmentIndex, viewport, } = operationData;
39
+ const clickedPoints = previewVoxelManager.getPoints();
40
+ if (!clickedPoints?.length) {
41
+ return;
42
+ }
43
+ const boundsIJK = previewVoxelManager
44
+ .getBoundsIJK()
45
+ .map((bound, i) => [
46
+ Math.min(bound[0], ...clickedPoints.map((point) => point[i])),
47
+ Math.max(bound[1], ...clickedPoints.map((point) => point[i])),
48
+ ]);
49
+ if (boundsIJK.find((it) => it[0] < 0 || it[1] > MAX_IMAGE_SIZE)) {
50
+ return;
51
+ }
52
+ const { toIJK, fromIJK, boundsIJKPrime, error } = normalizeViewportPlane(viewport, boundsIJK);
53
+ if (error) {
54
+ console.warn('Not performing island removal for planes not orthogonal to acquisition plane', error);
55
+ return;
56
+ }
57
+ const [width, height, depth] = fromIJK(segmentationVoxelManager.dimensions);
58
+ const floodedSet = new RLEVoxelMap(width, height, depth);
59
+ const getter = (i, j, k) => {
60
+ const index = segmentationVoxelManager.toIndex(toIJK([i, j, k]));
61
+ const oldVal = segmentationVoxelManager.getAtIndex(index);
62
+ if (oldVal === previewSegmentIndex || oldVal === segmentIndex) {
63
+ return SegmentationEnum.SEGMENT;
83
64
  }
84
- const islandMap = new Set(segmentationVoxelManager.points || []);
85
- floodedSet.clear();
86
- for (const index of islandMap.keys()) {
87
- if (floodedSet.has(index)) {
65
+ };
66
+ floodedSet.fillFrom(getter, boundsIJKPrime);
67
+ floodedSet.normalizer = { toIJK, fromIJK, boundsIJKPrime };
68
+ return floodedSet;
69
+ }
70
+ function removeInternalIslands(operationData, floodedSet) {
71
+ const { height, normalizer } = floodedSet;
72
+ const { toIJK } = normalizer;
73
+ const { previewVoxelManager, previewSegmentIndex } = operationData;
74
+ floodedSet.forEachRow((baseIndex, row) => {
75
+ let lastRle;
76
+ for (const rle of [...row]) {
77
+ if (rle.value !== SegmentationEnum.ISLAND) {
88
78
  continue;
89
79
  }
90
- let isInternal = true;
91
- const internalSet = new Set();
92
- const onFloodInternal = (i, j, k) => {
93
- const floodIndex = previewVoxelManager.toIndex([i, j, k]);
94
- floodedSet.add(floodIndex);
95
- if ((boundsIJK[0][0] !== boundsIJK[0][1] &&
96
- (i === boundsIJK[0][0] || i === boundsIJK[0][1])) ||
97
- (boundsIJK[1][0] !== boundsIJK[1][1] &&
98
- (j === boundsIJK[1][0] || j === boundsIJK[1][1])) ||
99
- (boundsIJK[2][0] !== boundsIJK[2][1] &&
100
- (k === boundsIJK[2][0] || k === boundsIJK[2][1]))) {
101
- isInternal = false;
102
- }
103
- if (isInternal) {
104
- internalSet.add(floodIndex);
105
- }
106
- };
107
- const pointIJK = previewVoxelManager.toIJK(index);
108
- if (getter(...pointIJK) !== 0) {
80
+ if (!lastRle) {
81
+ lastRle = rle;
109
82
  continue;
110
83
  }
111
- floodFill(getter, pointIJK, {
112
- onFlood: onFloodInternal,
113
- diagonals: false,
114
- });
115
- if (isInternal) {
116
- for (const index of internalSet) {
117
- previewVoxelManager.setAtIndex(index, previewSegmentIndex);
118
- }
84
+ for (let iPrime = lastRle.end; iPrime < rle.start; iPrime++) {
85
+ floodedSet.set(baseIndex + iPrime, SegmentationEnum.INTERIOR);
119
86
  }
87
+ lastRle = rle;
120
88
  }
121
- triggerSegmentationDataModified(operationData.segmentationId, previewVoxelManager.getArrayOfModifiedSlices(), previewSegmentIndex);
122
- },
123
- };
89
+ });
90
+ floodedSet.forEach((baseIndex, rle) => {
91
+ if (rle.value !== SegmentationEnum.INTERIOR) {
92
+ return;
93
+ }
94
+ const [, jPrime, kPrime] = floodedSet.toIJK(baseIndex);
95
+ const rowPrev = jPrime > 0 ? floodedSet.getRun(jPrime - 1, kPrime) : null;
96
+ const rowNext = jPrime + 1 < height ? floodedSet.getRun(jPrime + 1, kPrime) : null;
97
+ const prevCovers = covers(rle, rowPrev);
98
+ const nextCovers = covers(rle, rowNext);
99
+ if (rle.end - rle.start > 2 && (!prevCovers || !nextCovers)) {
100
+ floodedSet.floodFill(rle.start, jPrime, kPrime, SegmentationEnum.EXTERIOR, { singlePlane: true });
101
+ }
102
+ });
103
+ floodedSet.forEach((baseIndex, rle) => {
104
+ if (rle.value !== SegmentationEnum.INTERIOR) {
105
+ return;
106
+ }
107
+ for (let iPrime = rle.start; iPrime < rle.end; iPrime++) {
108
+ const clearPoint = toIJK(floodedSet.toIJK(baseIndex + iPrime));
109
+ previewVoxelManager.setAtIJKPoint(clearPoint, previewSegmentIndex);
110
+ }
111
+ });
112
+ return previewVoxelManager.getArrayOfModifiedSlices();
113
+ }
114
+ function removeExternalIslands(operationData, floodedSet) {
115
+ const { previewVoxelManager } = operationData;
116
+ const { toIJK, fromIJK } = floodedSet.normalizer;
117
+ const clickedPoints = previewVoxelManager.getPoints();
118
+ let floodedCount = 0;
119
+ clickedPoints.forEach((clickedPoint) => {
120
+ const ijkPrime = fromIJK(clickedPoint);
121
+ const index = floodedSet.toIndex(ijkPrime);
122
+ const [iPrime, jPrime, kPrime] = ijkPrime;
123
+ if (floodedSet.get(index) === SegmentationEnum.SEGMENT) {
124
+ floodedCount += floodedSet.floodFill(iPrime, jPrime, kPrime, SegmentationEnum.ISLAND);
125
+ }
126
+ });
127
+ if (floodedCount === 0) {
128
+ return;
129
+ }
130
+ const callback = (index, rle) => {
131
+ const [, jPrime, kPrime] = floodedSet.toIJK(index);
132
+ if (rle.value !== SegmentationEnum.ISLAND) {
133
+ for (let iPrime = rle.start; iPrime < rle.end; iPrime++) {
134
+ const clearPoint = toIJK([iPrime, jPrime, kPrime]);
135
+ previewVoxelManager.setAtIJKPoint(clearPoint, null);
136
+ }
137
+ }
138
+ };
139
+ floodedSet.forEach(callback, { rowModified: true });
140
+ return floodedCount;
141
+ }
142
+ export function covers(rle, row) {
143
+ if (!row) {
144
+ return false;
145
+ }
146
+ let { start } = rle;
147
+ const { end } = rle;
148
+ for (const rowRle of row) {
149
+ if (start >= rowRle.start && start < rowRle.end) {
150
+ start = rowRle.end;
151
+ if (start >= end) {
152
+ return true;
153
+ }
154
+ }
155
+ }
156
+ return false;
157
+ }
@@ -38,7 +38,9 @@ export default {
38
38
  operationData.segmentationVoxelManager;
39
39
  operationData.previewVoxelManager = preview.previewVoxelManager;
40
40
  }
41
- if (segmentIndex === null || !previewSegmentIndex) {
41
+ if (segmentIndex === undefined ||
42
+ segmentIndex === null ||
43
+ !previewSegmentIndex) {
42
44
  return;
43
45
  }
44
46
  const configColor = previewColors?.[segmentIndex];
@@ -1,7 +1,7 @@
1
1
  import StrategyCallbacks from '../../../../enums/StrategyCallbacks';
2
2
  export default {
3
3
  [StrategyCallbacks.Fill]: (operationData) => {
4
- const { segmentsLocked, segmentationImageData, segmentationVoxelManager, previewVoxelManager: previewVoxelManager, brushStrategy, centerIJK, } = operationData;
4
+ const { segmentsLocked, segmentationImageData, segmentationVoxelManager, previewVoxelManager, brushStrategy, centerIJK, } = operationData;
5
5
  const isWithinThreshold = brushStrategy.createIsInThreshold?.(operationData);
6
6
  const { setValue } = brushStrategy;
7
7
  const callback = isWithinThreshold
@@ -1,5 +1,4 @@
1
1
  import StrategyCallbacks from '../../../../enums/StrategyCallbacks';
2
- import { triggerEvent, eventTarget } from '@cornerstonejs/core';
3
2
  export default {
4
3
  [StrategyCallbacks.INTERNAL_setValue]: (operationData, { value, index }) => {
5
4
  const { segmentsLocked, segmentIndex, previewVoxelManager, previewSegmentIndex, segmentationVoxelManager, } = operationData;
@@ -30,7 +30,7 @@ const sphereComposition = {
30
30
  };
31
31
  const SPHERE_STRATEGY = new BrushStrategy('Sphere', compositions.regionFill, compositions.setValue, sphereComposition, compositions.determineSegmentIndex, compositions.preview, compositions.labelmapStatistics);
32
32
  const fillInsideSphere = SPHERE_STRATEGY.strategyFunction;
33
- const SPHERE_THRESHOLD_STRATEGY = new BrushStrategy('SphereThreshold', ...SPHERE_STRATEGY.compositions, compositions.dynamicThreshold, compositions.threshold, compositions.islandRemoval);
33
+ const SPHERE_THRESHOLD_STRATEGY = new BrushStrategy('SphereThreshold', ...SPHERE_STRATEGY.compositions, compositions.dynamicThreshold, compositions.threshold);
34
34
  const SPHERE_THRESHOLD_STRATEGY_ISLAND = new BrushStrategy('SphereThreshold', ...SPHERE_STRATEGY.compositions, compositions.dynamicThreshold, compositions.threshold, compositions.islandRemoval);
35
35
  const thresholdInsideSphere = SPHERE_THRESHOLD_STRATEGY.strategyFunction;
36
36
  const thresholdInsideSphereIsland = SPHERE_THRESHOLD_STRATEGY_ISLAND.strategyFunction;
@@ -0,0 +1,19 @@
1
+ import type { Types } from '@cornerstonejs/core';
2
+ export default function normalizeViewportPlane(viewport: Types.IViewport, boundsIJK: Types.BoundsIJK): {
3
+ toIJK: any;
4
+ boundsIJKPrime: any;
5
+ fromIJK: any;
6
+ error: string;
7
+ } | {
8
+ boundsIJKPrime: any;
9
+ toIJK: (ijkPrime: any) => any;
10
+ fromIJK: (ijk: any) => any;
11
+ type: string;
12
+ error?: undefined;
13
+ } | {
14
+ boundsIJKPrime: any;
15
+ toIJK: ([j, k, i]: [any, any, any]) => any[];
16
+ fromIJK: ([i, j, k]: [any, any, any]) => any[];
17
+ type: string;
18
+ error?: undefined;
19
+ };
@@ -0,0 +1,35 @@
1
+ import { BaseVolumeViewport, utilities } from '@cornerstonejs/core';
2
+ const { isEqual } = utilities;
3
+ const acquisitionMapping = {
4
+ toIJK: (ijkPrime) => ijkPrime,
5
+ fromIJK: (ijk) => ijk,
6
+ type: 'acquistion',
7
+ };
8
+ const jkMapping = {
9
+ toIJK: ([j, k, i]) => [i, j, k],
10
+ fromIJK: ([i, j, k]) => [j, k, i],
11
+ type: 'jk',
12
+ };
13
+ const ikMapping = {
14
+ toIJK: ([i, k, j]) => [i, j, k],
15
+ fromIJK: ([i, j, k]) => [i, k, j],
16
+ type: 'ik',
17
+ };
18
+ export default function normalizeViewportPlane(viewport, boundsIJK) {
19
+ if (!(viewport instanceof BaseVolumeViewport)) {
20
+ return { ...acquisitionMapping, boundsIJKPrime: boundsIJK };
21
+ }
22
+ const { viewPlaneNormal } = viewport.getCamera();
23
+ const mapping = (isEqual(Math.abs(viewPlaneNormal[0]), 1) && jkMapping) ||
24
+ (isEqual(Math.abs(viewPlaneNormal[1]), 1) && ikMapping) ||
25
+ (isEqual(Math.abs(viewPlaneNormal[2]), 1) && acquisitionMapping);
26
+ if (!mapping) {
27
+ return {
28
+ toIJK: null,
29
+ boundsIJKPrime: null,
30
+ fromIJK: null,
31
+ error: `Only mappings orthogonal to acquisition plane are permitted, but requested ${viewPlaneNormal}`,
32
+ };
33
+ }
34
+ return { ...mapping, boundsIJKPrime: mapping.fromIJK(boundsIJK) };
35
+ }
@@ -1,7 +1,6 @@
1
1
  import type { Types } from '@cornerstonejs/core';
2
2
  type FloodFillResult = {
3
3
  flooded: Types.Point2[] | Types.Point3[];
4
- boundaries: Types.Point2[] | Types.Point3[];
5
4
  };
6
5
  type FloodFillGetter3D = (x: number, y: number, z: number) => unknown;
7
6
  type FloodFillGetter2D = (x: number, y: number) => unknown;
@@ -11,5 +10,7 @@ type FloodFillOptions = {
11
10
  onBoundary?: (x: number, y: number, z?: number) => void;
12
11
  equals?: (a: any, b: any) => boolean;
13
12
  diagonals?: boolean;
13
+ bounds?: Map<number, Types.Point2 | Types.Point3>;
14
+ filter?: (point: any) => boolean;
14
15
  };
15
16
  export type { FloodFillResult, FloodFillGetter, FloodFillOptions };
@@ -4,7 +4,9 @@ export default class VolumetricCalculator extends BasicStatsCalculator {
4
4
  const { spacing } = options;
5
5
  const stats = BasicStatsCalculator.getStatistics();
6
6
  const volumeUnit = spacing ? 'mm\xb3' : 'voxels\xb3';
7
- const volumeScale = spacing ? spacing[0] * spacing[1] * spacing[2] : 1;
7
+ const volumeScale = spacing
8
+ ? spacing[0] * spacing[1] * spacing[2] * 1000
9
+ : 1;
8
10
  stats.volume = {
9
11
  value: Array.isArray(stats.count.value)
10
12
  ? stats.count.value.map((v) => v * volumeScale)
@@ -2,20 +2,20 @@ function floodFill(getter, seed, options = {}) {
2
2
  const onFlood = options.onFlood;
3
3
  const onBoundary = options.onBoundary;
4
4
  const equals = options.equals;
5
+ const filter = options.filter;
5
6
  const diagonals = options.diagonals || false;
6
7
  const startNode = get(seed);
7
8
  const permutations = prunedPermutations();
8
9
  const stack = [];
9
10
  const flooded = [];
10
11
  const visits = new Set();
11
- const bounds = new Map();
12
+ const bounds = options.bounds;
12
13
  stack.push({ currentArgs: seed });
13
14
  while (stack.length > 0) {
14
15
  flood(stack.pop());
15
16
  }
16
17
  return {
17
18
  flooded,
18
- boundaries: boundaries(),
19
19
  };
20
20
  function flood(job) {
21
21
  const getArgs = job.currentArgs;
@@ -55,7 +55,7 @@ function floodFill(getter, seed, options = {}) {
55
55
  function markAsBoundary(prevArgs) {
56
56
  const [x, y, z = 0] = prevArgs;
57
57
  const iKey = x + 32768 + 65536 * (y + 32768 + 65536 * (z + 32768));
58
- bounds.set(iKey, prevArgs);
58
+ bounds?.set(iKey, prevArgs);
59
59
  if (onBoundary) {
60
60
  onBoundary(...prevArgs);
61
61
  }
@@ -67,6 +67,12 @@ function floodFill(getter, seed, options = {}) {
67
67
  for (let j = 0; j < getArgs.length; j += 1) {
68
68
  nextArgs[j] += perm[j];
69
69
  }
70
+ if (filter?.(nextArgs) === false) {
71
+ continue;
72
+ }
73
+ if (visited(nextArgs)) {
74
+ continue;
75
+ }
70
76
  stack.push({
71
77
  currentArgs: nextArgs,
72
78
  previousArgs: getArgs,
@@ -96,14 +102,6 @@ function floodFill(getter, seed, options = {}) {
96
102
  }
97
103
  return perms;
98
104
  }
99
- function boundaries() {
100
- const array = Array.from(bounds.values());
101
- array.reverse();
102
- return array;
103
- }
104
- }
105
- function defaultEquals(a, b) {
106
- return a === b;
107
105
  }
108
106
  function countNonZeroes(array) {
109
107
  let count = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cornerstonejs/tools",
3
- "version": "2.3.3",
3
+ "version": "2.5.0",
4
4
  "description": "Cornerstone3D Tools",
5
5
  "types": "./dist/esm/index.d.ts",
6
6
  "module": "./dist/esm/index.js",
@@ -104,7 +104,7 @@
104
104
  "canvas": "^2.11.2"
105
105
  },
106
106
  "peerDependencies": {
107
- "@cornerstonejs/core": "^2.3.3",
107
+ "@cornerstonejs/core": "^2.5.0",
108
108
  "@kitware/vtk.js": "32.1.1",
109
109
  "@types/d3-array": "^3.0.4",
110
110
  "@types/d3-interpolate": "^3.0.1",
@@ -123,5 +123,5 @@
123
123
  "type": "individual",
124
124
  "url": "https://ohif.org/donate"
125
125
  },
126
- "gitHead": "a438e4c7aeb322805f317b241d27cc3e60edb28b"
126
+ "gitHead": "e93ccb2022cd1e695f551aef666072b0cbb60952"
127
127
  }