Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
import type { Types } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/core';
import { addAnnotation } from '../../stateManagement';
import { addChildAnnotation } from '../../stateManagement/annotation/annotationState';
import type { PolylineInfoWorld } from './polylineInfoTypes';
import type { ContourSegmentationAnnotation } from '../../types';
import updateContourPolyline from '../contours/updateContourPolyline';
import { ContourWindingDirection } from '../../types/ContourAnnotation';

const DEFAULT_CONTOUR_SEG_TOOLNAME = 'PlanarFreehandContourSegmentationTool';

Expand All @@ -27,41 +31,97 @@ export default function addPolylinesToSegmentation(
polylinesInfo: PolylineInfoWorld[],
segmentIndex: number
) {
polylinesInfo.forEach(({ polyline, viewReference }) => {
polylinesInfo.forEach(({ polyline, viewReference, holePolylines }) => {
if (polyline.length < 3) {
return;
}

const contourSegmentationAnnotation = {
annotationUID: utilities.uuidv4(),
data: {
contour: {
closed: true,
polyline,
},
segmentation: {
segmentationId,
segmentIndex,
},
handles: {},
},
handles: {},
highlighted: false,
autoGenerated: false,
invalidated: false,
isLocked: false,
isVisible: true,
metadata: {
toolName: DEFAULT_CONTOUR_SEG_TOOLNAME,
...viewReference,
},
};
const parentAnnotation = createContourSegmentationAnnotation(
segmentationId,
segmentIndex,
viewReference
);

addAnnotation(contourSegmentationAnnotation, viewport.element);
addAnnotation(parentAnnotation, viewport.element);

const parentPolylineCanvas = polyline.map((point) =>
viewport.worldToCanvas(point)
);

updateContourPolyline(
parentAnnotation,
{
points: parentPolylineCanvas,
closed: true,
targetWindingDirection: ContourWindingDirection.Clockwise,
},
viewport
);

const currentSet = annotationUIDsMap?.get(segmentIndex) || new Set();
currentSet.add(contourSegmentationAnnotation.annotationUID);
currentSet.add(parentAnnotation.annotationUID);
annotationUIDsMap.set(segmentIndex, currentSet);

holePolylines?.forEach((holePolyline) => {
if (holePolyline.length < 3) {
return;
}

const holeAnnotation = createContourSegmentationAnnotation(
segmentationId,
segmentIndex,
viewReference
);

addAnnotation(holeAnnotation, viewport.element);
addChildAnnotation(parentAnnotation, holeAnnotation);

const holePolylineCanvas = holePolyline.map((point) =>
viewport.worldToCanvas(point)
);

updateContourPolyline(
holeAnnotation,
{
points: holePolylineCanvas,
closed: true,
targetWindingDirection: ContourWindingDirection.CounterClockwise,
},
viewport
);
});
});
return annotationUIDsMap;
}

function createContourSegmentationAnnotation(
segmentationId: string,
segmentIndex: number,
viewReference: Types.ViewReference
): ContourSegmentationAnnotation {
return {
annotationUID: utilities.uuidv4() as string,
data: {
contour: {
closed: true,
polyline: [],
},
segmentation: {
segmentationId,
segmentIndex,
},
handles: {},
cachedStats: {},
},
handles: {},
highlighted: false,
autoGenerated: false,
invalidated: false,
isLocked: false,
isVisible: true,
metadata: {
toolName: DEFAULT_CONTOUR_SEG_TOOLNAME,
...viewReference,
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@
*/

import type { Types } from '@cornerstonejs/core';
import { getAnnotation, removeAnnotation } from '../../stateManagement';
import {
getAnnotation,
removeAnnotation,
getChildAnnotations,
} from '../../stateManagement';
import type {
ContourSegmentationData,
ContourSegmentationAnnotation,
Expand Down Expand Up @@ -87,9 +91,20 @@ function getPolylinesInfoWorld(
annotationUID
) as ContourSegmentationAnnotation;
const { polyline } = annotation.data.contour;

const childAnnotations = getChildAnnotations(annotation);
const holePolylines =
childAnnotations.length > 0
? childAnnotations.map(
(child) =>
(child as ContourSegmentationAnnotation).data.contour.polyline
)
: undefined;

polylinesInfo.push({
polyline,
viewReference: getViewReferenceFromAnnotation(annotation),
...(holePolylines ? { holePolylines } : {}),
});
}
return polylinesInfo;
Expand Down Expand Up @@ -134,18 +149,32 @@ function extractPolylinesInCanvasSpace(
}

const polyLinesInfoCanvas1 = polyLinesInfoWorld1.map(
({ polyline, viewReference }) => {
({ polyline, viewReference, holePolylines }) => {
return {
polyline: convertContourPolylineToCanvasSpace(polyline, viewport),
viewReference,
...(holePolylines?.length
? {
holePolylines: holePolylines.map((hole) =>
convertContourPolylineToCanvasSpace(hole, viewport)
),
}
: {}),
};
}
);
const polyLinesInfoCanvas2 = polyLinesInfoWorld2.map(
({ polyline, viewReference }) => {
({ polyline, viewReference, holePolylines }) => {
return {
polyline: convertContourPolylineToCanvasSpace(polyline, viewport),
viewReference,
...(holePolylines?.length
? {
holePolylines: holePolylines.map((hole) =>
convertContourPolylineToCanvasSpace(hole, viewport)
),
}
: {}),
};
}
);
Expand Down Expand Up @@ -263,12 +292,19 @@ function applyLogicalOperation(
break;
}
// Convert merged polylines back to world space using their associated viewReference
const polyLinesWorld = polylinesMerged.map(({ polyline, viewReference }) => {
return {
const polyLinesWorld: PolylineInfoWorld[] = polylinesMerged.map(
({ polyline, viewReference, holePolylines }) => ({
polyline: convertContourPolylineToWorld(polyline, viewport),
viewReference,
};
});
...(holePolylines?.length
? {
holePolylines: holePolylines.map((hole) =>
convertContourPolylineToWorld(hole, viewport)
),
}
: {}),
})
);

const resultSegment = options;
const segmentation = getSegmentation(resultSegment.segmentationId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import type { Types } from '@cornerstonejs/core';
export type PolylineInfoWorld = {
polyline: Types.Point3[];
viewReference: Types.ViewReference;
/** Hole polylines in world space (child contours with opposite winding). */
holePolylines?: Types.Point3[][];
};

export type PolylineInfoCanvas = {
polyline: Types.Point2[];
viewReference: Types.ViewReference;
/** Hole polylines in canvas space (child contours with opposite winding). */
holePolylines?: Types.Point2[][];
};
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ export function intersectPolylinesSets(
continue;
}
const intersection = checkIntersection(polyA.polyline, polyB.polyline);
if (intersection.hasIntersection && !intersection.isContourHole) {
if (intersection.isContourHole) {
result.push({ ...polyA });
} else if (intersection.hasIntersection) {
const intersectionRegions = cleanupPolylines(
intersectPolylines(polyA.polyline, polyB.polyline)
);
Expand All @@ -43,6 +45,9 @@ export function intersectPolylinesSets(
});
});
}
} else if (intersection.isTargetInsideSource) {
// polyB is entirely inside polyA — the intersection is polyB itself
result.push({ ...polyB });
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
removeDuplicatePoints,
} from './sharedOperations';
import arePolylinesIdentical from '../math/polyline/arePolylinesIdentical';
import containsPoints from '../math/polyline/containsPoints';
import type { PolylineInfoCanvas } from './polylineInfoTypes';
import type { ContourSegmentationAnnotation } from '../../types';
import { getViewReferenceFromAnnotation } from './getViewReferenceFromAnnotation';
Expand Down Expand Up @@ -58,19 +59,57 @@ export function subtractPolylineSets(
polylineB.polyline
)
);
const existingHoles = currentPolyline.holePolylines ?? [];
for (const subtractedPolyline of subtractedPolylines) {
const cleaned = removeDuplicatePoints(subtractedPolyline);
if (cleaned.length >= 3) {
const holesStillInside = existingHoles.filter((hole) =>
containsPoints(cleaned, hole)
);
newPolylines.push({
polyline: cleaned,
viewReference: currentPolyline.viewReference,
...(holesStillInside.length > 0
? { holePolylines: holesStillInside }
: {}),
});
}
}
} else if (intersection.isTargetInsideSource) {
const existingHoles = currentPolyline.holePolylines ?? [];
const isInsideExistingHole = existingHoles.some((hole) =>
containsPoints(hole, polylineB.polyline)
);
if (isInsideExistingHole) {
// It's already in a hole, so subtracting it changes nothing.
newPolylines.push({
polyline: currentPolyline.polyline,
viewReference: currentPolyline.viewReference,
...(currentPolyline.holePolylines
? { holePolylines: currentPolyline.holePolylines }
: {}),
});
} else {
// Subtrahend is fully inside the minuend — cut a hole (no edge crossings).
newPolylines.push({
polyline: currentPolyline.polyline,
viewReference: currentPolyline.viewReference,
holePolylines: [
...(currentPolyline.holePolylines ?? []),
polylineB.polyline,
],
});
}
} else if (intersection.isContourHole) {
// Minuend is fully inside the subtrahend — removed entirely.
continue;
} else {
newPolylines.push({
polyline: currentPolyline.polyline,
viewReference: currentPolyline.viewReference,
...(currentPolyline.holePolylines
? { holePolylines: currentPolyline.holePolylines }
: {}),
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,19 @@ export function checkIntersection(
): {
hasIntersection: boolean;
isContourHole: boolean;
isTargetInsideSource: boolean;
} {
const sourceAABB = math.polyline.getAABB(sourcePolyline);
const targetAABB = math.polyline.getAABB(targetPolyline);

const aabbIntersect = math.aabb.intersectAABB(sourceAABB, targetAABB);

if (!aabbIntersect) {
return { hasIntersection: false, isContourHole: false };
return {
hasIntersection: false,
isContourHole: false,
isTargetInsideSource: false,
};
}

const lineSegmentsIntersect = math.polyline.intersectPolyline(
Expand All @@ -89,9 +94,15 @@ export function checkIntersection(
!lineSegmentsIntersect &&
math.polyline.containsPoints(targetPolyline, sourcePolyline);

// Target is fully contained inside source (no edge crossings)
const isTargetInsideSource =
!lineSegmentsIntersect &&
!isContourHole &&
math.polyline.containsPoints(sourcePolyline, targetPolyline);

const hasIntersection = lineSegmentsIntersect || isContourHole;

return { hasIntersection, isContourHole };
return { hasIntersection, isContourHole, isTargetInsideSource };
}

/**
Expand Down
Loading