@cornerstonejs/tools 5.1.2 → 5.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/eventListeners/annotations/contourSegmentation/contourSegmentationCompleted.js +11 -44
- package/dist/esm/tools/annotation/LivewireContourTool.js +3 -2
- package/dist/esm/tools/annotation/planarFreehandROITool/drawLoop.js +6 -2
- package/dist/esm/utilities/contourSegmentation/addPolylinesToSegmentation.js +54 -27
- package/dist/esm/utilities/contourSegmentation/applyContourStroke.d.ts +4 -0
- package/dist/esm/utilities/contourSegmentation/applyContourStroke.js +102 -0
- package/dist/esm/utilities/contourSegmentation/bridgeWeaklyConnected.d.ts +4 -0
- package/dist/esm/utilities/contourSegmentation/bridgeWeaklyConnected.js +126 -0
- package/dist/esm/utilities/contourSegmentation/clipperBooleanOps.d.ts +13 -0
- package/dist/esm/utilities/contourSegmentation/clipperBooleanOps.js +126 -0
- package/dist/esm/utilities/contourSegmentation/logicalOperators.js +27 -9
- package/dist/esm/utilities/contourSegmentation/mergeMultipleAnnotations.js +37 -46
- package/dist/esm/utilities/contourSegmentation/polylineInfoTypes.d.ts +2 -0
- package/dist/esm/utilities/contourSegmentation/polylineIntersect.js +3 -32
- package/dist/esm/utilities/contourSegmentation/polylineSetOps.d.ts +3 -0
- package/dist/esm/utilities/contourSegmentation/polylineSetOps.js +62 -0
- package/dist/esm/utilities/contourSegmentation/polylineSubtract.js +17 -55
- package/dist/esm/utilities/contourSegmentation/polylineUnify.js +15 -62
- package/dist/esm/utilities/contourSegmentation/polylineXor.js +3 -39
- package/dist/esm/utilities/contourSegmentation/sharedOperations.d.ts +3 -1
- package/dist/esm/utilities/contourSegmentation/sharedOperations.js +50 -58
- package/dist/esm/utilities/segmentation/getSegmentIndexAtWorldPoint.js +14 -18
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +5 -4
package/dist/esm/eventListeners/annotations/contourSegmentation/contourSegmentationCompleted.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { eventTarget, triggerEvent } from '@cornerstonejs/core';
|
|
2
2
|
import getViewportsForAnnotation from '../../../utilities/getViewportsForAnnotation';
|
|
3
|
-
import
|
|
4
|
-
import { areSameSegment, isContourSegmentationAnnotation, } from '../../../utilities/contourSegmentation';
|
|
3
|
+
import isContourSegmentationAnnotation from '../../../utilities/contourSegmentation/isContourSegmentationAnnotation';
|
|
5
4
|
import { getToolGroupForViewport } from '../../../store/ToolGroupManager';
|
|
6
|
-
import {
|
|
7
|
-
import { processMultipleIntersections } from '../../../utilities/contourSegmentation/mergeMultipleAnnotations';
|
|
8
|
-
import { convertContourPolylineToCanvasSpace, createPolylineHole, combinePolylines, } from '../../../utilities/contourSegmentation/sharedOperations';
|
|
5
|
+
import { addContourStroke, removeContourStroke, } from '../../../utilities/contourSegmentation/applyContourStroke';
|
|
9
6
|
import { Events } from '../../../enums';
|
|
10
7
|
const DEFAULT_CONTOUR_SEG_TOOL_NAME = 'PlanarFreehandContourSegmentationTool';
|
|
11
8
|
export default async function contourSegmentationCompletedListener(evt) {
|
|
@@ -15,41 +12,20 @@ export default async function contourSegmentationCompletedListener(evt) {
|
|
|
15
12
|
return;
|
|
16
13
|
}
|
|
17
14
|
const viewport = getViewport(sourceAnnotation);
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
element: viewport.element,
|
|
22
|
-
sourceAnnotation,
|
|
23
|
-
});
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
const sourcePolyline = convertContourPolylineToCanvasSpace(sourceAnnotation.data.contour.polyline, viewport);
|
|
27
|
-
const intersectingContours = findAllIntersectingContours(viewport, sourcePolyline, contourSegmentationAnnotations);
|
|
28
|
-
if (!intersectingContours.length) {
|
|
29
|
-
triggerEvent(eventTarget, Events.ANNOTATION_CUT_MERGE_PROCESS_COMPLETED, {
|
|
30
|
-
element: viewport.element,
|
|
31
|
-
sourceAnnotation,
|
|
32
|
-
});
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
if (intersectingContours.length > 1) {
|
|
36
|
-
processMultipleIntersections(viewport, sourceAnnotation, sourcePolyline, intersectingContours);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
const { targetAnnotation, targetPolyline, isContourHole } = intersectingContours[0];
|
|
40
|
-
if (isContourHole) {
|
|
41
|
-
const { contourHoleProcessingEnabled = false } = evt.detail;
|
|
42
|
-
if (!contourHoleProcessingEnabled) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
createPolylineHole(viewport, targetAnnotation, sourceAnnotation);
|
|
15
|
+
const { contourHoleProcessingEnabled = false } = evt.detail;
|
|
16
|
+
if (contourHoleProcessingEnabled) {
|
|
17
|
+
removeContourStroke(viewport, sourceAnnotation);
|
|
46
18
|
}
|
|
47
19
|
else {
|
|
48
|
-
|
|
20
|
+
addContourStroke(viewport, sourceAnnotation);
|
|
49
21
|
}
|
|
22
|
+
triggerEvent(eventTarget, Events.ANNOTATION_CUT_MERGE_PROCESS_COMPLETED, {
|
|
23
|
+
element: viewport.element,
|
|
24
|
+
sourceAnnotation,
|
|
25
|
+
});
|
|
50
26
|
}
|
|
51
27
|
function isFreehandContourSegToolRegisteredForViewport(viewport, silent = false) {
|
|
52
|
-
const toolName =
|
|
28
|
+
const toolName = DEFAULT_CONTOUR_SEG_TOOL_NAME;
|
|
53
29
|
const toolGroup = getToolGroupForViewport(viewport.id, viewport.renderingEngineId);
|
|
54
30
|
let errorMessage;
|
|
55
31
|
if (!toolGroup) {
|
|
@@ -71,12 +47,3 @@ function getViewport(annotation) {
|
|
|
71
47
|
const viewportWithToolRegistered = viewports.find((viewport) => isFreehandContourSegToolRegisteredForViewport(viewport, true));
|
|
72
48
|
return viewportWithToolRegistered ?? viewports[0];
|
|
73
49
|
}
|
|
74
|
-
function getValidContourSegmentationAnnotations(viewport, sourceAnnotation) {
|
|
75
|
-
const { annotationUID: sourceAnnotationUID } = sourceAnnotation;
|
|
76
|
-
const allAnnotations = getAllAnnotations();
|
|
77
|
-
return allAnnotations.filter((targetAnnotation) => targetAnnotation.annotationUID &&
|
|
78
|
-
targetAnnotation.annotationUID !== sourceAnnotationUID &&
|
|
79
|
-
isContourSegmentationAnnotation(targetAnnotation) &&
|
|
80
|
-
areSameSegment(targetAnnotation, sourceAnnotation) &&
|
|
81
|
-
viewport.isReferenceViewable(targetAnnotation.metadata));
|
|
82
|
-
}
|
|
@@ -445,9 +445,10 @@ class LivewireContourTool extends ContourSegmentationBaseTool {
|
|
|
445
445
|
const sourceViewport = viewport;
|
|
446
446
|
if (csUtils.viewportSupportsDisplaySetPresentation(sourceViewport)) {
|
|
447
447
|
const dataId = sourceViewport.getSourceDataId();
|
|
448
|
-
voiRange = ((
|
|
448
|
+
voiRange = ((dataId
|
|
449
449
|
? sourceViewport.getDisplaySetPresentation(dataId)?.voiRange
|
|
450
|
-
: undefined) ??
|
|
450
|
+
: undefined) ??
|
|
451
|
+
sourceViewport.getDefaultVOIRange(dataId) ??
|
|
451
452
|
undefined);
|
|
452
453
|
}
|
|
453
454
|
else {
|
|
@@ -11,6 +11,7 @@ import { resolveVectorToPeak } from './findOpenUShapedContourVectorToPeak';
|
|
|
11
11
|
import { polyline } from '../../../utilities/math';
|
|
12
12
|
import { removeAnnotation } from '../../../stateManagement/annotation/annotationState';
|
|
13
13
|
import { ContourWindingDirection } from '../../../types/ContourAnnotation';
|
|
14
|
+
import { bridgeSelfIntersectingPolyline } from '../../../utilities/contourSegmentation/bridgeWeaklyConnected';
|
|
14
15
|
const { addCanvasPointsToArray, pointsAreWithinCloseContourProximity, getFirstLineSegmentIntersectionIndexes, getSubPixelSpacingAndXYDirections, } = polyline;
|
|
15
16
|
function activateDraw(evt, annotation, viewportIdsToRender) {
|
|
16
17
|
const eventDetail = evt.detail;
|
|
@@ -90,7 +91,9 @@ function mouseDragDrawCallback(evt) {
|
|
|
90
91
|
}
|
|
91
92
|
else {
|
|
92
93
|
const crossingIndex = this.findCrossingIndexDuringCreate(evt);
|
|
93
|
-
|
|
94
|
+
const crossingClosesContour = crossingIndex !== undefined &&
|
|
95
|
+
pointsAreWithinCloseContourProximity(canvasPoints[0], canvasPoints[crossingIndex], this.configuration.closeContourProximity);
|
|
96
|
+
if (crossingClosesContour) {
|
|
94
97
|
this.applyCreateOnCross(evt, crossingIndex);
|
|
95
98
|
}
|
|
96
99
|
else {
|
|
@@ -136,9 +139,10 @@ function completeDrawClosedContour(element, options) {
|
|
|
136
139
|
const { viewport } = enabledElement;
|
|
137
140
|
addCanvasPointsToArray(element, canvasPoints, canvasPoints[0], this.commonData);
|
|
138
141
|
canvasPoints.pop();
|
|
139
|
-
const
|
|
142
|
+
const smoothedPoints = shouldSmooth(this.configuration, annotation)
|
|
140
143
|
? getInterpolatedPoints(this.configuration, canvasPoints)
|
|
141
144
|
: canvasPoints;
|
|
145
|
+
const updatedPoints = bridgeSelfIntersectingPolyline(smoothedPoints);
|
|
142
146
|
this.updateContourPolyline(annotation, {
|
|
143
147
|
points: updatedPoints,
|
|
144
148
|
closed: true,
|
|
@@ -1,39 +1,66 @@
|
|
|
1
1
|
import { utilities } from '@cornerstonejs/core';
|
|
2
2
|
import { addAnnotation } from '../../stateManagement';
|
|
3
|
+
import { addChildAnnotation } from '../../stateManagement/annotation/annotationState';
|
|
4
|
+
import updateContourPolyline from '../contours/updateContourPolyline';
|
|
5
|
+
import { ContourWindingDirection } from '../../types/ContourAnnotation';
|
|
3
6
|
const DEFAULT_CONTOUR_SEG_TOOLNAME = 'PlanarFreehandContourSegmentationTool';
|
|
4
7
|
export default function addPolylinesToSegmentation(viewport, annotationUIDsMap, segmentationId, polylinesInfo, segmentIndex) {
|
|
5
|
-
polylinesInfo.forEach(({ polyline, viewReference }) => {
|
|
8
|
+
polylinesInfo.forEach(({ polyline, viewReference, holePolylines }) => {
|
|
6
9
|
if (polyline.length < 3) {
|
|
7
10
|
return;
|
|
8
11
|
}
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
segmentationId,
|
|
18
|
-
segmentIndex,
|
|
19
|
-
},
|
|
20
|
-
handles: {},
|
|
21
|
-
},
|
|
22
|
-
handles: {},
|
|
23
|
-
highlighted: false,
|
|
24
|
-
autoGenerated: false,
|
|
25
|
-
invalidated: false,
|
|
26
|
-
isLocked: false,
|
|
27
|
-
isVisible: true,
|
|
28
|
-
metadata: {
|
|
29
|
-
toolName: DEFAULT_CONTOUR_SEG_TOOLNAME,
|
|
30
|
-
...viewReference,
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
addAnnotation(contourSegmentationAnnotation, viewport.element);
|
|
12
|
+
const parentAnnotation = createContourSegmentationAnnotation(segmentationId, segmentIndex, viewReference);
|
|
13
|
+
addAnnotation(parentAnnotation, viewport.element);
|
|
14
|
+
const parentPolylineCanvas = polyline.map((point) => viewport.worldToCanvas(point));
|
|
15
|
+
updateContourPolyline(parentAnnotation, {
|
|
16
|
+
points: parentPolylineCanvas,
|
|
17
|
+
closed: true,
|
|
18
|
+
targetWindingDirection: ContourWindingDirection.Clockwise,
|
|
19
|
+
}, viewport);
|
|
34
20
|
const currentSet = annotationUIDsMap?.get(segmentIndex) || new Set();
|
|
35
|
-
currentSet.add(
|
|
21
|
+
currentSet.add(parentAnnotation.annotationUID);
|
|
36
22
|
annotationUIDsMap.set(segmentIndex, currentSet);
|
|
23
|
+
holePolylines?.forEach((holePolyline) => {
|
|
24
|
+
if (holePolyline.length < 3) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const holeAnnotation = createContourSegmentationAnnotation(segmentationId, segmentIndex, viewReference);
|
|
28
|
+
addAnnotation(holeAnnotation, viewport.element);
|
|
29
|
+
addChildAnnotation(parentAnnotation, holeAnnotation);
|
|
30
|
+
const holePolylineCanvas = holePolyline.map((point) => viewport.worldToCanvas(point));
|
|
31
|
+
updateContourPolyline(holeAnnotation, {
|
|
32
|
+
points: holePolylineCanvas,
|
|
33
|
+
closed: true,
|
|
34
|
+
targetWindingDirection: ContourWindingDirection.CounterClockwise,
|
|
35
|
+
}, viewport);
|
|
36
|
+
});
|
|
37
37
|
});
|
|
38
38
|
return annotationUIDsMap;
|
|
39
39
|
}
|
|
40
|
+
function createContourSegmentationAnnotation(segmentationId, segmentIndex, viewReference) {
|
|
41
|
+
return {
|
|
42
|
+
annotationUID: utilities.uuidv4(),
|
|
43
|
+
data: {
|
|
44
|
+
contour: {
|
|
45
|
+
closed: true,
|
|
46
|
+
polyline: [],
|
|
47
|
+
},
|
|
48
|
+
segmentation: {
|
|
49
|
+
segmentationId,
|
|
50
|
+
segmentIndex,
|
|
51
|
+
},
|
|
52
|
+
handles: {},
|
|
53
|
+
cachedStats: {},
|
|
54
|
+
},
|
|
55
|
+
handles: {},
|
|
56
|
+
highlighted: false,
|
|
57
|
+
autoGenerated: false,
|
|
58
|
+
invalidated: false,
|
|
59
|
+
isLocked: false,
|
|
60
|
+
isVisible: true,
|
|
61
|
+
metadata: {
|
|
62
|
+
toolName: DEFAULT_CONTOUR_SEG_TOOLNAME,
|
|
63
|
+
...viewReference,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Types } from '@cornerstonejs/core';
|
|
2
|
+
import type { ContourSegmentationAnnotation } from '../../types';
|
|
3
|
+
export declare function addContourStroke(viewport: Types.IViewport, sourceAnnotation: ContourSegmentationAnnotation): void;
|
|
4
|
+
export declare function removeContourStroke(viewport: Types.IViewport, sourceAnnotation: ContourSegmentationAnnotation): void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ContourWindingDirection } from '../../types/ContourAnnotation';
|
|
2
|
+
import { applyBoolean, BooleanOp, splitSelfIntersections, } from './clipperBooleanOps';
|
|
3
|
+
import { unifyWeaklyConnectedPolygons } from './bridgeWeaklyConnected';
|
|
4
|
+
import { convertContourPolylineToCanvasSpace, createNewAnnotationFromPolyline, getContourHolesData, updateViewportsForAnnotations, } from './sharedOperations';
|
|
5
|
+
import { getAllAnnotations } from '../../stateManagement/annotation/annotationState';
|
|
6
|
+
import { addAnnotation, addChildAnnotation, removeAnnotation, } from '../../stateManagement/annotation/annotationState';
|
|
7
|
+
import { addContourSegmentationAnnotation } from './addContourSegmentationAnnotation';
|
|
8
|
+
import { removeContourSegmentationAnnotation } from './removeContourSegmentationAnnotation';
|
|
9
|
+
import { triggerAnnotationModified } from '../../stateManagement/annotation/helpers/state';
|
|
10
|
+
import areSameSegment from './areSameSegment';
|
|
11
|
+
import isContourSegmentationAnnotation from './isContourSegmentationAnnotation';
|
|
12
|
+
import { hasToolByName } from '../../store/addTool';
|
|
13
|
+
const DEFAULT_CONTOUR_SEG_TOOL_NAME = 'PlanarFreehandContourSegmentationTool';
|
|
14
|
+
export function addContourStroke(viewport, sourceAnnotation) {
|
|
15
|
+
applyStroke(viewport, sourceAnnotation, BooleanOp.Union);
|
|
16
|
+
}
|
|
17
|
+
export function removeContourStroke(viewport, sourceAnnotation) {
|
|
18
|
+
applyStroke(viewport, sourceAnnotation, BooleanOp.Difference);
|
|
19
|
+
}
|
|
20
|
+
function applyStroke(viewport, sourceAnnotation, op) {
|
|
21
|
+
if (!hasToolByName(DEFAULT_CONTOUR_SEG_TOOL_NAME)) {
|
|
22
|
+
console.warn(`${DEFAULT_CONTOUR_SEG_TOOL_NAME} is not registered. Cannot apply stroke.`);
|
|
23
|
+
removeAnnotationCompletely(sourceAnnotation);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const sourcePolyline = convertContourPolylineToCanvasSpace(sourceAnnotation.data.contour.polyline, viewport);
|
|
27
|
+
if (sourcePolyline.length < 3) {
|
|
28
|
+
removeAnnotationCompletely(sourceAnnotation);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const targets = collectSamePlaneSegmentTargets(viewport, sourceAnnotation);
|
|
32
|
+
const subjects = targets.map((t) => {
|
|
33
|
+
const outer = convertContourPolylineToCanvasSpace(t.data.contour.polyline, viewport);
|
|
34
|
+
const holes = getContourHolesData(viewport, t).map((h) => h.polyline);
|
|
35
|
+
return { outer, holes: holes.length ? holes : undefined };
|
|
36
|
+
});
|
|
37
|
+
const clips = splitSelfIntersections(sourcePolyline);
|
|
38
|
+
if (clips.length === 0) {
|
|
39
|
+
removeAnnotationCompletely(sourceAnnotation);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const booleanResult = applyBoolean(subjects, clips, op);
|
|
43
|
+
const resultPolygons = op === BooleanOp.Union
|
|
44
|
+
? unifyWeaklyConnectedPolygons(booleanResult)
|
|
45
|
+
: booleanResult;
|
|
46
|
+
const toRemove = [sourceAnnotation];
|
|
47
|
+
for (const t of targets) {
|
|
48
|
+
toRemove.push(t);
|
|
49
|
+
for (const h of getContourHolesData(viewport, t)) {
|
|
50
|
+
toRemove.push(h.annotation);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const annotation of toRemove) {
|
|
54
|
+
removeAnnotationCompletely(annotation);
|
|
55
|
+
}
|
|
56
|
+
const template = targets[0] ?? sourceAnnotation;
|
|
57
|
+
const { element } = viewport;
|
|
58
|
+
for (const polygon of resultPolygons) {
|
|
59
|
+
if (polygon.outer.length < 3) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const parent = createNewAnnotationFromPolyline(viewport, template, polygon.outer, ContourWindingDirection.Clockwise);
|
|
63
|
+
addAnnotation(parent, element);
|
|
64
|
+
addContourSegmentationAnnotation(parent);
|
|
65
|
+
polygon.holes?.forEach((holePolyline) => {
|
|
66
|
+
if (holePolyline.length < 3) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const hole = createNewAnnotationFromPolyline(viewport, template, holePolyline, ContourWindingDirection.CounterClockwise);
|
|
70
|
+
addAnnotation(hole, element);
|
|
71
|
+
addChildAnnotation(parent, hole);
|
|
72
|
+
triggerAnnotationModified(hole, element);
|
|
73
|
+
});
|
|
74
|
+
triggerAnnotationModified(parent, element);
|
|
75
|
+
}
|
|
76
|
+
updateViewportsForAnnotations(viewport, toRemove);
|
|
77
|
+
}
|
|
78
|
+
function collectSamePlaneSegmentTargets(viewport, sourceAnnotation) {
|
|
79
|
+
const sourceUID = sourceAnnotation.annotationUID;
|
|
80
|
+
return getAllAnnotations().filter((candidate) => {
|
|
81
|
+
if (!candidate.annotationUID || candidate.annotationUID === sourceUID) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (!isContourSegmentationAnnotation(candidate)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (!areSameSegment(candidate, sourceAnnotation)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (!viewport.isReferenceViewable(candidate.metadata)) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (candidate.parentAnnotationUID) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function removeAnnotationCompletely(annotation) {
|
|
100
|
+
removeAnnotation(annotation.annotationUID);
|
|
101
|
+
removeContourSegmentationAnnotation(annotation);
|
|
102
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Types } from '@cornerstonejs/core';
|
|
2
|
+
import { type PolygonWithHoles } from './clipperBooleanOps';
|
|
3
|
+
export declare function unifyWeaklyConnectedPolygons(polygons: PolygonWithHoles[]): PolygonWithHoles[];
|
|
4
|
+
export declare function bridgeSelfIntersectingPolyline(line: Types.Point2[]): Types.Point2[];
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import * as math from '../math';
|
|
2
|
+
import { splitSelfIntersections, } from './clipperBooleanOps';
|
|
3
|
+
const TOUCH_EPS = 1e-2;
|
|
4
|
+
const TOUCH_EPS_SQ = TOUCH_EPS * TOUCH_EPS;
|
|
5
|
+
function dist2(a, b) {
|
|
6
|
+
const dx = a[0] - b[0];
|
|
7
|
+
const dy = a[1] - b[1];
|
|
8
|
+
return dx * dx + dy * dy;
|
|
9
|
+
}
|
|
10
|
+
function boxesWithinEps(a, b) {
|
|
11
|
+
return (a.minX <= b.maxX + TOUCH_EPS &&
|
|
12
|
+
b.minX <= a.maxX + TOUCH_EPS &&
|
|
13
|
+
a.minY <= b.maxY + TOUCH_EPS &&
|
|
14
|
+
b.minY <= a.maxY + TOUCH_EPS);
|
|
15
|
+
}
|
|
16
|
+
function ringsTouch(a, b) {
|
|
17
|
+
for (let i = 0; i < a.length; i++) {
|
|
18
|
+
for (let j = 0; j < b.length; j++) {
|
|
19
|
+
if (dist2(a[i], b[j]) <= TOUCH_EPS_SQ) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
function spliceTwoRings(a, b) {
|
|
27
|
+
const bb = math.polyline.getWindingDirection(b) !==
|
|
28
|
+
math.polyline.getWindingDirection(a)
|
|
29
|
+
? [...b].reverse()
|
|
30
|
+
: b;
|
|
31
|
+
let best = Infinity;
|
|
32
|
+
let ai = 0;
|
|
33
|
+
let bj = 0;
|
|
34
|
+
for (let i = 0; i < a.length; i++) {
|
|
35
|
+
for (let j = 0; j < bb.length; j++) {
|
|
36
|
+
const d = dist2(a[i], bb[j]);
|
|
37
|
+
if (d < best) {
|
|
38
|
+
best = d;
|
|
39
|
+
ai = i;
|
|
40
|
+
bj = j;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const loop = [...bb.slice(bj), ...bb.slice(0, bj)];
|
|
45
|
+
return [...a.slice(0, ai + 1), ...loop, loop[0], ...a.slice(ai + 1)];
|
|
46
|
+
}
|
|
47
|
+
function groupTouchingOuters(outers) {
|
|
48
|
+
const n = outers.length;
|
|
49
|
+
const parent = Array.from({ length: n }, (_, i) => i);
|
|
50
|
+
const find = (x) => {
|
|
51
|
+
let root = x;
|
|
52
|
+
while (parent[root] !== root) {
|
|
53
|
+
root = parent[root];
|
|
54
|
+
}
|
|
55
|
+
while (parent[x] !== root) {
|
|
56
|
+
const next = parent[x];
|
|
57
|
+
parent[x] = root;
|
|
58
|
+
x = next;
|
|
59
|
+
}
|
|
60
|
+
return root;
|
|
61
|
+
};
|
|
62
|
+
const union = (x, y) => {
|
|
63
|
+
parent[find(x)] = find(y);
|
|
64
|
+
};
|
|
65
|
+
const boxes = outers.map((o) => math.polyline.getAABB(o));
|
|
66
|
+
for (let i = 0; i < n; i++) {
|
|
67
|
+
for (let j = i + 1; j < n; j++) {
|
|
68
|
+
if (boxesWithinEps(boxes[i], boxes[j]) &&
|
|
69
|
+
ringsTouch(outers[i], outers[j])) {
|
|
70
|
+
union(i, j);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const groups = new Map();
|
|
75
|
+
for (let i = 0; i < n; i++) {
|
|
76
|
+
const root = find(i);
|
|
77
|
+
const group = groups.get(root);
|
|
78
|
+
if (group) {
|
|
79
|
+
group.push(i);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
groups.set(root, [i]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return [...groups.values()];
|
|
86
|
+
}
|
|
87
|
+
export function unifyWeaklyConnectedPolygons(polygons) {
|
|
88
|
+
if (polygons.length < 2) {
|
|
89
|
+
return polygons;
|
|
90
|
+
}
|
|
91
|
+
const outers = polygons.map((p) => p.outer);
|
|
92
|
+
const groups = groupTouchingOuters(outers);
|
|
93
|
+
if (groups.length === polygons.length) {
|
|
94
|
+
return polygons;
|
|
95
|
+
}
|
|
96
|
+
return groups.map((group) => {
|
|
97
|
+
if (group.length === 1) {
|
|
98
|
+
return polygons[group[0]];
|
|
99
|
+
}
|
|
100
|
+
let outer = polygons[group[0]].outer;
|
|
101
|
+
for (let k = 1; k < group.length; k++) {
|
|
102
|
+
outer = spliceTwoRings(outer, polygons[group[k]].outer);
|
|
103
|
+
}
|
|
104
|
+
const holes = group.flatMap((idx) => polygons[idx].holes ?? []);
|
|
105
|
+
return holes.length ? { outer, holes } : { outer };
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
export function bridgeSelfIntersectingPolyline(line) {
|
|
109
|
+
const polygons = splitSelfIntersections(line);
|
|
110
|
+
if (polygons.length === 0) {
|
|
111
|
+
return line;
|
|
112
|
+
}
|
|
113
|
+
if (polygons.length === 1 && !polygons[0].holes?.length) {
|
|
114
|
+
return line;
|
|
115
|
+
}
|
|
116
|
+
const unified = unifyWeaklyConnectedPolygons(polygons);
|
|
117
|
+
const main = unified.reduce((largest, candidate) => math.polyline.getArea(candidate.outer) >
|
|
118
|
+
math.polyline.getArea(largest.outer)
|
|
119
|
+
? candidate
|
|
120
|
+
: largest);
|
|
121
|
+
let outer = main.outer;
|
|
122
|
+
for (const hole of main.holes ?? []) {
|
|
123
|
+
outer = spliceTwoRings(outer, hole);
|
|
124
|
+
}
|
|
125
|
+
return outer;
|
|
126
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Types } from '@cornerstonejs/core';
|
|
2
|
+
export type PolygonWithHoles = {
|
|
3
|
+
outer: Types.Point2[];
|
|
4
|
+
holes?: Types.Point2[][];
|
|
5
|
+
};
|
|
6
|
+
export declare enum BooleanOp {
|
|
7
|
+
Union = 0,
|
|
8
|
+
Difference = 1,
|
|
9
|
+
Intersection = 2,
|
|
10
|
+
Xor = 3
|
|
11
|
+
}
|
|
12
|
+
export declare function applyBoolean(subjects: PolygonWithHoles[], clips: PolygonWithHoles[], op: BooleanOp): PolygonWithHoles[];
|
|
13
|
+
export declare function splitSelfIntersections(polyline: Types.Point2[]): PolygonWithHoles[];
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Clipper, ClipType, FillRule, PolyTreeD, } from 'clipper2-ts';
|
|
2
|
+
const PRECISION = 4;
|
|
3
|
+
export var BooleanOp;
|
|
4
|
+
(function (BooleanOp) {
|
|
5
|
+
BooleanOp[BooleanOp["Union"] = 0] = "Union";
|
|
6
|
+
BooleanOp[BooleanOp["Difference"] = 1] = "Difference";
|
|
7
|
+
BooleanOp[BooleanOp["Intersection"] = 2] = "Intersection";
|
|
8
|
+
BooleanOp[BooleanOp["Xor"] = 3] = "Xor";
|
|
9
|
+
})(BooleanOp || (BooleanOp = {}));
|
|
10
|
+
const opToClipType = {
|
|
11
|
+
[BooleanOp.Union]: ClipType.Union,
|
|
12
|
+
[BooleanOp.Difference]: ClipType.Difference,
|
|
13
|
+
[BooleanOp.Intersection]: ClipType.Intersection,
|
|
14
|
+
[BooleanOp.Xor]: ClipType.Xor,
|
|
15
|
+
};
|
|
16
|
+
function point2ToPathD(polyline) {
|
|
17
|
+
const len = polyline.length;
|
|
18
|
+
const out = new Array(len);
|
|
19
|
+
for (let i = 0; i < len; i++) {
|
|
20
|
+
out[i] = { x: polyline[i][0], y: polyline[i][1] };
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function pathDToPoint2(path) {
|
|
25
|
+
const len = path.length;
|
|
26
|
+
const out = new Array(len);
|
|
27
|
+
for (let i = 0; i < len; i++) {
|
|
28
|
+
out[i] = [path[i].x, path[i].y];
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
function polygonsToPathsD(polygons) {
|
|
33
|
+
const paths = [];
|
|
34
|
+
for (const p of polygons) {
|
|
35
|
+
if (p.outer.length >= 3) {
|
|
36
|
+
paths.push(point2ToPathD(p.outer));
|
|
37
|
+
}
|
|
38
|
+
if (p.holes) {
|
|
39
|
+
for (const hole of p.holes) {
|
|
40
|
+
if (hole.length >= 3) {
|
|
41
|
+
paths.push(point2ToPathD(hole));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return paths;
|
|
47
|
+
}
|
|
48
|
+
function polyTreeToPolygons(tree) {
|
|
49
|
+
const out = [];
|
|
50
|
+
collectOuters(tree, out);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function collectOuters(node, out) {
|
|
54
|
+
for (let i = 0; i < node.count; i++) {
|
|
55
|
+
const child = node.child(i);
|
|
56
|
+
if (child.isHole) {
|
|
57
|
+
collectOuters(child, out);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const outerPoly = child.poly;
|
|
61
|
+
if (!outerPoly || outerPoly.length < 3) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const polygon = { outer: pathDToPoint2(outerPoly) };
|
|
65
|
+
const holes = [];
|
|
66
|
+
for (let j = 0; j < child.count; j++) {
|
|
67
|
+
const grand = child.child(j);
|
|
68
|
+
if (grand.isHole) {
|
|
69
|
+
const holePoly = grand.poly;
|
|
70
|
+
if (holePoly && holePoly.length >= 3) {
|
|
71
|
+
holes.push(pathDToPoint2(holePoly));
|
|
72
|
+
}
|
|
73
|
+
collectOuters(grand, out);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (holes.length > 0) {
|
|
77
|
+
polygon.holes = holes;
|
|
78
|
+
}
|
|
79
|
+
out.push(polygon);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function clonePolygons(polygons) {
|
|
83
|
+
return polygons.map((p) => ({
|
|
84
|
+
outer: p.outer.map((pt) => [pt[0], pt[1]]),
|
|
85
|
+
holes: p.holes?.map((h) => h.map((pt) => [pt[0], pt[1]])),
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
export function applyBoolean(subjects, clips, op) {
|
|
89
|
+
if (subjects.length === 0) {
|
|
90
|
+
if (op === BooleanOp.Union || op === BooleanOp.Xor) {
|
|
91
|
+
return clonePolygons(clips);
|
|
92
|
+
}
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
if (clips.length === 0) {
|
|
96
|
+
if (op === BooleanOp.Intersection) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
return clonePolygons(subjects);
|
|
100
|
+
}
|
|
101
|
+
const subjectPaths = polygonsToPathsD(subjects);
|
|
102
|
+
const clipPaths = polygonsToPathsD(clips);
|
|
103
|
+
if (subjectPaths.length === 0) {
|
|
104
|
+
if (op === BooleanOp.Union || op === BooleanOp.Xor) {
|
|
105
|
+
return clonePolygons(clips);
|
|
106
|
+
}
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
if (clipPaths.length === 0) {
|
|
110
|
+
if (op === BooleanOp.Intersection) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
return clonePolygons(subjects);
|
|
114
|
+
}
|
|
115
|
+
const tree = new PolyTreeD();
|
|
116
|
+
Clipper.booleanOpDWithPolyTree(opToClipType[op], subjectPaths, clipPaths, tree, FillRule.EvenOdd, PRECISION);
|
|
117
|
+
return polyTreeToPolygons(tree);
|
|
118
|
+
}
|
|
119
|
+
export function splitSelfIntersections(polyline) {
|
|
120
|
+
if (polyline.length < 3) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
const tree = new PolyTreeD();
|
|
124
|
+
Clipper.booleanOpDWithPolyTree(ClipType.Union, [point2ToPathD(polyline)], null, tree, FillRule.EvenOdd, PRECISION);
|
|
125
|
+
return polyTreeToPolygons(tree);
|
|
126
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getAnnotation, removeAnnotation } from '../../stateManagement';
|
|
1
|
+
import { getAnnotation, removeAnnotation, getChildAnnotations, } from '../../stateManagement';
|
|
2
2
|
import { convertContourPolylineToCanvasSpace, convertContourPolylineToWorld, } from './sharedOperations';
|
|
3
3
|
import addPolylinesToSegmentation from './addPolylinesToSegmentation';
|
|
4
4
|
import { getSegmentation } from '../../stateManagement/segmentation/getSegmentation';
|
|
@@ -29,9 +29,14 @@ function getPolylinesInfoWorld(contourRepresentationData, segmentIndex) {
|
|
|
29
29
|
for (const annotationUID of annotationUIDs) {
|
|
30
30
|
const annotation = getAnnotation(annotationUID);
|
|
31
31
|
const { polyline } = annotation.data.contour;
|
|
32
|
+
const childAnnotations = getChildAnnotations(annotation);
|
|
33
|
+
const holePolylines = childAnnotations.length > 0
|
|
34
|
+
? childAnnotations.map((child) => child.data.contour.polyline)
|
|
35
|
+
: undefined;
|
|
32
36
|
polylinesInfo.push({
|
|
33
37
|
polyline,
|
|
34
38
|
viewReference: getViewReferenceFromAnnotation(annotation),
|
|
39
|
+
...(holePolylines ? { holePolylines } : {}),
|
|
35
40
|
});
|
|
36
41
|
}
|
|
37
42
|
return polylinesInfo;
|
|
@@ -51,16 +56,26 @@ function extractPolylinesInCanvasSpace(viewport, segment1, segment2) {
|
|
|
51
56
|
if (!polyLinesInfoWorld1 || !polyLinesInfoWorld2) {
|
|
52
57
|
return;
|
|
53
58
|
}
|
|
54
|
-
const polyLinesInfoCanvas1 = polyLinesInfoWorld1.map(({ polyline, viewReference }) => {
|
|
59
|
+
const polyLinesInfoCanvas1 = polyLinesInfoWorld1.map(({ polyline, viewReference, holePolylines }) => {
|
|
55
60
|
return {
|
|
56
61
|
polyline: convertContourPolylineToCanvasSpace(polyline, viewport),
|
|
57
62
|
viewReference,
|
|
63
|
+
...(holePolylines?.length
|
|
64
|
+
? {
|
|
65
|
+
holePolylines: holePolylines.map((hole) => convertContourPolylineToCanvasSpace(hole, viewport)),
|
|
66
|
+
}
|
|
67
|
+
: {}),
|
|
58
68
|
};
|
|
59
69
|
});
|
|
60
|
-
const polyLinesInfoCanvas2 = polyLinesInfoWorld2.map(({ polyline, viewReference }) => {
|
|
70
|
+
const polyLinesInfoCanvas2 = polyLinesInfoWorld2.map(({ polyline, viewReference, holePolylines }) => {
|
|
61
71
|
return {
|
|
62
72
|
polyline: convertContourPolylineToCanvasSpace(polyline, viewport),
|
|
63
73
|
viewReference,
|
|
74
|
+
...(holePolylines?.length
|
|
75
|
+
? {
|
|
76
|
+
holePolylines: holePolylines.map((hole) => convertContourPolylineToCanvasSpace(hole, viewport)),
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
64
79
|
};
|
|
65
80
|
});
|
|
66
81
|
return { polyLinesInfoCanvas1, polyLinesInfoCanvas2 };
|
|
@@ -120,12 +135,15 @@ function applyLogicalOperation(segment1, segment2, options, operation) {
|
|
|
120
135
|
polylinesMerged = unifyPolylineSets(polyLinesInfoCanvas1, polyLinesInfoCanvas2);
|
|
121
136
|
break;
|
|
122
137
|
}
|
|
123
|
-
const polyLinesWorld = polylinesMerged.map(({ polyline, viewReference }) => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
const polyLinesWorld = polylinesMerged.map(({ polyline, viewReference, holePolylines }) => ({
|
|
139
|
+
polyline: convertContourPolylineToWorld(polyline, viewport),
|
|
140
|
+
viewReference,
|
|
141
|
+
...(holePolylines?.length
|
|
142
|
+
? {
|
|
143
|
+
holePolylines: holePolylines.map((hole) => convertContourPolylineToWorld(hole, viewport)),
|
|
144
|
+
}
|
|
145
|
+
: {}),
|
|
146
|
+
}));
|
|
129
147
|
const resultSegment = options;
|
|
130
148
|
const segmentation = getSegmentation(resultSegment.segmentationId);
|
|
131
149
|
const segmentIndex = resultSegment.segmentIndex;
|