609 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Plaintext
		
	
	
			
		
		
	
	
			609 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Plaintext
		
	
	
| import {
 | |
|   eventTarget,
 | |
|   triggerEvent,
 | |
|   VolumeViewport,
 | |
|   utilities as csUtils
 | |
| } from '@cornerstonejs/core'
 | |
| 
 | |
| const { transformWorldToIndex } = csUtils
 | |
| import * as cornerstoneTools from '@cornerstonejs/tools'
 | |
| const {
 | |
|   Enums,
 | |
|   utilities,
 | |
|   annotation,
 | |
|   drawing
 | |
| 
 | |
| } = cornerstoneTools
 | |
| 
 | |
| // const { getWorldWidthAndHeightFromTwoPoints } = utilities.planar
 | |
| // const { roundNumber } = utilities
 | |
| 
 | |
| const { getAnnotations } = annotation.state
 | |
| const { isAnnotationVisible } = annotation.visibility
 | |
| const { isAnnotationLocked } = annotation.locking
 | |
| const drawHandlesSvg = drawing.drawHandles
 | |
| const drawCircleSvg = drawing.drawCircle
 | |
| const drawLinkedTextBoxSvg = drawing.drawLinkedTextBox
 | |
| const { getTextBoxCoordsCanvas } = utilities.drawing
 | |
| console.log(utilities)
 | |
| // const getCanvasCircleRadius
 | |
| // const getCanvasCircleCorners
 | |
| // const { getCanvasCircleRadius, getCanvasCircleCorners } = utilities.math.circle
 | |
| import {
 | |
|   getCalibratedLengthUnits,
 | |
|   getCalibratedAreaUnits,
 | |
|   getCalibratedScale,
 | |
|   getCalibratedAspect
 | |
| } from './../js/getCalibratedUnits'
 | |
| import { getModalityUnit } from './../js/getModalityUnit'
 | |
| import { pointInShapeCallback } from './../js/pointInShapeCallback'
 | |
| import { vec3 } from 'gl-matrix'
 | |
| class CircleROITool extends cornerstoneTools.CircleROITool {
 | |
|   static toolName;
 | |
|   // touchDragCallback: any;
 | |
|   // mouseDragCallback: any;
 | |
|   // _throttledCalculateCachedStats: any;
 | |
|   // editData: {
 | |
|   //   annotation: any;
 | |
|   //   viewportIdsToRender: Array<string>;
 | |
|   //   handleIndex?: number;
 | |
|   //   movingTextBox?: boolean;
 | |
|   //   newAnnotation?: boolean;
 | |
|   //   hasMoved?: boolean;
 | |
|   // } | null;
 | |
|   // isDrawing: boolean;
 | |
|   // isHandleOutsideImage = false;
 | |
| 
 | |
|   constructor(
 | |
|     toolProps,
 | |
|     defaultToolProps
 | |
|   ) {
 | |
|     super(toolProps, defaultToolProps)
 | |
| 
 | |
|     this._throttledCalculateCachedStats = utilities.throttle(
 | |
|       this._calculateCachedStats,
 | |
|       100,
 | |
|       { trailing: true }
 | |
|     )
 | |
| 
 | |
|     // this._throttledCalculateCachedStats = utilities.throttle(
 | |
|     //   this._calculateCachedStats,
 | |
|     //   100,
 | |
|     //   { trailing: true }
 | |
|     // )
 | |
|     // this._getTextLines = this.getTextLines
 | |
|   }
 | |
|   renderAnnotation = (enabledElement, svgDrawingHelper) => {
 | |
|     let renderStatus = false
 | |
|     const { viewport } = enabledElement
 | |
|     const { element } = viewport
 | |
|     // console.log(element)
 | |
|     let annotations = getAnnotations(this.getToolName(), element)
 | |
| 
 | |
|     if (!annotations || (annotations && annotations.length === 0)) {
 | |
|       return renderStatus
 | |
|     }
 | |
| 
 | |
|     annotations = this.filterInteractableAnnotationsForElement(
 | |
|       element,
 | |
|       annotations
 | |
|     )
 | |
| 
 | |
|     if (!annotations || (annotations && annotations.length === 0)) {
 | |
|       return renderStatus
 | |
|     }
 | |
| 
 | |
|     const targetId = this.getTargetId(viewport)
 | |
| 
 | |
|     const renderingEngine = viewport.getRenderingEngine()
 | |
| 
 | |
|     const styleSpecifier = {
 | |
|       toolGroupId: this.toolGroupId,
 | |
|       toolName: this.getToolName(),
 | |
|       viewportId: enabledElement.viewport.id
 | |
|     }
 | |
| 
 | |
|     for (let i = 0; i < annotations.length; i++) {
 | |
|       const annotation = annotations[i]
 | |
|       const { annotationUID, data } = annotation
 | |
|       const { handles } = data
 | |
|       const { points, activeHandleIndex } = handles
 | |
| 
 | |
|       styleSpecifier.annotationUID = annotationUID
 | |
| 
 | |
|       const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation)
 | |
|       const lineDash = this.getStyle('lineDash', styleSpecifier, annotation)
 | |
|       const color = this.getStyle('color', styleSpecifier, annotation)
 | |
| 
 | |
|       const canvasCoordinates = points.map((p) =>
 | |
|         viewport.worldToCanvas(p)
 | |
|       )
 | |
|       const center = canvasCoordinates[0]
 | |
|       const radius = getCanvasCircleRadius(canvasCoordinates)
 | |
|       const canvasCorners = getCanvasCircleCorners(canvasCoordinates)
 | |
| 
 | |
|       const { centerPointRadius } = this.configuration
 | |
| 
 | |
|       const modalityUnitOptions = {
 | |
|         isPreScaled: utilities.viewport.isViewportPreScaled(viewport, targetId),
 | |
| 
 | |
|         isSuvScaled: this.isSuvScaled(
 | |
|           viewport,
 | |
|           targetId,
 | |
|           annotation.metadata.referencedImageId
 | |
|         )
 | |
|       }
 | |
| 
 | |
|       // If cachedStats does not exist, or the unit is missing (as part of import/hydration etc.),
 | |
|       // force to recalculate the stats from the points
 | |
|       if (
 | |
|         !data.cachedStats[targetId] ||
 | |
|         data.cachedStats[targetId].areaUnit === undefined
 | |
|       ) {
 | |
|         data.cachedStats[targetId] = {
 | |
|           Modality: null,
 | |
|           area: null,
 | |
|           max: null,
 | |
|           mean: null,
 | |
|           stdDev: null,
 | |
|           areaUnit: null,
 | |
|           radius: null,
 | |
|           radiusUnit: null,
 | |
|           perimeter: null
 | |
|         }
 | |
|         console.log(111111)
 | |
|         this._calculateCachedStats(
 | |
|           annotation,
 | |
|           viewport,
 | |
|           renderingEngine,
 | |
|           enabledElement,
 | |
|           modalityUnitOptions
 | |
|         )
 | |
|       } else if (annotation.invalidated) {
 | |
|         console.log(22222)
 | |
| 
 | |
|         this._throttledCalculateCachedStats(
 | |
|           annotation,
 | |
|           viewport,
 | |
|           renderingEngine,
 | |
|           enabledElement,
 | |
|           modalityUnitOptions
 | |
|         )
 | |
| 
 | |
|         // If the invalidated data is as a result of volumeViewport manipulation
 | |
|         // of the tools, we need to invalidate the related viewports data, so that
 | |
|         // when scrolling to the related slice in which the tool were manipulated
 | |
|         // we re-render the correct tool position. This is due to stackViewport
 | |
|         // which doesn't have the full volume at each time, and we are only working
 | |
|         // on one slice at a time.
 | |
|         if (viewport instanceof VolumeViewport) {
 | |
|           const { referencedImageId } = annotation.metadata
 | |
| 
 | |
|           // invalidate all the relevant stackViewports if they are not
 | |
|           // at the referencedImageId
 | |
|           for (const targetId in data.cachedStats) {
 | |
|             if (targetId.startsWith('imageId')) {
 | |
|               const viewports = renderingEngine.getStackViewports()
 | |
| 
 | |
|               const invalidatedStack = viewports.find((vp) => {
 | |
|                 // The stack viewport that contains the imageId but is not
 | |
|                 // showing it currently
 | |
|                 const referencedImageURI =
 | |
|                   csUtils.imageIdToURI(referencedImageId)
 | |
|                 const hasImageURI = vp.hasImageURI(referencedImageURI)
 | |
|                 const currentImageURI = csUtils.imageIdToURI(
 | |
|                   vp.getCurrentImageId()
 | |
|                 )
 | |
|                 return hasImageURI && currentImageURI !== referencedImageURI
 | |
|               })
 | |
| 
 | |
|               if (invalidatedStack) {
 | |
|                 delete data.cachedStats[targetId]
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       // If rendering engine has been destroyed while rendering
 | |
|       if (!viewport.getRenderingEngine()) {
 | |
|         console.warn('Rendering Engine has been destroyed')
 | |
|         return renderStatus
 | |
|       }
 | |
| 
 | |
|       let activeHandleCanvasCoords
 | |
| 
 | |
|       if (!isAnnotationVisible(annotationUID)) {
 | |
|         continue
 | |
|       }
 | |
| 
 | |
|       if (
 | |
|         !isAnnotationLocked(annotation) &&
 | |
|         !this.editData &&
 | |
|         activeHandleIndex !== null
 | |
|       ) {
 | |
|         // Not locked or creating and hovering over handle, so render handle.
 | |
|         activeHandleCanvasCoords = [canvasCoordinates[activeHandleIndex]]
 | |
|       }
 | |
| 
 | |
|       if (activeHandleCanvasCoords) {
 | |
|         const handleGroupUID = '0'
 | |
|         drawHandlesSvg(
 | |
|           svgDrawingHelper,
 | |
|           annotationUID,
 | |
|           handleGroupUID,
 | |
|           activeHandleCanvasCoords,
 | |
|           {
 | |
|             color
 | |
|           }
 | |
|         )
 | |
|       }
 | |
| 
 | |
|       const dataId = `${annotationUID}-circle`
 | |
|       const circleUID = '0'
 | |
|       drawCircleSvg(
 | |
|         svgDrawingHelper,
 | |
|         annotationUID,
 | |
|         circleUID,
 | |
|         center,
 | |
|         radius,
 | |
|         {
 | |
|           color,
 | |
|           lineDash,
 | |
|           lineWidth
 | |
|         },
 | |
|         dataId
 | |
|       )
 | |
| 
 | |
|       // draw center point, if "centerPointRadius" configuration is valid.
 | |
|       if (centerPointRadius > 0) {
 | |
|         if (radius > 3 * centerPointRadius) {
 | |
|           drawCircleSvg(
 | |
|             svgDrawingHelper,
 | |
|             annotationUID,
 | |
|             `${circleUID}-center`,
 | |
|             center,
 | |
|             centerPointRadius,
 | |
|             {
 | |
|               color,
 | |
|               lineDash,
 | |
|               lineWidth
 | |
|             }
 | |
|           )
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       renderStatus = true
 | |
| 
 | |
|       const options = this.getLinkedTextBoxStyle(styleSpecifier, annotation)
 | |
|       if (!options.visibility) {
 | |
|         data.handles.textBox = {
 | |
|           hasMoved: false,
 | |
|           worldPosition: [0, 0, 0],
 | |
|           worldBoundingBox: {
 | |
|             topLeft: [0, 0, 0],
 | |
|             topRight: [0, 0, 0],
 | |
|             bottomLeft: [0, 0, 0],
 | |
|             bottomRight: [0, 0, 0]
 | |
|           }
 | |
|         }
 | |
|         continue
 | |
|       }
 | |
| 
 | |
|       const textLines = this.configuration.getTextLines(data, targetId)
 | |
|       if (!textLines || textLines.length === 0) {
 | |
|         continue
 | |
|       }
 | |
| 
 | |
|       // Poor man's cached?
 | |
|       let canvasTextBoxCoords
 | |
| 
 | |
|       if (!data.handles.textBox.hasMoved) {
 | |
|         canvasTextBoxCoords = getTextBoxCoordsCanvas(canvasCorners)
 | |
| 
 | |
|         data.handles.textBox.worldPosition =
 | |
|           viewport.canvasToWorld(canvasTextBoxCoords)
 | |
|       }
 | |
| 
 | |
|       const textBoxPosition = viewport.worldToCanvas(
 | |
|         data.handles.textBox.worldPosition
 | |
|       )
 | |
| 
 | |
|       const textBoxUID = '1'
 | |
|       const boundingBox = drawLinkedTextBoxSvg(
 | |
|         svgDrawingHelper,
 | |
|         annotationUID,
 | |
|         textBoxUID,
 | |
|         textLines,
 | |
|         textBoxPosition,
 | |
|         canvasCoordinates,
 | |
|         {},
 | |
|         options
 | |
|       )
 | |
| 
 | |
|       const { x: left, y: top, width, height } = boundingBox
 | |
| 
 | |
|       data.handles.textBox.worldBoundingBox = {
 | |
|         topLeft: viewport.canvasToWorld([left, top]),
 | |
|         topRight: viewport.canvasToWorld([left + width, top]),
 | |
|         bottomLeft: viewport.canvasToWorld([left, top + height]),
 | |
|         bottomRight: viewport.canvasToWorld([left + width, top + height])
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return renderStatus
 | |
|   };
 | |
|   _calculateCachedStats1 = (
 | |
|     annotation,
 | |
|     viewport,
 | |
|     renderingEngine,
 | |
|     enabledElement,
 | |
|     modalityUnitOptions
 | |
|   ) => {
 | |
|     const data = annotation.data
 | |
|     const { viewportId, renderingEngineId } = enabledElement
 | |
| 
 | |
|     const { points } = data.handles
 | |
| 
 | |
|     const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p))
 | |
|     const { viewPlaneNormal, viewUp } = viewport.getCamera()
 | |
| 
 | |
|     const [topLeftCanvas, bottomRightCanvas] = (
 | |
|       getCanvasCircleCorners(canvasCoordinates)
 | |
|     )
 | |
| 
 | |
|     const topLeftWorld = viewport.canvasToWorld(topLeftCanvas)
 | |
|     const bottomRightWorld = viewport.canvasToWorld(bottomRightCanvas)
 | |
|     const { cachedStats } = data
 | |
| 
 | |
|     const targetIds = Object.keys(cachedStats)
 | |
|     const worldPos1 = topLeftWorld
 | |
|     const worldPos2 = bottomRightWorld
 | |
|     for (let i = 0; i < targetIds.length; i++) {
 | |
|       const targetId = targetIds[i]
 | |
| 
 | |
|       const image = this.getTargetIdImage(targetId, renderingEngine)
 | |
| 
 | |
|       // If image does not exists for the targetId, skip. This can be due
 | |
|       // to various reasons such as if the target was a volumeViewport, and
 | |
|       // the volumeViewport has been decached in the meantime.
 | |
|       if (!image) {
 | |
|         continue
 | |
|       }
 | |
| 
 | |
|       const { dimensions, imageData, metadata } = image
 | |
| 
 | |
|       const worldPos1Index = transformWorldToIndex(imageData, worldPos1)
 | |
| 
 | |
|       worldPos1Index[0] = Math.floor(worldPos1Index[0])
 | |
|       worldPos1Index[1] = Math.floor(worldPos1Index[1])
 | |
|       worldPos1Index[2] = Math.floor(worldPos1Index[2])
 | |
| 
 | |
|       const worldPos2Index = transformWorldToIndex(imageData, worldPos2)
 | |
| 
 | |
|       worldPos2Index[0] = Math.floor(worldPos2Index[0])
 | |
|       worldPos2Index[1] = Math.floor(worldPos2Index[1])
 | |
|       worldPos2Index[2] = Math.floor(worldPos2Index[2])
 | |
| 
 | |
|       // Check if one of the indexes are inside the volume, this then gives us
 | |
|       // Some area to do stats over.
 | |
| 
 | |
|       if (this._isInsideVolume(worldPos1Index, worldPos2Index, dimensions)) {
 | |
|         const iMin = Math.min(worldPos1Index[0], worldPos2Index[0])
 | |
|         const iMax = Math.max(worldPos1Index[0], worldPos2Index[0])
 | |
| 
 | |
|         const jMin = Math.min(worldPos1Index[1], worldPos2Index[1])
 | |
|         const jMax = Math.max(worldPos1Index[1], worldPos2Index[1])
 | |
| 
 | |
|         const kMin = Math.min(worldPos1Index[2], worldPos2Index[2])
 | |
|         const kMax = Math.max(worldPos1Index[2], worldPos2Index[2])
 | |
| 
 | |
|         const boundsIJK = [
 | |
|           [iMin, iMax],
 | |
|           [jMin, jMax],
 | |
|           [kMin, kMax]
 | |
|         ]
 | |
| 
 | |
|         const center = [
 | |
|           (topLeftWorld[0] + bottomRightWorld[0]) / 2,
 | |
|           (topLeftWorld[1] + bottomRightWorld[1]) / 2,
 | |
|           (topLeftWorld[2] + bottomRightWorld[2]) / 2
 | |
|         ]
 | |
| 
 | |
|         const ellipseObj = {
 | |
|           center,
 | |
|           xRadius: Math.abs(topLeftWorld[0] - bottomRightWorld[0]) / 2,
 | |
|           yRadius: Math.abs(topLeftWorld[1] - bottomRightWorld[1]) / 2,
 | |
|           zRadius: Math.abs(topLeftWorld[2] - bottomRightWorld[2]) / 2
 | |
|         }
 | |
|         const { worldWidth, worldHeight } = getWorldWidthAndHeightFromTwoPoints(
 | |
|           viewPlaneNormal,
 | |
|           viewUp,
 | |
|           worldPos1,
 | |
|           worldPos2
 | |
|         )
 | |
|         const isEmptyArea = worldWidth === 0 && worldHeight === 0
 | |
|         const scale = getCalibratedScale(image)
 | |
|         const aspect = getCalibratedAspect(image)
 | |
|         const area = Math.abs(
 | |
|           Math.PI *
 | |
|             (worldWidth / scale / 2) *
 | |
|             (worldHeight / aspect / scale / 2)
 | |
|         )
 | |
| 
 | |
|         const modalityUnit = getModalityUnit(
 | |
|           metadata.Modality,
 | |
|           annotation.metadata.referencedImageId,
 | |
|           modalityUnitOptions
 | |
|         )
 | |
|         const pointsInShape = pointInShapeCallback(
 | |
|           imageData,
 | |
|           (pointLPS, pointIJK) => pointInEllipse(ellipseObj, pointLPS),
 | |
|           this.configuration.statsCalculator.statsCallback,
 | |
|           boundsIJK
 | |
|         )
 | |
| 
 | |
|         const stats = this.configuration.statsCalculator.getStatistics()
 | |
| 
 | |
|         cachedStats[targetId] = {
 | |
|           Modality: metadata.Modality,
 | |
|           area,
 | |
|           mean: stats[1] && stats[1].value ? stats[1].value : null,
 | |
|           max: stats[0] && stats[0].value ? stats[0].value : null,
 | |
|           stdDev: stats[2] && stats[2].value ? stats[2].value : null,
 | |
|           statsArray: stats,
 | |
|           pointsInShape: pointsInShape,
 | |
|           isEmptyArea,
 | |
|           areaUnit: getCalibratedAreaUnits(null, image),
 | |
|           radius: worldWidth / 2 / scale,
 | |
|           radiusUnit: getCalibratedLengthUnits(null, image),
 | |
|           perimeter: (2 * Math.PI * (worldWidth / 2)) / scale,
 | |
|           modalityUnit
 | |
|         }
 | |
|       } else {
 | |
|         this.isHandleOutsideImage = true
 | |
| 
 | |
|         cachedStats[targetId] = {
 | |
|           Modality: metadata.Modality
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     annotation.invalidated = false
 | |
| 
 | |
|     // Dispatching annotation modified
 | |
|     const eventType = Enums.Events.ANNOTATION_MODIFIED
 | |
| 
 | |
|     const eventDetail = {
 | |
|       annotation,
 | |
|       viewportId,
 | |
|       renderingEngineId
 | |
|     }
 | |
|     triggerEvent(eventTarget, eventType, eventDetail)
 | |
| 
 | |
|     return cachedStats
 | |
|   }
 | |
| }
 | |
| function getCanvasCircleCorners(
 | |
|   circleCanvasPoints
 | |
| ) {
 | |
|   const [center, end] = circleCanvasPoints
 | |
|   const radius = utilities.math.point.distanceToPoint(center, end)
 | |
| 
 | |
|   const topLeft = [center[0] - radius, center[1] - radius]
 | |
|   const bottomRight = [center[0] + radius, center[1] + radius]
 | |
| 
 | |
|   return [topLeft, bottomRight]
 | |
| }
 | |
| function getCanvasCircleRadius(
 | |
|   circleCanvasPoints
 | |
| ) {
 | |
|   const [center, end] = circleCanvasPoints
 | |
|   return utilities.math.point.distanceToPoint(center, end)
 | |
| }
 | |
| function getWorldWidthAndHeightFromTwoPoints(
 | |
|   viewPlaneNormal,
 | |
|   viewUp,
 | |
|   worldPos1,
 | |
|   worldPos2
 | |
| ) {
 | |
|   const viewRight = vec3.create()
 | |
| 
 | |
|   vec3.cross(viewRight, viewUp, viewPlaneNormal)
 | |
| 
 | |
|   const pos1 = vec3.fromValues(...worldPos1)
 | |
|   const pos2 = vec3.fromValues(...worldPos2)
 | |
| 
 | |
|   const diagonal = vec3.create()
 | |
|   vec3.subtract(diagonal, pos1, pos2)
 | |
| 
 | |
|   const diagonalLength = vec3.length(diagonal)
 | |
| 
 | |
|   // When the two points are very close to each other return width as 0
 | |
|   // to avoid NaN the cosTheta formula calculation
 | |
|   if (diagonalLength < 0.0001) {
 | |
|     return { worldWidth: 0, worldHeight: 0 }
 | |
|   }
 | |
| 
 | |
|   const cosTheta =
 | |
|     vec3.dot(diagonal, viewRight) / (diagonalLength * vec3.length(viewRight))
 | |
| 
 | |
|   const sinTheta = Math.sqrt(1 - cosTheta * cosTheta)
 | |
| 
 | |
|   const worldWidth = sinTheta * diagonalLength
 | |
|   const worldHeight = cosTheta * diagonalLength
 | |
| 
 | |
|   return { worldWidth, worldHeight }
 | |
| }
 | |
| function pointInEllipse(
 | |
|   ellipse,
 | |
|   pointLPS
 | |
| ) {
 | |
|   const { center: circleCenterWorld, xRadius, yRadius, zRadius } = ellipse
 | |
|   const [x, y, z] = pointLPS
 | |
|   const [x0, y0, z0] = circleCenterWorld
 | |
| 
 | |
|   let inside = 0
 | |
|   if (xRadius !== 0) {
 | |
|     inside += ((x - x0) * (x - x0)) / (xRadius * xRadius)
 | |
|   }
 | |
| 
 | |
|   if (yRadius !== 0) {
 | |
|     inside += ((y - y0) * (y - y0)) / (yRadius * yRadius)
 | |
|   }
 | |
| 
 | |
|   if (zRadius !== 0) {
 | |
|     inside += ((z - z0) * (z - z0)) / (zRadius * zRadius)
 | |
|   }
 | |
| 
 | |
|   return inside <= 1
 | |
| }
 | |
| // function defaultGetTextLines(data, targetId) {
 | |
| //   const cachedVolumeStats = data.cachedStats[targetId]
 | |
| //   const {
 | |
| //     radius,
 | |
| //     radiusUnit,
 | |
| //     area,
 | |
| //     mean,
 | |
| //     stdDev,
 | |
| //     max,
 | |
| //     isEmptyArea,
 | |
| //     areaUnit,
 | |
| //     modalityUnit
 | |
| //   } = cachedVolumeStats
 | |
| 
 | |
| //   const textLines = []
 | |
| 
 | |
| //   if (radius) {
 | |
| //     const radiusLine = isEmptyArea
 | |
| //       ? `Radius: Oblique not supported`
 | |
| //       : `Radius: ${roundNumber(radius)} ${radiusUnit}`
 | |
| //     textLines.push(radiusLine)
 | |
| //   }
 | |
| 
 | |
| //   if (area) {
 | |
| //     const areaLine = isEmptyArea
 | |
| //       ? `Area: Oblique not supported`
 | |
| //       : `Area: ${roundNumber(area)} ${areaUnit}`
 | |
| //     textLines.push(areaLine)
 | |
| //   }
 | |
| 
 | |
| //   if (mean) {
 | |
| //     textLines.push(`Mean: ${roundNumber(mean)} ${modalityUnit}`)
 | |
| //   }
 | |
| 
 | |
| //   if (max) {
 | |
| //     textLines.push(`Max: ${roundNumber(max)} ${modalityUnit}`)
 | |
| //   }
 | |
| 
 | |
| //   if (stdDev) {
 | |
| //     textLines.push(`Std Dev: ${roundNumber(stdDev)} ${modalityUnit}`)
 | |
| //   }
 | |
| 
 | |
| //   return textLines
 | |
| // }
 | |
| 
 | |
| CircleROITool.toolName = 'CircleROI'
 | |
| export default CircleROITool
 | |
| 
 |