All files / tools/src/eventListeners/annotations/contourSegmentation contourSegmentationCompleted.ts

53.48% Statements 23/43
38.7% Branches 12/31
100% Functions 6/6
53.48% Lines 23/43

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219                                                      428x                             7x       7x       7x 7x           7x   7x       7x                                                                                                                                                         7x   7x             7x   7x   7x         7x       7x                           7x 7x 7x             7x                                   7x   7x 7x   13x                    
import { eventTarget, triggerEvent, type Types } from '@cornerstonejs/core';
import type { ContourSegmentationAnnotation } from '../../../types/ContourSegmentationAnnotation';
import getViewportsForAnnotation from '../../../utilities/getViewportsForAnnotation';
import { getAllAnnotations } from '../../../stateManagement/annotation/annotationState';
import type {
  AnnotationCompletedEventType,
  ContourAnnotationCompletedEventDetail,
} from '../../../types/EventTypes';
import type { Annotation } from '../../../types';
import {
  areSameSegment,
  isContourSegmentationAnnotation,
} from '../../../utilities/contourSegmentation';
import { getToolGroupForViewport } from '../../../store/ToolGroupManager';
import { findAllIntersectingContours } from '../../../utilities/contourSegmentation/getIntersectingAnnotations';
import { processMultipleIntersections } from '../../../utilities/contourSegmentation/mergeMultipleAnnotations';
import {
  convertContourPolylineToCanvasSpace,
  createPolylineHole,
  combinePolylines,
} from '../../../utilities/contourSegmentation/sharedOperations';
import { Events } from '../../../enums';
 
/**
 * Default tool name for contour segmentation operations.
 * This tool is used as the default when creating new combined/subtracted contours.
 */
const DEFAULT_CONTOUR_SEG_TOOL_NAME = 'PlanarFreehandContourSegmentationTool';
 
/**
 * Event listener for the 'ANNOTATION_COMPLETED' event, specifically for contour segmentations.
 * This function processes a newly completed contour segmentation. If the new contour
 * intersects with existing contour segmentations on the same segment, it will
 * either combine them or use the new contour to create holes in the existing ones.
 * Now supports multiple intersections and merging multiple annotations.
 *
 * @param evt - The event object triggered when an annotation is completed.
 * @returns A promise that resolves when the processing is complete.
 */
export default async function contourSegmentationCompletedListener(
  evt: AnnotationCompletedEventType
): Promise<void> {
  const sourceAnnotation = evt.detail
    .annotation as ContourSegmentationAnnotation;
 
  // Ensure the completed annotation is a contour segmentation
  Iif (!isContourSegmentationAnnotation(sourceAnnotation)) {
    return;
  }
 
  const viewport = getViewport(sourceAnnotation);
  const contourSegmentationAnnotations = getValidContourSegmentationAnnotations(
    viewport,
    sourceAnnotation
  );
 
  // If no other relevant contour segmentations exist, there's nothing to combine or make a hole in.
  Eif (!contourSegmentationAnnotations.length) {
    // we trigger the event here as here is the place where the source Annotation is not removed
    triggerEvent(eventTarget, Events.ANNOTATION_CUT_MERGE_PROCESS_COMPLETED, {
      element: viewport.element,
      sourceAnnotation,
    });
    return;
  }
 
  const sourcePolyline = convertContourPolylineToCanvasSpace(
    sourceAnnotation.data.contour.polyline,
    viewport
  );
 
  // Find all intersecting contours instead of just one
  const intersectingContours = findAllIntersectingContours(
    viewport,
    sourcePolyline,
    contourSegmentationAnnotations
  );
 
  // If no intersecting contours are found, do nothing.
  if (!intersectingContours.length) {
    // we trigger the event here as here is the place where the source Annotation is not removed
    triggerEvent(eventTarget, Events.ANNOTATION_CUT_MERGE_PROCESS_COMPLETED, {
      element: viewport.element,
      sourceAnnotation,
    });
 
    return;
  }
 
  // Handle multiple intersections
  if (intersectingContours.length > 1) {
    // Process multiple intersections using the new utility
    processMultipleIntersections(
      viewport,
      sourceAnnotation,
      sourcePolyline,
      intersectingContours
    );
 
    return;
  }
 
  // Handle single intersection (backward compatibility)
  const { targetAnnotation, targetPolyline, isContourHole } =
    intersectingContours[0];
 
  if (isContourHole) {
    // Check if hole processing is enabled for this specific event
    const { contourHoleProcessingEnabled = false } =
      evt.detail as ContourAnnotationCompletedEventDetail;
 
    // Do not create holes when contourHoleProcessingEnabled is `false`
    if (!contourHoleProcessingEnabled) {
      return;
    }
 
    createPolylineHole(viewport, targetAnnotation, sourceAnnotation);
  } else {
    combinePolylines(
      viewport,
      targetAnnotation,
      targetPolyline,
      sourceAnnotation,
      sourcePolyline
    );
  }
}
 
/**
 * Checks if the 'PlanarFreehandContourSegmentationTool' is registered and
 * configured (active or passive) for a given viewport.
 *
 * @param viewport - The viewport to check.
 * @param silent - If true, suppresses console warnings. Defaults to false.
 * @returns True if the tool is registered and configured, false otherwise.
 */
function isFreehandContourSegToolRegisteredForViewport(
  viewport: Types.IViewport,
  silent = false
): boolean {
  const toolName = 'PlanarFreehandContourSegmentationTool';
 
  const toolGroup = getToolGroupForViewport(
    viewport.id,
    viewport.renderingEngineId
  );
 
  let errorMessage;
 
  Iif (!toolGroup) {
    errorMessage = `ToolGroup not found for viewport ${viewport.id}`;
  } else Iif (!toolGroup.hasTool(toolName)) {
    errorMessage = `Tool ${toolName} not added to ${toolGroup.id} toolGroup`;
  } else Iif (!toolGroup.getToolOptions(toolName)) {
    // getToolOptions returns undefined if the tool is not active or passive
    errorMessage = `Tool ${toolName} must be in active/passive state in ${toolGroup.id} toolGroup`;
  }
 
  Iif (errorMessage && !silent) {
    console.warn(errorMessage);
  }
 
  return !errorMessage;
}
 
/**
 * Retrieves a suitable viewport for processing the given annotation.
 * It prioritizes viewports where the 'PlanarFreehandContourSegmentationTool'
 * is registered. If no such viewport is found, it returns the first viewport
 * associated with the annotation. This is because projecting polylines for hole
 * creation might still be possible even if the full tool isn't registered for appending/removing contours.
 *
 * @param annotation - The annotation for which to find a viewport.
 * @returns The most suitable `Types.IViewport` instance, or the first associated viewport.
 */
function getViewport(annotation: Annotation): Types.IViewport {
  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];
}
 
/**
 * Retrieves all valid contour segmentation annotations that are:
 * 1. Not the source annotation itself.
 * 2. Contour segmentation annotations.
 * 3. On the same segment as the source annotation.
 * 4. Viewable in the given viewport (i.e., on the same image plane/slice).
 *
 * @param viewport - The viewport context.
 * @param sourceAnnotation - The source contour segmentation annotation.
 * @returns An array of `ContourSegmentationAnnotation` objects that meet the criteria.
 */
function getValidContourSegmentationAnnotations(
  viewport: Types.IViewport,
  sourceAnnotation: ContourSegmentationAnnotation
): ContourSegmentationAnnotation[] {
  const { annotationUID: sourceAnnotationUID } = sourceAnnotation;
 
  const allAnnotations = getAllAnnotations();
  return allAnnotations.filter(
    (targetAnnotation) =>
      targetAnnotation.annotationUID &&
      targetAnnotation.annotationUID !== sourceAnnotationUID &&
      isContourSegmentationAnnotation(targetAnnotation) &&
      areSameSegment(
        targetAnnotation as ContourSegmentationAnnotation,
        sourceAnnotation
      ) &&
      viewport.isReferenceViewable(targetAnnotation.metadata) // Checks if annotation is on the same slice/orientation
  ) as ContourSegmentationAnnotation[];
}