Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -16,52 +16,111 @@
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';

function createContourSegmentationAnnotation(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since addPolylinesToSegmentation is the main, exported function, let's move this one below it. It makes for a better diff in the PR too.

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,
},
};
}

export default function addPolylinesToSegmentation(
viewport: Types.IViewport,
annotationUIDsMap: Map<number, Set<string>>,
segmentationId: string,
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(parentAnnotation, viewport.element);

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

addAnnotation(contourSegmentationAnnotation, viewport.element);
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,
},
viewport
);
});
});
return annotationUIDsMap;
}
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's points are all inside source -> target is a hole inside source
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is a little confusing to me. In this case target is not necessarily a hold inside source right? Would a better comment be // Target is fully contained inside source (no edge crossings)?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, comment updated.

const isTargetInsideSource =
!lineSegmentsIntersect &&
!isContourHole &&
math.polyline.containsPoints(sourcePolyline, targetPolyline);

const hasIntersection = lineSegmentsIntersect || isContourHole;

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

/**
Expand Down
Loading