@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.
Files changed (25) hide show
  1. package/dist/esm/eventListeners/annotations/contourSegmentation/contourSegmentationCompleted.js +11 -44
  2. package/dist/esm/tools/annotation/LivewireContourTool.js +3 -2
  3. package/dist/esm/tools/annotation/planarFreehandROITool/drawLoop.js +6 -2
  4. package/dist/esm/utilities/contourSegmentation/addPolylinesToSegmentation.js +54 -27
  5. package/dist/esm/utilities/contourSegmentation/applyContourStroke.d.ts +4 -0
  6. package/dist/esm/utilities/contourSegmentation/applyContourStroke.js +102 -0
  7. package/dist/esm/utilities/contourSegmentation/bridgeWeaklyConnected.d.ts +4 -0
  8. package/dist/esm/utilities/contourSegmentation/bridgeWeaklyConnected.js +126 -0
  9. package/dist/esm/utilities/contourSegmentation/clipperBooleanOps.d.ts +13 -0
  10. package/dist/esm/utilities/contourSegmentation/clipperBooleanOps.js +126 -0
  11. package/dist/esm/utilities/contourSegmentation/logicalOperators.js +27 -9
  12. package/dist/esm/utilities/contourSegmentation/mergeMultipleAnnotations.js +37 -46
  13. package/dist/esm/utilities/contourSegmentation/polylineInfoTypes.d.ts +2 -0
  14. package/dist/esm/utilities/contourSegmentation/polylineIntersect.js +3 -32
  15. package/dist/esm/utilities/contourSegmentation/polylineSetOps.d.ts +3 -0
  16. package/dist/esm/utilities/contourSegmentation/polylineSetOps.js +62 -0
  17. package/dist/esm/utilities/contourSegmentation/polylineSubtract.js +17 -55
  18. package/dist/esm/utilities/contourSegmentation/polylineUnify.js +15 -62
  19. package/dist/esm/utilities/contourSegmentation/polylineXor.js +3 -39
  20. package/dist/esm/utilities/contourSegmentation/sharedOperations.d.ts +3 -1
  21. package/dist/esm/utilities/contourSegmentation/sharedOperations.js +50 -58
  22. package/dist/esm/utilities/segmentation/getSegmentIndexAtWorldPoint.js +14 -18
  23. package/dist/esm/version.d.ts +1 -1
  24. package/dist/esm/version.js +1 -1
  25. 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, clearParentAnnotation, } from '../../stateManagement/annotation/annotationState';
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 allAnnotationsToRemove = [sourceAnnotation];
37
- const allResultPolylines = [];
38
- const allHoles = [];
39
- mergeOperations.forEach(({ targetAnnotation }) => {
40
- const holes = getContourHolesData(viewport, targetAnnotation);
41
- allHoles.push(...holes);
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
- if (shouldMerge) {
47
- let resultPolyline = sourcePolyline;
48
- mergeOperations.forEach(({ targetPolyline }) => {
49
- resultPolyline = math.polyline.mergePolylines(resultPolyline, targetPolyline);
50
- });
51
- allResultPolylines.push(resultPolyline);
52
- }
53
- else {
54
- mergeOperations.forEach(({ targetPolyline }) => {
55
- const subtractedPolylines = math.polyline.subtractPolylines(targetPolyline, sourcePolyline);
56
- allResultPolylines.push(...subtractedPolylines);
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
- const newAnnotations = [];
66
- allResultPolylines.forEach((polyline) => {
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 newAnnotation = createNewAnnotationFromPolyline(viewport, baseAnnotation, polyline);
72
- addAnnotation(newAnnotation, element);
73
- addContourSegmentationAnnotation(newAnnotation);
74
- triggerAnnotationModified(newAnnotation, viewport.element);
75
- newAnnotations.push(newAnnotation);
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: ContourWindingDirection.Clockwise,
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 { checkIntersection, cleanupPolylines } from './sharedOperations';
2
- import { intersectPolylines } from '../math/polyline';
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
- if (!set1.length || !set2.length) {
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,3 @@
1
+ import type { PolylineInfoCanvas } from './polylineInfoTypes';
2
+ import { BooleanOp } from './clipperBooleanOps';
3
+ export declare function runBooleanOpByView(setA: PolylineInfoCanvas[], setB: PolylineInfoCanvas[], op: BooleanOp): PolylineInfoCanvas[];
@@ -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 * as math from '../math';
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 { areViewReferencesEqual } from './areViewReferencesEqual';
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
- const result = [];
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 = [...basePolylineSet];
52
- for (let i = 0; i < subtractorSets.length; i++) {
53
- result = subtractPolylineSets(result, subtractorSets[i]);
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 basePolylines = baseAnnotations.map((annotation) => ({
59
- polyline: convertContourPolylineToCanvasSpace(annotation.data.contour.polyline, viewport),
60
- viewReference: getViewReferenceFromAnnotation(annotation),
61
- }));
62
- const subtractorPolylines = subtractorAnnotations.map((annotation) => ({
63
- polyline: convertContourPolylineToCanvasSpace(annotation.data.contour.polyline, viewport),
64
- viewReference: getViewReferenceFromAnnotation(annotation),
65
- }));
66
- return subtractPolylineSets(basePolylines, subtractorPolylines);
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 * as math from '../math';
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 { areViewReferencesEqual } from './areViewReferencesEqual';
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
- const result = [];
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 = [...polylineSets[0]];
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 polylinesSetA = annotationsSetA.map((annotation) => ({
71
- polyline: convertContourPolylineToCanvasSpace(annotation.data.contour.polyline, viewport),
72
- viewReference: getViewReferenceFromAnnotation(annotation),
73
- }));
74
- const polylinesSetB = annotationsSetB.map((annotation) => ({
75
- polyline: convertContourPolylineToCanvasSpace(annotation.data.contour.polyline, viewport),
76
- viewReference: getViewReferenceFromAnnotation(annotation),
77
- }));
78
- return unifyPolylineSets(polylinesSetA, polylinesSetB);
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 { cleanupPolylines } from './sharedOperations';
2
- import arePolylinesIdentical from '../math/polyline/arePolylinesIdentical';
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
- if (!polylinesSetA.length && !polylinesSetB.length) {
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, clearParentAnnotation, } from '../../stateManagement/annotation/annotationState';
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 { hasIntersection: false, isContourHole: false };
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 hasIntersection = lineSegmentsIntersect || isContourHole;
41
- return { hasIntersection, isContourHole };
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 unassignedContourHolesSet = new Set(contourHolesData);
75
- const reassignedContourHolesMap = new Map();
76
- const assignHoleToPolyline = (parentPolyline, holeData) => {
77
- let holes = reassignedContourHolesMap.get(parentPolyline);
78
- if (!holes) {
79
- holes = [];
80
- reassignedContourHolesMap.set(parentPolyline, holes);
81
- }
82
- holes.push(holeData);
83
- unassignedContourHolesSet.delete(holeData);
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 { metadata, data } = targetAnnotation;
106
- const { handles, segmentation } = data;
107
- const { textBox } = handles;
108
- removeAnnotation(sourceAnnotation.annotationUID);
109
- removeAnnotation(targetAnnotation.annotationUID);
110
- removeContourSegmentationAnnotation(sourceAnnotation);
111
- removeContourSegmentationAnnotation(targetAnnotation);
112
- const newAnnotations = [];
113
- for (let i = 0; i < newPolylines.length; i++) {
114
- const polyline = newPolylines[i];
115
- if (!polyline || polyline.length < 3) {
116
- console.warn('Skipping creation of new annotation due to invalid polyline:', polyline);
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 newAnnotation = createNewAnnotationFromPolyline(viewport, targetAnnotation, polyline);
120
- addAnnotation(newAnnotation, element);
121
- addContourSegmentationAnnotation(newAnnotation);
122
- triggerAnnotationModified(newAnnotation, viewport.element);
123
- newAnnotations.push(newAnnotation);
124
- reassignedContourHolesMap
125
- .get(polyline)
126
- ?.forEach((holeData) => addChildAnnotation(newAnnotation, holeData.annotation));
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: ContourWindingDirection.Clockwise,
160
+ targetWindingDirection: windingDirection,
169
161
  }, viewport);
170
162
  return newAnnotation;
171
163
  }