@cornerstonejs/tools 3.24.0 → 3.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/stateManagement/segmentation/utilities/convertContourHoles.d.ts +1 -0
- package/dist/esm/stateManagement/segmentation/utilities/convertContourHoles.js +66 -0
- package/dist/esm/stateManagement/segmentation/utilities/getAnnotationsUIDMapFromSegmentation.d.ts +1 -0
- package/dist/esm/stateManagement/segmentation/utilities/getAnnotationsUIDMapFromSegmentation.js +17 -0
- package/dist/esm/stateManagement/segmentation/utilities/index.d.ts +2 -0
- package/dist/esm/stateManagement/segmentation/utilities/index.js +3 -0
- package/dist/esm/stateManagement/segmentation/utilities/removeContourHoles.js +0 -1
- package/dist/esm/tools/annotation/CircleROITool.js +93 -53
- package/dist/esm/tools/annotation/SplineROITool.d.ts +2 -0
- package/dist/esm/tools/annotation/SplineROITool.js +18 -0
- package/dist/esm/tools/segmentation/CircleROIStartEndThresholdTool.d.ts +1 -1
- package/dist/esm/tools/segmentation/CircleROIStartEndThresholdTool.js +62 -46
- package/dist/esm/types/ToolSpecificAnnotationTypes.d.ts +8 -2
- package/dist/esm/types/index.d.ts +2 -1
- package/dist/esm/utilities/contourSegmentation/addPolylinesToSegmentation.d.ts +2 -1
- package/dist/esm/utilities/contourSegmentation/addPolylinesToSegmentation.js +3 -4
- package/dist/esm/utilities/contourSegmentation/areViewReferencesEqual.d.ts +2 -0
- package/dist/esm/utilities/contourSegmentation/areViewReferencesEqual.js +23 -0
- package/dist/esm/utilities/contourSegmentation/copyAnnotation.d.ts +3 -0
- package/dist/esm/utilities/contourSegmentation/copyAnnotation.js +86 -0
- package/dist/esm/utilities/contourSegmentation/getViewReferenceFromAnnotation.d.ts +3 -0
- package/dist/esm/utilities/contourSegmentation/getViewReferenceFromAnnotation.js +20 -0
- package/dist/esm/utilities/contourSegmentation/index.d.ts +7 -3
- package/dist/esm/utilities/contourSegmentation/index.js +7 -3
- package/dist/esm/utilities/contourSegmentation/logicalOperators.d.ts +21 -12
- package/dist/esm/utilities/contourSegmentation/logicalOperators.js +148 -43
- package/dist/esm/utilities/contourSegmentation/polylineInfoTypes.d.ts +9 -0
- package/dist/esm/utilities/contourSegmentation/polylineInfoTypes.js +0 -0
- package/dist/esm/utilities/contourSegmentation/polylineIntersect.d.ts +2 -0
- package/dist/esm/utilities/contourSegmentation/polylineIntersect.js +34 -0
- package/dist/esm/utilities/contourSegmentation/polylineSubtract.d.ts +6 -0
- package/dist/esm/utilities/contourSegmentation/polylineSubtract.js +67 -0
- package/dist/esm/utilities/contourSegmentation/polylineUnify.d.ts +6 -0
- package/dist/esm/utilities/contourSegmentation/polylineUnify.js +79 -0
- package/dist/esm/utilities/contourSegmentation/polylineXor.d.ts +2 -0
- package/dist/esm/utilities/contourSegmentation/polylineXor.js +41 -0
- package/dist/esm/utilities/contourSegmentation/sharedOperations.d.ts +2 -0
- package/dist/esm/utilities/contourSegmentation/sharedOperations.js +44 -0
- package/dist/esm/utilities/math/polyline/arePolylinesIdentical.d.ts +2 -0
- package/dist/esm/utilities/math/polyline/arePolylinesIdentical.js +53 -0
- package/dist/esm/utilities/math/polyline/combinePolyline.d.ts +1 -2
- package/dist/esm/utilities/math/polyline/combinePolyline.js +17 -47
- package/dist/esm/utilities/math/polyline/index.d.ts +5 -2
- package/dist/esm/utilities/math/polyline/index.js +5 -2
- package/dist/esm/utilities/math/polyline/intersectPolylines.d.ts +2 -0
- package/dist/esm/utilities/math/polyline/intersectPolylines.js +271 -0
- package/dist/esm/utilities/math/polyline/robustSegmentIntersection.d.ts +37 -0
- package/dist/esm/utilities/math/polyline/robustSegmentIntersection.js +78 -0
- package/dist/esm/utilities/math/polyline/subtractPolylines.d.ts +2 -0
- package/dist/esm/utilities/math/polyline/subtractPolylines.js +210 -0
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +3 -3
- package/dist/esm/utilities/contourSegmentation/unifyPolylineSets.d.ts +0 -31
- package/dist/esm/utilities/contourSegmentation/unifyPolylineSets.js +0 -110
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as math from '../math';
|
|
2
|
+
import { checkIntersection, convertContourPolylineToCanvasSpace, } from './sharedOperations';
|
|
3
|
+
import arePolylinesIdentical from '../math/polyline/arePolylinesIdentical';
|
|
4
|
+
import { getViewReferenceFromAnnotation } from './getViewReferenceFromAnnotation';
|
|
5
|
+
import { areViewReferencesEqual } from './areViewReferencesEqual';
|
|
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;
|
|
55
|
+
}
|
|
56
|
+
export function unifyMultiplePolylineSets(polylineSets) {
|
|
57
|
+
if (polylineSets.length === 0) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
if (polylineSets.length === 1) {
|
|
61
|
+
return [...polylineSets[0]];
|
|
62
|
+
}
|
|
63
|
+
let result = [...polylineSets[0]];
|
|
64
|
+
for (let i = 1; i < polylineSets.length; i++) {
|
|
65
|
+
result = unifyPolylineSets(result, polylineSets[i]);
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
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);
|
|
79
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { cleanupPolylines } from './sharedOperations';
|
|
2
|
+
import arePolylinesIdentical from '../math/polyline/arePolylinesIdentical';
|
|
3
|
+
import { subtractPolylineSets } from './polylineSubtract';
|
|
4
|
+
import { areViewReferencesEqual } from './areViewReferencesEqual';
|
|
5
|
+
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;
|
|
41
|
+
}
|
|
@@ -14,3 +14,5 @@ export declare function createPolylineHole(viewport: Types.IViewport, targetAnno
|
|
|
14
14
|
export declare function combinePolylines(viewport: Types.IViewport, targetAnnotation: ContourSegmentationAnnotation, targetPolyline: Types.Point2[], sourceAnnotation: ContourSegmentationAnnotation, sourcePolyline: Types.Point2[]): void;
|
|
15
15
|
export declare function createNewAnnotationFromPolyline(viewport: Types.IViewport, templateAnnotation: ContourSegmentationAnnotation, polyline: Types.Point2[]): ContourSegmentationAnnotation;
|
|
16
16
|
export declare function updateViewportsForAnnotations(viewport: Types.IViewport, annotations: ContourSegmentationAnnotation[]): void;
|
|
17
|
+
export declare function removeDuplicatePoints(polyline: Types.Point2[]): Types.Point2[];
|
|
18
|
+
export declare function cleanupPolylines(polylines: Types.Point2[][]): Types.Point2[][];
|
|
@@ -9,6 +9,7 @@ import { triggerAnnotationModified } from '../../stateManagement/annotation/help
|
|
|
9
9
|
import triggerAnnotationRenderForViewportIds from '../triggerAnnotationRenderForViewportIds';
|
|
10
10
|
import { getViewportIdsWithToolToRender } from '../viewportFilters';
|
|
11
11
|
import { hasToolByName } from '../../store/addTool';
|
|
12
|
+
const TOLERANCE = 1e-10;
|
|
12
13
|
const DEFAULT_CONTOUR_SEG_TOOL_NAME = 'PlanarFreehandContourSegmentationTool';
|
|
13
14
|
export function convertContourPolylineToCanvasSpace(polyline, viewport) {
|
|
14
15
|
const numPoints = polyline.length;
|
|
@@ -181,3 +182,46 @@ export function updateViewportsForAnnotations(viewport, annotations) {
|
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
}
|
|
185
|
+
export function removeDuplicatePoints(polyline) {
|
|
186
|
+
if (!polyline || polyline.length < 2) {
|
|
187
|
+
return polyline;
|
|
188
|
+
}
|
|
189
|
+
const cleaned = [polyline[0]];
|
|
190
|
+
for (let i = 1; i < polyline.length; i++) {
|
|
191
|
+
const currentPoint = polyline[i];
|
|
192
|
+
const lastPoint = cleaned[cleaned.length - 1];
|
|
193
|
+
const dx = Math.abs(currentPoint[0] - lastPoint[0]);
|
|
194
|
+
const dy = Math.abs(currentPoint[1] - lastPoint[1]);
|
|
195
|
+
if (dx > TOLERANCE || dy > TOLERANCE) {
|
|
196
|
+
cleaned.push(currentPoint);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return cleaned;
|
|
200
|
+
}
|
|
201
|
+
export function cleanupPolylines(polylines) {
|
|
202
|
+
const validPolylines = [];
|
|
203
|
+
const seenPolylines = new Set();
|
|
204
|
+
for (let polyline of polylines) {
|
|
205
|
+
if (!polyline || polyline.length < 3) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
polyline = removeDuplicatePoints(polyline);
|
|
209
|
+
if (polyline.length < 3) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const sortedPoints = [...polyline].sort((a, b) => {
|
|
213
|
+
if (a[0] !== b[0]) {
|
|
214
|
+
return a[0] - b[0];
|
|
215
|
+
}
|
|
216
|
+
return a[1] - b[1];
|
|
217
|
+
});
|
|
218
|
+
const polylineKey = sortedPoints
|
|
219
|
+
.map((p) => `${p[0].toFixed(6)},${p[1].toFixed(6)}`)
|
|
220
|
+
.join('|');
|
|
221
|
+
if (!seenPolylines.has(polylineKey)) {
|
|
222
|
+
seenPolylines.add(polylineKey);
|
|
223
|
+
validPolylines.push(polyline);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return validPolylines;
|
|
227
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { pointsAreEqual } from './robustSegmentIntersection';
|
|
2
|
+
export default function arePolylinesIdentical(poly1, poly2) {
|
|
3
|
+
if (poly1.length !== poly2.length) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const len = poly1.length;
|
|
7
|
+
if (len === 0) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
let identicalForward = true;
|
|
11
|
+
for (let i = 0; i < len; i++) {
|
|
12
|
+
if (!pointsAreEqual(poly1[i], poly2[i])) {
|
|
13
|
+
identicalForward = false;
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (identicalForward) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
let identicalReverse = true;
|
|
21
|
+
for (let i = 0; i < len; i++) {
|
|
22
|
+
if (!pointsAreEqual(poly1[i], poly2[len - 1 - i])) {
|
|
23
|
+
identicalReverse = false;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (identicalReverse) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
for (let offset = 1; offset < len; offset++) {
|
|
31
|
+
let cyclicForward = true;
|
|
32
|
+
for (let i = 0; i < len; i++) {
|
|
33
|
+
if (!pointsAreEqual(poly1[i], poly2[(i + offset) % len])) {
|
|
34
|
+
cyclicForward = false;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (cyclicForward) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
let cyclicReverse = true;
|
|
42
|
+
for (let i = 0; i < len; i++) {
|
|
43
|
+
if (!pointsAreEqual(poly1[i], poly2[(len - 1 - i + offset) % len])) {
|
|
44
|
+
cyclicReverse = false;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (cyclicReverse) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import type { Types } from '@cornerstonejs/core';
|
|
2
2
|
declare function mergePolylines(targetPolyline: Types.Point2[], sourcePolyline: Types.Point2[]): Types.Point2[];
|
|
3
|
-
|
|
4
|
-
export { mergePolylines, subtractPolylines };
|
|
3
|
+
export { mergePolylines };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as mathPoint from '../point';
|
|
2
2
|
import getLineSegmentIntersectionsIndexes from './getLineSegmentIntersectionsIndexes';
|
|
3
3
|
import containsPoint from './containsPoint';
|
|
4
|
+
import containsPoints from './containsPoints';
|
|
5
|
+
import intersectPolyline from './intersectPolyline';
|
|
4
6
|
import getNormal2 from './getNormal2';
|
|
5
7
|
import { glMatrix, vec3 } from 'gl-matrix';
|
|
6
8
|
import getLinesIntersection from './getLinesIntersection';
|
|
@@ -123,12 +125,21 @@ function getSourceAndTargetPointsList(targetPolyline, sourcePolyline) {
|
|
|
123
125
|
return { targetPolylinePoints, sourcePolylinePoints };
|
|
124
126
|
}
|
|
125
127
|
function getUnvisitedOutsidePoint(polylinePoints) {
|
|
128
|
+
for (let i = 0, len = polylinePoints.length; i < len; i++) {
|
|
129
|
+
const point = polylinePoints[i];
|
|
130
|
+
if (!point.visited &&
|
|
131
|
+
point.position === PolylinePointPosition.Outside &&
|
|
132
|
+
point.type === PolylinePointType.Vertex) {
|
|
133
|
+
return point;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
126
136
|
for (let i = 0, len = polylinePoints.length; i < len; i++) {
|
|
127
137
|
const point = polylinePoints[i];
|
|
128
138
|
if (!point.visited && point.position === PolylinePointPosition.Outside) {
|
|
129
139
|
return point;
|
|
130
140
|
}
|
|
131
141
|
}
|
|
142
|
+
return undefined;
|
|
132
143
|
}
|
|
133
144
|
function mergePolylines(targetPolyline, sourcePolyline) {
|
|
134
145
|
const targetNormal = getNormal2(targetPolyline);
|
|
@@ -137,6 +148,11 @@ function mergePolylines(targetPolyline, sourcePolyline) {
|
|
|
137
148
|
if (!glMatrix.equals(1, dotNormals)) {
|
|
138
149
|
sourcePolyline = sourcePolyline.slice().reverse();
|
|
139
150
|
}
|
|
151
|
+
const lineSegmentsIntersect = intersectPolyline(sourcePolyline, targetPolyline);
|
|
152
|
+
const targetContainedInSource = !lineSegmentsIntersect && containsPoints(sourcePolyline, targetPolyline);
|
|
153
|
+
if (targetContainedInSource) {
|
|
154
|
+
return sourcePolyline.slice();
|
|
155
|
+
}
|
|
140
156
|
const { targetPolylinePoints } = getSourceAndTargetPointsList(targetPolyline, sourcePolyline);
|
|
141
157
|
const startPoint = getUnvisitedOutsidePoint(targetPolylinePoints);
|
|
142
158
|
if (!startPoint) {
|
|
@@ -165,50 +181,4 @@ function mergePolylines(targetPolyline, sourcePolyline) {
|
|
|
165
181
|
}
|
|
166
182
|
return mergedPolyline;
|
|
167
183
|
}
|
|
168
|
-
|
|
169
|
-
const targetNormal = getNormal2(targetPolyline);
|
|
170
|
-
const sourceNormal = getNormal2(sourcePolyline);
|
|
171
|
-
const dotNormals = vec3.dot(sourceNormal, targetNormal);
|
|
172
|
-
if (!glMatrix.equals(-1, dotNormals)) {
|
|
173
|
-
sourcePolyline = sourcePolyline.slice().reverse();
|
|
174
|
-
}
|
|
175
|
-
const { targetPolylinePoints } = getSourceAndTargetPointsList(targetPolyline, sourcePolyline);
|
|
176
|
-
let startPoint = null;
|
|
177
|
-
const subtractedPolylines = [];
|
|
178
|
-
let outerIterationCount = 0;
|
|
179
|
-
const maxOuterIterations = targetPolyline.length * 2;
|
|
180
|
-
while ((startPoint = getUnvisitedOutsidePoint(targetPolylinePoints)) &&
|
|
181
|
-
outerIterationCount < maxOuterIterations) {
|
|
182
|
-
outerIterationCount++;
|
|
183
|
-
const subtractedPolyline = [startPoint.coordinates];
|
|
184
|
-
let currentPoint = startPoint.next;
|
|
185
|
-
let innerIterationCount = 0;
|
|
186
|
-
const maxInnerIterations = targetPolyline.length + sourcePolyline.length + 1000;
|
|
187
|
-
startPoint.visited = true;
|
|
188
|
-
while (currentPoint !== startPoint &&
|
|
189
|
-
innerIterationCount < maxInnerIterations) {
|
|
190
|
-
innerIterationCount++;
|
|
191
|
-
currentPoint.visited = true;
|
|
192
|
-
if (currentPoint.type === PolylinePointType.Intersection &&
|
|
193
|
-
currentPoint.cloned) {
|
|
194
|
-
currentPoint = currentPoint.next;
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
subtractedPolyline.push(currentPoint.coordinates);
|
|
198
|
-
currentPoint = currentPoint.next;
|
|
199
|
-
if (!currentPoint) {
|
|
200
|
-
console.warn('Broken linked list detected in subtractPolylines, breaking inner loop');
|
|
201
|
-
break;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (innerIterationCount >= maxInnerIterations) {
|
|
205
|
-
console.warn('Maximum inner iterations reached in subtractPolylines, possible infinite loop detected');
|
|
206
|
-
}
|
|
207
|
-
subtractedPolylines.push(subtractedPolyline);
|
|
208
|
-
}
|
|
209
|
-
if (outerIterationCount >= maxOuterIterations) {
|
|
210
|
-
console.warn('Maximum outer iterations reached in subtractPolylines, possible infinite loop detected');
|
|
211
|
-
}
|
|
212
|
-
return subtractedPolylines;
|
|
213
|
-
}
|
|
214
|
-
export { mergePolylines, subtractPolylines };
|
|
184
|
+
export { mergePolylines };
|
|
@@ -7,7 +7,9 @@ import getSignedArea from './getSignedArea';
|
|
|
7
7
|
import getWindingDirection from './getWindingDirection';
|
|
8
8
|
import getNormal3 from './getNormal3';
|
|
9
9
|
import getNormal2 from './getNormal2';
|
|
10
|
-
import
|
|
10
|
+
import subtractPolylines from './subtractPolylines';
|
|
11
|
+
import intersectPolylines from './intersectPolylines';
|
|
12
|
+
import { mergePolylines } from './combinePolyline';
|
|
11
13
|
import intersectPolyline from './intersectPolyline';
|
|
12
14
|
import decimate from './decimate';
|
|
13
15
|
import getFirstLineSegmentIntersectionIndexes from './getFirstLineSegmentIntersectionIndexes';
|
|
@@ -21,4 +23,5 @@ import pointCanProjectOnLine from './pointCanProjectOnLine';
|
|
|
21
23
|
import { isPointInsidePolyline3D } from './isPointInsidePolyline3D';
|
|
22
24
|
import { projectTo2D } from './projectTo2D';
|
|
23
25
|
import convexHull from './convexHull';
|
|
24
|
-
|
|
26
|
+
import arePolylinesIdentical from './arePolylinesIdentical';
|
|
27
|
+
export { isClosed, containsPoint, containsPoints, getAABB, getArea, getSignedArea, getWindingDirection, getNormal3, getNormal2, intersectPolyline, decimate, getFirstLineSegmentIntersectionIndexes, getLineSegmentIntersectionsIndexes, getLineSegmentIntersectionsCoordinates, getClosestLineSegmentIntersection, getSubPixelSpacingAndXYDirections, pointsAreWithinCloseContourProximity, addCanvasPointsToArray, pointCanProjectOnLine, mergePolylines, subtractPolylines, intersectPolylines, isPointInsidePolyline3D, projectTo2D, convexHull, arePolylinesIdentical, };
|
|
@@ -7,7 +7,9 @@ import getSignedArea from './getSignedArea';
|
|
|
7
7
|
import getWindingDirection from './getWindingDirection';
|
|
8
8
|
import getNormal3 from './getNormal3';
|
|
9
9
|
import getNormal2 from './getNormal2';
|
|
10
|
-
import
|
|
10
|
+
import subtractPolylines from './subtractPolylines';
|
|
11
|
+
import intersectPolylines from './intersectPolylines';
|
|
12
|
+
import { mergePolylines } from './combinePolyline';
|
|
11
13
|
import intersectPolyline from './intersectPolyline';
|
|
12
14
|
import decimate from './decimate';
|
|
13
15
|
import getFirstLineSegmentIntersectionIndexes from './getFirstLineSegmentIntersectionIndexes';
|
|
@@ -21,4 +23,5 @@ import pointCanProjectOnLine from './pointCanProjectOnLine';
|
|
|
21
23
|
import { isPointInsidePolyline3D } from './isPointInsidePolyline3D';
|
|
22
24
|
import { projectTo2D } from './projectTo2D';
|
|
23
25
|
import convexHull from './convexHull';
|
|
24
|
-
|
|
26
|
+
import arePolylinesIdentical from './arePolylinesIdentical';
|
|
27
|
+
export { isClosed, containsPoint, containsPoints, getAABB, getArea, getSignedArea, getWindingDirection, getNormal3, getNormal2, intersectPolyline, decimate, getFirstLineSegmentIntersectionIndexes, getLineSegmentIntersectionsIndexes, getLineSegmentIntersectionsCoordinates, getClosestLineSegmentIntersection, getSubPixelSpacingAndXYDirections, pointsAreWithinCloseContourProximity, addCanvasPointsToArray, pointCanProjectOnLine, mergePolylines, subtractPolylines, intersectPolylines, isPointInsidePolyline3D, projectTo2D, convexHull, arePolylinesIdentical, };
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { vec2 } from 'gl-matrix';
|
|
2
|
+
import containsPoint from './containsPoint';
|
|
3
|
+
import getSignedArea from './getSignedArea';
|
|
4
|
+
import { EPSILON, IntersectionDirection, pointsAreEqual, PolylineNodeType, robustSegmentIntersection, } from './robustSegmentIntersection';
|
|
5
|
+
export default function intersectPolylines(mainPolyCoords, clipPolyCoordsInput) {
|
|
6
|
+
if (mainPolyCoords.length < 3 || clipPolyCoordsInput.length < 3) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
let clipPolyCoords = clipPolyCoordsInput.slice();
|
|
10
|
+
const mainArea = getSignedArea(mainPolyCoords);
|
|
11
|
+
const clipArea = getSignedArea(clipPolyCoords);
|
|
12
|
+
if (Math.abs(mainArea) < EPSILON || Math.abs(clipArea) < EPSILON) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
if (mainArea < 0) {
|
|
16
|
+
mainPolyCoords = mainPolyCoords.slice().reverse();
|
|
17
|
+
}
|
|
18
|
+
if (clipArea < 0) {
|
|
19
|
+
clipPolyCoords = clipPolyCoords.slice().reverse();
|
|
20
|
+
}
|
|
21
|
+
const currentClipPolyForPIP = clipPolyCoords;
|
|
22
|
+
const intersections = [];
|
|
23
|
+
for (let i = 0; i < mainPolyCoords.length; i++) {
|
|
24
|
+
const p1 = mainPolyCoords[i];
|
|
25
|
+
const p2 = mainPolyCoords[(i + 1) % mainPolyCoords.length];
|
|
26
|
+
for (let j = 0; j < clipPolyCoords.length; j++) {
|
|
27
|
+
const q1 = clipPolyCoords[j];
|
|
28
|
+
const q2 = clipPolyCoords[(j + 1) % clipPolyCoords.length];
|
|
29
|
+
const intersectPt = robustSegmentIntersection(p1, p2, q1, q2);
|
|
30
|
+
if (intersectPt) {
|
|
31
|
+
const lenP = Math.sqrt(vec2.squaredDistance(p1, p2));
|
|
32
|
+
const lenQ = Math.sqrt(vec2.squaredDistance(q1, q2));
|
|
33
|
+
intersections.push({
|
|
34
|
+
coord: [...intersectPt],
|
|
35
|
+
seg1Idx: i,
|
|
36
|
+
seg2Idx: j,
|
|
37
|
+
alpha1: lenP < EPSILON
|
|
38
|
+
? 0
|
|
39
|
+
: Math.sqrt(vec2.squaredDistance(p1, intersectPt)) / lenP,
|
|
40
|
+
alpha2: lenQ < EPSILON
|
|
41
|
+
? 0
|
|
42
|
+
: Math.sqrt(vec2.squaredDistance(q1, intersectPt)) / lenQ,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (intersections.length === 0) {
|
|
48
|
+
if (containsPoint(currentClipPolyForPIP, mainPolyCoords[0]) &&
|
|
49
|
+
mainPolyCoords.every((pt) => containsPoint(currentClipPolyForPIP, pt))) {
|
|
50
|
+
return [[...mainPolyCoords.map((p) => [...p])]];
|
|
51
|
+
}
|
|
52
|
+
if (containsPoint(mainPolyCoords, clipPolyCoords[0]) &&
|
|
53
|
+
clipPolyCoords.every((pt) => containsPoint(mainPolyCoords, pt))) {
|
|
54
|
+
return [[...clipPolyCoords.map((p) => [...p])]];
|
|
55
|
+
}
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const buildAugmentedList = (polyCoords, polyIndex, allIntersections) => {
|
|
59
|
+
const augmentedList = [];
|
|
60
|
+
let nodeIdCounter = 0;
|
|
61
|
+
for (let i = 0; i < polyCoords.length; i++) {
|
|
62
|
+
const p1 = polyCoords[i];
|
|
63
|
+
augmentedList.push({
|
|
64
|
+
id: `${polyIndex}_v${nodeIdCounter++}`,
|
|
65
|
+
coordinates: [...p1],
|
|
66
|
+
type: PolylineNodeType.Vertex,
|
|
67
|
+
originalPolyIndex: polyIndex,
|
|
68
|
+
originalVertexIndex: i,
|
|
69
|
+
next: null,
|
|
70
|
+
prev: null,
|
|
71
|
+
isIntersection: false,
|
|
72
|
+
visited: false,
|
|
73
|
+
processedInPath: false,
|
|
74
|
+
intersectionDir: IntersectionDirection.Unknown,
|
|
75
|
+
});
|
|
76
|
+
const segmentIntersections = allIntersections
|
|
77
|
+
.filter((isect) => (polyIndex === 0 ? isect.seg1Idx : isect.seg2Idx) === i)
|
|
78
|
+
.sort((a, b) => (polyIndex === 0 ? a.alpha1 : a.alpha2) -
|
|
79
|
+
(polyIndex === 0 ? b.alpha1 : b.alpha2));
|
|
80
|
+
for (const isect of segmentIntersections) {
|
|
81
|
+
if (augmentedList.length > 0 &&
|
|
82
|
+
pointsAreEqual(augmentedList[augmentedList.length - 1].coordinates, isect.coord)) {
|
|
83
|
+
const lastNode = augmentedList[augmentedList.length - 1];
|
|
84
|
+
if (!lastNode.isIntersection) {
|
|
85
|
+
lastNode.isIntersection = true;
|
|
86
|
+
lastNode.intersectionInfo = isect;
|
|
87
|
+
lastNode.alpha = polyIndex === 0 ? isect.alpha1 : isect.alpha2;
|
|
88
|
+
lastNode.type = PolylineNodeType.Intersection;
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
augmentedList.push({
|
|
93
|
+
id: `${polyIndex}_i${nodeIdCounter++}`,
|
|
94
|
+
coordinates: [...isect.coord],
|
|
95
|
+
type: PolylineNodeType.Intersection,
|
|
96
|
+
originalPolyIndex: polyIndex,
|
|
97
|
+
next: null,
|
|
98
|
+
prev: null,
|
|
99
|
+
isIntersection: true,
|
|
100
|
+
visited: false,
|
|
101
|
+
processedInPath: false,
|
|
102
|
+
alpha: polyIndex === 0 ? isect.alpha1 : isect.alpha2,
|
|
103
|
+
intersectionInfo: isect,
|
|
104
|
+
intersectionDir: IntersectionDirection.Unknown,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const finalList = [];
|
|
109
|
+
if (augmentedList.length > 0) {
|
|
110
|
+
finalList.push(augmentedList[0]);
|
|
111
|
+
for (let i = 1; i < augmentedList.length; i++) {
|
|
112
|
+
if (!pointsAreEqual(augmentedList[i].coordinates, finalList[finalList.length - 1].coordinates)) {
|
|
113
|
+
finalList.push(augmentedList[i]);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const lastNodeInFinal = finalList[finalList.length - 1];
|
|
117
|
+
if (augmentedList[i].isIntersection &&
|
|
118
|
+
augmentedList[i].intersectionInfo) {
|
|
119
|
+
lastNodeInFinal.isIntersection = true;
|
|
120
|
+
lastNodeInFinal.intersectionInfo =
|
|
121
|
+
augmentedList[i].intersectionInfo;
|
|
122
|
+
lastNodeInFinal.alpha = augmentedList[i].alpha;
|
|
123
|
+
lastNodeInFinal.type = PolylineNodeType.Intersection;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (finalList.length > 1 &&
|
|
129
|
+
pointsAreEqual(finalList[0].coordinates, finalList[finalList.length - 1].coordinates)) {
|
|
130
|
+
const firstNode = finalList[0];
|
|
131
|
+
const lastNodePopped = finalList.pop();
|
|
132
|
+
if (lastNodePopped.isIntersection &&
|
|
133
|
+
!firstNode.isIntersection &&
|
|
134
|
+
lastNodePopped.intersectionInfo) {
|
|
135
|
+
firstNode.isIntersection = true;
|
|
136
|
+
firstNode.intersectionInfo = lastNodePopped.intersectionInfo;
|
|
137
|
+
firstNode.alpha = lastNodePopped.alpha;
|
|
138
|
+
firstNode.type = PolylineNodeType.Intersection;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (finalList.length > 0) {
|
|
142
|
+
for (let i = 0; i < finalList.length; i++) {
|
|
143
|
+
finalList[i].next = finalList[(i + 1) % finalList.length];
|
|
144
|
+
finalList[i].prev =
|
|
145
|
+
finalList[(i - 1 + finalList.length) % finalList.length];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return finalList;
|
|
149
|
+
};
|
|
150
|
+
const mainAugmented = buildAugmentedList(mainPolyCoords, 0, intersections);
|
|
151
|
+
const clipAugmented = buildAugmentedList(clipPolyCoords, 1, intersections);
|
|
152
|
+
if (mainAugmented.length === 0 || clipAugmented.length === 0) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
mainAugmented.forEach((mainNode) => {
|
|
156
|
+
if (mainNode.isIntersection && mainNode.intersectionInfo) {
|
|
157
|
+
const mainIntersectData = mainNode.intersectionInfo;
|
|
158
|
+
const partnerNode = clipAugmented.find((clipNode) => clipNode.isIntersection &&
|
|
159
|
+
clipNode.intersectionInfo &&
|
|
160
|
+
pointsAreEqual(clipNode.coordinates, mainNode.coordinates) &&
|
|
161
|
+
clipNode.intersectionInfo.seg1Idx === mainIntersectData.seg1Idx &&
|
|
162
|
+
clipNode.intersectionInfo.seg2Idx === mainIntersectData.seg2Idx);
|
|
163
|
+
if (partnerNode) {
|
|
164
|
+
mainNode.partnerNode = partnerNode;
|
|
165
|
+
partnerNode.partnerNode = mainNode;
|
|
166
|
+
const v_arrival_main = vec2.subtract(vec2.create(), mainNode.coordinates, mainNode.prev.coordinates);
|
|
167
|
+
const v_departure_clip = vec2.subtract(vec2.create(), partnerNode.next.coordinates, partnerNode.coordinates);
|
|
168
|
+
const crossZ = v_arrival_main[0] * v_departure_clip[1] -
|
|
169
|
+
v_arrival_main[1] * v_departure_clip[0];
|
|
170
|
+
if (crossZ > EPSILON) {
|
|
171
|
+
mainNode.intersectionDir = IntersectionDirection.Entering;
|
|
172
|
+
partnerNode.intersectionDir = IntersectionDirection.Exiting;
|
|
173
|
+
}
|
|
174
|
+
else if (crossZ < -EPSILON) {
|
|
175
|
+
mainNode.intersectionDir = IntersectionDirection.Exiting;
|
|
176
|
+
partnerNode.intersectionDir = IntersectionDirection.Entering;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
const midPrevMainSeg = [
|
|
180
|
+
(mainNode.prev.coordinates[0] + mainNode.coordinates[0]) / 2,
|
|
181
|
+
(mainNode.prev.coordinates[1] + mainNode.coordinates[1]) / 2,
|
|
182
|
+
];
|
|
183
|
+
if (containsPoint(currentClipPolyForPIP, midPrevMainSeg)) {
|
|
184
|
+
mainNode.intersectionDir = IntersectionDirection.Exiting;
|
|
185
|
+
partnerNode.intersectionDir = IntersectionDirection.Entering;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
mainNode.intersectionDir = IntersectionDirection.Entering;
|
|
189
|
+
partnerNode.intersectionDir = IntersectionDirection.Exiting;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
mainNode.isIntersection = false;
|
|
195
|
+
mainNode.intersectionInfo = undefined;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
const resultPolygons = [];
|
|
200
|
+
for (const startCand of mainAugmented) {
|
|
201
|
+
if (!startCand.isIntersection ||
|
|
202
|
+
startCand.visited ||
|
|
203
|
+
startCand.intersectionDir !== IntersectionDirection.Entering) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
let currentPathCoords = [];
|
|
207
|
+
let currentNode = startCand;
|
|
208
|
+
let onMainList = true;
|
|
209
|
+
const pathStartNode = startCand;
|
|
210
|
+
let safetyBreak = 0;
|
|
211
|
+
const maxIter = (mainAugmented.length + clipAugmented.length) * 2;
|
|
212
|
+
mainAugmented.forEach((n) => (n.processedInPath = false));
|
|
213
|
+
clipAugmented.forEach((n) => (n.processedInPath = false));
|
|
214
|
+
do {
|
|
215
|
+
if (safetyBreak++ > maxIter) {
|
|
216
|
+
console.warn('Intersection: Max iterations in path tracing.', pathStartNode.id, currentNode.id);
|
|
217
|
+
currentPathCoords = [];
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
if (currentNode.processedInPath && currentNode !== pathStartNode) {
|
|
221
|
+
console.warn('Intersection: Path processing loop detected, discarding path segment.', pathStartNode.id, currentNode.id);
|
|
222
|
+
currentPathCoords = [];
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
currentNode.processedInPath = true;
|
|
226
|
+
currentNode.visited = true;
|
|
227
|
+
if (currentPathCoords.length === 0 ||
|
|
228
|
+
!pointsAreEqual(currentPathCoords[currentPathCoords.length - 1], currentNode.coordinates)) {
|
|
229
|
+
currentPathCoords.push([...currentNode.coordinates]);
|
|
230
|
+
}
|
|
231
|
+
let switchedList = false;
|
|
232
|
+
if (currentNode.isIntersection && currentNode.partnerNode) {
|
|
233
|
+
if (onMainList) {
|
|
234
|
+
currentNode = currentNode.partnerNode;
|
|
235
|
+
onMainList = false;
|
|
236
|
+
switchedList = true;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
currentNode = currentNode.partnerNode;
|
|
240
|
+
onMainList = true;
|
|
241
|
+
switchedList = true;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (!switchedList) {
|
|
245
|
+
currentNode = currentNode.next;
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
currentNode = currentNode.next;
|
|
249
|
+
}
|
|
250
|
+
} while (currentNode !== pathStartNode ||
|
|
251
|
+
(onMainList && currentNode.originalPolyIndex !== 0) ||
|
|
252
|
+
(!onMainList && currentNode.originalPolyIndex !== 1));
|
|
253
|
+
if (safetyBreak > maxIter || currentPathCoords.length === 0) {
|
|
254
|
+
}
|
|
255
|
+
else if (currentPathCoords.length > 0 &&
|
|
256
|
+
pointsAreEqual(currentPathCoords[0], currentPathCoords[currentPathCoords.length - 1])) {
|
|
257
|
+
currentPathCoords.pop();
|
|
258
|
+
}
|
|
259
|
+
if (currentPathCoords.length >= 3) {
|
|
260
|
+
const resultArea = getSignedArea(currentPathCoords);
|
|
261
|
+
if (mainArea > 0 && resultArea < 0) {
|
|
262
|
+
currentPathCoords.reverse();
|
|
263
|
+
}
|
|
264
|
+
else if (mainArea < 0 && resultArea > 0) {
|
|
265
|
+
currentPathCoords.reverse();
|
|
266
|
+
}
|
|
267
|
+
resultPolygons.push(currentPathCoords.map((p) => [...p]));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return resultPolygons;
|
|
271
|
+
}
|