diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index 6dadd7ce47..3de27cfdeb 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -1463,7 +1463,7 @@ type ContourSegmentationAnnotationData = { originalPolyline?: Types_2.Point3[]; }; }; - handles: { + handles?: { interpolationSources?: Types_2.PointsManager[]; }; onInterpolationComplete?: (annotation: ContourSegmentationAnnotation) => unknown; @@ -3691,6 +3691,8 @@ export class PlanarFreehandContourSegmentationTool extends PlanarFreehandROITool // (undocumented) protected isContourSegmentationTool(): boolean; // (undocumented) + protected renderAnnotationInstance(renderContext: AnnotationRenderContext): boolean; + // (undocumented) static toolName: any; } diff --git a/packages/tools/examples/planarFreehandContourSegmentationTool/index.ts b/packages/tools/examples/planarFreehandContourSegmentationTool/index.ts index 1f29c41b44..eef6458835 100644 --- a/packages/tools/examples/planarFreehandContourSegmentationTool/index.ts +++ b/packages/tools/examples/planarFreehandContourSegmentationTool/index.ts @@ -434,7 +434,7 @@ async function run() { mouseButton: MouseBindings.Primary, // Left Click }, { - mouseButton: MouseBindings.Primary, // Left Click + mouseButton: MouseBindings.Primary, // Shift + Left Click modifierKey: KeyboardBindings.Shift, }, ], diff --git a/packages/tools/examples/segmentSelect/index.ts b/packages/tools/examples/segmentSelect/index.ts index 7b4031ec1b..ee601b7b45 100644 --- a/packages/tools/examples/segmentSelect/index.ts +++ b/packages/tools/examples/segmentSelect/index.ts @@ -110,6 +110,7 @@ function setupTools(toolGroupId, isContour = false) { if (isContour) { toolGroup.addTool(PlanarFreehandContourSegmentationTool.toolName); + toolGroup.setToolPassive(PlanarFreehandContourSegmentationTool.toolName); } addManipulationBindings(toolGroup); @@ -223,6 +224,7 @@ async function run() { const config = segmentation.config.getGlobalConfig(); config.representations.LABELMAP.activeSegmentOutlineWidthDelta = 3; + config.representations.CONTOUR.activeSegmentOutlineWidthDelta = 3; } run(); diff --git a/packages/tools/src/eventListeners/annotations/annotationRemovedListener.ts b/packages/tools/src/eventListeners/annotations/annotationRemovedListener.ts new file mode 100644 index 0000000000..d85194378a --- /dev/null +++ b/packages/tools/src/eventListeners/annotations/annotationRemovedListener.ts @@ -0,0 +1,13 @@ +import { AnnotationRemovedEventType } from '../../types/EventTypes'; +import * as contourSegUtils from '../../utilities/contourSegmentation'; +import { contourSegmentationRemoved } from './contourSegmentation'; + +export default function annotationRemovedListener( + evt: AnnotationRemovedEventType +) { + const annotation = evt.detail.annotation; + + if (contourSegUtils.isContourSegmentationAnnotation(annotation)) { + contourSegmentationRemoved(evt); + } +} diff --git a/packages/tools/src/eventListeners/annotations/contourSegmentation/contourSegmentationCompleted.ts b/packages/tools/src/eventListeners/annotations/contourSegmentation/contourSegmentationCompleted.ts index f93e89ca85..4f5d54e815 100644 --- a/packages/tools/src/eventListeners/annotations/contourSegmentation/contourSegmentationCompleted.ts +++ b/packages/tools/src/eventListeners/annotations/contourSegmentation/contourSegmentationCompleted.ts @@ -4,8 +4,8 @@ import { Types, } from '@cornerstonejs/core'; import { ContourSegmentationAnnotation } from '../../../types/ContourSegmentationAnnotation'; +import getViewportsForAnnotation from '../../../utilities/getViewportsForAnnotation'; import { - getViewportForAnnotation, math, triggerAnnotationRenderForViewportIds, } from '../../../utilities'; @@ -26,6 +26,7 @@ import * as contourUtils from '../../../utilities/contours'; import * as contourSegUtils from '../../../utilities/contourSegmentation'; import { ToolGroupManager, hasTool as cstHasTool } from '../../../store'; import { PlanarFreehandContourSegmentationTool } from '../../../tools'; +import type { Annotation } from '../../../types'; import type { ContourAnnotation } from '../../../types/ContourAnnotation'; import { ContourWindingDirection } from '../../../types/ContourAnnotation'; @@ -41,7 +42,7 @@ export default async function contourSegmentationCompletedListener( return; } - const viewport = getViewportForAnnotation(sourceAnnotation); + const viewport = getViewport(sourceAnnotation); const contourSegmentationAnnotations = getValidContourSegmentationAnnotations(sourceAnnotation); @@ -64,10 +65,6 @@ export default async function contourSegmentationCompletedListener( return; } - if (!isFreehandContourSegToolRegistered(viewport)) { - return; - } - const { targetAnnotation, targetPolyline, isContourHole } = targetAnnotationInfo; @@ -92,30 +89,43 @@ export default async function contourSegmentationCompletedListener( } } -function isFreehandContourSegToolRegistered(viewport: Types.IViewport) { +function isFreehandContourSegToolRegisteredForViewport( + viewport: Types.IViewport, + silent = false +) { const { toolName } = PlanarFreehandContourSegmentationTool; - if (!cstHasTool(PlanarFreehandContourSegmentationTool)) { - console.warn(`${toolName} is not registered in cornerstone`); - return false; - } - const toolGroup = ToolGroupManager.getToolGroupForViewport( viewport.id, viewport.renderingEngineId ); + let errorMessage; + if (!toolGroup.hasTool(toolName)) { - console.warn(`Tool ${toolName} not added to ${toolGroup.id} toolGroup`); - return false; + errorMessage = `Tool ${toolName} not added to ${toolGroup.id} toolGroup`; + } else if (!toolGroup.getToolOptions(toolName)) { + errorMessage = `Tool ${toolName} must be in active/passive state`; } - if (!toolGroup.getToolOptions(toolName)) { - console.warn(`Tool ${toolName} must be in active/passive state`); - return false; + if (errorMessage && !silent) { + console.warn(errorMessage); } - return true; + return !errorMessage; +} + +function getViewport(annotation: Annotation) { + const viewports = getViewportsForAnnotation(annotation); + const viewportWithToolRegistered = viewports.find((viewport) => + isFreehandContourSegToolRegisteredForViewport(viewport, true) + ); + + // Returns the first viewport even if freehand contour segmentation is not + // registered because it can be used to project the polyline to create holes. + // Another verification is done before appending/removing contours which is + // possible only when the tool is registered. + return viewportWithToolRegistered ?? viewports[0]; } function convertContourPolylineToCanvasSpace( @@ -136,11 +146,6 @@ function getValidContourSegmentationAnnotations( sourceAnnotation: ContourSegmentationAnnotation ): ContourSegmentationAnnotation[] { const { annotationUID: sourceAnnotationUID } = sourceAnnotation; - const { FrameOfReferenceUID } = sourceAnnotation.metadata; - - if (!FrameOfReferenceUID) { - return []; - } // Get all annotations and filter all contour segmentations locally const allAnnotations = getAllAnnotations(); @@ -242,13 +247,25 @@ function getContourHolesData( }); } -async function combinePolylines( +function combinePolylines( viewport: Types.IViewport, targetAnnotation: ContourSegmentationAnnotation, targetPolyline: Types.Point2[], sourceAnnotation: ContourSegmentationAnnotation, sourcePolyline: Types.Point2[] ) { + if (!cstHasTool(PlanarFreehandContourSegmentationTool)) { + console.warn( + `${PlanarFreehandContourSegmentationTool.toolName} is not registered in cornerstone` + ); + return; + } + + // Cannot append/remove an annotation if it will not be available on any viewport + if (!isFreehandContourSegToolRegisteredForViewport(viewport)) { + return; + } + const sourceStartPoint = sourcePolyline[0]; const mergePolylines = math.polyline.containsPoint( targetPolyline, @@ -329,7 +346,7 @@ async function combinePolylines( const polyline = newPolylines[i]; const startPoint = viewport.canvasToWorld(polyline[0]); const endPoint = viewport.canvasToWorld(polyline[polyline.length - 1]); - const newAnnotation: ContourAnnotation = { + const newAnnotation: ContourSegmentationAnnotation = { metadata: { ...metadata, toolName: DEFAULT_CONTOUR_SEG_TOOLNAME, @@ -368,6 +385,7 @@ async function combinePolylines( ); addAnnotation(newAnnotation, element); + contourSegUtils.addContourSegmentationAnnotation(newAnnotation); reassignedContourHolesMap .get(polyline) diff --git a/packages/tools/src/eventListeners/annotations/contourSegmentation/contourSegmentationRemoved.ts b/packages/tools/src/eventListeners/annotations/contourSegmentation/contourSegmentationRemoved.ts new file mode 100644 index 0000000000..c96fbb3f7b --- /dev/null +++ b/packages/tools/src/eventListeners/annotations/contourSegmentation/contourSegmentationRemoved.ts @@ -0,0 +1,11 @@ +import type { AnnotationRemovedEventType } from '../../../types/EventTypes'; +import type { ContourSegmentationAnnotation } from '../../../types/ContourSegmentationAnnotation'; +import { removeContourSegmentationAnnotation } from '../../../utilities/contourSegmentation'; + +export default function contourSegmentationRemovedListener( + evt: AnnotationRemovedEventType +) { + const annotation = evt.detail.annotation as ContourSegmentationAnnotation; + + removeContourSegmentationAnnotation(annotation); +} diff --git a/packages/tools/src/eventListeners/annotations/contourSegmentation/index.ts b/packages/tools/src/eventListeners/annotations/contourSegmentation/index.ts index e8fd0cea86..85f0c97083 100644 --- a/packages/tools/src/eventListeners/annotations/contourSegmentation/index.ts +++ b/packages/tools/src/eventListeners/annotations/contourSegmentation/index.ts @@ -1 +1,2 @@ export { default as contourSegmentationCompleted } from './contourSegmentationCompleted'; +export { default as contourSegmentationRemoved } from './contourSegmentationRemoved'; diff --git a/packages/tools/src/eventListeners/annotations/index.ts b/packages/tools/src/eventListeners/annotations/index.ts index 019e190429..24514a9f6f 100644 --- a/packages/tools/src/eventListeners/annotations/index.ts +++ b/packages/tools/src/eventListeners/annotations/index.ts @@ -1,9 +1,11 @@ import annotationCompletedListener from './annotationCompletedListener'; import annotationSelectionListener from './annotationSelectionListener'; import annotationModifiedListener from './annotationModifiedListener'; +import annotationRemovedListener from './annotationRemovedListener'; export { annotationCompletedListener, annotationSelectionListener, annotationModifiedListener, + annotationRemovedListener, }; diff --git a/packages/tools/src/eventListeners/index.ts b/packages/tools/src/eventListeners/index.ts index f1d7a2ef90..4f65e87743 100644 --- a/packages/tools/src/eventListeners/index.ts +++ b/packages/tools/src/eventListeners/index.ts @@ -13,6 +13,7 @@ import { annotationCompletedListener, annotationSelectionListener, annotationModifiedListener, + annotationRemovedListener, } from './annotations'; //import touchEventListeners from './touchEventListeners'; @@ -29,4 +30,5 @@ export { annotationCompletedListener, annotationSelectionListener, annotationModifiedListener, + annotationRemovedListener, }; diff --git a/packages/tools/src/init.ts b/packages/tools/src/init.ts index 80fa08c13f..25fec3308b 100644 --- a/packages/tools/src/init.ts +++ b/packages/tools/src/init.ts @@ -6,6 +6,7 @@ import { addEnabledElement, removeEnabledElement } from './store'; import { resetCornerstoneToolsState } from './store/state'; import { annotationCompletedListener, + annotationRemovedListener, annotationSelectionListener, annotationModifiedListener, segmentationDataModifiedEventListener, @@ -124,6 +125,11 @@ function _addCornerstoneToolsEventListeners() { annotationSelectionListener ); + eventTarget.addEventListener( + TOOLS_EVENTS.ANNOTATION_REMOVED, + annotationRemovedListener + ); + /** * Segmentation */ diff --git a/packages/tools/src/tools/annotation/PlanarFreehandContourSegmentationTool.ts b/packages/tools/src/tools/annotation/PlanarFreehandContourSegmentationTool.ts index 2db8806ead..68623a4f37 100644 --- a/packages/tools/src/tools/annotation/PlanarFreehandContourSegmentationTool.ts +++ b/packages/tools/src/tools/annotation/PlanarFreehandContourSegmentationTool.ts @@ -1,5 +1,8 @@ import { utilities } from '@cornerstonejs/core'; import type { PublicToolProps } from '../../types'; +import type { AnnotationRenderContext } from '../../types'; +import { PlanarFreehandContourSegmentationAnnotation } from '../../types/ToolSpecificAnnotationTypes'; +import { triggerSegmentationDataModified } from '../../stateManagement/segmentation/triggerSegmentationEvents'; import PlanarFreehandROITool from './PlanarFreehandROITool'; class PlanarFreehandContourSegmentationTool extends PlanarFreehandROITool { @@ -28,6 +31,28 @@ class PlanarFreehandContourSegmentationTool extends PlanarFreehandROITool { // Re-enable contour segmentation behavior disabled by PlanarFreehandROITool return true; } + + protected renderAnnotationInstance( + renderContext: AnnotationRenderContext + ): boolean { + const annotation = + renderContext.annotation as PlanarFreehandContourSegmentationAnnotation; + const { invalidated } = annotation; + + // Render the annotation before triggering events + const renderResult = super.renderAnnotationInstance(renderContext); + + if (invalidated) { + const { segmentationId } = annotation.data.segmentation; + + // This event is trigged by ContourSegmentationBaseTool but PlanarFreehandROITool + // is the only contour class that does not call `renderAnnotationInstace` from + // its base class. + triggerSegmentationDataModified(segmentationId); + } + + return renderResult; + } } PlanarFreehandContourSegmentationTool.toolName = diff --git a/packages/tools/src/types/ContourSegmentationAnnotation.ts b/packages/tools/src/types/ContourSegmentationAnnotation.ts index b1c3259a57..62e19b850e 100644 --- a/packages/tools/src/types/ContourSegmentationAnnotation.ts +++ b/packages/tools/src/types/ContourSegmentationAnnotation.ts @@ -16,7 +16,7 @@ export type ContourSegmentationAnnotationData = { originalPolyline?: Types.Point3[]; }; }; - handles: { + handles?: { /** * Segmentation contours can be interpolated between slices to produce * intermediate data. The interpolation sources are source data for diff --git a/packages/tools/src/utilities/getViewportForAnnotation.ts b/packages/tools/src/utilities/getViewportForAnnotation.ts index 3f7b6d89d6..0feb4c4925 100644 --- a/packages/tools/src/utilities/getViewportForAnnotation.ts +++ b/packages/tools/src/utilities/getViewportForAnnotation.ts @@ -1,7 +1,5 @@ -import { getEnabledElements, utilities as csUtils } from '@cornerstonejs/core'; import type { Annotation } from '../types'; - -const { isEqual } = csUtils; +import getViewportsForAnnotation from './getViewportsForAnnotation'; /** * Finds a matching viewport in terms of the orientation of the annotation data @@ -13,17 +11,7 @@ const { isEqual } = csUtils; * @returns The viewport to display in */ export default function getViewportForAnnotation(annotation: Annotation) { - const { metadata } = annotation; - const enabledElement = getEnabledElements().find((enabledElement) => { - if (enabledElement.FrameOfReferenceUID === metadata.FrameOfReferenceUID) { - const viewport = enabledElement.viewport; - const { viewPlaneNormal, viewUp } = viewport.getCamera(); - return ( - isEqual(viewPlaneNormal, metadata.viewPlaneNormal) && - (!metadata.viewUp || isEqual(viewUp, metadata.viewUp)) - ); - } - return; - }); - return enabledElement?.viewport; + const viewports = getViewportsForAnnotation(annotation); + + return viewports.length ? viewports[0] : undefined; } diff --git a/packages/tools/src/utilities/getViewportsForAnnotation.ts b/packages/tools/src/utilities/getViewportsForAnnotation.ts new file mode 100644 index 0000000000..cb353ee52e --- /dev/null +++ b/packages/tools/src/utilities/getViewportsForAnnotation.ts @@ -0,0 +1,31 @@ +import { getEnabledElements, utilities as csUtils } from '@cornerstonejs/core'; +import type { Annotation } from '../types'; + +const { isEqual } = csUtils; + +/** + * Finds a all matching viewports in terms of the orientation of the annotation data + * and the frame of reference. This doesn't mean the annotation IS being displayed + * on these viewports, just that it could be by navigating the slice, and/or pan/zoom, + * without changing the orientation. + * + * @param annotation - Annotation to find the viewports that it could display in + * @returns All viewports to display in + */ +export default function getViewportsForAnnotation(annotation: Annotation) { + const { metadata } = annotation; + + return getEnabledElements() + .filter((enabledElement) => { + if (enabledElement.FrameOfReferenceUID === metadata.FrameOfReferenceUID) { + const viewport = enabledElement.viewport; + const { viewPlaneNormal, viewUp } = viewport.getCamera(); + return ( + isEqual(viewPlaneNormal, metadata.viewPlaneNormal) && + (!metadata.viewUp || isEqual(viewUp, metadata.viewUp)) + ); + } + return; + }) + .map((enabledElement) => enabledElement.viewport); +}