diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index fd61772a68..f07e927aa2 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -860,6 +860,9 @@ export class BrushTool extends BaseTool { protected updateCursor(evt: EventTypes_2.InteractionEventType): void; } +// @public (undocumented) +function calculatePerimeter(polyline: number[][], closed: boolean): number; + // @public (undocumented) abstract class Calculator { // (undocumented) @@ -1552,7 +1555,8 @@ declare namespace contours { updateContourPolyline, interpolation, acceptAutogeneratedInterpolations, - findHandlePolylineIndex + findHandlePolylineIndex, + calculatePerimeter } } @@ -3771,7 +3775,6 @@ export class PlanarFreehandContourSegmentationTool extends PlanarFreehandROITool type PlanarFreehandROIAnnotation = ContourAnnotation & { data: { label?: string; - isOpenContour?: boolean; isOpenUShapeContour?: boolean; openUShapeContourVectorToPeak?: Types_2.Point3[]; cachedStats?: ROICachedStats; diff --git a/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.ts b/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.ts index fd4b123bab..25f28dff6c 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.ts @@ -33,7 +33,7 @@ class PlanarFreehandROI { imageToWorldCoords, metadata ) { - const { defaultState, SCOORDGroup, ReferencedFrameNumber } = + const { defaultState, NUMGroup, SCOORDGroup, ReferencedFrameNumber } = MeasurementReport.getSetupMeasurementData( MeasurementGroup, sopInstanceUIDToImageIdMap, @@ -79,8 +79,7 @@ class PlanarFreehandROI { const state = defaultState; state.annotation.data = { - polyline: worldCoords, - isOpenContour, + contour: { polyline: worldCoords, closed: !isOpenContour }, handles: { points, activeHandleIndex: null, @@ -88,6 +87,13 @@ class PlanarFreehandROI { hasMoved: false } }, + cachedStats: { + [`imageId:${referencedImageId}`]: { + area: NUMGroup + ? NUMGroup.MeasuredValueSequence.NumericValue + : null + } + }, frameNumber: ReferencedFrameNumber }; @@ -96,7 +102,9 @@ class PlanarFreehandROI { static getTID300RepresentationArguments(tool, worldToImageCoords) { const { data, finding, findingSites, metadata } = tool; - const { isOpenContour, polyline } = data; + + const { polyline, closed } = data.contour; + const isOpenContour = closed !== true; const { referencedImageId } = metadata; @@ -118,13 +126,20 @@ class PlanarFreehandROI { points.push([firstPoint[0], firstPoint[1]]); } - const area = 0; // TODO -> The tool doesn't have these stats yet. - const perimeter = 0; + const { area, areaUnit, modalityUnit, perimeter, mean, max, stdDev } = + data.cachedStats[`imageId:${referencedImageId}`] || {}; return { + /** From cachedStats */ points, area, + areaUnit, perimeter, + modalityUnit, + mean, + max, + stdDev, + /** Other */ trackingIdentifierTextValue, finding, findingSites: findingSites || [] diff --git a/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts b/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts index 743a92b45f..4fbe322299 100644 --- a/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts +++ b/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts @@ -45,6 +45,7 @@ import pointInShapeCallback from '../../utilities/pointInShapeCallback'; import { isViewportPreScaled } from '../../utilities/viewport/isViewportPreScaled'; import { getModalityUnit } from '../../utilities/getModalityUnit'; import { BasicStatsCalculator } from '../../utilities/math/basic'; +import calculatePerimeter from '../../utilities/contours/calculatePerimeter'; import ContourSegmentationBaseTool from '../base/ContourSegmentationBaseTool'; import { KeyboardBindings, ChangeTypes } from '../../enums'; @@ -52,6 +53,7 @@ const { pointCanProjectOnLine } = polyline; const { EPSILON } = CONSTANTS; const PARALLEL_THRESHOLD = 1 - EPSILON; + /** * PlanarFreehandROITool lets you draw annotations that define an arbitrarily drawn region. * You can use the PlanarFreehandROITool in all perpendicular views (axial, sagittal, coronal), @@ -243,7 +245,7 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { */ epsilon: 0.1, }, - calculateStats: false, + calculateStats: true, getTextLines: defaultGetTextLines, statsCalculator: BasicStatsCalculator, }, @@ -294,7 +296,9 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { ); this.activateDraw(evt, annotation, viewportIdsToRender); + evt.preventDefault(); + triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender); return annotation; @@ -350,6 +354,8 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { } else { this.activateOpenContourEdit(evt, annotation, viewportIdsToRender); } + + evt.preventDefault(); }; /** @@ -536,16 +542,21 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { annotation.data.handles.points.length = 0; }; - return csUtils.deepMerge(contourAnnotation, { - data: { - contour: { - polyline: [[...worldPos]], + const annotation = csUtils.deepMerge( + contourAnnotation, + { + data: { + contour: { + polyline: [[...worldPos]], + }, + label: '', + cachedStats: {}, }, - label: '', - cachedStats: {}, - }, - onInterpolationComplete, - }); + onInterpolationComplete, + } + ); + + return annotation; } protected getAnnotationStyle(context) { @@ -682,7 +693,7 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { ) => { const { data } = annotation; const { cachedStats } = data; - const { polyline: points } = data.contour; + const { polyline: points, closed } = data.contour; const targetIds = Object.keys(cachedStats); @@ -835,6 +846,7 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { cachedStats[targetId] = { Modality: metadata.Modality, area, + perimeter: calculatePerimeter(canvasCoordinates, closed), mean: stats.mean?.value, max: stats.max?.value, stdDev: stats.stdDev?.value, @@ -920,8 +932,16 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { function defaultGetTextLines(data, targetId): string[] { const cachedVolumeStats = data.cachedStats[targetId]; - const { area, mean, stdDev, max, isEmptyArea, areaUnit, modalityUnit } = - cachedVolumeStats || {}; + const { + area, + mean, + stdDev, + perimeter, + max, + isEmptyArea, + areaUnit, + modalityUnit, + } = cachedVolumeStats || {}; const textLines: string[] = []; @@ -944,6 +964,10 @@ function defaultGetTextLines(data, targetId): string[] { textLines.push(`Std Dev: ${roundNumber(stdDev)} ${modalityUnit}`); } + if (perimeter) { + textLines.push(`Perimeter: ${roundNumber(perimeter)} ${modalityUnit}`); + } + return textLines; } diff --git a/packages/tools/src/types/ToolSpecificAnnotationTypes.ts b/packages/tools/src/types/ToolSpecificAnnotationTypes.ts index d3a523d91b..4933b3b2d9 100644 --- a/packages/tools/src/types/ToolSpecificAnnotationTypes.ts +++ b/packages/tools/src/types/ToolSpecificAnnotationTypes.ts @@ -285,7 +285,6 @@ export interface CircleROIStartEndThresholdAnnotation extends Annotation { export type PlanarFreehandROIAnnotation = ContourAnnotation & { data: { label?: string; - isOpenContour?: boolean; isOpenUShapeContour?: boolean; // Present if isOpenUShapeContour is true: openUShapeContourVectorToPeak?: Types.Point3[]; diff --git a/packages/tools/src/utilities/contours/calculatePerimeter.ts b/packages/tools/src/utilities/contours/calculatePerimeter.ts new file mode 100644 index 0000000000..08fc4ba6b7 --- /dev/null +++ b/packages/tools/src/utilities/contours/calculatePerimeter.ts @@ -0,0 +1,31 @@ +/** + * Calculates the perimeter of a polyline. + * + * @param polyline - The polyline represented as an array of points. + * @param closed - Indicates whether the polyline is closed or not. + * @returns The perimeter of the polyline. + */ +function calculatePerimeter(polyline: number[][], closed: boolean): number { + let perimeter = 0; + + for (let i = 0; i < polyline.length - 1; i++) { + const point1 = polyline[i]; + const point2 = polyline[i + 1]; + perimeter += Math.sqrt( + Math.pow(point2[0] - point1[0], 2) + Math.pow(point2[1] - point1[1], 2) + ); + } + + if (closed) { + const firstPoint = polyline[0]; + const lastPoint = polyline[polyline.length - 1]; + perimeter += Math.sqrt( + Math.pow(lastPoint[0] - firstPoint[0], 2) + + Math.pow(lastPoint[1] - firstPoint[1], 2) + ); + } + + return perimeter; +} + +export default calculatePerimeter; diff --git a/packages/tools/src/utilities/contours/index.ts b/packages/tools/src/utilities/contours/index.ts index 0d7fe97a94..22c1dab529 100644 --- a/packages/tools/src/utilities/contours/index.ts +++ b/packages/tools/src/utilities/contours/index.ts @@ -10,6 +10,7 @@ import updateContourPolyline from './updateContourPolyline'; import acceptAutogeneratedInterpolations from './interpolation/acceptAutogeneratedInterpolations'; import * as interpolation from './interpolation'; import findHandlePolylineIndex from './findHandlePolylineIndex'; +import calculatePerimeter from './calculatePerimeter'; export { areCoplanarContours, @@ -24,4 +25,5 @@ export { interpolation, acceptAutogeneratedInterpolations, findHandlePolylineIndex, + calculatePerimeter, }; diff --git a/packages/tools/src/utilities/segmentation/triggerSegmentationRender.ts b/packages/tools/src/utilities/segmentation/triggerSegmentationRender.ts index a10cfb289a..170eb50041 100644 --- a/packages/tools/src/utilities/segmentation/triggerSegmentationRender.ts +++ b/packages/tools/src/utilities/segmentation/triggerSegmentationRender.ts @@ -11,7 +11,7 @@ import { getToolGroupForViewport, } from '../../store/ToolGroupManager'; -import SegmentationDisplayTool from '../../tools/displayTools/SegmentationDisplayTool'; +import { SegmentationDisplayTool } from '../../tools'; import { SegmentationRenderedEventDetail } from '../../types/EventTypes'; /**