From adffaa53b5a3173653c8463d1a527c379cb649cb Mon Sep 17 00:00:00 2001 From: Alireza Date: Mon, 28 Aug 2023 21:15:05 -0400 Subject: [PATCH 1/6] wip for new brush tool --- .../src/RenderingEngine/BaseVolumeViewport.ts | 23 ++ packages/core/src/types/ViewportProperties.ts | 4 +- packages/tools/src/drawingSvg/drawCircle.ts | 14 +- .../tools/src/tools/segmentation/BrushTool.ts | 354 ++++++++++-------- .../segmentation/strategies/fillCircle.ts | 115 +++--- .../src/types/ToolSpecificAnnotationTypes.ts | 8 + .../planar/filterAnnotationsWithinSlice.ts | 11 +- 7 files changed, 316 insertions(+), 213 deletions(-) diff --git a/packages/core/src/RenderingEngine/BaseVolumeViewport.ts b/packages/core/src/RenderingEngine/BaseVolumeViewport.ts index b57e2b90c8..1785865777 100644 --- a/packages/core/src/RenderingEngine/BaseVolumeViewport.ts +++ b/packages/core/src/RenderingEngine/BaseVolumeViewport.ts @@ -12,6 +12,7 @@ import { import { BlendModes, Events, + InterpolationType, OrientationAxis, ViewportStatus, VOILUTFunctionType, @@ -367,6 +368,23 @@ abstract class BaseVolumeViewport extends Viewport implements IVolumeViewport { return newRGBTransferFunction; } + private setInterpolationType( + interpolationType: InterpolationType, + volumeId?: string + ) { + const applicableVolumeActorInfo = this._getApplicableVolumeActor(volumeId); + + if (!applicableVolumeActorInfo) { + return; + } + + const { volumeActor } = applicableVolumeActorInfo; + const volumeProperty = volumeActor.getProperty(); + + // @ts-ignore + volumeProperty.setInterpolationType(interpolationType); + } + /** * Sets the properties for the volume viewport on the volume * (if fusion, it sets it for the first volume in the fusion) @@ -449,6 +467,7 @@ abstract class BaseVolumeViewport extends Viewport implements IVolumeViewport { invert, colormap, preset, + interpolationType, }: VolumeViewportProperties = {}, volumeId?: string, suppressEvents = false @@ -467,6 +486,10 @@ abstract class BaseVolumeViewport extends Viewport implements IVolumeViewport { this.setVOI(voiRange, volumeId, suppressEvents); } + if (typeof interpolationType !== 'undefined') { + this.setInterpolationType(interpolationType); + } + if (VOILUTFunction !== undefined) { this.setVOILUTFunction(VOILUTFunction, volumeId, suppressEvents); } diff --git a/packages/core/src/types/ViewportProperties.ts b/packages/core/src/types/ViewportProperties.ts index a7493f1185..4163e7fcc3 100644 --- a/packages/core/src/types/ViewportProperties.ts +++ b/packages/core/src/types/ViewportProperties.ts @@ -1,4 +1,4 @@ -import { VOILUTFunctionType } from '../enums'; +import { InterpolationType, VOILUTFunctionType } from '../enums'; import { VOIRange } from './voi'; /** @@ -11,6 +11,8 @@ type ViewportProperties = { VOILUTFunction?: VOILUTFunctionType; /** invert flag - whether the image is inverted */ invert?: boolean; + /** interpolation type */ + interpolationType?: InterpolationType; }; export type { ViewportProperties }; diff --git a/packages/tools/src/drawingSvg/drawCircle.ts b/packages/tools/src/drawingSvg/drawCircle.ts index a30f8a2940..ac5fc7fed8 100644 --- a/packages/tools/src/drawingSvg/drawCircle.ts +++ b/packages/tools/src/drawingSvg/drawCircle.ts @@ -15,13 +15,23 @@ function drawCircle( options = {}, dataId = '' ): void { - const { color, fill, width, lineWidth, lineDash } = Object.assign( + const { + color, + fill, + width, + lineWidth, + lineDash, + fillOpacity, + strokeOpacity, + } = Object.assign( { color: 'dodgerblue', fill: 'transparent', width: '2', lineDash: undefined, lineWidth: undefined, + strokeOpacity: 1, + fillOpacity: 1, }, options ); @@ -42,6 +52,8 @@ function drawCircle( fill, 'stroke-width': strokeWidth, 'stroke-dasharray': lineDash, + 'fill-opacity': fillOpacity, // setting fill opacity + 'stroke-opacity': strokeOpacity, // setting stroke opacity }; if (existingCircleElement) { diff --git a/packages/tools/src/tools/segmentation/BrushTool.ts b/packages/tools/src/tools/segmentation/BrushTool.ts index 791a831ebe..e7c9dd92dd 100644 --- a/packages/tools/src/tools/segmentation/BrushTool.ts +++ b/packages/tools/src/tools/segmentation/BrushTool.ts @@ -1,3 +1,4 @@ +import { vec3 } from 'gl-matrix'; import { cache, getEnabledElement, StackViewport } from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; @@ -7,9 +8,15 @@ import type { EventTypes, SVGDrawingHelper, } from '../../types'; -import { BaseTool } from '../base'; +import { AnnotationTool, BaseTool } from '../base'; import { fillInsideSphere } from './strategies/fillSphere'; import { eraseInsideSphere } from './strategies/eraseSphere'; +import { + addAnnotation, + getAnnotations, + getAnnotation, + removeAnnotation, +} from '../../stateManagement/annotation/annotationState'; import { thresholdInsideCircle, fillInsideCircle, @@ -22,7 +29,7 @@ import { hideElementCursor, } from '../../cursors/elementCursor'; -import triggerAnnotationRenderForViewportUIDs from '../../utilities/triggerAnnotationRenderForViewportIds'; +import triggerAnnotationRenderForViewportIds from '../../utilities/triggerAnnotationRenderForViewportIds'; import { config as segmentationConfig, segmentLocking, @@ -31,11 +38,12 @@ import { activeSegmentation, } from '../../stateManagement/segmentation'; import { LabelmapSegmentationData } from '../../types/LabelmapTypes'; +import { BrushCursor } from '../../types/ToolSpecificAnnotationTypes'; /** * @public */ -class BrushTool extends BaseTool { +class BrushTool extends AnnotationTool { static toolName; private _editData: { segmentation: Types.IImageVolume; @@ -94,13 +102,14 @@ class BrushTool extends BaseTool { this._hoverData = undefined; } - preMouseDownCallback = ( + addNewAnnotation = ( evt: EventTypes.MouseDownActivateEventType - ): boolean => { + ): BrushCursor => { const eventData = evt.detail; - const { element } = eventData; + const { element, currentPoints } = eventData; const enabledElement = getEnabledElement(element); + const worldPos = currentPoints.world; const { viewport, renderingEngine } = enabledElement; if (viewport instanceof StackViewport) { @@ -142,27 +151,77 @@ class BrushTool extends BaseTool { segmentsLocked, }; - this._activateDraw(element); - hideElementCursor(element); + this._addAnnotation(viewport, worldPos); + this._activateDraw(element); + evt.preventDefault(); - triggerAnnotationRenderForViewportUIDs( - renderingEngine, - viewportIdsToRender + triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender); + + return annotation; + }; + + private _addAnnotation = (viewport, worldPos) => { + const FrameOfReferenceUID = viewport.getFrameOfReferenceUID(); + const camera = viewport.getCamera(); + + const { viewPlaneNormal, viewUp } = camera; + + const referencedImageId = this.getReferencedImageId( + viewport, + worldPos, + viewPlaneNormal, + viewUp ); - return true; + const annotation = { + highlighted: true, + invalidated: true, + annotationUID: 'brushCursorUID', + metadata: { + toolName: this.getToolName(), + viewPlaneNormal: [...viewPlaneNormal], + viewUp: [...viewUp], + FrameOfReferenceUID, + referencedImageId, + }, + data: { + handles: { + points: [[...this._calculateBrushLocation(viewport, worldPos)]], + activeHandleIndex: null, + textBox: { + hasMoved: false, + worldPosition: [0, 0, 0], + worldBoundingBox: { + topLeft: [0, 0, 0], + topRight: [0, 0, 0], + bottomLeft: [0, 0, 0], + bottomRight: [0, 0, 0], + }, + }, + }, + label: '', + cachedStats: {}, + }, + }; + + addAnnotation(annotation, viewport.element); + + return annotation; }; - mouseMoveCallback = (evt: EventTypes.InteractionEventType): void => { + mouseMoveCallback = (evt: EventTypes.InteractionEventType) => { if (this.mode === ToolModes.Active) { - this.updateCursor(evt); + this._updateBrushLocation(evt); } }; - private updateCursor(evt: EventTypes.InteractionEventType) { + private _updateBrushLocation( + evt: EventTypes.InteractionEventType, + drag = false + ) { const eventData = evt.detail; const { element } = eventData; const { currentPoints } = eventData; @@ -170,8 +229,11 @@ class BrushTool extends BaseTool { const enabledElement = getEnabledElement(element); const { renderingEngine, viewport } = enabledElement; - const camera = viewport.getCamera(); - const { viewPlaneNormal, viewUp } = camera; + let annotation = getAnnotation('brushCursorUID'); + + if (!annotation || !drag) { + annotation = this._addAnnotation(viewport, currentPoints.world); + } const toolGroupId = this.toolGroupId; @@ -197,22 +259,7 @@ class BrushTool extends BaseTool { const viewportIdsToRender = [viewport.id]; - // Center of circle in canvas Coordinates - - const brushCursor = { - metadata: { - viewPlaneNormal: [...viewPlaneNormal], - viewUp: [...viewUp], - FrameOfReferenceUID: viewport.getFrameOfReferenceUID(), - referencedImageId: '', - toolName: this.getToolName(), - segmentColor, - }, - data: {}, - }; - this._hoverData = { - brushCursor, centerCanvas, segmentIndex, segmentationId, @@ -221,12 +268,13 @@ class BrushTool extends BaseTool { viewportIdsToRender, }; - this._calculateCursor(element, centerCanvas); + if (drag) { + const pos = this._calculateBrushLocation(viewport, centerCanvas); + annotation.data.handles.points.push(pos); + annotation.data.invalidated = false; + } - triggerAnnotationRenderForViewportUIDs( - renderingEngine, - viewportIdsToRender - ); + triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender); } private _dragCallback = (evt: EventTypes.InteractionEventType): void => { @@ -235,82 +283,58 @@ class BrushTool extends BaseTool { const enabledElement = getEnabledElement(element); const { renderingEngine } = enabledElement; - const { imageVolume, segmentation, segmentsLocked } = this._editData; - - this.updateCursor(evt); - - const { - segmentIndex, - segmentationId, - segmentationRepresentationUID, - brushCursor, - viewportIdsToRender, - } = this._hoverData; - - const { data } = brushCursor; - const { viewPlaneNormal, viewUp } = brushCursor.metadata; - - triggerAnnotationRenderForViewportUIDs( - renderingEngine, - viewportIdsToRender - ); + this._updateBrushLocation(evt, true); - const operationData = { - points: data.handles.points, - volume: segmentation, // todo: just pass the segmentationId instead - imageVolume, - segmentIndex, - segmentsLocked, - viewPlaneNormal, - toolGroupId: this.toolGroupId, - segmentationId, - segmentationRepresentationUID, - viewUp, - strategySpecificConfiguration: - this.configuration.strategySpecificConfiguration, - }; - - this.applyActiveStrategy(enabledElement, operationData); + const { viewportIdsToRender } = this._hoverData; + triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender); }; - private _calculateCursor(element, centerCanvas) { - const enabledElement = getEnabledElement(element); - const { viewport } = enabledElement; + private _calculateBrushLocation(viewport, centerCanvas) { const { canvasToWorld } = viewport; + const camera = viewport.getCamera(); const { brushSize } = this.configuration; - // Center of circle in canvas Coordinates - const radius = brushSize; + const viewUp = vec3.fromValues( + camera.viewUp[0], + camera.viewUp[1], + camera.viewUp[2] + ); + const viewPlaneNormal = vec3.fromValues( + camera.viewPlaneNormal[0], + camera.viewPlaneNormal[1], + camera.viewPlaneNormal[2] + ); + const viewRight = vec3.create(); + + vec3.cross(viewRight, viewUp, viewPlaneNormal); + + // in the world coordinate system, the brushSize is the radius of the circle + // in mm - const bottomCanvas: Types.Point2 = [ + const centerCursorInWorld: Types.Point3 = canvasToWorld([ centerCanvas[0], - centerCanvas[1] + radius, - ]; - const topCanvas: Types.Point2 = [centerCanvas[0], centerCanvas[1] - radius]; - const leftCanvas: Types.Point2 = [ - centerCanvas[0] - radius, centerCanvas[1], - ]; - const rightCanvas: Types.Point2 = [ - centerCanvas[0] + radius, - centerCanvas[1], - ]; - - const { brushCursor } = this._hoverData; - const { data } = brushCursor; - - if (data.handles === undefined) { - data.handles = {}; + ]); + + const bottomCursorInWorld = vec3.create(); + const topCursorInWorld = vec3.create(); + const leftCursorInWorld = vec3.create(); + const rightCursorInWorld = vec3.create(); + + // Calculate the bottom and top points of the circle in world coordinates + for (let i = 0; i <= 2; i++) { + bottomCursorInWorld[i] = centerCursorInWorld[i] - viewUp[i] * brushSize; + topCursorInWorld[i] = centerCursorInWorld[i] + viewUp[i] * brushSize; + leftCursorInWorld[i] = centerCursorInWorld[i] - viewRight[i] * brushSize; + rightCursorInWorld[i] = centerCursorInWorld[i] + viewRight[i] * brushSize; } - data.handles.points = [ - canvasToWorld(bottomCanvas), - canvasToWorld(topCanvas), - canvasToWorld(leftCanvas), - canvasToWorld(rightCanvas), + return [ + bottomCursorInWorld, + topCursorInWorld, + leftCursorInWorld, + rightCursorInWorld, ]; - - data.invalidated = false; } private _endCallback = (evt: EventTypes.InteractionEventType): void => { @@ -318,32 +342,29 @@ class BrushTool extends BaseTool { const { element } = eventData; const { imageVolume, segmentation, segmentsLocked } = this._editData; - const { - segmentIndex, - segmentationId, - segmentationRepresentationUID, - brushCursor, - } = this._hoverData; + const { segmentIndex, segmentationId, segmentationRepresentationUID } = + this._hoverData; - const { data } = brushCursor; - const { viewPlaneNormal, viewUp } = brushCursor.metadata; + const annotation = getAnnotation('brushCursorUID'); + const { viewPlaneNormal, viewUp } = annotation.metadata; this._deactivateDraw(element); resetElementCursor(element); + removeAnnotation('brushCursorUID'); const enabledElement = getEnabledElement(element); const { viewport } = enabledElement; this._editData = null; - this.updateCursor(evt); + this._updateBrushLocation(evt); if (viewport instanceof StackViewport) { throw new Error('Not implemented yet'); } const operationData = { - points: data.handles.points, + points: annotation.data.handles.points, volume: segmentation, imageVolume, segmentIndex, @@ -358,6 +379,8 @@ class BrushTool extends BaseTool { }; this.applyActiveStrategy(enabledElement, operationData); + + triggerAnnotationRenderForViewportIds; }; /** @@ -407,12 +430,14 @@ class BrushTool extends BaseTool { renderAnnotation( enabledElement: Types.IEnabledElement, svgDrawingHelper: SVGDrawingHelper - ): void { + ): boolean { + const renderStatus = false; if (!this._hoverData) { return; } const { viewport } = enabledElement; + const { element } = viewport; const viewportIdsToRender = this._hoverData.viewportIdsToRender; @@ -420,53 +445,76 @@ class BrushTool extends BaseTool { return; } - const brushCursor = this._hoverData.brushCursor; - - if (brushCursor.data.invalidated === true) { - const { centerCanvas } = this._hoverData; - const { element } = viewport; + let annotations = getAnnotations(this.getToolName(), viewport.element); - // This can be set true when changing the brush size programmatically - // whilst the cursor is being rendered. - this._calculateCursor(element, centerCanvas); + // Todo: We don't need this anymore, filtering happens in triggerAnnotationRender + if (!annotations?.length) { + return renderStatus; } - const toolMetadata = brushCursor.metadata; - const annotationUID = toolMetadata.brushCursorUID; - - const data = brushCursor.data; - const { points } = data.handles; - const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p)); - - const bottom = canvasCoordinates[0]; - const top = canvasCoordinates[1]; - - const center = [ - Math.floor((bottom[0] + top[0]) / 2), - Math.floor((bottom[1] + top[1]) / 2), - ]; - - const radius = Math.abs(bottom[1] - Math.floor((bottom[1] + top[1]) / 2)); - - const color = `rgb(${toolMetadata.segmentColor.slice(0, 3)})`; + annotations = this.filterInteractableAnnotationsForElement( + element, + annotations + ); - // If rendering engine has been destroyed while rendering - if (!viewport.getRenderingEngine()) { - console.warn('Rendering Engine has been destroyed'); - return; + if (!annotations?.length) { + return renderStatus; } - const circleUID = '0'; - drawCircleSvg( - svgDrawingHelper, - annotationUID, - circleUID, - center as Types.Point2, - radius, - { - color, - } - ); + // const brushCursor = this._hoverData.brushCursor; + + // if (brushCursor.data.invalidated === true) { + // const { centerCanvas } = this._hoverData; + // // This can be set true when changing the brush size programmatically + // // whilst the cursor is being rendered. + // this._calculateBrushLocation(viewport, centerCanvas); + // } + + const segmentColor = this._hoverData.segmentColor; + const annotationUID = 'brushCursorUID'; + let counter = 0; + annotations.forEach((toolData) => { + const data = toolData.data; + const { points: pointsList } = data.handles; + + pointsList.map((points) => { + const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p)); + + const bottom = canvasCoordinates[0]; + const top = canvasCoordinates[1]; + + const center = [ + Math.floor((bottom[0] + top[0]) / 2), + Math.floor((bottom[1] + top[1]) / 2), + ]; + + const radius = Math.abs( + bottom[1] - Math.floor((bottom[1] + top[1]) / 2) + ); + + const color = `rgb(${segmentColor.slice(0, 3)})`; + + // If rendering engine has been destroyed while rendering + if (!viewport.getRenderingEngine()) { + console.warn('Rendering Engine has been destroyed'); + return; + } + + const circleUID = '0'; + drawCircleSvg( + svgDrawingHelper, + `${annotationUID}-${counter++}`, + circleUID, + center as Types.Point2, + radius, + { + color, + lineWidth: 1, + strokeOpacity: 0.5, + } + ); + }); + }); } } diff --git a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts index 1eb997e649..69de3e083d 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts @@ -45,72 +45,77 @@ function fillCircle( // Average the points to get the center of the ellipse const center = vec3.fromValues(0, 0, 0); - points.forEach((point) => { - vec3.add(center, center, point); - }); - vec3.scale(center, center, 1 / points.length); - - const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p)); + const modifiedSlicesToUse = new Set() as Set; - // 1. From the drawn tool: Get the ellipse (circle) topLeft and bottomRight - // corners in canvas coordinates - const [topLeftCanvas, bottomRightCanvas] = - getCanvasEllipseCorners(canvasCoordinates); + points?.map((points) => { + points.forEach((point) => { + vec3.add(center, center, point); + }); + vec3.scale(center, center, 1 / points.length); - // 2. Find the extent of the ellipse (circle) in IJK index space of the image - const topLeftWorld = viewport.canvasToWorld(topLeftCanvas); - const bottomRightWorld = viewport.canvasToWorld(bottomRightCanvas); + const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p)); - const ellipsoidCornersIJK = [ - transformWorldToIndex(imageData, topLeftWorld), - transformWorldToIndex(imageData, bottomRightWorld), - ]; + // 1. From the drawn tool: Get the ellipse (circle) topLeft and bottomRight + // corners in canvas coordinates + const [topLeftCanvas, bottomRightCanvas] = + getCanvasEllipseCorners(canvasCoordinates); - const boundsIJK = getBoundingBoxAroundShape(ellipsoidCornersIJK, dimensions); + // 2. Find the extent of the ellipse (circle) in IJK index space of the image + const topLeftWorld = viewport.canvasToWorld(topLeftCanvas); + const bottomRightWorld = viewport.canvasToWorld(bottomRightCanvas); - // using circle as a form of ellipse - const ellipseObj = { - center: center as Types.Point3, - 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 ellipsoidCornersIJK = [ + transformWorldToIndex(imageData, topLeftWorld), + transformWorldToIndex(imageData, bottomRightWorld), + ]; - const modifiedSlicesToUse = new Set() as Set; - - let callback; + const boundsIJK = getBoundingBoxAroundShape( + ellipsoidCornersIJK, + dimensions + ); - if (threshold) { - callback = ({ value, index, pointIJK }) => { - if (segmentsLocked.includes(value)) { - return; - } + // using circle as a form of ellipse + const ellipseObj = { + center: center as Types.Point3, + xRadius: Math.abs(topLeftWorld[0] - bottomRightWorld[0]) / 2, + yRadius: Math.abs(topLeftWorld[1] - bottomRightWorld[1]) / 2, + zRadius: Math.abs(topLeftWorld[2] - bottomRightWorld[2]) / 2, + }; - if ( - isWithinThreshold(index, imageVolume, strategySpecificConfiguration) - ) { + let callback; + + if (threshold) { + callback = ({ value, index, pointIJK }) => { + if (segmentsLocked.includes(value)) { + return; + } + + if ( + isWithinThreshold(index, imageVolume, strategySpecificConfiguration) + ) { + scalarData[index] = segmentIndex; + //Todo: I don't think this will always be index 2 in streamingImageVolume? + modifiedSlicesToUse.add(pointIJK[2]); + } + }; + } else { + callback = ({ value, index, pointIJK }) => { + if (segmentsLocked.includes(value)) { + return; + } scalarData[index] = segmentIndex; //Todo: I don't think this will always be index 2 in streamingImageVolume? modifiedSlicesToUse.add(pointIJK[2]); - } - }; - } else { - callback = ({ value, index, pointIJK }) => { - if (segmentsLocked.includes(value)) { - return; - } - scalarData[index] = segmentIndex; - //Todo: I don't think this will always be index 2 in streamingImageVolume? - modifiedSlicesToUse.add(pointIJK[2]); - }; - } - - pointInShapeCallback( - imageData, - (pointLPS, pointIJK) => pointInEllipse(ellipseObj, pointLPS), - callback, - boundsIJK - ); + }; + } + + pointInShapeCallback( + imageData, + (pointLPS, pointIJK) => pointInEllipse(ellipseObj, pointLPS), + callback, + boundsIJK + ); + }); const arrayOfSlices: number[] = Array.from(modifiedSlicesToUse); diff --git a/packages/tools/src/types/ToolSpecificAnnotationTypes.ts b/packages/tools/src/types/ToolSpecificAnnotationTypes.ts index 944c9f8e35..f927c45801 100644 --- a/packages/tools/src/types/ToolSpecificAnnotationTypes.ts +++ b/packages/tools/src/types/ToolSpecificAnnotationTypes.ts @@ -310,3 +310,11 @@ export interface ScaleOverlayAnnotation extends Annotation { viewportId: string; }; } + +export interface BrushCursor extends Annotation { + data: { + handles: { + points: [Types.Point3]; + }; + }; +} diff --git a/packages/tools/src/utilities/planar/filterAnnotationsWithinSlice.ts b/packages/tools/src/utilities/planar/filterAnnotationsWithinSlice.ts index b99d8d97e7..92fa19bf34 100644 --- a/packages/tools/src/utilities/planar/filterAnnotationsWithinSlice.ts +++ b/packages/tools/src/utilities/planar/filterAnnotationsWithinSlice.ts @@ -83,12 +83,17 @@ export default function filterAnnotationsWithinSlice( const annotationsWithinSlice = []; for (const annotation of annotationsWithParallelNormals) { - const data = annotation.data; - const point = data.handles.points[0]; - if (!annotation.isVisible) { continue; } + + const data = annotation.data; + + // pick a sample point from the annotation + const point = Array.isArray(data.handles.points) + ? data.handles.points[0][0] + : data.handles.points[0]; + // A = point // B = focal point // P = normal From d168e74148a725bf5299f23e3960437e312299fc Mon Sep 17 00:00:00 2001 From: Alireza Date: Mon, 28 Aug 2023 22:27:42 -0400 Subject: [PATCH 2/6] one segmentation data modified per interaction --- .../tools/src/tools/segmentation/BrushTool.ts | 203 ++++++++++-------- .../segmentation/strategies/fillCircle.ts | 125 +++++------ .../segmentation/strategies/fillSphere.ts | 56 ++--- .../planar/filterAnnotationsWithinSlice.ts | 11 +- 4 files changed, 208 insertions(+), 187 deletions(-) diff --git a/packages/tools/src/tools/segmentation/BrushTool.ts b/packages/tools/src/tools/segmentation/BrushTool.ts index e7c9dd92dd..61681774c0 100644 --- a/packages/tools/src/tools/segmentation/BrushTool.ts +++ b/packages/tools/src/tools/segmentation/BrushTool.ts @@ -8,7 +8,7 @@ import type { EventTypes, SVGDrawingHelper, } from '../../types'; -import { AnnotationTool, BaseTool } from '../base'; +import { AnnotationTool } from '../base'; import { fillInsideSphere } from './strategies/fillSphere'; import { eraseInsideSphere } from './strategies/eraseSphere'; import { @@ -51,7 +51,6 @@ class BrushTool extends AnnotationTool { segmentsLocked: number[]; // } | null; private _hoverData?: { - brushCursor: any; segmentationId: string; segmentIndex: number; segmentationRepresentationUID: string; @@ -86,6 +85,18 @@ class BrushTool extends AnnotationTool { super(toolProps, defaultToolProps); } + handleSelectedCallback = () => { + return; + }; + + toolSelectedCallback = () => { + return; + }; + + cancel = () => { + this._hoverData = undefined; + }; + onSetToolPassive = () => { this.disableCursor(); }; @@ -95,6 +106,15 @@ class BrushTool extends AnnotationTool { }; onSetToolDisabled = () => { + removeAnnotation('brushCursorUID'); + this.disableCursor(); + }; + + onSetToolActive = () => { + this.disableCursor(); + }; + + onSetConfiguration = () => { this.disableCursor(); }; @@ -153,7 +173,7 @@ class BrushTool extends AnnotationTool { hideElementCursor(element); - this._addAnnotation(viewport, worldPos); + const annotation = this._addAnnotation(viewport, worldPos); this._activateDraw(element); evt.preventDefault(); @@ -163,7 +183,7 @@ class BrushTool extends AnnotationTool { return annotation; }; - private _addAnnotation = (viewport, worldPos) => { + private _addAnnotation = (viewport, worldPos): BrushCursor => { const FrameOfReferenceUID = viewport.getFrameOfReferenceUID(); const camera = viewport.getCamera(); @@ -189,37 +209,26 @@ class BrushTool extends AnnotationTool { }, data: { handles: { - points: [[...this._calculateBrushLocation(viewport, worldPos)]], - activeHandleIndex: null, - textBox: { - hasMoved: false, - worldPosition: [0, 0, 0], - worldBoundingBox: { - topLeft: [0, 0, 0], - topRight: [0, 0, 0], - bottomLeft: [0, 0, 0], - bottomRight: [0, 0, 0], - }, - }, + points: [...this._calculateBrushLocation(viewport, worldPos)], }, - label: '', - cachedStats: {}, }, - }; + } as BrushCursor; addAnnotation(annotation, viewport.element); return annotation; }; - mouseMoveCallback = (evt: EventTypes.InteractionEventType) => { + mouseMoveCallback = (evt: EventTypes.MouseMoveEventType) => { if (this.mode === ToolModes.Active) { this._updateBrushLocation(evt); } + + return true; }; private _updateBrushLocation( - evt: EventTypes.InteractionEventType, + evt: EventTypes.MouseMoveEventType, drag = false ) { const eventData = evt.detail; @@ -230,9 +239,15 @@ class BrushTool extends AnnotationTool { const { renderingEngine, viewport } = enabledElement; let annotation = getAnnotation('brushCursorUID'); - - if (!annotation || !drag) { + if (!drag && !annotation) { annotation = this._addAnnotation(viewport, currentPoints.world); + } else if (!drag) { + // already we have an annotation, but we are not dragging + // so just update the location + annotation.data.handles.points = [ + ...this._calculateBrushLocation(viewport, currentPoints.canvas), + ]; + annotation.data.invalidated = false; } const toolGroupId = this.toolGroupId; @@ -270,14 +285,14 @@ class BrushTool extends AnnotationTool { if (drag) { const pos = this._calculateBrushLocation(viewport, centerCanvas); - annotation.data.handles.points.push(pos); + annotation.data.handles.points.push(...pos); annotation.data.invalidated = false; } triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender); } - private _dragCallback = (evt: EventTypes.InteractionEventType): void => { + private _dragCallback = (evt: EventTypes.MouseMoveEventType): void => { const eventData = evt.detail; const { element } = eventData; const enabledElement = getEnabledElement(element); @@ -289,7 +304,7 @@ class BrushTool extends AnnotationTool { triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender); }; - private _calculateBrushLocation(viewport, centerCanvas) { + private _calculateBrushLocation(viewport, centerCanvas): Types.Point3[] { const { canvasToWorld } = viewport; const camera = viewport.getCamera(); const { brushSize } = this.configuration; @@ -310,7 +325,6 @@ class BrushTool extends AnnotationTool { // in the world coordinate system, the brushSize is the radius of the circle // in mm - const centerCursorInWorld: Types.Point3 = canvasToWorld([ centerCanvas[0], centerCanvas[1], @@ -330,14 +344,14 @@ class BrushTool extends AnnotationTool { } return [ - bottomCursorInWorld, - topCursorInWorld, - leftCursorInWorld, - rightCursorInWorld, + bottomCursorInWorld as Types.Point3, + topCursorInWorld as Types.Point3, + leftCursorInWorld as Types.Point3, + rightCursorInWorld as Types.Point3, ]; } - private _endCallback = (evt: EventTypes.InteractionEventType): void => { + private _endCallback = (evt: EventTypes.MouseMoveEventType): void => { const eventData = evt.detail; const { element } = eventData; @@ -351,19 +365,16 @@ class BrushTool extends AnnotationTool { this._deactivateDraw(element); resetElementCursor(element); + + // remove the annotation svg since we need to draw for the next drag removeAnnotation('brushCursorUID'); const enabledElement = getEnabledElement(element); - const { viewport } = enabledElement; this._editData = null; this._updateBrushLocation(evt); - if (viewport instanceof StackViewport) { - throw new Error('Not implemented yet'); - } - - const operationData = { + this.applyActiveStrategy(enabledElement, { points: annotation.data.handles.points, volume: segmentation, imageVolume, @@ -374,13 +385,15 @@ class BrushTool extends AnnotationTool { segmentationId, segmentationRepresentationUID, viewUp, + lazyCalculation: true, strategySpecificConfiguration: this.configuration.strategySpecificConfiguration, - }; - - this.applyActiveStrategy(enabledElement, operationData); + }); - triggerAnnotationRenderForViewportIds; + triggerAnnotationRenderForViewportIds( + enabledElement.renderingEngine, + this._hoverData.viewportIdsToRender + ); }; /** @@ -421,12 +434,15 @@ class BrushTool extends AnnotationTool { public invalidateBrushCursor() { if (this._hoverData !== undefined) { - const { data } = this._hoverData.brushCursor; - - data.invalidated = true; + const annotation = getAnnotation('brushCursorUID'); + annotation.data.invalidated = true; } } + public isPointNearTool(): boolean { + return false; + } + renderAnnotation( enabledElement: Types.IEnabledElement, svgDrawingHelper: SVGDrawingHelper @@ -447,7 +463,6 @@ class BrushTool extends AnnotationTool { let annotations = getAnnotations(this.getToolName(), viewport.element); - // Todo: We don't need this anymore, filtering happens in triggerAnnotationRender if (!annotations?.length) { return renderStatus; } @@ -461,60 +476,60 @@ class BrushTool extends AnnotationTool { return renderStatus; } - // const brushCursor = this._hoverData.brushCursor; + const annotation = annotations[0]; - // if (brushCursor.data.invalidated === true) { - // const { centerCanvas } = this._hoverData; - // // This can be set true when changing the brush size programmatically - // // whilst the cursor is being rendered. - // this._calculateBrushLocation(viewport, centerCanvas); - // } + if (annotation.data.invalidated === true) { + const { centerCanvas } = this._hoverData; + // This can be set true when changing the brush size programmatically + // whilst the cursor is being rendered. + this._calculateBrushLocation(viewport, centerCanvas); + } const segmentColor = this._hoverData.segmentColor; const annotationUID = 'brushCursorUID'; let counter = 0; - annotations.forEach((toolData) => { - const data = toolData.data; - const { points: pointsList } = data.handles; - - pointsList.map((points) => { - const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p)); - - const bottom = canvasCoordinates[0]; - const top = canvasCoordinates[1]; - - const center = [ - Math.floor((bottom[0] + top[0]) / 2), - Math.floor((bottom[1] + top[1]) / 2), - ]; - - const radius = Math.abs( - bottom[1] - Math.floor((bottom[1] + top[1]) / 2) - ); - - const color = `rgb(${segmentColor.slice(0, 3)})`; - - // If rendering engine has been destroyed while rendering - if (!viewport.getRenderingEngine()) { - console.warn('Rendering Engine has been destroyed'); - return; + const data = annotation.data; + const { points: pointsList } = data.handles; + + const notDrag = pointsList.length === 4; + + for (let i = 0; i < pointsList.length; i += 4) { + // we only need two points to draw a circle so four is redundant + const points = [pointsList[i], pointsList[i + 1]]; + const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p)); + + const bottom = canvasCoordinates[0]; + const top = canvasCoordinates[1]; + + const center = [ + Math.floor((bottom[0] + top[0]) / 2), + Math.floor((bottom[1] + top[1]) / 2), + ]; + + const radius = Math.abs(bottom[1] - Math.floor((bottom[1] + top[1]) / 2)); + + const color = `rgb(${segmentColor.slice(0, 3)})`; + + // If rendering engine has been destroyed while rendering + if (!viewport.getRenderingEngine()) { + console.warn('Rendering Engine has been destroyed'); + return; + } + + const circleUID = '0'; + drawCircleSvg( + svgDrawingHelper, + `${annotationUID}-${counter++}`, + circleUID, + center as Types.Point2, + radius, + { + color, + lineWidth: notDrag ? 2 : 0.75, + strokeOpacity: notDrag ? 1 : 0.6, } - - const circleUID = '0'; - drawCircleSvg( - svgDrawingHelper, - `${annotationUID}-${counter++}`, - circleUID, - center as Types.Point2, - radius, - { - color, - lineWidth: 1, - strokeOpacity: 0.5, - } - ); - }); - }); + ); + } } } diff --git a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts index 69de3e083d..53da438459 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts @@ -15,7 +15,8 @@ const { transformWorldToIndex } = csUtils; type OperationData = { segmentationId: string; imageVolume: Types.IImageVolume; - points: any; // Todo:fix + points: any; + lazyCalculation: boolean; volume: Types.IImageVolume; segmentIndex: number; segmentsLocked: number[]; @@ -25,6 +26,35 @@ type OperationData = { constraintFn: () => boolean; }; +function calculateEllipseAndBounds(points, viewport, imageData, dimensions) { + const center = vec3.fromValues(0, 0, 0); + points.forEach((point) => vec3.add(center, center, point)); + vec3.scale(center, center, 1 / points.length); + + const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p)); + const [topLeftCanvas, bottomRightCanvas] = + getCanvasEllipseCorners(canvasCoordinates); + + const topLeftWorld = viewport.canvasToWorld(topLeftCanvas); + const bottomRightWorld = viewport.canvasToWorld(bottomRightCanvas); + + const ellipsoidCornersIJK = [ + transformWorldToIndex(imageData, topLeftWorld), + transformWorldToIndex(imageData, bottomRightWorld), + ]; + + const boundsIJK = getBoundingBoxAroundShape(ellipsoidCornersIJK, dimensions); + + const ellipseObj = { + center: center as Types.Point3, + xRadius: Math.abs(topLeftWorld[0] - bottomRightWorld[0]) / 2, + yRadius: Math.abs(topLeftWorld[1] - bottomRightWorld[1]) / 2, + zRadius: Math.abs(topLeftWorld[2] - bottomRightWorld[2]) / 2, + }; + + return { ellipseObj, boundsIJK }; +} + function fillCircle( enabledElement: Types.IEnabledElement, operationData: OperationData, @@ -38,83 +68,56 @@ function fillCircle( segmentIndex, segmentationId, strategySpecificConfiguration, + lazyCalculation, } = operationData; const { imageData, dimensions } = segmentationVolume; const scalarData = segmentationVolume.getScalarData(); const { viewport } = enabledElement; - // Average the points to get the center of the ellipse - const center = vec3.fromValues(0, 0, 0); const modifiedSlicesToUse = new Set() as Set; + const indicesToFill = new Set() as Set; - points?.map((points) => { - points.forEach((point) => { - vec3.add(center, center, point); - }); - vec3.scale(center, center, 1 / points.length); - - const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p)); - - // 1. From the drawn tool: Get the ellipse (circle) topLeft and bottomRight - // corners in canvas coordinates - const [topLeftCanvas, bottomRightCanvas] = - getCanvasEllipseCorners(canvasCoordinates); - - // 2. Find the extent of the ellipse (circle) in IJK index space of the image - const topLeftWorld = viewport.canvasToWorld(topLeftCanvas); - const bottomRightWorld = viewport.canvasToWorld(bottomRightCanvas); + function callback({ value, index, pointIJK }) { + if (segmentsLocked.includes(value)) { + return; + } + if ( + !threshold || + isWithinThreshold(index, imageVolume, strategySpecificConfiguration) + ) { + indicesToFill.add(index); + modifiedSlicesToUse.add(pointIJK[2]); + } + } - const ellipsoidCornersIJK = [ - transformWorldToIndex(imageData, topLeftWorld), - transformWorldToIndex(imageData, bottomRightWorld), - ]; + let pointsChunks; + if (lazyCalculation) { + pointsChunks = []; + for (let i = 0; i < points.length; i += 4) { + pointsChunks.push(points.slice(i, i + 4)); + } + } else { + pointsChunks = [points]; + } - const boundsIJK = getBoundingBoxAroundShape( - ellipsoidCornersIJK, + for (let i = 0; i < pointsChunks.length; i++) { + const pointsChunk = pointsChunks[i]; + const { ellipseObj, boundsIJK } = calculateEllipseAndBounds( + pointsChunk, + viewport, + imageData, dimensions ); - - // using circle as a form of ellipse - const ellipseObj = { - center: center as Types.Point3, - xRadius: Math.abs(topLeftWorld[0] - bottomRightWorld[0]) / 2, - yRadius: Math.abs(topLeftWorld[1] - bottomRightWorld[1]) / 2, - zRadius: Math.abs(topLeftWorld[2] - bottomRightWorld[2]) / 2, - }; - - let callback; - - if (threshold) { - callback = ({ value, index, pointIJK }) => { - if (segmentsLocked.includes(value)) { - return; - } - - if ( - isWithinThreshold(index, imageVolume, strategySpecificConfiguration) - ) { - scalarData[index] = segmentIndex; - //Todo: I don't think this will always be index 2 in streamingImageVolume? - modifiedSlicesToUse.add(pointIJK[2]); - } - }; - } else { - callback = ({ value, index, pointIJK }) => { - if (segmentsLocked.includes(value)) { - return; - } - scalarData[index] = segmentIndex; - //Todo: I don't think this will always be index 2 in streamingImageVolume? - modifiedSlicesToUse.add(pointIJK[2]); - }; - } - pointInShapeCallback( imageData, (pointLPS, pointIJK) => pointInEllipse(ellipseObj, pointLPS), callback, boundsIJK ); + } + + indicesToFill.forEach((index) => { + scalarData[index] = segmentIndex; }); const arrayOfSlices: number[] = Array.from(modifiedSlicesToUse); diff --git a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts index d066f18b46..93ed1360d9 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts @@ -11,6 +11,7 @@ type OperationData = { segmentsLocked: number[]; viewPlaneNormal: Types.Point3; viewUp: Types.Point3; + lazyCalculation?: boolean; constraintFn: () => boolean; }; @@ -26,37 +27,44 @@ function fillSphere( segmentIndex, segmentationId, points, + lazyCalculation, } = operationData; - const { imageData, dimensions } = segmentation; const scalarData = segmentation.getScalarData(); - const scalarIndex = []; + const modifiedSlicesToUse = new Set(); - const callback = ({ index, value }) => { - if (segmentsLocked.includes(value)) { - return; + let pointsChunks; + if (lazyCalculation) { + pointsChunks = []; + for (let i = 0; i < points.length; i += 4) { + pointsChunks.push(points.slice(i, i + 4)); } - scalarData[index] = segmentIndex; - scalarIndex.push(index); - }; + } else { + pointsChunks = [points]; + } + + for (let i = 0; i < pointsChunks.length; i++) { + const pointsChunk = pointsChunks[i]; + + const callback = ({ index, value, pointIJK }) => { + if (segmentsLocked.includes(value)) { + return; + } + scalarData[index] = segmentIndex; + modifiedSlicesToUse.add( + Math.floor(index / (dimensions[0] * dimensions[1])) + ); + }; - pointInSurroundingSphereCallback( - imageData, - [points[0], points[1]], - callback, - viewport as Types.IVolumeViewport - ); + pointInSurroundingSphereCallback( + imageData, + pointsChunk.slice(0, 2), + callback, + viewport as Types.IVolumeViewport + ); + } - // Since the scalar indexes start from the top left corner of the cube, the first - // slice that needs to be rendered can be calculated from the first mask coordinate - // divided by the zMultiple, as well as the last slice for the last coordinate - const zMultiple = dimensions[0] * dimensions[1]; - const minSlice = Math.floor(scalarIndex[0] / zMultiple); - const maxSlice = Math.floor(scalarIndex[scalarIndex.length - 1] / zMultiple); - const sliceArray = Array.from( - { length: maxSlice - minSlice + 1 }, - (v, k) => k + minSlice - ); + const sliceArray = Array.from(modifiedSlicesToUse); triggerSegmentationDataModified(segmentationId, sliceArray); } diff --git a/packages/tools/src/utilities/planar/filterAnnotationsWithinSlice.ts b/packages/tools/src/utilities/planar/filterAnnotationsWithinSlice.ts index 92fa19bf34..b99d8d97e7 100644 --- a/packages/tools/src/utilities/planar/filterAnnotationsWithinSlice.ts +++ b/packages/tools/src/utilities/planar/filterAnnotationsWithinSlice.ts @@ -83,17 +83,12 @@ export default function filterAnnotationsWithinSlice( const annotationsWithinSlice = []; for (const annotation of annotationsWithParallelNormals) { + const data = annotation.data; + const point = data.handles.points[0]; + if (!annotation.isVisible) { continue; } - - const data = annotation.data; - - // pick a sample point from the annotation - const point = Array.isArray(data.handles.points) - ? data.handles.points[0][0] - : data.handles.points[0]; - // A = point // B = focal point // P = normal From 4a8ac8c7867f5275e4fcae107c68b115a76528d0 Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 30 Aug 2023 09:59:34 -0400 Subject: [PATCH 3/6] add download dicom data --- .../examples/segmentationExport/index.ts | 19 +++++----- .../adapters/Cornerstone/Segmentation_4X.js | 5 +-- .../Segmentation/generateSegmentation.ts | 6 ++-- .../src/adapters/helpers/downloadDICOMData.ts | 35 +++++++++++++++++++ .../adapters/src/adapters/helpers/index.ts | 3 +- .../_setViewports.ts | 25 ++++++++++--- .../tools/src/tools/base/AnnotationTool.ts | 34 +++++++++--------- 7 files changed, 90 insertions(+), 37 deletions(-) create mode 100644 packages/adapters/src/adapters/helpers/downloadDICOMData.ts diff --git a/packages/adapters/examples/segmentationExport/index.ts b/packages/adapters/examples/segmentationExport/index.ts index c44896a1f8..e95b44c744 100644 --- a/packages/adapters/examples/segmentationExport/index.ts +++ b/packages/adapters/examples/segmentationExport/index.ts @@ -14,9 +14,11 @@ import { createImageIdsAndCacheMetaData } from "../../../../utils/demo/helpers"; import * as cornerstoneTools from "@cornerstonejs/tools"; -import { adaptersSEG } from "@cornerstonejs/adapters"; +import { adaptersSEG, helpers } from "@cornerstonejs/adapters"; import dcmjs from "dcmjs"; +const { downloadDICOMData } = helpers; + // This is for debugging purposes console.warn( "Click on index.ts to open source code for this example --------->" @@ -130,15 +132,14 @@ addButtonToToolbar({ labelmapObj.metadata[segmentIndex] = segmentMetadata; }); - const segBlob = Cornerstone3D.Segmentation.generateSegmentation( - images, - labelmapObj, - metaData - ); + const generatedSegmentation = + Cornerstone3D.Segmentation.generateSegmentation( + images, + labelmapObj, + metaData + ); - //Create a URL for the binary. - const objectUrl = URL.createObjectURL(segBlob); - window.open(objectUrl); + downloadDICOMData(generatedSegmentation.dataset, "mySEG.dcm"); } }); diff --git a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js index bf030d8c25..24391e8918 100644 --- a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js +++ b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js @@ -191,10 +191,7 @@ function fillSegmentation(segmentation, inputLabelmaps3D, userOptions = {}) { segmentation.bitPackPixelData(); } - const buffer = Buffer.from(datasetToDict(segmentation.dataset).write()); - const segBlob = new Blob([buffer], { type: "application/dicom" }); - - return segBlob; + return segmentation; } function _getLabelmapsFromReferencedFrameIndicies( diff --git a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts index 62ae07fb14..66e77f42c3 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/Segmentation/generateSegmentation.ts @@ -10,7 +10,7 @@ const { Segmentation: SegmentationDerivation } = derivations; * @param images - An array of the cornerstone image objects, which includes imageId and metadata * @param labelmaps - An array of the 3D Volumes that contain the segmentation data. */ -function generateSegmentation(images, labelmaps, metadata, options) { +function generateSegmentation(images, labelmaps, metadata, options = {}) { const segmentation = _createMultiframeSegmentationFromReferencedImages( images, metadata, @@ -41,8 +41,8 @@ function _createMultiframeSegmentationFromReferencedImages( ...image, ...instance, // Todo: move to dcmjs tag style - SOPClassUID: instance.SopClassUID, - SOPInstanceUID: instance.SopInstanceUID, + SOPClassUID: instance.SopClassUID || instance.SOPClassUID, + SOPInstanceUID: instance.SopInstanceUID || instance.SOPInstanceUID, PixelData: image.getPixelData(), _vrMap: { PixelData: "OW" diff --git a/packages/adapters/src/adapters/helpers/downloadDICOMData.ts b/packages/adapters/src/adapters/helpers/downloadDICOMData.ts new file mode 100644 index 0000000000..dcf600b8a2 --- /dev/null +++ b/packages/adapters/src/adapters/helpers/downloadDICOMData.ts @@ -0,0 +1,35 @@ +import { data } from "dcmjs"; +import { Buffer } from "buffer"; +const { datasetToDict } = data; + +interface DicomDataset { + _meta?: any; + // other properties +} + +/** + * Trigger file download from an array buffer + * @param bufferOrDataset - ArrayBuffer or DicomDataset + * @param filename - name of the file to download + */ +export function downloadDICOMData( + bufferOrDataset: ArrayBuffer | DicomDataset, + filename: string +) { + let blob; + if (bufferOrDataset instanceof ArrayBuffer) { + blob = new Blob([bufferOrDataset], { type: "application/dicom" }); + } else { + if (!bufferOrDataset._meta) { + throw new Error("Dataset must have a _meta property"); + } + + const buffer = Buffer.from(datasetToDict(bufferOrDataset).write()); + blob = new Blob([buffer], { type: "application/dicom" }); + } + + const link = document.createElement("a"); + link.href = window.URL.createObjectURL(blob); + link.download = filename; + link.click(); +} diff --git a/packages/adapters/src/adapters/helpers/index.ts b/packages/adapters/src/adapters/helpers/index.ts index b852e82fef..91a7a1625a 100644 --- a/packages/adapters/src/adapters/helpers/index.ts +++ b/packages/adapters/src/adapters/helpers/index.ts @@ -1,5 +1,6 @@ import { toArray } from "./toArray"; import { codeMeaningEquals } from "./codeMeaningEquals"; import { graphicTypeEquals } from "./graphicTypeEquals"; +import { downloadDICOMData } from "./downloadDICOMData"; -export { toArray, codeMeaningEquals, graphicTypeEquals }; +export { toArray, codeMeaningEquals, graphicTypeEquals, downloadDICOMData }; diff --git a/packages/tools/examples/stackToVolumeWithAnnotations/_setViewports.ts b/packages/tools/examples/stackToVolumeWithAnnotations/_setViewports.ts index edde481007..f35b0e5819 100644 --- a/packages/tools/examples/stackToVolumeWithAnnotations/_setViewports.ts +++ b/packages/tools/examples/stackToVolumeWithAnnotations/_setViewports.ts @@ -66,6 +66,8 @@ async function _convertStackToVolumeViewport( const { id, element } = viewport; + const prevCamera = viewport.getCamera(); + let imageIds = viewport.getImageIds(); imageIds = imageIds.map((imageId) => { const imageURI = utilities.imageIdToURI(imageId); @@ -78,7 +80,6 @@ async function _convertStackToVolumeViewport( type: ViewportType.ORTHOGRAPHIC, element, defaultOptions: { - orientation: Enums.OrientationAxis.SAGITTAL, background: [0.2, 0.4, 0.2], }, }, @@ -98,11 +99,27 @@ async function _convertStackToVolumeViewport( // Set the volume to load volume.load(); + const volumeViewport = renderingEngine.getViewport(id); - setVolumesForViewports(renderingEngine, [{ volumeId }], [id]); + setVolumesForViewports( + renderingEngine, + [ + { + volumeId, + }, + ], + [id] + ); + + // preserve the slice location when switching from stack to volume + element.addEventListener(Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, () => { + volumeViewport.setCamera({ + focalPoint: prevCamera.focalPoint, + }); + volumeViewport.render(); + }); - // Render the image - renderingEngine.renderViewports([id]); + volumeViewport.render(); } export { _convertStackToVolumeViewport, _convertVolumeToStackViewport }; diff --git a/packages/tools/src/tools/base/AnnotationTool.ts b/packages/tools/src/tools/base/AnnotationTool.ts index dace35c958..6eeeadf4dd 100644 --- a/packages/tools/src/tools/base/AnnotationTool.ts +++ b/packages/tools/src/tools/base/AnnotationTool.ts @@ -185,24 +185,26 @@ abstract class AnnotationTool extends AnnotationDisplayTool { const { data } = annotation; const { points, textBox } = data.handles; - const { worldBoundingBox } = textBox; - if (worldBoundingBox) { - const canvasBoundingBox = { - topLeft: viewport.worldToCanvas(worldBoundingBox.topLeft), - topRight: viewport.worldToCanvas(worldBoundingBox.topRight), - bottomLeft: viewport.worldToCanvas(worldBoundingBox.bottomLeft), - bottomRight: viewport.worldToCanvas(worldBoundingBox.bottomRight), - }; + if (textBox) { + const { worldBoundingBox } = textBox; + if (worldBoundingBox) { + const canvasBoundingBox = { + topLeft: viewport.worldToCanvas(worldBoundingBox.topLeft), + topRight: viewport.worldToCanvas(worldBoundingBox.topRight), + bottomLeft: viewport.worldToCanvas(worldBoundingBox.bottomLeft), + bottomRight: viewport.worldToCanvas(worldBoundingBox.bottomRight), + }; - if ( - canvasCoords[0] >= canvasBoundingBox.topLeft[0] && - canvasCoords[0] <= canvasBoundingBox.bottomRight[0] && - canvasCoords[1] >= canvasBoundingBox.topLeft[1] && - canvasCoords[1] <= canvasBoundingBox.bottomRight[1] - ) { - data.handles.activeHandleIndex = null; - return textBox; + if ( + canvasCoords[0] >= canvasBoundingBox.topLeft[0] && + canvasCoords[0] <= canvasBoundingBox.bottomRight[0] && + canvasCoords[1] >= canvasBoundingBox.topLeft[1] && + canvasCoords[1] <= canvasBoundingBox.bottomRight[1] + ) { + data.handles.activeHandleIndex = null; + return textBox; + } } } From 48e36d5e68cad8b5c531b3ddb2f90f160a3636bb Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 30 Aug 2023 13:46:36 -0400 Subject: [PATCH 4/6] update api --- common/reviews/api/core.api.md | 3 +- .../api/streaming-image-volume-loader.api.md | 2 + common/reviews/api/tools.api.md | 37 ++++++++++++++++--- .../segmentation/strategies/eraseCircle.ts | 1 + .../segmentation/strategies/fillCircle.ts | 2 +- 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index 7279aa85ec..18de32d439 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -94,7 +94,7 @@ export abstract class BaseVolumeViewport extends Viewport implements IVolumeView // (undocumented) setOrientation(orientation: OrientationAxis, immediate?: boolean): void; // (undocumented) - setProperties({ voiRange, VOILUTFunction, invert, colormap, preset, }?: VolumeViewportProperties, volumeId?: string, suppressEvents?: boolean): void; + setProperties({ voiRange, VOILUTFunction, invert, colormap, preset, interpolationType, }?: VolumeViewportProperties, volumeId?: string, suppressEvents?: boolean): void; // (undocumented) abstract setSlabThickness(slabThickness: number, filterActorUIDs?: Array): void; // (undocumented) @@ -2738,6 +2738,7 @@ type ViewportProperties = { voiRange?: VOIRange; VOILUTFunction?: VOILUTFunctionType; invert?: boolean; + interpolationType?: InterpolationType; }; // @public (undocumented) diff --git a/common/reviews/api/streaming-image-volume-loader.api.md b/common/reviews/api/streaming-image-volume-loader.api.md index acc95bd96b..4a392a8b7e 100644 --- a/common/reviews/api/streaming-image-volume-loader.api.md +++ b/common/reviews/api/streaming-image-volume-loader.api.md @@ -1343,6 +1343,7 @@ interface IVolumeViewport extends IViewport { // (undocumented) useCPURendering: boolean; worldToCanvas: (worldPos: Point3) => Point2; + } // @public @@ -1611,6 +1612,7 @@ type ViewportProperties = { voiRange?: VOIRange; VOILUTFunction?: VOILUTFunctionType; invert?: boolean; + interpolationType?: InterpolationType; }; // @public (undocumented) diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index ec3b254bbe..2fad3e82d5 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -562,12 +562,34 @@ declare namespace boundingBox { type BoundsIJK = [Types_2.Point2, Types_2.Point2, Types_2.Point2]; // @public (undocumented) -export class BrushTool extends BaseTool { +interface BrushCursor extends Annotation { + // (undocumented) + data: { + handles: { + points: [Types_2.Point3]; + }; + }; +} + +// @public (undocumented) +export class BrushTool extends AnnotationTool { constructor(toolProps?: PublicToolProps, defaultToolProps?: ToolProps); // (undocumented) + addNewAnnotation: (evt: EventTypes_2.MouseDownActivateEventType) => BrushCursor; + // (undocumented) + cancel: () => void; + // (undocumented) + handleSelectedCallback: () => void; + // (undocumented) invalidateBrushCursor(): void; // (undocumented) - mouseMoveCallback: (evt: EventTypes_2.InteractionEventType) => void; + isPointNearTool(): boolean; + // (undocumented) + mouseMoveCallback: (evt: EventTypes_2.MouseMoveEventType) => boolean; + // (undocumented) + onSetConfiguration: () => void; + // (undocumented) + onSetToolActive: () => void; // (undocumented) onSetToolDisabled: () => void; // (undocumented) @@ -575,11 +597,11 @@ export class BrushTool extends BaseTool { // (undocumented) onSetToolPassive: () => void; // (undocumented) - preMouseDownCallback: (evt: EventTypes_2.MouseDownActivateEventType) => boolean; - // (undocumented) - renderAnnotation(enabledElement: Types_2.IEnabledElement, svgDrawingHelper: SVGDrawingHelper): void; + renderAnnotation(enabledElement: Types_2.IEnabledElement, svgDrawingHelper: SVGDrawingHelper): boolean; // (undocumented) static toolName: any; + // (undocumented) + toolSelectedCallback: () => void; } // @public (undocumented) @@ -3163,6 +3185,7 @@ interface IVolumeViewport extends IViewport { // (undocumented) useCPURendering: boolean; worldToCanvas: (worldPos: Point3) => Point2; + } // @public (undocumented) @@ -5143,7 +5166,8 @@ declare namespace ToolSpecificAnnotationTypes { AngleAnnotation, ReferenceCursor, ReferenceLineAnnotation, - ScaleOverlayAnnotation + ScaleOverlayAnnotation, + BrushCursor } } @@ -5449,6 +5473,7 @@ type ViewportProperties = { voiRange?: VOIRange; VOILUTFunction?: VOILUTFunctionType; invert?: boolean; + interpolationType?: InterpolationType; }; declare namespace visibility { diff --git a/packages/tools/src/tools/segmentation/strategies/eraseCircle.ts b/packages/tools/src/tools/segmentation/strategies/eraseCircle.ts index 72c01c9997..325a62972a 100644 --- a/packages/tools/src/tools/segmentation/strategies/eraseCircle.ts +++ b/packages/tools/src/tools/segmentation/strategies/eraseCircle.ts @@ -10,6 +10,7 @@ type OperationData = { segmentIndex: number; segmentsLocked: number[]; viewPlaneNormal: number[]; + lazyCalculation?: boolean; viewUp: number[]; strategySpecificConfiguration: any; constraintFn: () => boolean; diff --git a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts index 53da438459..e8a61c9b07 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts @@ -16,7 +16,7 @@ type OperationData = { segmentationId: string; imageVolume: Types.IImageVolume; points: any; - lazyCalculation: boolean; + lazyCalculation?: boolean; volume: Types.IImageVolume; segmentIndex: number; segmentsLocked: number[]; From 6e150d03a364032315b9d983426b538e22d3d606 Mon Sep 17 00:00:00 2001 From: Alireza Date: Fri, 1 Sep 2023 10:09:57 -0400 Subject: [PATCH 5/6] added review comments --- .../src/adapters/Cornerstone/Segmentation_4X.js | 15 +++++++-------- .../wadouri/metaData/metaDataProvider.ts | 4 ++-- .../tools/src/tools/segmentation/BrushTool.ts | 13 ++++++++++--- .../tools/segmentation/strategies/fillCircle.ts | 16 +++++++++++++++- .../tools/segmentation/strategies/fillSphere.ts | 16 +++++++++++++++- 5 files changed, 49 insertions(+), 15 deletions(-) diff --git a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js index 24391e8918..ebfc90de0c 100644 --- a/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js +++ b/packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js @@ -7,7 +7,6 @@ import { } from "dcmjs"; import ndarray from "ndarray"; import cloneDeep from "lodash.clonedeep"; -import { Buffer } from "buffer"; import { Events } from "../enums"; @@ -19,8 +18,7 @@ const { nearlyEqual } = utilities.orientation; -const { datasetToDict, BitArray, DicomMessage, DicomMetaDictionary } = - dcmjsData; +const { BitArray, DicomMessage, DicomMetaDictionary } = dcmjsData; const { Normalizer } = normalizers; const { Segmentation: SegmentationDerivation } = derivations; @@ -61,12 +59,13 @@ function generateSegmentation(images, inputLabelmaps3D, userOptions = {}) { } /** - * fillSegmentation - Fills a derived segmentation dataset with cornerstoneTools `LabelMap3D` data. + * Fills a given segmentation object with data from the input labelmaps3D * - * @param {object[]} segmentation An empty segmentation derived dataset. - * @param {Object|Object[]} inputLabelmaps3D The cornerstone `Labelmap3D` object, or an array of objects. - * @param {Object} userOptions Options object to override default options. - * @returns {Blob} description + * @param segmentation - The segmentation object to be filled. + * @param inputLabelmaps3D - An array of 3D labelmaps, or a single 3D labelmap. + * @param userOptions - Optional configuration settings. Will override the default options. + * + * @returns {object} The filled segmentation object. */ function fillSegmentation(segmentation, inputLabelmaps3D, userOptions = {}) { const options = Object.assign( diff --git a/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts b/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts index 981735f5e4..da8ae54228 100644 --- a/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts +++ b/packages/dicomImageLoader/src/imageLoader/wadouri/metaData/metaDataProvider.ts @@ -243,10 +243,10 @@ function metaDataProvider(type, imageId) { if (type === 'petImageModule') { return { - frameReferenceTime: dicomParser.floatString( + frameReferenceTime: dataSet.floatString( dataSet.string('x00541300') || '' ), - actualFrameDuration: dicomParser.intString(dataSet.string('x00181242')), + actualFrameDuration: dataSet.intString(dataSet.string('x00181242')), }; } diff --git a/packages/tools/src/tools/segmentation/BrushTool.ts b/packages/tools/src/tools/segmentation/BrushTool.ts index 61681774c0..c3c1116080 100644 --- a/packages/tools/src/tools/segmentation/BrushTool.ts +++ b/packages/tools/src/tools/segmentation/BrushTool.ts @@ -237,6 +237,7 @@ class BrushTool extends AnnotationTool { const centerCanvas = currentPoints.canvas; const enabledElement = getEnabledElement(element); const { renderingEngine, viewport } = enabledElement; + const { viewPlaneNormal, viewUp } = viewport.getCamera(); let annotation = getAnnotation('brushCursorUID'); if (!drag && !annotation) { @@ -247,6 +248,12 @@ class BrushTool extends AnnotationTool { annotation.data.handles.points = [ ...this._calculateBrushLocation(viewport, currentPoints.canvas), ]; + + // Update the camera props since we might have changed the cursor location + // and have been moving to a new viewport with different orientation without + // dragging or clicking + annotation.metadata.viewUp = viewUp; + annotation.metadata.viewPlaneNormal = viewPlaneNormal; annotation.data.invalidated = false; } @@ -456,7 +463,6 @@ class BrushTool extends AnnotationTool { const { element } = viewport; const viewportIdsToRender = this._hoverData.viewportIdsToRender; - if (!viewportIdsToRender.includes(viewport.id)) { return; } @@ -478,10 +484,11 @@ class BrushTool extends AnnotationTool { const annotation = annotations[0]; + // If brush size programmatically has been changed then we need to invalidate + // the cursor points to be recalculated for the rendering if (annotation.data.invalidated === true) { const { centerCanvas } = this._hoverData; - // This can be set true when changing the brush size programmatically - // whilst the cursor is being rendered. + this._calculateBrushLocation(viewport, centerCanvas); } diff --git a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts index e8a61c9b07..c96bc3f43e 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts @@ -15,7 +15,7 @@ const { transformWorldToIndex } = csUtils; type OperationData = { segmentationId: string; imageVolume: Types.IImageVolume; - points: any; + points: Types.Point3[]; lazyCalculation?: boolean; volume: Types.IImageVolume; segmentIndex: number; @@ -70,6 +70,11 @@ function fillCircle( strategySpecificConfiguration, lazyCalculation, } = operationData; + + if (points.length % 4 !== 0) { + throw new Error('The length of the points array must be a multiple of 4.'); + } + const { imageData, dimensions } = segmentationVolume; const scalarData = segmentationVolume.getScalarData(); const { viewport } = enabledElement; @@ -90,6 +95,15 @@ function fillCircle( } } + // Previously fillSphere and fillCircle (used in brushes) were acting on a + // single circle or sphere. However, that meant that we were modifying the + // segmentation scalar data on each drag (can be often +100 transactions). Lazy + // calculation allows us to only modify the segmentation scalar data once the + // user has finished drawing the circle or sphere. This is done by splitting + // the points into chunks and only triggering the segmentation data modified + // event once all the points have been processed. The tool need to provide the points + // in the correct order to be chunked here. Todo: Maybe we should move the chunk + // logic to the tool itself. let pointsChunks; if (lazyCalculation) { pointsChunks = []; diff --git a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts index 93ed1360d9..26750cc805 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts @@ -4,7 +4,7 @@ import { triggerSegmentationDataModified } from '../../../stateManagement/segmen import { pointInSurroundingSphereCallback } from '../../../utilities'; type OperationData = { - points: [Types.Point3, Types.Point3, Types.Point3, Types.Point3]; + points: Types.Point3[]; volume: Types.IImageVolume; segmentIndex: number; segmentationId: string; @@ -29,10 +29,24 @@ function fillSphere( points, lazyCalculation, } = operationData; + + if (points.length % 4 !== 0) { + throw new Error('The length of the points array must be a multiple of 4.'); + } + const { imageData, dimensions } = segmentation; const scalarData = segmentation.getScalarData(); const modifiedSlicesToUse = new Set(); + // Previously fillSphere and fillCircle (used in brushes) were acting on a + // single circle or sphere. However, that meant that we were modifying the + // segmentation scalar data on each drag (can be often +100 transactions). Lazy + // calculation allows us to only modify the segmentation scalar data once the + // user has finished drawing the circle or sphere. This is done by splitting + // the points into chunks and only triggering the segmentation data modified + // event once all the points have been processed. The tool need to provide the points + // in the correct order to be chunked here. Todo: Maybe we should move the chunk + // logic to the tool itself. let pointsChunks; if (lazyCalculation) { pointsChunks = []; From 3b7c03888e2e5747166c2399d3de17e4113f5d01 Mon Sep 17 00:00:00 2001 From: Alireza Date: Tue, 5 Sep 2023 11:52:09 -0400 Subject: [PATCH 6/6] wip --- .../tools/src/tools/segmentation/BrushTool.ts | 27 +++++++++++++++++-- .../segmentation/strategies/fillSphere.ts | 22 +++++++-------- .../src/types/ToolSpecificAnnotationTypes.ts | 8 ++++++ 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/packages/tools/src/tools/segmentation/BrushTool.ts b/packages/tools/src/tools/segmentation/BrushTool.ts index c3c1116080..4258bb7153 100644 --- a/packages/tools/src/tools/segmentation/BrushTool.ts +++ b/packages/tools/src/tools/segmentation/BrushTool.ts @@ -7,15 +7,16 @@ import type { ToolProps, EventTypes, SVGDrawingHelper, + Annotations, } from '../../types'; import { AnnotationTool } from '../base'; import { fillInsideSphere } from './strategies/fillSphere'; import { eraseInsideSphere } from './strategies/eraseSphere'; import { addAnnotation, - getAnnotations, getAnnotation, removeAnnotation, + getAnnotationManager, } from '../../stateManagement/annotation/annotationState'; import { thresholdInsideCircle, @@ -206,6 +207,7 @@ class BrushTool extends AnnotationTool { viewUp: [...viewUp], FrameOfReferenceUID, referencedImageId, + isBrushTool: true, }, data: { handles: { @@ -467,7 +469,28 @@ class BrushTool extends AnnotationTool { return; } - let annotations = getAnnotations(this.getToolName(), viewport.element); + // This is a tricky situation, our annotations are bound to the toolName (length, angle, etc) + // however, for our brushTool we can create multiple instances of it based on the same + // tool with different configuration/strategies e.g., brush, eraser, brushCircle, brushSphere + // brushThreshold etc.. So if we start by brushCircle and then switch to brushSphere below + // we don't get any annotations since the previous annotations are bound to the toolName (brushCircle) + // so we kind of need to look into the annotation manager and get the annotations any + // tool that was derived from the brush tool (have Brush as their parent tool) + // let annotations = getAnnotations(this.getToolName(), viewport.element); + const annotationManager = getAnnotationManager(); + const frameOfReferenceUID = viewport.getFrameOfReferenceUID(); + let annotations = annotationManager.getAnnotations( + frameOfReferenceUID + ) as Annotations; + + if (!Object.keys(annotations)?.length) { + return renderStatus; + } + + annotations = Object.values(annotations) + .flat() + // @ts-ignore + .filter((annotation) => annotation.metadata?.isBrushTool); if (!annotations?.length) { return renderStatus; diff --git a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts index 26750cc805..5f6d4ecd0a 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts @@ -57,22 +57,22 @@ function fillSphere( pointsChunks = [points]; } + const inPlane = dimensions[0] * dimensions[1]; + + const callback = ({ index, value }) => { + if (segmentsLocked.includes(value)) { + return; + } + scalarData[index] = segmentIndex; + modifiedSlicesToUse.add(Math.floor(index / inPlane)); + }; + for (let i = 0; i < pointsChunks.length; i++) { const pointsChunk = pointsChunks[i]; - const callback = ({ index, value, pointIJK }) => { - if (segmentsLocked.includes(value)) { - return; - } - scalarData[index] = segmentIndex; - modifiedSlicesToUse.add( - Math.floor(index / (dimensions[0] * dimensions[1])) - ); - }; - pointInSurroundingSphereCallback( imageData, - pointsChunk.slice(0, 2), + [pointsChunk[0], pointsChunk[1]], callback, viewport as Types.IVolumeViewport ); diff --git a/packages/tools/src/types/ToolSpecificAnnotationTypes.ts b/packages/tools/src/types/ToolSpecificAnnotationTypes.ts index f927c45801..b91bb046e6 100644 --- a/packages/tools/src/types/ToolSpecificAnnotationTypes.ts +++ b/packages/tools/src/types/ToolSpecificAnnotationTypes.ts @@ -312,6 +312,14 @@ export interface ScaleOverlayAnnotation extends Annotation { } export interface BrushCursor extends Annotation { + metadata: { + toolName: string; + viewUp: Types.Point3; + viewPlaneNormal: Types.Point3; + FrameOfReferenceUID: string; + referencedImageId: string; + isBrushTool: boolean; + }; data: { handles: { points: [Types.Point3];