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; // 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