diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue b/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue index 39c318f5..c3b4d767 100644 --- a/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue +++ b/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue @@ -563,6 +563,7 @@ import Others from '@/views/trials/trials-panel/reading/dicoms/components/Others import ClinicalData from '@/views/trials/trials-panel/reading/clinical-data' import FusionForm from './FusionForm.vue' import colorMap from './colorMap.vue' +import RectangleROITool from './tools/RectangleROITool' const { visibility } = annotation const { ViewportType, Events } = Enums const renderingEngineId = 'myRenderingEngine' @@ -579,7 +580,7 @@ const { LengthTool, BidirectionalTool, ArrowAnnotateTool, - RectangleROITool, + // RectangleROITool, PlanarFreehandROITool, CircleROITool, EraserTool, diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/tools/RectangleROITool.js b/src/views/trials/trials-panel/reading/dicoms3D/components/tools/RectangleROITool.js new file mode 100644 index 00000000..155fe933 --- /dev/null +++ b/src/views/trials/trials-panel/reading/dicoms3D/components/tools/RectangleROITool.js @@ -0,0 +1,306 @@ + +import { + VolumeViewport, + utilities as csUtils, +} from '@cornerstonejs/core' +import * as cornerstoneTools from '@cornerstonejs/tools' +const { + utilities, + annotation, + drawing + +} = cornerstoneTools + +const { getAnnotations } = annotation.state +const { isAnnotationVisible } = annotation.visibility +const { isAnnotationLocked } = annotation.locking + + +const drawHandlesSvg = drawing.drawHandles +const drawRectSvg = drawing.drawRectByCoordinates +const drawLinkedTextBoxSvg = drawing.drawLinkedTextBox +const { getTextBoxCoordsCanvas } = utilities.drawing + + +/** + * RectangleROIAnnotation let you draw annotations that measures the statistics + * such as area, max, mean and stdDev of a Rectangular region of interest. + * You can use RectangleROIAnnotation in all perpendicular views (axial, sagittal, coronal). + * Note: annotation tools in cornerstone3DTools exists in the exact location + * in the physical 3d space, as a result, by default, all annotations that are + * drawing in the same frameOfReference will get shared between viewports that + * are in the same frameOfReference. RectangleROI tool's text box lines are dynamically + * generated based on the viewport's underlying Modality. For instance, if + * the viewport is displaying CT, the text box will shown the statistics in Hounsfield units, + * and if the viewport is displaying PET, the text box will show the statistics in + * SUV units. + * + * The resulting annotation's data (statistics) and metadata (the + * state of the viewport while drawing was happening) will get added to the + * ToolState manager and can be accessed from the ToolState by calling getAnnotations + * or similar methods. + * + * ```js + * cornerstoneTools.addTool(RectangleROITool) + * + * const toolGroup = ToolGroupManager.createToolGroup('toolGroupId') + * + * toolGroup.addTool(RectangleROITool.toolName) + * + * toolGroup.addViewport('viewportId', 'renderingEngineId') + * + * toolGroup.setToolActive(RectangleROITool.toolName, { + * bindings: [ + * { + * mouseButton: MouseBindings.Primary, // Left Click + * }, + * ], + * }) + * ``` + * + * Read more in the Docs section of the website. + */ + +class RectangleROITool extends cornerstoneTools.RectangleROITool { + constructor( + toolProps, + defaultToolProps + ) { + super(toolProps, defaultToolProps); + } + + /** + * it is used to draw the rectangleROI annotation in each + * request animation frame. It calculates the updated cached statistics if + * data is invalidated and cache it. + * + * @param enabledElement - The Cornerstone's enabledElement. + * @param svgDrawingHelper - The svgDrawingHelper providing the context for drawing. + */ + renderAnnotation = ( + enabledElement, + svgDrawingHelper + ) => { + console.log(drawing) + let renderStatus = false; + const { viewport } = enabledElement; + const { element } = viewport; + + let annotations = getAnnotations(this.getToolName(), element); + + if (!annotations?.length) { + return renderStatus; + } + + annotations = this.filterInteractableAnnotationsForElement( + element, + annotations + ); + + if (!annotations?.length) { + 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 { points, activeHandleIndex } = data.handles; + const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p)); + + styleSpecifier.annotationUID = annotationUID; + + const { color, lineWidth, lineDash } = this.getAnnotationStyle({ + annotation, + styleSpecifier, + }); + + const { viewPlaneNormal, viewUp } = viewport.getCamera(); + + // 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 == null + ) { + data.cachedStats[targetId] = { + Modality: null, + area: null, + max: null, + mean: null, + stdDev: null, + areaUnit: null, + }; + + this._calculateCachedStats( + annotation, + viewPlaneNormal, + viewUp, + renderingEngine, + enabledElement + ); + } else if (annotation.invalidated) { + this._throttledCalculateCachedStats( + annotation, + viewPlaneNormal, + viewUp, + renderingEngine, + enabledElement + ); + + // If the invalidated data is as a result of volumeViewport manipulation + // of the tools, we need to invalidate the related stackViewports data if + // they are not at the referencedImageId, 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(annotationUID) && + !this.editData && + activeHandleIndex !== null && activeHandleIndex !== undefined + ) { + // 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}-rect`; + const rectangleUID = '0'; + drawRectSvg( + svgDrawingHelper, + annotationUID, + rectangleUID, + canvasCoordinates, + { + color, + lineDash, + lineWidth, + }, + dataId + ); + + 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; + } + + if (!data.handles.textBox.hasMoved) { + const canvasTextBoxCoords = getTextBoxCoordsCanvas(canvasCoordinates); + + 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; + }; + +} + + +export default RectangleROITool;