@cornerstonejs/tools 5.1.3 → 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/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/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +5 -4
|
@@ -2,13 +2,14 @@ import { utilities as csUtils, getEnabledElement } from '@cornerstonejs/core';
|
|
|
2
2
|
import { ContourWindingDirection } from '../../types/ContourAnnotation';
|
|
3
3
|
import * as math from '../math';
|
|
4
4
|
import updateContourPolyline from '../contours/updateContourPolyline';
|
|
5
|
-
import { addAnnotation, removeAnnotation, getChildAnnotations, addChildAnnotation,
|
|
5
|
+
import { addAnnotation, removeAnnotation, getChildAnnotations, addChildAnnotation, } from '../../stateManagement/annotation/annotationState';
|
|
6
6
|
import { addContourSegmentationAnnotation } from './addContourSegmentationAnnotation';
|
|
7
7
|
import { removeContourSegmentationAnnotation } from './removeContourSegmentationAnnotation';
|
|
8
8
|
import { triggerAnnotationModified } from '../../stateManagement/annotation/helpers/state';
|
|
9
9
|
import triggerAnnotationRenderForViewportIds from '../triggerAnnotationRenderForViewportIds';
|
|
10
10
|
import { getViewportIdsWithToolToRender } from '../viewportFilters';
|
|
11
11
|
import { hasToolByName, hasTool } from '../../store/addTool';
|
|
12
|
+
import { applyBoolean, BooleanOp, } from './clipperBooleanOps';
|
|
12
13
|
const DEFAULT_CONTOUR_SEG_TOOL_NAME = 'PlanarFreehandContourSegmentationTool';
|
|
13
14
|
function processMultipleIntersections(viewport, sourceAnnotation, sourcePolyline, intersectingContours) {
|
|
14
15
|
const holeOperations = intersectingContours.filter((item) => item.isContourHole);
|
|
@@ -33,51 +34,52 @@ function processMultipleIntersections(viewport, sourceAnnotation, sourcePolyline
|
|
|
33
34
|
}
|
|
34
35
|
function processSequentialIntersections(viewport, sourceAnnotation, sourcePolyline, mergeOperations) {
|
|
35
36
|
const { element } = viewport;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
allAnnotationsToRemove.push(targetAnnotation);
|
|
37
|
+
const subjects = mergeOperations.map(({ targetAnnotation, targetPolyline }) => {
|
|
38
|
+
const holes = getContourHolesData(viewport, targetAnnotation).map((h) => h.polyline);
|
|
39
|
+
return {
|
|
40
|
+
outer: targetPolyline,
|
|
41
|
+
holes: holes.length ? holes : undefined,
|
|
42
|
+
};
|
|
43
43
|
});
|
|
44
|
+
const clips = [{ outer: sourcePolyline }];
|
|
44
45
|
const sourceStartPoint = sourcePolyline[0];
|
|
45
46
|
const shouldMerge = mergeOperations.some(({ targetPolyline }) => math.polyline.containsPoint(targetPolyline, sourceStartPoint));
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
allAnnotationsToRemove.forEach((annotation) => {
|
|
47
|
+
const op = shouldMerge ? BooleanOp.Union : BooleanOp.Difference;
|
|
48
|
+
const resultPolygons = applyBoolean(subjects, clips, op);
|
|
49
|
+
const allHoleAnnotations = [];
|
|
50
|
+
const allAnnotationsToRemove = [
|
|
51
|
+
sourceAnnotation,
|
|
52
|
+
];
|
|
53
|
+
mergeOperations.forEach(({ targetAnnotation }) => {
|
|
54
|
+
allAnnotationsToRemove.push(targetAnnotation);
|
|
55
|
+
getContourHolesData(viewport, targetAnnotation).forEach((h) => allHoleAnnotations.push(h.annotation));
|
|
56
|
+
});
|
|
57
|
+
[...allAnnotationsToRemove, ...allHoleAnnotations].forEach((annotation) => {
|
|
60
58
|
removeAnnotation(annotation.annotationUID);
|
|
61
59
|
removeContourSegmentationAnnotation(annotation);
|
|
62
60
|
});
|
|
63
|
-
allHoles.forEach((holeData) => clearParentAnnotation(holeData.annotation));
|
|
64
61
|
const baseAnnotation = mergeOperations[0].targetAnnotation;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (!polyline || polyline.length < 3) {
|
|
68
|
-
console.warn('Skipping creation of new annotation due to invalid polyline:', polyline);
|
|
62
|
+
resultPolygons.forEach((polygon) => {
|
|
63
|
+
if (polygon.outer.length < 3) {
|
|
69
64
|
return;
|
|
70
65
|
}
|
|
71
|
-
const
|
|
72
|
-
addAnnotation(
|
|
73
|
-
addContourSegmentationAnnotation(
|
|
74
|
-
triggerAnnotationModified(
|
|
75
|
-
|
|
66
|
+
const parent = createNewAnnotationFromPolyline(viewport, baseAnnotation, polygon.outer, ContourWindingDirection.Clockwise);
|
|
67
|
+
addAnnotation(parent, element);
|
|
68
|
+
addContourSegmentationAnnotation(parent);
|
|
69
|
+
triggerAnnotationModified(parent, element);
|
|
70
|
+
polygon.holes?.forEach((holePolyline) => {
|
|
71
|
+
if (holePolyline.length < 3) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const hole = createNewAnnotationFromPolyline(viewport, baseAnnotation, holePolyline, ContourWindingDirection.CounterClockwise);
|
|
75
|
+
addAnnotation(hole, element);
|
|
76
|
+
addChildAnnotation(parent, hole);
|
|
77
|
+
triggerAnnotationModified(hole, element);
|
|
78
|
+
});
|
|
76
79
|
});
|
|
77
|
-
reassignHolesToNewAnnotations(viewport, allHoles, newAnnotations);
|
|
78
80
|
updateViewportsForAnnotations(viewport, allAnnotationsToRemove);
|
|
79
81
|
}
|
|
80
|
-
function createNewAnnotationFromPolyline(viewport, baseAnnotation, polyline) {
|
|
82
|
+
function createNewAnnotationFromPolyline(viewport, baseAnnotation, polyline, windingDirection = ContourWindingDirection.Clockwise) {
|
|
81
83
|
const startPointWorld = viewport.canvasToWorld(polyline[0]);
|
|
82
84
|
const endPointWorld = viewport.canvasToWorld(polyline[polyline.length - 1]);
|
|
83
85
|
const newAnnotation = {
|
|
@@ -115,21 +117,10 @@ function createNewAnnotationFromPolyline(viewport, baseAnnotation, polyline) {
|
|
|
115
117
|
updateContourPolyline(newAnnotation, {
|
|
116
118
|
points: polyline,
|
|
117
119
|
closed: true,
|
|
118
|
-
targetWindingDirection:
|
|
120
|
+
targetWindingDirection: windingDirection,
|
|
119
121
|
}, viewport);
|
|
120
122
|
return newAnnotation;
|
|
121
123
|
}
|
|
122
|
-
function reassignHolesToNewAnnotations(viewport, holes, newAnnotations) {
|
|
123
|
-
holes.forEach((holeData) => {
|
|
124
|
-
const parentAnnotation = newAnnotations.find((annotation) => {
|
|
125
|
-
const parentPolyline = convertContourPolylineToCanvasSpace(annotation.data.contour.polyline, viewport);
|
|
126
|
-
return math.polyline.containsPoints(parentPolyline, holeData.polyline);
|
|
127
|
-
});
|
|
128
|
-
if (parentAnnotation) {
|
|
129
|
-
addChildAnnotation(parentAnnotation, holeData.annotation);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
124
|
function getContourHolesData(viewport, annotation) {
|
|
134
125
|
return getChildAnnotations(annotation).map((holeAnnotation) => {
|
|
135
126
|
const contourHoleAnnotation = holeAnnotation;
|
|
@@ -2,8 +2,10 @@ import type { Types } from '@cornerstonejs/core';
|
|
|
2
2
|
export type PolylineInfoWorld = {
|
|
3
3
|
polyline: Types.Point3[];
|
|
4
4
|
viewReference: Types.ViewReference;
|
|
5
|
+
holePolylines?: Types.Point3[][];
|
|
5
6
|
};
|
|
6
7
|
export type PolylineInfoCanvas = {
|
|
7
8
|
polyline: Types.Point2[];
|
|
8
9
|
viewReference: Types.ViewReference;
|
|
10
|
+
holePolylines?: Types.Point2[][];
|
|
9
11
|
};
|
|
@@ -1,34 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import arePolylinesIdentical from '../math/polyline/arePolylinesIdentical';
|
|
4
|
-
import { areViewReferencesEqual } from './areViewReferencesEqual';
|
|
1
|
+
import { runBooleanOpByView } from './polylineSetOps';
|
|
2
|
+
import { BooleanOp } from './clipperBooleanOps';
|
|
5
3
|
export function intersectPolylinesSets(set1, set2) {
|
|
6
|
-
|
|
7
|
-
return [];
|
|
8
|
-
}
|
|
9
|
-
const result = [];
|
|
10
|
-
for (const polyA of set1) {
|
|
11
|
-
for (const polyB of set2) {
|
|
12
|
-
if (!areViewReferencesEqual(polyA.viewReference, polyB.viewReference)) {
|
|
13
|
-
continue;
|
|
14
|
-
}
|
|
15
|
-
if (arePolylinesIdentical(polyA.polyline, polyB.polyline)) {
|
|
16
|
-
result.push({ ...polyA });
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
const intersection = checkIntersection(polyA.polyline, polyB.polyline);
|
|
20
|
-
if (intersection.hasIntersection && !intersection.isContourHole) {
|
|
21
|
-
const intersectionRegions = cleanupPolylines(intersectPolylines(polyA.polyline, polyB.polyline));
|
|
22
|
-
if (intersectionRegions && intersectionRegions.length > 0) {
|
|
23
|
-
intersectionRegions.forEach((region) => {
|
|
24
|
-
result.push({
|
|
25
|
-
polyline: region,
|
|
26
|
-
viewReference: polyA.viewReference,
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return result;
|
|
4
|
+
return runBooleanOpByView(set1, set2, BooleanOp.Intersection);
|
|
34
5
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { areViewReferencesEqual } from './areViewReferencesEqual';
|
|
2
|
+
import { applyBoolean, BooleanOp, } from './clipperBooleanOps';
|
|
3
|
+
function toPolygon(info) {
|
|
4
|
+
return {
|
|
5
|
+
outer: info.polyline,
|
|
6
|
+
holes: info.holePolylines?.length ? info.holePolylines : undefined,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function groupByViewReference(set) {
|
|
10
|
+
const groups = [];
|
|
11
|
+
for (const info of set) {
|
|
12
|
+
if (info.polyline.length < 3) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const existing = groups.find((g) => areViewReferencesEqual(g.viewReference, info.viewReference));
|
|
16
|
+
if (existing) {
|
|
17
|
+
existing.polygons.push(toPolygon(info));
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
groups.push({
|
|
21
|
+
viewReference: info.viewReference,
|
|
22
|
+
polygons: [toPolygon(info)],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return groups;
|
|
27
|
+
}
|
|
28
|
+
function flatten(result, viewReference) {
|
|
29
|
+
return result.map((p) => ({
|
|
30
|
+
polyline: p.outer,
|
|
31
|
+
viewReference,
|
|
32
|
+
...(p.holes?.length ? { holePolylines: p.holes } : {}),
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
export function runBooleanOpByView(setA, setB, op) {
|
|
36
|
+
const aGroups = groupByViewReference(setA);
|
|
37
|
+
const bGroups = groupByViewReference(setB);
|
|
38
|
+
const out = [];
|
|
39
|
+
const matchedB = new Set();
|
|
40
|
+
for (const aGroup of aGroups) {
|
|
41
|
+
const bGroup = bGroups.find((g) => areViewReferencesEqual(g.viewReference, aGroup.viewReference));
|
|
42
|
+
if (bGroup) {
|
|
43
|
+
matchedB.add(bGroup);
|
|
44
|
+
const result = applyBoolean(aGroup.polygons, bGroup.polygons, op);
|
|
45
|
+
out.push(...flatten(result, aGroup.viewReference));
|
|
46
|
+
}
|
|
47
|
+
else if (op === BooleanOp.Intersection) {
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
out.push(...flatten(aGroup.polygons, aGroup.viewReference));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (op === BooleanOp.Union || op === BooleanOp.Xor) {
|
|
54
|
+
for (const bGroup of bGroups) {
|
|
55
|
+
if (matchedB.has(bGroup)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
out.push(...flatten(bGroup.polygons, bGroup.viewReference));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
@@ -1,67 +1,29 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { checkIntersection, cleanupPolylines, convertContourPolylineToCanvasSpace, removeDuplicatePoints, } from './sharedOperations';
|
|
3
|
-
import arePolylinesIdentical from '../math/polyline/arePolylinesIdentical';
|
|
1
|
+
import { convertContourPolylineToCanvasSpace } from './sharedOperations';
|
|
4
2
|
import { getViewReferenceFromAnnotation } from './getViewReferenceFromAnnotation';
|
|
5
|
-
import {
|
|
3
|
+
import { runBooleanOpByView } from './polylineSetOps';
|
|
4
|
+
import { BooleanOp } from './clipperBooleanOps';
|
|
5
|
+
import { getChildAnnotations } from '../../stateManagement/annotation/annotationState';
|
|
6
6
|
export function subtractPolylineSets(polylinesSetA, polylinesSetB) {
|
|
7
|
-
|
|
8
|
-
for (let i = 0; i < polylinesSetA.length; i++) {
|
|
9
|
-
let currentPolylines = [polylinesSetA[i]];
|
|
10
|
-
for (let j = 0; j < polylinesSetB.length; j++) {
|
|
11
|
-
const polylineB = polylinesSetB[j];
|
|
12
|
-
const newPolylines = [];
|
|
13
|
-
for (const currentPolyline of currentPolylines) {
|
|
14
|
-
if (!areViewReferencesEqual(currentPolyline.viewReference, polylineB.viewReference)) {
|
|
15
|
-
newPolylines.push(currentPolyline);
|
|
16
|
-
continue;
|
|
17
|
-
}
|
|
18
|
-
if (arePolylinesIdentical(currentPolyline.polyline, polylineB.polyline)) {
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
const intersection = checkIntersection(currentPolyline.polyline, polylineB.polyline);
|
|
22
|
-
if (intersection.hasIntersection && !intersection.isContourHole) {
|
|
23
|
-
const subtractedPolylines = cleanupPolylines(math.polyline.subtractPolylines(currentPolyline.polyline, polylineB.polyline));
|
|
24
|
-
for (const subtractedPolyline of subtractedPolylines) {
|
|
25
|
-
const cleaned = removeDuplicatePoints(subtractedPolyline);
|
|
26
|
-
if (cleaned.length >= 3) {
|
|
27
|
-
newPolylines.push({
|
|
28
|
-
polyline: cleaned,
|
|
29
|
-
viewReference: currentPolyline.viewReference,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
newPolylines.push({
|
|
36
|
-
polyline: currentPolyline.polyline,
|
|
37
|
-
viewReference: currentPolyline.viewReference,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
currentPolylines = newPolylines;
|
|
42
|
-
}
|
|
43
|
-
result.push(...currentPolylines);
|
|
44
|
-
}
|
|
45
|
-
return result;
|
|
7
|
+
return runBooleanOpByView(polylinesSetA, polylinesSetB, BooleanOp.Difference);
|
|
46
8
|
}
|
|
47
9
|
export function subtractMultiplePolylineSets(basePolylineSet, subtractorSets) {
|
|
48
10
|
if (subtractorSets.length === 0) {
|
|
49
11
|
return [...basePolylineSet];
|
|
50
12
|
}
|
|
51
|
-
let result =
|
|
52
|
-
for (
|
|
53
|
-
result = subtractPolylineSets(result,
|
|
13
|
+
let result = basePolylineSet;
|
|
14
|
+
for (const subtractor of subtractorSets) {
|
|
15
|
+
result = subtractPolylineSets(result, subtractor);
|
|
54
16
|
}
|
|
55
17
|
return result;
|
|
56
18
|
}
|
|
57
19
|
export function subtractAnnotationPolylines(baseAnnotations, subtractorAnnotations, viewport) {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
return subtractPolylineSets(
|
|
20
|
+
const toInfo = (annotation) => {
|
|
21
|
+
const holePolylines = getChildAnnotations(annotation).map((child) => convertContourPolylineToCanvasSpace(child.data.contour.polyline, viewport));
|
|
22
|
+
return {
|
|
23
|
+
polyline: convertContourPolylineToCanvasSpace(annotation.data.contour.polyline, viewport),
|
|
24
|
+
viewReference: getViewReferenceFromAnnotation(annotation),
|
|
25
|
+
...(holePolylines.length ? { holePolylines } : {}),
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
return subtractPolylineSets(baseAnnotations.map(toInfo), subtractorAnnotations.map(toInfo));
|
|
67
29
|
}
|
|
@@ -1,57 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { checkIntersection, convertContourPolylineToCanvasSpace, } from './sharedOperations';
|
|
3
|
-
import arePolylinesIdentical from '../math/polyline/arePolylinesIdentical';
|
|
1
|
+
import { convertContourPolylineToCanvasSpace } from './sharedOperations';
|
|
4
2
|
import { getViewReferenceFromAnnotation } from './getViewReferenceFromAnnotation';
|
|
5
|
-
import {
|
|
3
|
+
import { runBooleanOpByView } from './polylineSetOps';
|
|
4
|
+
import { BooleanOp } from './clipperBooleanOps';
|
|
5
|
+
import { getChildAnnotations } from '../../stateManagement/annotation/annotationState';
|
|
6
6
|
export function unifyPolylineSets(polylinesSetA, polylinesSetB) {
|
|
7
|
-
|
|
8
|
-
const processedFromA = new Set();
|
|
9
|
-
const processedFromB = new Set();
|
|
10
|
-
for (let i = 0; i < polylinesSetA.length; i++) {
|
|
11
|
-
if (processedFromA.has(i)) {
|
|
12
|
-
continue;
|
|
13
|
-
}
|
|
14
|
-
const polylineA = polylinesSetA[i];
|
|
15
|
-
let merged = false;
|
|
16
|
-
for (let j = 0; j < polylinesSetB.length; j++) {
|
|
17
|
-
if (processedFromB.has(j)) {
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
const polylineB = polylinesSetB[j];
|
|
21
|
-
if (!areViewReferencesEqual(polylineA.viewReference, polylineB.viewReference)) {
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
if (arePolylinesIdentical(polylineA.polyline, polylineB.polyline)) {
|
|
25
|
-
result.push(polylineA);
|
|
26
|
-
processedFromA.add(i);
|
|
27
|
-
processedFromB.add(j);
|
|
28
|
-
merged = true;
|
|
29
|
-
break;
|
|
30
|
-
}
|
|
31
|
-
const intersection = checkIntersection(polylineA.polyline, polylineB.polyline);
|
|
32
|
-
if (intersection.hasIntersection && !intersection.isContourHole) {
|
|
33
|
-
const mergedPolyline = math.polyline.mergePolylines(polylineA.polyline, polylineB.polyline);
|
|
34
|
-
result.push({
|
|
35
|
-
polyline: mergedPolyline,
|
|
36
|
-
viewReference: polylineA.viewReference,
|
|
37
|
-
});
|
|
38
|
-
processedFromA.add(i);
|
|
39
|
-
processedFromB.add(j);
|
|
40
|
-
merged = true;
|
|
41
|
-
break;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
if (!merged) {
|
|
45
|
-
result.push(polylineA);
|
|
46
|
-
processedFromA.add(i);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
for (let j = 0; j < polylinesSetB.length; j++) {
|
|
50
|
-
if (!processedFromB.has(j)) {
|
|
51
|
-
result.push(polylinesSetB[j]);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return result;
|
|
7
|
+
return runBooleanOpByView(polylinesSetA, polylinesSetB, BooleanOp.Union);
|
|
55
8
|
}
|
|
56
9
|
export function unifyMultiplePolylineSets(polylineSets) {
|
|
57
10
|
if (polylineSets.length === 0) {
|
|
@@ -60,20 +13,20 @@ export function unifyMultiplePolylineSets(polylineSets) {
|
|
|
60
13
|
if (polylineSets.length === 1) {
|
|
61
14
|
return [...polylineSets[0]];
|
|
62
15
|
}
|
|
63
|
-
let result =
|
|
16
|
+
let result = polylineSets[0];
|
|
64
17
|
for (let i = 1; i < polylineSets.length; i++) {
|
|
65
18
|
result = unifyPolylineSets(result, polylineSets[i]);
|
|
66
19
|
}
|
|
67
20
|
return result;
|
|
68
21
|
}
|
|
69
22
|
export function unifyAnnotationPolylines(annotationsSetA, annotationsSetB, viewport) {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
return unifyPolylineSets(
|
|
23
|
+
const toInfo = (annotation) => {
|
|
24
|
+
const holePolylines = getChildAnnotations(annotation).map((child) => convertContourPolylineToCanvasSpace(child.data.contour.polyline, viewport));
|
|
25
|
+
return {
|
|
26
|
+
polyline: convertContourPolylineToCanvasSpace(annotation.data.contour.polyline, viewport),
|
|
27
|
+
viewReference: getViewReferenceFromAnnotation(annotation),
|
|
28
|
+
...(holePolylines.length ? { holePolylines } : {}),
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
return unifyPolylineSets(annotationsSetA.map(toInfo), annotationsSetB.map(toInfo));
|
|
79
32
|
}
|
|
@@ -1,41 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { subtractPolylineSets } from './polylineSubtract';
|
|
4
|
-
import { areViewReferencesEqual } from './areViewReferencesEqual';
|
|
1
|
+
import { runBooleanOpByView } from './polylineSetOps';
|
|
2
|
+
import { BooleanOp } from './clipperBooleanOps';
|
|
5
3
|
export function xorPolylinesSets(polylinesSetA, polylinesSetB) {
|
|
6
|
-
|
|
7
|
-
return [];
|
|
8
|
-
}
|
|
9
|
-
if (!polylinesSetA.length) {
|
|
10
|
-
return polylinesSetB;
|
|
11
|
-
}
|
|
12
|
-
if (!polylinesSetB.length) {
|
|
13
|
-
return polylinesSetA;
|
|
14
|
-
}
|
|
15
|
-
if (polylinesSetA.length === polylinesSetB.length) {
|
|
16
|
-
let allIdentical = true;
|
|
17
|
-
for (let i = 0; i < polylinesSetA.length; i++) {
|
|
18
|
-
let foundMatch = false;
|
|
19
|
-
for (let j = 0; j < polylinesSetB.length; j++) {
|
|
20
|
-
if (!areViewReferencesEqual(polylinesSetA[i].viewReference, polylinesSetB[j].viewReference)) {
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
if (arePolylinesIdentical(polylinesSetA[i].polyline, polylinesSetB[j].polyline)) {
|
|
24
|
-
foundMatch = true;
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
if (!foundMatch) {
|
|
29
|
-
allIdentical = false;
|
|
30
|
-
break;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (allIdentical) {
|
|
34
|
-
return [];
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
const aMinusB = subtractPolylineSets(polylinesSetA, polylinesSetB);
|
|
38
|
-
const bMinusA = subtractPolylineSets(polylinesSetB, polylinesSetA);
|
|
39
|
-
const xorResult = [...aMinusB, ...bMinusA];
|
|
40
|
-
return xorResult;
|
|
4
|
+
return runBooleanOpByView(polylinesSetA, polylinesSetB, BooleanOp.Xor);
|
|
41
5
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Types } from '@cornerstonejs/core';
|
|
2
2
|
import type { ContourSegmentationAnnotation } from '../../types/ContourSegmentationAnnotation';
|
|
3
|
+
import { ContourWindingDirection } from '../../types/ContourAnnotation';
|
|
3
4
|
export declare function convertContourPolylineToCanvasSpace(polyline: Types.Point3[], viewport: Types.IViewport): Types.Point2[];
|
|
4
5
|
export declare function convertContourPolylineToWorld(polyline: Types.Point2[], viewport: Types.IViewport): Types.Point3[];
|
|
5
6
|
export declare function checkIntersection(sourcePolyline: Types.Point2[], targetPolyline: Types.Point2[]): {
|
|
6
7
|
hasIntersection: boolean;
|
|
7
8
|
isContourHole: boolean;
|
|
9
|
+
isTargetInsideSource: boolean;
|
|
8
10
|
};
|
|
9
11
|
export declare function getContourHolesData(viewport: Types.IViewport, annotation: ContourSegmentationAnnotation): Array<{
|
|
10
12
|
annotation: ContourSegmentationAnnotation;
|
|
@@ -12,7 +14,7 @@ export declare function getContourHolesData(viewport: Types.IViewport, annotatio
|
|
|
12
14
|
}>;
|
|
13
15
|
export declare function createPolylineHole(viewport: Types.IViewport, targetAnnotation: ContourSegmentationAnnotation, holeAnnotation: ContourSegmentationAnnotation): void;
|
|
14
16
|
export declare function combinePolylines(viewport: Types.IViewport, targetAnnotation: ContourSegmentationAnnotation, targetPolyline: Types.Point2[], sourceAnnotation: ContourSegmentationAnnotation, sourcePolyline: Types.Point2[]): void;
|
|
15
|
-
export declare function createNewAnnotationFromPolyline(viewport: Types.IViewport, templateAnnotation: ContourSegmentationAnnotation, polyline: Types.Point2[]): ContourSegmentationAnnotation;
|
|
17
|
+
export declare function createNewAnnotationFromPolyline(viewport: Types.IViewport, templateAnnotation: ContourSegmentationAnnotation, polyline: Types.Point2[], windingDirection?: ContourWindingDirection): ContourSegmentationAnnotation;
|
|
16
18
|
export declare function updateViewportsForAnnotations(viewport: Types.IViewport, annotations: ContourSegmentationAnnotation[]): void;
|
|
17
19
|
export declare function removeDuplicatePoints(polyline: Types.Point2[]): Types.Point2[];
|
|
18
20
|
export declare function cleanupPolylines(polylines: Types.Point2[][]): Types.Point2[][];
|
|
@@ -2,13 +2,14 @@ import { getEnabledElement, utilities as csUtils } from '@cornerstonejs/core';
|
|
|
2
2
|
import { ContourWindingDirection } from '../../types/ContourAnnotation';
|
|
3
3
|
import * as math from '../math';
|
|
4
4
|
import updateContourPolyline from '../contours/updateContourPolyline';
|
|
5
|
-
import { addAnnotation, removeAnnotation, getChildAnnotations, addChildAnnotation,
|
|
5
|
+
import { addAnnotation, removeAnnotation, getChildAnnotations, addChildAnnotation, } from '../../stateManagement/annotation/annotationState';
|
|
6
6
|
import { addContourSegmentationAnnotation } from './addContourSegmentationAnnotation';
|
|
7
7
|
import { removeContourSegmentationAnnotation } from './removeContourSegmentationAnnotation';
|
|
8
8
|
import { triggerAnnotationModified } from '../../stateManagement/annotation/helpers/state';
|
|
9
9
|
import triggerAnnotationRenderForViewportIds from '../triggerAnnotationRenderForViewportIds';
|
|
10
10
|
import { getViewportIdsWithToolToRender } from '../viewportFilters';
|
|
11
11
|
import { hasToolByName } from '../../store/addTool';
|
|
12
|
+
import { applyBoolean, BooleanOp, } from './clipperBooleanOps';
|
|
12
13
|
const TOLERANCE = 1e-10;
|
|
13
14
|
const DEFAULT_CONTOUR_SEG_TOOL_NAME = 'PlanarFreehandContourSegmentationTool';
|
|
14
15
|
export function convertContourPolylineToCanvasSpace(polyline, viewport) {
|
|
@@ -32,13 +33,20 @@ export function checkIntersection(sourcePolyline, targetPolyline) {
|
|
|
32
33
|
const targetAABB = math.polyline.getAABB(targetPolyline);
|
|
33
34
|
const aabbIntersect = math.aabb.intersectAABB(sourceAABB, targetAABB);
|
|
34
35
|
if (!aabbIntersect) {
|
|
35
|
-
return {
|
|
36
|
+
return {
|
|
37
|
+
hasIntersection: false,
|
|
38
|
+
isContourHole: false,
|
|
39
|
+
isTargetInsideSource: false,
|
|
40
|
+
};
|
|
36
41
|
}
|
|
37
42
|
const lineSegmentsIntersect = math.polyline.intersectPolyline(sourcePolyline, targetPolyline);
|
|
38
43
|
const isContourHole = !lineSegmentsIntersect &&
|
|
39
44
|
math.polyline.containsPoints(targetPolyline, sourcePolyline);
|
|
40
|
-
const
|
|
41
|
-
|
|
45
|
+
const isTargetInsideSource = !lineSegmentsIntersect &&
|
|
46
|
+
!isContourHole &&
|
|
47
|
+
math.polyline.containsPoints(sourcePolyline, targetPolyline);
|
|
48
|
+
const hasIntersection = lineSegmentsIntersect || isContourHole || isTargetInsideSource;
|
|
49
|
+
return { hasIntersection, isContourHole, isTargetInsideSource };
|
|
42
50
|
}
|
|
43
51
|
export function getContourHolesData(viewport, annotation) {
|
|
44
52
|
return getChildAnnotations(annotation).map((holeAnnotation) => {
|
|
@@ -71,63 +79,47 @@ export function combinePolylines(viewport, targetAnnotation, targetPolyline, sou
|
|
|
71
79
|
const sourceStartPoint = sourcePolyline[0];
|
|
72
80
|
const mergePolylines = math.polyline.containsPoint(targetPolyline, sourceStartPoint);
|
|
73
81
|
const contourHolesData = getContourHolesData(viewport, targetAnnotation);
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
};
|
|
85
|
-
const newPolylines = [];
|
|
86
|
-
if (mergePolylines) {
|
|
87
|
-
const mergedPolyline = math.polyline.mergePolylines(targetPolyline, sourcePolyline);
|
|
88
|
-
newPolylines.push(mergedPolyline);
|
|
89
|
-
Array.from(unassignedContourHolesSet.keys()).forEach((holeData) => assignHoleToPolyline(mergedPolyline, holeData));
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
const subtractedPolylines = math.polyline.subtractPolylines(targetPolyline, sourcePolyline);
|
|
93
|
-
subtractedPolylines.forEach((newPolyline) => {
|
|
94
|
-
newPolylines.push(newPolyline);
|
|
95
|
-
Array.from(unassignedContourHolesSet.keys()).forEach((holeData) => {
|
|
96
|
-
const containsHole = math.polyline.containsPoints(newPolyline, holeData.polyline);
|
|
97
|
-
if (containsHole) {
|
|
98
|
-
assignHoleToPolyline(newPolyline, holeData);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
Array.from(reassignedContourHolesMap.values()).forEach((contourHolesDataArray) => contourHolesDataArray.forEach((contourHoleData) => clearParentAnnotation(contourHoleData.annotation)));
|
|
82
|
+
const subjects = [
|
|
83
|
+
{
|
|
84
|
+
outer: targetPolyline,
|
|
85
|
+
holes: contourHolesData.length
|
|
86
|
+
? contourHolesData.map((h) => h.polyline)
|
|
87
|
+
: undefined,
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
const clips = [{ outer: sourcePolyline }];
|
|
91
|
+
const resultPolygons = applyBoolean(subjects, clips, mergePolylines ? BooleanOp.Union : BooleanOp.Difference);
|
|
104
92
|
const { element } = viewport;
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
continue;
|
|
93
|
+
const annotationsToRemove = [
|
|
94
|
+
sourceAnnotation,
|
|
95
|
+
targetAnnotation,
|
|
96
|
+
...contourHolesData.map((h) => h.annotation),
|
|
97
|
+
];
|
|
98
|
+
annotationsToRemove.forEach((annotation) => {
|
|
99
|
+
removeAnnotation(annotation.annotationUID);
|
|
100
|
+
removeContourSegmentationAnnotation(annotation);
|
|
101
|
+
});
|
|
102
|
+
resultPolygons.forEach((polygon) => {
|
|
103
|
+
if (polygon.outer.length < 3) {
|
|
104
|
+
return;
|
|
118
105
|
}
|
|
119
|
-
const
|
|
120
|
-
addAnnotation(
|
|
121
|
-
addContourSegmentationAnnotation(
|
|
122
|
-
triggerAnnotationModified(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
106
|
+
const parent = createNewAnnotationFromPolyline(viewport, targetAnnotation, polygon.outer, ContourWindingDirection.Clockwise);
|
|
107
|
+
addAnnotation(parent, element);
|
|
108
|
+
addContourSegmentationAnnotation(parent);
|
|
109
|
+
triggerAnnotationModified(parent, element);
|
|
110
|
+
polygon.holes?.forEach((holePolyline) => {
|
|
111
|
+
if (holePolyline.length < 3) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const hole = createNewAnnotationFromPolyline(viewport, targetAnnotation, holePolyline, ContourWindingDirection.CounterClockwise);
|
|
115
|
+
addAnnotation(hole, element);
|
|
116
|
+
addChildAnnotation(parent, hole);
|
|
117
|
+
triggerAnnotationModified(hole, element);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
128
120
|
updateViewportsForAnnotations(viewport, [targetAnnotation, sourceAnnotation]);
|
|
129
121
|
}
|
|
130
|
-
export function createNewAnnotationFromPolyline(viewport, templateAnnotation, polyline) {
|
|
122
|
+
export function createNewAnnotationFromPolyline(viewport, templateAnnotation, polyline, windingDirection = ContourWindingDirection.Clockwise) {
|
|
131
123
|
const startPointWorld = viewport.canvasToWorld(polyline[0]);
|
|
132
124
|
const endPointWorld = viewport.canvasToWorld(polyline[polyline.length - 1]);
|
|
133
125
|
const newAnnotation = {
|
|
@@ -165,7 +157,7 @@ export function createNewAnnotationFromPolyline(viewport, templateAnnotation, po
|
|
|
165
157
|
updateContourPolyline(newAnnotation, {
|
|
166
158
|
points: polyline,
|
|
167
159
|
closed: true,
|
|
168
|
-
targetWindingDirection:
|
|
160
|
+
targetWindingDirection: windingDirection,
|
|
169
161
|
}, viewport);
|
|
170
162
|
return newAnnotation;
|
|
171
163
|
}
|
package/dist/esm/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "5.1.
|
|
1
|
+
export declare const version = "5.1.4";
|
package/dist/esm/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '5.1.
|
|
1
|
+
export const version = '5.1.4';
|