@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.
Files changed (23) hide show
  1. package/dist/esm/eventListeners/annotations/contourSegmentation/contourSegmentationCompleted.js +11 -44
  2. package/dist/esm/tools/annotation/planarFreehandROITool/drawLoop.js +6 -2
  3. package/dist/esm/utilities/contourSegmentation/addPolylinesToSegmentation.js +54 -27
  4. package/dist/esm/utilities/contourSegmentation/applyContourStroke.d.ts +4 -0
  5. package/dist/esm/utilities/contourSegmentation/applyContourStroke.js +102 -0
  6. package/dist/esm/utilities/contourSegmentation/bridgeWeaklyConnected.d.ts +4 -0
  7. package/dist/esm/utilities/contourSegmentation/bridgeWeaklyConnected.js +126 -0
  8. package/dist/esm/utilities/contourSegmentation/clipperBooleanOps.d.ts +13 -0
  9. package/dist/esm/utilities/contourSegmentation/clipperBooleanOps.js +126 -0
  10. package/dist/esm/utilities/contourSegmentation/logicalOperators.js +27 -9
  11. package/dist/esm/utilities/contourSegmentation/mergeMultipleAnnotations.js +37 -46
  12. package/dist/esm/utilities/contourSegmentation/polylineInfoTypes.d.ts +2 -0
  13. package/dist/esm/utilities/contourSegmentation/polylineIntersect.js +3 -32
  14. package/dist/esm/utilities/contourSegmentation/polylineSetOps.d.ts +3 -0
  15. package/dist/esm/utilities/contourSegmentation/polylineSetOps.js +62 -0
  16. package/dist/esm/utilities/contourSegmentation/polylineSubtract.js +17 -55
  17. package/dist/esm/utilities/contourSegmentation/polylineUnify.js +15 -62
  18. package/dist/esm/utilities/contourSegmentation/polylineXor.js +3 -39
  19. package/dist/esm/utilities/contourSegmentation/sharedOperations.d.ts +3 -1
  20. package/dist/esm/utilities/contourSegmentation/sharedOperations.js +50 -58
  21. package/dist/esm/version.d.ts +1 -1
  22. package/dist/esm/version.js +1 -1
  23. package/package.json +5 -4
@@ -1,11 +1,8 @@
1
1
  import { eventTarget, triggerEvent } from '@cornerstonejs/core';
2
2
  import getViewportsForAnnotation from '../../../utilities/getViewportsForAnnotation';
3
- import { getAllAnnotations } from '../../../stateManagement/annotation/annotationState';
4
- import { areSameSegment, isContourSegmentationAnnotation, } from '../../../utilities/contourSegmentation';
3
+ import isContourSegmentationAnnotation from '../../../utilities/contourSegmentation/isContourSegmentationAnnotation';
5
4
  import { getToolGroupForViewport } from '../../../store/ToolGroupManager';
6
- import { findAllIntersectingContours } from '../../../utilities/contourSegmentation/getIntersectingAnnotations';
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 contourSegmentationAnnotations = getValidContourSegmentationAnnotations(viewport, sourceAnnotation);
19
- if (!contourSegmentationAnnotations.length) {
20
- triggerEvent(eventTarget, Events.ANNOTATION_CUT_MERGE_PROCESS_COMPLETED, {
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
- combinePolylines(viewport, targetAnnotation, targetPolyline, sourceAnnotation, sourcePolyline);
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 = 'PlanarFreehandContourSegmentationTool';
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
- }
@@ -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
- if (crossingIndex !== undefined) {
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 updatedPoints = shouldSmooth(this.configuration, annotation)
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 contourSegmentationAnnotation = {
10
- annotationUID: utilities.uuidv4(),
11
- data: {
12
- contour: {
13
- closed: true,
14
- polyline,
15
- },
16
- segmentation: {
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(contourSegmentationAnnotation.annotationUID);
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
- return {
125
- polyline: convertContourPolylineToWorld(polyline, viewport),
126
- viewReference,
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;