diff --git a/packages/cornerstone-render/src/RenderingEngine/Viewport.ts b/packages/cornerstone-render/src/RenderingEngine/Viewport.ts index 69f22d11ee..802e07c4c4 100644 --- a/packages/cornerstone-render/src/RenderingEngine/Viewport.ts +++ b/packages/cornerstone-render/src/RenderingEngine/Viewport.ts @@ -346,7 +346,7 @@ class Viewport { this._suppressCameraModifiedEvents = false } - protected setCameraNoEvent(camera: ICamera) { + protected setCameraNoEvent(camera: ICamera): void { this._suppressCameraModifiedEvents = true this.setCamera(camera) this._suppressCameraModifiedEvents = false @@ -621,7 +621,7 @@ class Viewport { // // Compensating for the flipped viewport. Since our method for flipping is // flipping the actor matrix itself, the focal point won't change; therefore, - // we need to accomodate for this required change elsewhere + // we need to accommodate for this required change elsewhere // vec3.sub(dir, viewport.applyFlipTx(focalPoint), point) position: <Point3>this.applyFlipTx(vtkCamera.getPosition() as Point3), focalPoint: <Point3>this.applyFlipTx(vtkCamera.getFocalPoint() as Point3), diff --git a/packages/cornerstone-render/src/types/Plane.ts b/packages/cornerstone-render/src/types/Plane.ts new file mode 100644 index 0000000000..2ab94696c2 --- /dev/null +++ b/packages/cornerstone-render/src/types/Plane.ts @@ -0,0 +1,4 @@ +// Plane equation Ax+By+Cz=D, plane is defined by [A, B, C, D] +type Plane = [number, number, number, number] + +export default Plane diff --git a/packages/cornerstone-render/src/types/index.ts b/packages/cornerstone-render/src/types/index.ts index 2c261854cb..2279980d5f 100644 --- a/packages/cornerstone-render/src/types/index.ts +++ b/packages/cornerstone-render/src/types/index.ts @@ -24,6 +24,7 @@ import type Orientation from './Orientation' import type Point2 from './Point2' import type Point3 from './Point3' import type Point4 from './Point4' +import type Plane from './Plane' import type IStreamingImageVolume from './IStreamingImageVolume' import type ViewportInputOptions from './ViewportInputOptions' import type IImageData from './IImageData' @@ -85,6 +86,7 @@ export type { Point2, Point3, Point4, + Plane, ViewportInputOptions, VOIRange, VOI, diff --git a/packages/cornerstone-render/src/utilities/planar.ts b/packages/cornerstone-render/src/utilities/planar.ts index f3994b1e58..fd2f7cf373 100644 --- a/packages/cornerstone-render/src/utilities/planar.ts +++ b/packages/cornerstone-render/src/utilities/planar.ts @@ -1,18 +1,15 @@ -import { Point3 } from '../types' +import { Point3, Plane } from '../types' +import { vec3, mat3 } from 'gl-matrix' /** * It calculates the intersection of a line and a plane. * Plane equation is Ax+By+Cz=D * @param p0 [x,y,z] of the first point of the line * @param p1 [x,y,z] of the second point of the line - * @param plane [A, B, C, D] Plane parameter + * @param plane [A, B, C, D] Plane parameter: Ax+By+Cz=D * @returns [X,Y,Z] coordinates of the intersection */ -function linePlaneIntersection( - p0: Point3, - p1: Point3, - plane: [number, number, number, number] -): Point3 { +function linePlaneIntersection(p0: Point3, p1: Point3, plane: Plane): Point3 { const [x0, y0, z0] = p0 const [x1, y1, z1] = p1 const [A, B, C, D] = plane @@ -27,8 +24,49 @@ function linePlaneIntersection( return [X, Y, Z] } +/** + * + * @param normal normal vector + * @param point a point on the plane + * @returns [A, B,C, D] of plane equation A*X + B*Y + C*Z = D + */ +function planeEquation(normal: Point3, point: Point3 | vec3): Plane { + const [A, B, C] = normal + const D = A * point[0] + B * point[1] + C * point[2] + return [A, B, C, D] +} + +/** + * Computes the intersection of three planes in 3D space with equations: + * A1*X + B1*Y + C1*Z = D1 + * A2*X + B2*Y + C2*Z = D2 + * A3*X + B3*Y + C3*Z = D3 + * @returns {x, y, z} the intersection in the world coordinate + */ +function threePlaneIntersection( + firstPlane: Plane, + secondPlane: Plane, + thirdPlane: Plane +): Point3 { + const [A1, B1, C1, D1] = firstPlane + const [A2, B2, C2, D2] = secondPlane + const [A3, B3, C3, D3] = thirdPlane + const m0 = mat3.fromValues(A1, A2, A3, B1, B2, B3, C1, C2, C3) + const m1 = mat3.fromValues(D1, D2, D3, B1, B2, B3, C1, C2, C3) + const m2 = mat3.fromValues(A1, A2, A3, D1, D2, D3, C1, C2, C3) + const m3 = mat3.fromValues(A1, A2, A3, B1, B2, B3, D1, D2, D3) + + // TODO: handle no intersection scenario + const x = mat3.determinant(m1) / mat3.determinant(m0) + const y = mat3.determinant(m2) / mat3.determinant(m0) + const z = mat3.determinant(m3) / mat3.determinant(m0) + return [x, y, z] +} + const planar = { linePlaneIntersection, + planeEquation, + threePlaneIntersection, } export default planar diff --git a/packages/cornerstone-tools/src/eventDispatchers/cameraModifiedEventDispatcher.ts b/packages/cornerstone-tools/src/eventDispatchers/cameraModifiedEventDispatcher.ts index 34735e1b2c..00bbc6e1c4 100644 --- a/packages/cornerstone-tools/src/eventDispatchers/cameraModifiedEventDispatcher.ts +++ b/packages/cornerstone-tools/src/eventDispatchers/cameraModifiedEventDispatcher.ts @@ -19,8 +19,6 @@ const onCameraModified = function (evt) { Enabled, ]) - // todo: this will trigger crosshair tool onCameraModified in cases - // where crosshair is not active, shall we only filter active? enabledTools.forEach((tool) => { if (tool.onCameraModified) { tool.onCameraModified(evt) diff --git a/packages/cornerstone-tools/src/store/ToolGroupManager/createToolGroup.ts b/packages/cornerstone-tools/src/store/ToolGroupManager/createToolGroup.ts index 88d6990df5..9a9438b6dc 100644 --- a/packages/cornerstone-tools/src/store/ToolGroupManager/createToolGroup.ts +++ b/packages/cornerstone-tools/src/store/ToolGroupManager/createToolGroup.ts @@ -1,10 +1,10 @@ -import { ToolBindings } from '../../enums' +import { ToolBindings, ToolModes } from '../../enums' import { getRenderingEngine } from '@ohif/cornerstone-render' import { state } from '../index' import IToolGroup from './IToolGroup' import ISetToolModeOptions from '../../types/ISetToolModeOptions' -import ToolModes from '../../enums/ToolModes' import deepmerge from '../../util/deepMerge' + import { MouseCursor, SVGMouseCursor } from '../../cursors' import { initElementCursor } from '../../cursors/elementCursor' @@ -154,6 +154,10 @@ function createToolGroup(toolGroupId: string): IToolGroup | undefined { if (this.isPrimaryButtonBinding(toolModeOptions)) { this.resetViewportsCursor(this._toolInstances[toolName]) } + + if (typeof this._toolInstances[toolName].init === 'function') { + this._toolInstances[toolName].init(this.viewports) + } this.refreshViewports() }, setToolPassive: function ( diff --git a/packages/cornerstone-tools/src/tools/CrosshairsTool.ts b/packages/cornerstone-tools/src/tools/CrosshairsTool.ts index f34ef43ca2..2fd03143f4 100644 --- a/packages/cornerstone-tools/src/tools/CrosshairsTool.ts +++ b/packages/cornerstone-tools/src/tools/CrosshairsTool.ts @@ -1,7 +1,16 @@ import { BaseAnnotationTool } from './base' // ~~ VTK Viewport -import { getEnabledElement, RenderingEngine } from '@ohif/cornerstone-render' -import { addToolState, getToolState } from '../stateManagement/toolState' +import { + getEnabledElement, + RenderingEngine, + getRenderingEngine, + Utilities as csUtils, +} from '@ohif/cornerstone-render' +import { + addToolState, + getToolState, + removeToolStateByToolDataUID, +} from '../stateManagement/toolState' import { drawCircle as drawCircleSvg, drawHandles as drawHandlesSvg, @@ -22,7 +31,6 @@ import { Point2, Point3, } from '../types' -import { ToolGroupManager } from '../store' import { isToolDataLocked } from '../stateManagement/toolDataLocking' import triggerAnnotationRenderForViewportUIDs from '../util/triggerAnnotationRenderForViewportUIDs' @@ -40,6 +48,12 @@ interface ToolConfiguration { } } +type ViewportInput = { + renderingEngineUID: string + viewportUID: string +} +type ViewportInputs = Array<ViewportInput> + interface CrosshairsSpecificToolData extends ToolSpecificToolData { data: { handles: { @@ -128,65 +142,189 @@ export default class CrosshairsTool extends BaseAnnotationTool { this._mouseDragCallback = this._mouseDragCallback.bind(this) } - addNewMeasurement( - evt: CustomEvent, - interactionType: string - ): CrosshairsSpecificToolData { - // not used, but is necessary if BaseAnnotationTool. - // NOTE: this is a BaseAnnotationTool and not a BaseTool, because in future - // we will likely pre-filter all tools using typeof / instanceof - // in the mouse down dispatchers where we check for methods like pointNearTool. - const toolSpecificToolData = { + /** + * Gets the camera from the viewport, and adds crosshairs toolData for the viewport + * to the toolStateManager. If any toolData is found in the toolStateManager, it + * overwrites it. + * @param {renderingEngineUID, viewportUID} + * @returns {normal, point} viewPlaneNormal and center of viewport canvas in world space + */ + initializeViewport = ({ + renderingEngineUID, + viewportUID, + }: ViewportInput): { + normal: Point3 + point: Point3 + } => { + const renderingEngine = getRenderingEngine(renderingEngineUID) + const viewport = renderingEngine.getViewport(viewportUID) + const { element } = viewport + const enabledElement = getEnabledElement(element) + const { FrameOfReferenceUID, sceneUID } = enabledElement + const { position, focalPoint, viewPlaneNormal } = viewport.getCamera() + + // Check if there is already toolData for this viewport + let toolState = getToolState(enabledElement, this.name) + toolState = this.filterInteractableToolStateForElement(element, toolState) + + if (toolState.length) { + // If found, it will override it by removing the toolData and adding it later + removeToolStateByToolDataUID(element, toolState[0].metadata.toolDataUID) + } + + const toolData = { metadata: { - viewPlaneNormal: <Point3>[0, 0, 0], - viewUp: <Point3>[0, 0, 0], - toolDataUID: '1', - FrameOfReferenceUID: '1', - referencedImageId: '1', + cameraPosition: <Point3>[...position], + cameraFocalPoint: <Point3>[...focalPoint], + FrameOfReferenceUID, toolName: this.name, }, data: { handles: { rotationPoints: [], // rotation handles, used for rotation interactions slabThicknessPoints: [], // slab thickness handles, used for setting the slab thickness - activeOperation: null, // 0 translation, 1 rotation handles, 2 slab thickness handles - toolCenter: <Point3>[0, 0, 0], // Used in testings + toolCenter: this.toolCenter, }, active: false, + // Todo: add enum for active Operations + activeOperation: null, // 0 translation, 1 rotation handles, 2 slab thickness handles activeViewportUIDs: [], // a list of the viewport uids connected to the reference lines being translated - viewportUID: '1', - sceneUID: '1', + viewportUID, + sceneUID, }, } - return toolSpecificToolData + resetElementCursor(element) + + addToolState(element, toolData) + + return { + normal: viewPlaneNormal, + point: viewport.canvasToWorld([ + viewport.sWidth / 2, + viewport.sHeight / 2, + ]), + } } - cancel = () => { - console.log('Not implemented yet') + /** + * When activated, it initializes the crosshairs. It begins by computing + * the intersection of viewports associated with the crosshairs instance. + * When all three views are accessible, the intersection (e.g., crosshairs tool centre) + * will be an exact point in space; however, with two viewports, because the + * intersection of two planes is a line, it assumes the last view is between the centre + * of the two rendering viewports. + * @param viewports Array of viewportInputs which each item containing {viewportUID, renderingEngineUID} + */ + init = (viewports: ViewportInputs): void => { + if (!viewports.length || viewports.length === 1) { + throw new Error( + 'For crosshairs to operate, at least two viewports must be given.' + ) + } + + // Todo: handle two same view viewport, or more than 3 viewports + const [firstViewport, secondViewport, thirdViewport] = viewports + + // Initialize first viewport + const { normal: normal1, point: point1 } = + this.initializeViewport(firstViewport) + + // Initialize second viewport + const { normal: normal2, point: point2 } = + this.initializeViewport(secondViewport) + + let normal3 = <Point3>[0, 0, 0] + let point3 = vec3.create() + + // If there are three viewports + if (thirdViewport) { + ;({ normal: normal3, point: point3 } = + this.initializeViewport(thirdViewport)) + } else { + // If there are only two views (viewport) associated with the crosshairs: + // In this situation, we don't have a third information to find the + // exact intersection, and we "assume" the third view is looking at + // a location in between the first and second view centers + vec3.add(point3, point1, point2) + vec3.scale(point3, point3, 0.5) + vec3.cross(normal3, normal1, normal2) + } + + // Planes of each viewport + const firstPlane = csUtils.planar.planeEquation(normal1, point1) + const secondPlane = csUtils.planar.planeEquation(normal2, point2) + const thirdPlane = csUtils.planar.planeEquation(normal3, point3) + + // Calculating the intersection of 3 planes + // prettier-ignore + this.toolCenter = csUtils.planar.threePlaneIntersection(firstPlane, secondPlane, thirdPlane) } - getHandleNearImagePoint = (element, toolData, canvasCoords, proximity) => { - // We need a better way of surfacing this... - const { - viewportUid: viewportUID, - sceneUid: sceneUID, - renderingEngineUid: renderingEngineUID, - } = element.dataset - const toolGroups = ToolGroupManager.getToolGroups( - renderingEngineUID, - sceneUID, - viewportUID + /** + * For Crosshairs it handle the click event. + * @param evt + * @param interactionType + * @returns + */ + addNewMeasurement( + evt: CustomEvent, + interactionType: string + ): CrosshairsSpecificToolData { + const eventData = evt.detail + const { element } = eventData + + const { currentPoints } = eventData + const jumpWorld = currentPoints.world + + const enabledElement = getEnabledElement(element) + const { viewport } = enabledElement + this._jump(enabledElement, jumpWorld) + + const toolState = getToolState(enabledElement, this.name) + const filteredToolState = this.filterInteractableToolStateForElement( + viewport.element, + toolState ) - const groupTools = toolGroups[0]?.toolOptions - const mode = groupTools[this.name]?.mode - // We don't want this annotation tool to render or be interactive unless its - // active - if (mode !== 'Active') { - return undefined + // viewport ToolData + const { data } = filteredToolState[0] + + const { rotationPoints } = data.handles + const viewportUIDArray = [] + // put all the draggable reference lines in the viewportUIDArray + for (let i = 0; i < rotationPoints.length - 1; ++i) { + const otherViewport = rotationPoints[i][1] + const viewportControllable = this._getReferenceLineControllable( + otherViewport.uid + ) + const viewportDraggableRotatable = + this._getReferenceLineDraggableRotatable(otherViewport.uid) + if (!viewportControllable || !viewportDraggableRotatable) { + continue + } + viewportUIDArray.push(otherViewport.uid) + // rotation handles are two per viewport + i++ } + data.activeViewportUIDs = [...viewportUIDArray] + // set translation operation + data.handles.activeOperation = OPERATION.DRAG + + evt.preventDefault() + + hideElementCursor(element) + + this._activateModify(element) + return filteredToolState[0] + } + + cancel = () => { + console.log('Not implemented yet') + } + + getHandleNearImagePoint = (element, toolData, canvasCoords, proximity) => { const enabledElement = getEnabledElement(element) const { viewport } = enabledElement @@ -247,74 +385,7 @@ export default class CrosshairsTool extends BaseAnnotationTool { proximity, interactionType ) => { - // We need a better way of surfacing this... - const { - viewportUid: viewportUID, - sceneUid: sceneUID, - renderingEngineUid: renderingEngineUID, - } = element.dataset - const toolGroups = ToolGroupManager.getToolGroups( - renderingEngineUID, - sceneUID, - viewportUID - ) - const groupTools = toolGroups[0]?.toolOptions - const mode = groupTools[this.name]?.mode - - // We don't want this annotation tool to render or be interactive unless its - // active - if (mode !== 'Active') { - return false - } - - // This iterates all instances of Crosshairs across all toolGroups - // And updates `isCrosshairsActive` if ANY are active? - let isCrosshairsActive = false - for (let i = 0; i < toolGroups.length; ++i) { - const toolGroup = toolGroups[i] - const tool = toolGroup.toolOptions['Crosshairs'] - - if (tool.mode === 'Active') { - isCrosshairsActive = true - break - } - } - - const { data } = toolData if (this._pointNearTool(element, toolData, canvasCoords, 6)) { - return true - } else if (data.activeViewportUIDs.length === 0) { - const enabledElement = getEnabledElement(element) - const { viewport } = enabledElement - const jumpWorld = viewport.canvasToWorld(canvasCoords) - - this._jump(enabledElement, jumpWorld) - - const { rotationPoints } = data.handles - const viewportUIDArray = [] - // put all the draggable reference lines in the viewportUIDArray - for (let i = 0; i < rotationPoints.length - 1; ++i) { - const otherViewport = rotationPoints[i][1] - const viewportControllable = this._getReferenceLineControllable( - otherViewport.uid - ) - const viewportDraggableRotatable = - this._getReferenceLineDraggableRotatable(otherViewport.uid) - - if (!viewportControllable || !viewportDraggableRotatable) { - continue - } - - viewportUIDArray.push(otherViewport.uid) - - // rotation handles are two for viewport - i++ - } - - data.activeViewportUIDs = [...viewportUIDArray] - // set translation operation - data.handles.activeOperation = OPERATION.DRAG - return true } @@ -336,80 +407,11 @@ export default class CrosshairsTool extends BaseAnnotationTool { evt.preventDefault() } - _isCrosshairsActive({ renderingEngineUID, sceneUID, viewportUID }) { - const toolGroups = ToolGroupManager.getToolGroups( - renderingEngineUID, - sceneUID, - viewportUID - ) - - // This iterates all instances of Crosshairs across all toolGroups - // And updates `isCrosshairsActive` if ANY are active? - let isCrosshairsActive = false - for (let i = 0; i < toolGroups.length; ++i) { - const toolGroup = toolGroups[i] - const tool = toolGroup.toolOptions['Crosshairs'] - - if (tool.mode === 'Active') { - isCrosshairsActive = true - break - } - } - - // So if none are active, we have nothing to render, and we peace out - return isCrosshairsActive - } - - _initCrosshairs = (evt, toolState) => { - const eventData = evt.detail - const { element } = eventData - const enabledElement = getEnabledElement(element) - const { viewport, FrameOfReferenceUID, viewportUID, sceneUID } = - enabledElement - const { sHeight, sWidth, canvasToWorld } = viewport - const centerCanvas: Point2 = [sWidth * 0.5, sHeight * 0.5] - - // Calculate the crosshair center - // NOTE: it is assumed that all the active/linked viewports share the same crosshair center. - // This because the rotation operations rotates also all the other active/intersecting reference lines of the same angle - this.toolCenter = canvasToWorld(centerCanvas) - - const camera = viewport.getCamera() - const { position, focalPoint } = camera - - const toolData = { - metadata: { - cameraPosition: <Point3>[...position], - cameraFocalPoint: <Point3>[...focalPoint], - FrameOfReferenceUID, - toolName: this.name, - }, - data: { - handles: { - rotationPoints: [], // rotation handles, used for rotation interactions - slabThicknessPoints: [], // slab thickness handles, used for setting the slab thickness - toolCenter: this.toolCenter, - }, - active: false, - activeOperation: null, // 0 translation, 1 rotation handles, 2 slab thickness handles - activeViewportUIDs: [], // a list of the viewport uids connected to the reference lines being translated - viewportUID, - sceneUID, - }, - } - - // NOTE: rotation handles are initialized in renderTool when drawing. - - addToolState(element, toolData) - - resetElementCursor(element) - } - onCameraModified = (evt) => { const eventData = evt.detail const { element } = eventData const enabledElement = getEnabledElement(element) - const { FrameOfReferenceUID, renderingEngine, viewport } = enabledElement + const { renderingEngine, viewport } = enabledElement const requireSameOrientation = false const viewportUIDsToRender = getViewportUIDsWithToolToRender( @@ -418,23 +420,12 @@ export default class CrosshairsTool extends BaseAnnotationTool { requireSameOrientation ) - let toolState = getToolState(enabledElement, this.name) - let filteredToolState = this.filterInteractableToolStateForElement( + const toolState = getToolState(enabledElement, this.name) + const filteredToolState = this.filterInteractableToolStateForElement( element, toolState ) - if (!filteredToolState.length && FrameOfReferenceUID) { - this._initCrosshairs(evt, toolState) - - toolState = getToolState(enabledElement, this.name) - - filteredToolState = this.filterInteractableToolStateForElement( - element, - toolState - ) - } - // viewport ToolData const viewportToolData = filteredToolState[0] as CrosshairsSpecificToolData @@ -442,12 +433,6 @@ export default class CrosshairsTool extends BaseAnnotationTool { return } - // if ( - // !this._isCrosshairsActive({ renderingEngineUID, sceneUID, viewportUID }) - // ) { - // return - // } - // -- Update the camera of other linked viewports in the same scene that // have the same camera in case of translation // -- Update the crosshair center in world coordinates in toolData. @@ -507,34 +492,34 @@ export default class CrosshairsTool extends BaseAnnotationTool { ) { // update linked view in the same scene that have the same camera // this goes here, because the parent viewport translation may happen in another tool - const otherLinkedViewportsToolDataWithSameCameraDirection = - this._filterLinkedViewportWithSameOrientationAndScene( - enabledElement, - toolState - ) - - for ( - let i = 0; - i < otherLinkedViewportsToolDataWithSameCameraDirection.length; - ++i - ) { - const toolData = - otherLinkedViewportsToolDataWithSameCameraDirection[i] - const { data } = toolData - const scene = renderingEngine.getScene(data.sceneUID) - const otherViewport = scene.getViewport(data.viewportUID) - const camera = otherViewport.getCamera() - - const newFocalPoint = [0, 0, 0] - const newPosition = [0, 0, 0] - - vtkMath.add(camera.focalPoint, deltaCameraPosition, newFocalPoint) - vtkMath.add(camera.position, deltaCameraPosition, newPosition) - - // updated cached "previous" camera position and focal point - toolData.metadata.cameraPosition = [...currentCamera.position] - toolData.metadata.cameraFocalPoint = [...currentCamera.focalPoint] - } + // const otherLinkedViewportsToolDataWithSameCameraDirection = + // this._filterLinkedViewportWithSameOrientationAndScene( + // enabledElement, + // toolState + // ) + + // for ( + // let i = 0; + // i < otherLinkedViewportsToolDataWithSameCameraDirection.length; + // ++i + // ) { + // const toolData = + // otherLinkedViewportsToolDataWithSameCameraDirection[i] + // const { data } = toolData + // const scene = renderingEngine.getScene(data.sceneUID) + // const otherViewport = scene.getViewport(data.viewportUID) + // const camera = otherViewport.getCamera() + + // const newFocalPoint = [0, 0, 0] + // const newPosition = [0, 0, 0] + + // vtkMath.add(camera.focalPoint, deltaCameraPosition, newFocalPoint) + // vtkMath.add(camera.position, deltaCameraPosition, newPosition) + + // // updated cached "previous" camera position and focal point + // toolData.metadata.cameraPosition = [...currentCamera.position] + // toolData.metadata.cameraFocalPoint = [...currentCamera.focalPoint] + // } // update center of the crosshair this.toolCenter[0] += deltaCameraPosition[0] @@ -628,17 +613,6 @@ export default class CrosshairsTool extends BaseAnnotationTool { } renderToolData(evt: CustomEvent, svgDrawingHelper: any): void { - const { renderingEngineUID, sceneUID, viewportUID } = evt.detail - - // This iterates all instances of Crosshairs across all toolGroups - // And updates `isCrosshairsActive` if ANY are active? - // So if none are active, we have nothing to render, and we peace out - if ( - !this._isCrosshairsActive({ renderingEngineUID, sceneUID, viewportUID }) - ) { - return - } - const eventData = evt.detail const { element } = eventData const toolState = getToolState(svgDrawingHelper.enabledElement, this.name) @@ -724,6 +698,7 @@ export default class CrosshairsTool extends BaseAnnotationTool { const { focalPoint } = camera const focalPointCanvas = viewport.worldToCanvas(focalPoint) + // Todo: focalpointCanvas is a lot, how is it doing arithmetics on it?? const canvasBox = [ focalPointCanvas - sWidth * 0.5, focalPointCanvas + sWidth * 0.5, @@ -743,7 +718,37 @@ export default class CrosshairsTool extends BaseAnnotationTool { ) vec2.normalize(canvasUnitVectorFromCenter, canvasUnitVectorFromCenter) + // Graphic: + // Mid -> SlabThickness handle + // Short -> Rotation handle + // Long + // | + // | + // | + // Mid + // | + // | + // | + // Short + // | + // | + // | + // Long --- Mid--- Short--- Center --- Short --- Mid --- Long + // | + // | + // | + // Short + // | + // | + // | + // Mid + // | + // | + // | + // Long const canvasVectorFromCenterLong = vec2.create() + + // Todo: configuration should provide constants below (100, 0.25, 0.15, 0.04) vec2.scale( canvasVectorFromCenterLong, canvasUnitVectorFromCenter, @@ -765,10 +770,11 @@ export default class CrosshairsTool extends BaseAnnotationTool { vec2.scale( canvasVectorFromCenterStart, canvasUnitVectorFromCenter, - canvasDiagonalLength * 0.05 + // Don't put a gap if the the third view is missing + otherViewportToolData.length === 2 ? canvasDiagonalLength * 0.04 : 0 ) - // points for reference lines + // Computing Reference start and end (4 lines per viewport in case of 3 view MPR) const refLinePointOne = vec2.create() const refLinePointTwo = vec2.create() const refLinePointThree = vec2.create() @@ -792,10 +798,12 @@ export default class CrosshairsTool extends BaseAnnotationTool { canvasVectorFromCenterLong ) + // Clipping lines to be only included in a box (canvas), we don't want + // the lines goes beyond canvas liangBarksyClip(refLinePointOne, refLinePointTwo, canvasBox) liangBarksyClip(refLinePointThree, refLinePointFour, canvasBox) - // points for rotation handles + // Computing rotation handle positions const rotHandleOne = vec2.create() vec2.subtract( rotHandleOne, @@ -806,21 +814,24 @@ export default class CrosshairsTool extends BaseAnnotationTool { const rotHandleTwo = vec2.create() vec2.add(rotHandleTwo, crosshairCenterCanvas, canvasVectorFromCenterMid) - // get world information for lines and points (vertical world distances) - let stHanlesCenterCanvas = vec2.clone(crosshairCenterCanvas) + // Computing SlabThickness (st below) position + + // SlabThickness center in canvas + let stHandlesCenterCanvas = vec2.clone(crosshairCenterCanvas) if ( !otherViewportDraggableRotatable && otherViewportSlabThicknessControlsOn ) { - stHanlesCenterCanvas = vec2.clone(otherViewportCenterCanvas) + stHandlesCenterCanvas = vec2.clone(otherViewportCenterCanvas) } - let stHanlesCenterWorld = [...this.toolCenter] + // SlabThickness center in world + let stHandlesCenterWorld = [...this.toolCenter] if ( !otherViewportDraggableRotatable && otherViewportSlabThicknessControlsOn ) { - stHanlesCenterWorld = [...otherViewportCenterWorld] + stHandlesCenterWorld = [...otherViewportCenterWorld] } const worldUnitVectorFromCenter: Point3 = [0, 0, 0] @@ -848,7 +859,7 @@ export default class CrosshairsTool extends BaseAnnotationTool { const worldVerticalRefPoint: Point3 = [0, 0, 0] vtkMath.add( - stHanlesCenterWorld, + stHandlesCenterWorld, worldOrthoVectorFromCenter, worldVerticalRefPoint ) @@ -862,20 +873,24 @@ export default class CrosshairsTool extends BaseAnnotationTool { const canvasOrthoVectorFromCenter = vec2.create() vec2.subtract( canvasOrthoVectorFromCenter, - stHanlesCenterCanvas, + stHandlesCenterCanvas, canvasVerticalRefPoint ) const stLinePointOne = vec2.create() vec2.subtract( stLinePointOne, - stHanlesCenterCanvas, + stHandlesCenterCanvas, canvasVectorFromCenterLong ) vec2.add(stLinePointOne, stLinePointOne, canvasOrthoVectorFromCenter) const stLinePointTwo = vec2.create() - vec2.add(stLinePointTwo, stHanlesCenterCanvas, canvasVectorFromCenterLong) + vec2.add( + stLinePointTwo, + stHandlesCenterCanvas, + canvasVectorFromCenterLong + ) vec2.add(stLinePointTwo, stLinePointTwo, canvasOrthoVectorFromCenter) liangBarksyClip(stLinePointOne, stLinePointTwo, canvasBox) @@ -883,7 +898,7 @@ export default class CrosshairsTool extends BaseAnnotationTool { const stLinePointThree = vec2.create() vec2.add( stLinePointThree, - stHanlesCenterCanvas, + stHandlesCenterCanvas, canvasVectorFromCenterLong ) vec2.subtract( @@ -895,7 +910,7 @@ export default class CrosshairsTool extends BaseAnnotationTool { const stLinePointFour = vec2.create() vec2.subtract( stLinePointFour, - stHanlesCenterCanvas, + stHandlesCenterCanvas, canvasVectorFromCenterLong ) vec2.subtract( @@ -914,19 +929,19 @@ export default class CrosshairsTool extends BaseAnnotationTool { vec2.subtract( stHandleOne, - stHanlesCenterCanvas, + stHandlesCenterCanvas, canvasVectorFromCenterShort ) vec2.add(stHandleOne, stHandleOne, canvasOrthoVectorFromCenter) - vec2.add(stHandleTwo, stHanlesCenterCanvas, canvasVectorFromCenterShort) + vec2.add(stHandleTwo, stHandlesCenterCanvas, canvasVectorFromCenterShort) vec2.add(stHandleTwo, stHandleTwo, canvasOrthoVectorFromCenter) vec2.subtract( stHandleThree, - stHanlesCenterCanvas, + stHandlesCenterCanvas, canvasVectorFromCenterShort ) vec2.subtract(stHandleThree, stHandleThree, canvasOrthoVectorFromCenter) - vec2.add(stHandleFour, stHanlesCenterCanvas, canvasVectorFromCenterShort) + vec2.add(stHandleFour, stHandlesCenterCanvas, canvasVectorFromCenterShort) vec2.subtract(stHandleFour, stHandleFour, canvasOrthoVectorFromCenter) referenceLines.push([ @@ -1794,6 +1809,12 @@ export default class CrosshairsTool extends BaseAnnotationTool { angle *= -1 } + // Rounding the angle to allow rotated handles to be undone + // If we don't round and rotate handles clockwise by 0.0131233 radians, + // there's no assurance that the counter-clockwise rotation occurs at + // precisely -0.0131233, resulting in the drawn annotations being lost. + angle = Math.round(angle * 100) / 100 + const rotationAxis = viewport.getCamera().viewPlaneNormal // @ts-ignore : vtkjs incorrect typing const { matrix } = vtkMatrixBuilder @@ -1950,8 +1971,8 @@ export default class CrosshairsTool extends BaseAnnotationTool { otherViewport.setSlabThickness(null) } else { otherViewport.setSlabThickness(slabThicknessValue) - otherViewport.render() } + otherViewport.render() } } ) diff --git a/packages/cornerstone-tools/test/CrosshairsTool_test.js b/packages/cornerstone-tools/test/CrosshairsTool_test.js index b44fd28572..2c0077c686 100644 --- a/packages/cornerstone-tools/test/CrosshairsTool_test.js +++ b/packages/cornerstone-tools/test/CrosshairsTool_test.js @@ -109,9 +109,6 @@ describe('Cornerstone Tools: ', () => { this.testToolGroup.addTool('Crosshairs', { configuration: {}, }) - this.testToolGroup.setToolActive('Crosshairs', { - bindings: [{ mouseButton: 1 }], - }) this.renderingEngine = new RenderingEngine(renderingEngineUID) registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader) @@ -206,6 +203,10 @@ describe('Cornerstone Tools: ', () => { CornerstoneTools3DEvents.ANNOTATION_RENDERED, crosshairsEventHandler ) + + this.testToolGroup.setToolActive('Crosshairs', { + bindings: [{ mouseButton: 1 }], + }) } element1.addEventListener(EVENTS.IMAGE_RENDERED, renderEventHandler) @@ -299,6 +300,10 @@ describe('Cornerstone Tools: ', () => { return } + this.testToolGroup.setToolActive('Crosshairs', { + bindings: [{ mouseButton: 1 }], + }) + const vp1 = this.renderingEngine.getViewport(viewportUID1) const { imageData } = vp1.getImageData() @@ -395,6 +400,10 @@ describe('Cornerstone Tools: ', () => { return } + this.testToolGroup.setToolActive('Crosshairs', { + bindings: [{ mouseButton: 1 }], + }) + const vp1 = this.renderingEngine.getViewport(viewportUID1) const { imageData } = vp1.getImageData() diff --git a/packages/demo/src/App.tsx b/packages/demo/src/App.tsx index 91971b968c..c83ae381db 100644 --- a/packages/demo/src/App.tsx +++ b/packages/demo/src/App.tsx @@ -24,6 +24,7 @@ import { resetCPURenderingOnlyForDebugOrTests } from '@ohif/cornerstone-render' import SegmentationRender from './ExampleSegmentationRender' import RenderToCanvasExample from './ExampleRenderToCanvas' +import CrosshairsExample from './ExampleCrosshairs' function LinkOut({ href, text }) { return ( @@ -91,6 +92,11 @@ function Index() { url: '/segmentationRender', text: 'Example for demonstrating the rendering of Segmentation', }, + { + title: 'Crosshairs', + url: '/crosshairs', + text: 'Example for Crosshairs', + }, { title: 'Canvas Resize', url: '/canvasResize', @@ -279,6 +285,11 @@ function AppRouter() { children: <VTKSetVolumesExample />, }) + const crosshairs = () => + Example({ + children: <CrosshairsExample />, + }) + const cacheDecache = () => Example({ children: <CacheDecacheExample />, @@ -359,6 +370,7 @@ function AppRouter() { <Route exact path="/testUtilsVolume/" render={TestVolume} /> <Route exact path="/calibratedImages/" render={calibratedImages} /> <Route exact path="/segmentationRender/" render={segmentationRender} /> + <Route exact path="/crosshairs/" render={crosshairs} /> <Route exact path="/toolDisplayConfiguration/" diff --git a/packages/demo/src/ExampleCrosshairs.tsx b/packages/demo/src/ExampleCrosshairs.tsx new file mode 100644 index 0000000000..7aa88de4ed --- /dev/null +++ b/packages/demo/src/ExampleCrosshairs.tsx @@ -0,0 +1,407 @@ +import React, { Component } from 'react' +import { + cache, + RenderingEngine, + createAndCacheVolume, + ORIENTATION, + VIEWPORT_TYPE, +} from '@ohif/cornerstone-render' +import { ToolBindings, ToolModes } from '@ohif/cornerstone-tools' +import * as csTools3d from '@ohif/cornerstone-tools' + +import vtkConstants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants' + +import { + setCTWWWC, + setPetTransferFunction, +} from './helpers/transferFunctionHelpers' + +import getImageIds from './helpers/getImageIds' +import ViewportGrid from './components/ViewportGrid' +import { initToolGroups, addToolsToToolGroups } from './initToolGroups' +import './ExampleVTKMPR.css' +import { + renderingEngineUID, + ctVolumeUID, + ptVolumeUID, + SCENE_IDS, + VIEWPORT_IDS, + ANNOTATION_TOOLS, + prostateVolumeUID, +} from './constants' + +const VOLUME = 'volume' + +window.cache = cache + +let ctSceneToolGroup, prostateSceneToolGroup + +const { BlendMode } = vtkConstants + +const toolsToUse = ['WindowLevel', 'Pan', 'Zoom', ...ANNOTATION_TOOLS] + +class CrosshairsExample extends Component { + state = { + progressText: 'fetching metadata...', + metadataLoaded: false, + petColorMapIndex: 0, + layoutIndex: 0, + destroyed: false, + // + viewportGrid: { + numCols: 3, + numRows: 2, + viewports: [{}, {}, {}, {}, {}], + }, + leftClickTool: 'WindowLevel', + toolGroupName: 'FirstRow', + toolGroups: {}, + ctWindowLevelDisplay: { ww: 0, wc: 0 }, + ptThresholdDisplay: 5, + } + + constructor(props) { + super(props) + + csTools3d.init() + this._elementNodes = new Map() + this._offScreenRef = React.createRef() + + this._viewportGridRef = React.createRef() + + this.ctImageIds = getImageIds('ct1', VOLUME) + this.prostateImageIds = getImageIds('prostateX', VOLUME) + + Promise.all([this.ctImageIds, this.prostateImageIds]).then(() => + this.setState({ progressText: 'Loading data...' }) + ) + + this.viewportGridResizeObserver = new ResizeObserver((entries) => { + // ThrottleFn? May not be needed. This is lightning fast. + // Set in mount + if (this.renderingEngine) { + this.renderingEngine.resize() + this.renderingEngine.render() + } + }) + } + + /** + * LIFECYCLE + */ + async componentDidMount() { + ;({ ctSceneToolGroup, prostateSceneToolGroup } = initToolGroups()) + + const ctImageIds = await this.ctImageIds + const prostateImageIds = await this.prostateImageIds + + const renderingEngine = new RenderingEngine(renderingEngineUID) + + this.renderingEngine = renderingEngine + window.renderingEngine = renderingEngine + + const viewportInput = [ + // CT volume axial + { + sceneUID: SCENE_IDS.CT, + viewportUID: VIEWPORT_IDS.CT.AXIAL, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + element: this._elementNodes.get(0), + defaultOptions: { + orientation: ORIENTATION.AXIAL, + background: [0, 0, 0], + }, + }, + { + sceneUID: SCENE_IDS.CT, + viewportUID: VIEWPORT_IDS.CT.SAGITTAL, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + element: this._elementNodes.get(1), + defaultOptions: { + orientation: ORIENTATION.SAGITTAL, + background: [0, 0, 0], + }, + }, + { + sceneUID: SCENE_IDS.CT, + viewportUID: VIEWPORT_IDS.CT.CORONAL, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + element: this._elementNodes.get(2), + defaultOptions: { + orientation: ORIENTATION.CORONAL, + background: [0, 0, 0], + }, + }, + { + sceneUID: SCENE_IDS.PROSTATE, + viewportUID: VIEWPORT_IDS.PROSTATE.AXIAL, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + element: this._elementNodes.get(3), + defaultOptions: { + orientation: ORIENTATION.AXIAL, + background: [0, 0, 0], + }, + }, + { + sceneUID: SCENE_IDS.PROSTATE, + viewportUID: VIEWPORT_IDS.PROSTATE.SAGITTAL, + type: VIEWPORT_TYPE.ORTHOGRAPHIC, + element: this._elementNodes.get(4), + defaultOptions: { + orientation: ORIENTATION.SAGITTAL, + background: [0, 0, 0], + }, + }, + ] + + renderingEngine.setViewports(viewportInput) + + // volume ct + ctSceneToolGroup.addViewports( + renderingEngineUID, + SCENE_IDS.CT, + VIEWPORT_IDS.CT.AXIAL + ) + ctSceneToolGroup.addViewports( + renderingEngineUID, + SCENE_IDS.CT, + VIEWPORT_IDS.CT.SAGITTAL + ) + ctSceneToolGroup.addViewports( + renderingEngineUID, + SCENE_IDS.CT, + VIEWPORT_IDS.CT.CORONAL + ) + prostateSceneToolGroup.addViewports( + renderingEngineUID, + SCENE_IDS.PROSTATE, + VIEWPORT_IDS.PROSTATE.AXIAL + ) + prostateSceneToolGroup.addViewports( + renderingEngineUID, + SCENE_IDS.PROSTATE, + VIEWPORT_IDS.PROSTATE.SAGITTAL + ) + + addToolsToToolGroups({ + ctSceneToolGroup, + prostateSceneToolGroup, + }) + + window.ctSceneToolGroup = ctSceneToolGroup + this.setState({ + toolGroups: { + FirstRow: ctSceneToolGroup, + SecondRow: prostateSceneToolGroup, + }, + }) + + renderingEngine.render() + + // This only creates the volumes, it does not actually load all + // of the pixel data (yet) + const ctVolume = await createAndCacheVolume(ctVolumeUID, { + imageIds: ctImageIds, + }) + const prostateVolume = await createAndCacheVolume(prostateVolumeUID, { + imageIds: prostateImageIds, + }) + + ctVolume.load() + prostateVolume.load() + + const ctScene = renderingEngine.getScene(SCENE_IDS.CT) + const prostateScene = renderingEngine.getScene(SCENE_IDS.PROSTATE) + await ctScene.setVolumes([ + { + volumeUID: ctVolumeUID, + callback: setCTWWWC, + blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND, + }, + ]) + await prostateScene.setVolumes([ + { + volumeUID: prostateVolumeUID, + blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND, + }, + ]) + + // This will initialise volumes in GPU memory + renderingEngine.render() + + // Start listening for resize + this.viewportGridResizeObserver.observe(this._viewportGridRef.current) + } + + componentDidUpdate(prevProps, prevState) { + const { layoutIndex } = this.state + const { renderingEngine } = this + const onLoad = () => this.setState({ progressText: 'Loaded.' }) + } + + componentWillUnmount() { + // Stop listening for resize + if (this.viewportGridResizeObserver) { + this.viewportGridResizeObserver.disconnect() + } + + cache.purgeCache() + csTools3d.destroy() + + this.renderingEngine.destroy() + } + + setToolMode = (toolMode) => { + const toolGroup = this.state.toolGroups[this.state.toolGroupName] + if (toolMode === ToolModes.Active) { + const activeTool = toolGroup.getActivePrimaryButtonTools() + if (activeTool) { + toolGroup.setToolPassive(activeTool) + } + + toolGroup.setToolActive(this.state.leftClickTool, { + bindings: [{ mouseButton: ToolBindings.Mouse.Primary }], + }) + } else if (toolMode === ToolModes.Passive) { + toolGroup.setToolPassive(this.state.leftClickTool) + } else if (toolMode === ToolModes.Enabled) { + toolGroup.setToolEnabled(this.state.leftClickTool) + } else if (toolMode === ToolModes.Disabled) { + toolGroup.setToolDisabled(this.state.leftClickTool) + } + } + + showOffScreenCanvas = () => { + // remove all children + this._offScreenRef.current.innerHTML = '' + const uri = this.renderingEngine._debugRender() + const image = document.createElement('img') + image.src = uri + image.setAttribute('width', '100%') + + this._offScreenRef.current.appendChild(image) + } + + hideOffScreenCanvas = () => { + // remove all children + this._offScreenRef.current.innerHTML = '' + } + + render() { + return ( + <div style={{ paddingBottom: '55px' }}> + <div className="row"> + <div className="col-xs-12" style={{ margin: '8px 0' }}> + <h2>Crosshairs example ({this.state.progressText})</h2> + <p> + This demo demonstrates the use of crosshairs on two studies that + don't share the same frameOfReference. + </p> + <p> + Top row: CT scene from patient 1 and Bottom row: PET scene from + patient2 + </p> + </div> + <div + className="col-xs-12" + style={{ margin: '8px 0', marginLeft: '-4px' }} + > + {/* Hide until we update react in a better way {fusionWLDisplay} */} + </div> + </div> + <span>Set this tool </span> + <select + value={this.state.leftClickTool} + onChange={(evt) => { + this.setState({ leftClickTool: evt.target.value }) + }} + > + {toolsToUse.map((toolName) => ( + <option key={toolName} value={toolName}> + {toolName} + </option> + ))} + </select> + <span style={{ marginLeft: '4px' }}>for this toolGroup </span> + <select + value={this.state.toolGroupName} + onChange={(evt) => { + this.setState({ toolGroupName: evt.target.value }) + }} + > + {Object.keys(this.state.toolGroups).map((toolGroupName) => ( + <option key={toolGroupName} value={toolGroupName}> + {toolGroupName} + </option> + ))} + </select> + <button + style={{ marginLeft: '4px' }} + onClick={() => this.setToolMode(ToolModes.Active)} + > + Active + </button> + <button + style={{ marginLeft: '4px' }} + onClick={() => this.setToolMode(ToolModes.Passive)} + > + Passive + </button> + <button + style={{ marginLeft: '4px' }} + onClick={() => this.setToolMode(ToolModes.Enabled)} + > + Enabled + </button> + <button + style={{ marginLeft: '4px' }} + onClick={() => this.setToolMode(ToolModes.Disabled)} + > + Disabled + </button> + + <ViewportGrid + numCols={this.state.viewportGrid.numCols} + numRows={this.state.viewportGrid.numRows} + renderingEngine={this.renderingEngine} + style={{ minHeight: '650px', marginTop: '35px' }} + ref={this._viewportGridRef} + > + {this.state.viewportGrid.viewports.map((vp, i) => ( + <div + style={{ + width: '100%', + height: '100%', + border: '2px solid grey', + background: 'black', + }} + ref={(c) => this._elementNodes.set(i, c)} + onContextMenu={(e) => e.preventDefault()} + key={i} + /> + ))} + </ViewportGrid> + <div> + <h1>OffScreen Canvas Render</h1> + <button + onClick={this.showOffScreenCanvas} + className="btn btn-primary" + style={{ margin: '2px 4px' }} + > + Show OffScreenCanvas + </button> + <button + onClick={this.hideOffScreenCanvas} + className="btn btn-primary" + style={{ margin: '2px 4px' }} + > + Hide OffScreenCanvas + </button> + <div ref={this._offScreenRef}></div> + </div> + </div> + ) + } +} + +export default CrosshairsExample diff --git a/packages/demo/src/ExampleEnableDisableAPI.tsx b/packages/demo/src/ExampleEnableDisableAPI.tsx index adadd92278..c06e6f5e17 100644 --- a/packages/demo/src/ExampleEnableDisableAPI.tsx +++ b/packages/demo/src/ExampleEnableDisableAPI.tsx @@ -445,7 +445,7 @@ class EnableDisableViewportExample extends Component { this._offScreenRef.current.appendChild(image) } - hidOffScreenCanvas = () => { + hideOffScreenCanvas = () => { // remove all children this._offScreenRef.current.innerHTML = '' } @@ -552,7 +552,7 @@ class EnableDisableViewportExample extends Component { Show OffScreenCanvas </button> <button - onClick={this.hidOffScreenCanvas} + onClick={this.hideOffScreenCanvas} className="btn btn-primary" style={{ margin: '2px 4px' }} > diff --git a/packages/demo/src/ExampleFlipViewport.tsx b/packages/demo/src/ExampleFlipViewport.tsx index 1f45d33919..784e28bb4f 100644 --- a/packages/demo/src/ExampleFlipViewport.tsx +++ b/packages/demo/src/ExampleFlipViewport.tsx @@ -40,10 +40,7 @@ const { BlendMode } = vtkConstants window.cache = cache let ctSceneToolGroup, - stackCTViewportToolGroup, - stackPTViewportToolGroup, - stackDXViewportToolGroup, - ptSceneToolGroup + stackCTViewportToolGroup const toolsToUse = ANNOTATION_TOOLS const ctLayoutTools = ['Levels'].concat(toolsToUse) @@ -112,9 +109,6 @@ class FlipViewportExample extends Component { ;({ ctSceneToolGroup, stackCTViewportToolGroup, - stackPTViewportToolGroup, - stackDXViewportToolGroup, - ptSceneToolGroup, } = initToolGroups({ configuration: { preventHandleOutsideImage: true }, })) @@ -196,9 +190,6 @@ class FlipViewportExample extends Component { addToolsToToolGroups({ ctSceneToolGroup, stackCTViewportToolGroup, - stackPTViewportToolGroup, - stackDXViewportToolGroup, - ptSceneToolGroup, }) this.axialSync.add({ @@ -303,7 +294,7 @@ class FlipViewportExample extends Component { this._offScreenRef.current.appendChild(image) } - hidOffScreenCanvas = () => { + hideOffScreenCanvas = () => { // remove children this._offScreenRef.current.innerHTML = '' } @@ -452,7 +443,7 @@ class FlipViewportExample extends Component { Show OffScreenCanvas </button> <button - onClick={this.hidOffScreenCanvas} + onClick={this.hideOffScreenCanvas} className="btn btn-primary" style={{ margin: '2px 4px' }} > diff --git a/packages/demo/src/ExampleModifierKeys.tsx b/packages/demo/src/ExampleModifierKeys.tsx index 5bf2869e85..797f45e294 100644 --- a/packages/demo/src/ExampleModifierKeys.tsx +++ b/packages/demo/src/ExampleModifierKeys.tsx @@ -289,6 +289,11 @@ class ModifierKeysExample extends Component { this._offScreenRef.current.appendChild(image) } + hideOffScreenCanvas = () => { + // remove all children + this._offScreenRef.current.innerHTML = '' + } + rotateViewport = (rotateDeg) => { // remove all children const vp = this.renderingEngine.getViewport(VIEWPORT_IDS.STACK.CT) @@ -452,7 +457,7 @@ class ModifierKeysExample extends Component { Show OffScreenCanvas </button> <button - onClick={this.hidOffScreenCanvas} + onClick={this.hideOffScreenCanvas} className="btn btn-primary" style={{ margin: '2px 4px' }} > diff --git a/packages/demo/src/ExampleNineStackViewport.tsx b/packages/demo/src/ExampleNineStackViewport.tsx index 2923eda1e3..cbfe12218d 100644 --- a/packages/demo/src/ExampleNineStackViewport.tsx +++ b/packages/demo/src/ExampleNineStackViewport.tsx @@ -222,7 +222,7 @@ class NineStackViewportExample extends Component { this._offScreenRef.current.appendChild(image) } - hidOffScreenCanvas = () => { + hideOffScreenCanvas = () => { // remove all children this._offScreenRef.current.innerHTML = '' } @@ -271,7 +271,7 @@ class NineStackViewportExample extends Component { Show OffScreenCanvas </button> <button - onClick={this.hidOffScreenCanvas} + onClick={this.hideOffScreenCanvas} className="btn btn-primary" style={{ margin: '2px 4px' }} > diff --git a/packages/demo/src/ExampleOneStack.tsx b/packages/demo/src/ExampleOneStack.tsx index 6c6f944808..bd11ce190c 100644 --- a/packages/demo/src/ExampleOneStack.tsx +++ b/packages/demo/src/ExampleOneStack.tsx @@ -251,6 +251,11 @@ class OneStackExample extends Component { this._offScreenRef.current.appendChild(image) } + hideOffScreenCanvas = () => { + // remove all children + this._offScreenRef.current.innerHTML = '' + } + rotateViewport = (rotateDeg) => { // remove all children const vp = this.renderingEngine.getViewport(VIEWPORT_IDS.STACK.CT) @@ -399,7 +404,7 @@ class OneStackExample extends Component { Show OffScreenCanvas </button> <button - onClick={this.hidOffScreenCanvas} + onClick={this.hideOffScreenCanvas} className="btn btn-primary" style={{ margin: '2px 4px' }} > diff --git a/packages/demo/src/ExampleOneVolume.tsx b/packages/demo/src/ExampleOneVolume.tsx index 2b66dddedc..3958aff38c 100644 --- a/packages/demo/src/ExampleOneVolume.tsx +++ b/packages/demo/src/ExampleOneVolume.tsx @@ -278,6 +278,11 @@ class OneVolumeExample extends Component { this._offScreenRef.current.appendChild(image) } + hideOffScreenCanvas = () => { + // remove all children + this._offScreenRef.current.innerHTML = '' + } + render() { return ( <div style={{ paddingBottom: '55px' }}> @@ -337,7 +342,7 @@ class OneVolumeExample extends Component { Show OffScreenCanvas </button> <button - onClick={this.hidOffScreenCanvas} + onClick={this.hideOffScreenCanvas} className="btn btn-primary" style={{ margin: '2px 4px' }} > diff --git a/packages/demo/src/ExampleStackViewport.tsx b/packages/demo/src/ExampleStackViewport.tsx index 3dc40d67c6..1de879ca48 100644 --- a/packages/demo/src/ExampleStackViewport.tsx +++ b/packages/demo/src/ExampleStackViewport.tsx @@ -480,7 +480,7 @@ class StackViewportExample extends Component { this._offScreenRef.current.appendChild(image) } - hidOffScreenCanvas = () => { + hideOffScreenCanvas = () => { // remove all children this._offScreenRef.current.innerHTML = '' } @@ -780,7 +780,7 @@ class StackViewportExample extends Component { Show OffScreenCanvas </button> <button - onClick={this.hidOffScreenCanvas} + onClick={this.hideOffScreenCanvas} className="btn btn-primary" style={{ margin: '2px 4px' }} > diff --git a/packages/demo/src/ExampleTestUtils.tsx b/packages/demo/src/ExampleTestUtils.tsx index ac29f942f8..f7a4045dd2 100644 --- a/packages/demo/src/ExampleTestUtils.tsx +++ b/packages/demo/src/ExampleTestUtils.tsx @@ -194,6 +194,11 @@ class testUtil extends Component { this._offScreenRef.current.appendChild(image) } + hideOffScreenCanvas = () => { + // remove all children + this._offScreenRef.current.innerHTML = '' + } + rotateViewport = (rotateDeg) => { // remove all childs const vp = this.renderingEngine.getViewport(VIEWPORT_IDS.STACK.CT) @@ -285,7 +290,7 @@ class testUtil extends Component { Show OffScreenCanvas </button> <button - onClick={this.hidOffScreenCanvas} + onClick={this.hideOffScreenCanvas} className="btn btn-primary" style={{ margin: '2px 4px' }} > diff --git a/packages/demo/src/ExampleTestUtilsVolume.tsx b/packages/demo/src/ExampleTestUtilsVolume.tsx index 3ae1586b4e..c11159e3a8 100644 --- a/packages/demo/src/ExampleTestUtilsVolume.tsx +++ b/packages/demo/src/ExampleTestUtilsVolume.tsx @@ -263,6 +263,11 @@ class testUtilVolume extends Component { this._offScreenRef.current.appendChild(image) } + hideOffScreenCanvas = () => { + // remove all children + this._offScreenRef.current.innerHTML = '' + } + rotateViewport = (rotateDeg) => { // remove all childs const vp = this.renderingEngine.getViewport(VIEWPORT_IDS.STACK.CT) @@ -327,7 +332,7 @@ class testUtilVolume extends Component { Show OffScreenCanvas </button> <button - onClick={this.hidOffScreenCanvas} + onClick={this.hideOffScreenCanvas} className="btn btn-primary" style={{ margin: '2px 4px' }} > diff --git a/packages/demo/src/ExampleToolDisplayConfiguration.tsx b/packages/demo/src/ExampleToolDisplayConfiguration.tsx index ecca9af17f..65386edded 100644 --- a/packages/demo/src/ExampleToolDisplayConfiguration.tsx +++ b/packages/demo/src/ExampleToolDisplayConfiguration.tsx @@ -328,7 +328,7 @@ class ToolDisplayConfigurationExample extends Component { this._offScreenRef.current.appendChild(image) } - hidOffScreenCanvas = () => { + hideOffScreenCanvas = () => { // remove all children this._offScreenRef.current.innerHTML = '' } @@ -422,7 +422,7 @@ class ToolDisplayConfigurationExample extends Component { Show OffScreenCanvas </button> <button - onClick={this.hidOffScreenCanvas} + onClick={this.hideOffScreenCanvas} className="btn btn-primary" style={{ margin: '2px 4px' }} > diff --git a/packages/demo/src/ExampleVTKMPR.tsx b/packages/demo/src/ExampleVTKMPR.tsx index 4e4a09d857..12da742342 100644 --- a/packages/demo/src/ExampleVTKMPR.tsx +++ b/packages/demo/src/ExampleVTKMPR.tsx @@ -134,9 +134,6 @@ class MPRExample extends Component { ptSceneToolGroup, fusionSceneToolGroup, ptMipSceneToolGroup, - ctVRSceneToolGroup, - ctObliqueToolGroup, - ptTypesSceneToolGroup, } = initToolGroups()) this.ctVolumeUID = ctVolumeUID @@ -171,8 +168,6 @@ class MPRExample extends Component { ptSceneToolGroup, fusionSceneToolGroup, ptMipSceneToolGroup, - ctVRSceneToolGroup, - ctObliqueToolGroup, ptTypesSceneToolGroup, }) @@ -402,7 +397,6 @@ class MPRExample extends Component { toolName ) - this.renderingEngine.render() this.setState({ ptCtLeftClickTool: toolName }) } @@ -490,7 +484,7 @@ class MPRExample extends Component { style={{ margin: '8px 0', marginLeft: '-4px' }} > {/* LAYOUT BUTTONS */} - {filteredLayoutButtons.map((layout) => ( + {/* {filteredLayoutButtons.map((layout) => ( <button key={layout.id} onClick={() => this.swapLayout(layout.id)} @@ -499,7 +493,7 @@ class MPRExample extends Component { > {layout.text} </button> - ))} + ))} */} {/* TOGGLES */} {fusionButtons} {/* Hide until we update react in a better way {fusionWLDisplay} */} diff --git a/packages/demo/src/config/default.js b/packages/demo/src/config/default.js index 6f298900d1..b1d0367709 100644 --- a/packages/demo/src/config/default.js +++ b/packages/demo/src/config/default.js @@ -62,8 +62,9 @@ export default { SeriesInstanceUID: '1.2.826.0.1.3680043.8.498.95761650623702141948748826678788764509', }, + // ProstateX-0000 T2-tra prostateX: { - wadoRsRoot: 'http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/rs', + wadoRsRoot: 'https://d1jjp7d53qmguz.cloudfront.net/dicomweb', StudyInstanceUID: '1.3.6.1.4.1.14519.5.2.1.7311.5101.158323547117540061132729905711', SeriesInstanceUID: diff --git a/packages/demo/src/constants/index.js b/packages/demo/src/constants/index.js index aca9241eef..077407780c 100644 --- a/packages/demo/src/constants/index.js +++ b/packages/demo/src/constants/index.js @@ -1,6 +1,7 @@ const renderingEngineUID = 'PETCTRenderingEngine' const ptVolumeUID = 'cornerstoneStreamingImageVolume:PET_VOLUME' const ctVolumeUID = 'cornerstoneStreamingImageVolume:CT_VOLUME' +const prostateVolumeUID = 'cornerstoneStreamingImageVolume:PROSTATE_VOLUME' const ctVolumeTestUID = 'fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0' const ptVolumeTestUID = 'fakeVolumeLoader:volumeURI_100_100_4_1_1_1_0' const ctStackUID = 'CT_Stack' @@ -15,6 +16,7 @@ const SCENE_IDS = { STACK: 'stackScene', CT: 'ctScene', PT: 'ptScene', + PROSTATE: 'prostateScene', FUSION: 'fusionScene', PTMIP: 'ptMipScene', CTVR: 'ctVRScene', @@ -36,6 +38,11 @@ const VIEWPORT_IDS = { SAGITTAL: 'ctSagittal', CORONAL: 'ctCoronal', }, + PROSTATE: { + AXIAL: 'prostateAxial', + SAGITTAL: 'prostateSagittal', + CORONAL: 'prostateCoronal', + }, PT: { AXIAL: 'ptAxial', SAGITTAL: 'ptSagittal', @@ -74,6 +81,7 @@ const TOOL_GROUP_UIDS = { STACK_DX: 'stackDXToolGroup', CT: 'ctSceneToolGroup', PT: 'ptSceneToolGroup', + PROSTATE: 'prostateSceneToolGroup', FUSION: 'fusionSceneToolGroup', PTMIP: 'ptMipSceneToolGroup', CTVR: 'ctVRSceneToolGroup', @@ -108,6 +116,7 @@ export { renderingEngineUID, ptVolumeUID, ctVolumeUID, + prostateVolumeUID, ctStackUID, ctVolumeTestUID, ptVolumeTestUID, diff --git a/packages/demo/src/initToolGroups.js b/packages/demo/src/initToolGroups.js index 05282c281f..4108200994 100644 --- a/packages/demo/src/initToolGroups.js +++ b/packages/demo/src/initToolGroups.js @@ -7,6 +7,7 @@ import { ctVolumeTestUID, ptVolumeTestUID, VIEWPORT_IDS, + prostateVolumeUID, } from './constants' const { PanTool, @@ -54,6 +55,9 @@ let viewportReferenceLineControllable = [ VIEWPORT_IDS.PT.AXIAL, VIEWPORT_IDS.PT.SAGITTAL, VIEWPORT_IDS.PT.CORONAL, + VIEWPORT_IDS.PROSTATE.AXIAL, + VIEWPORT_IDS.PROSTATE.SAGITTAL, + VIEWPORT_IDS.PROSTATE.CORONAL, VIEWPORT_IDS.FUSION.AXIAL, VIEWPORT_IDS.FUSION.SAGITTAL, VIEWPORT_IDS.FUSION.CORONAL, @@ -94,6 +98,9 @@ let viewportReferenceLineDraggableRotatable = [ VIEWPORT_IDS.PT.AXIAL, VIEWPORT_IDS.PT.SAGITTAL, VIEWPORT_IDS.PT.CORONAL, + VIEWPORT_IDS.PROSTATE.AXIAL, + VIEWPORT_IDS.PROSTATE.SAGITTAL, + VIEWPORT_IDS.PROSTATE.CORONAL, VIEWPORT_IDS.FUSION.AXIAL, VIEWPORT_IDS.FUSION.SAGITTAL, VIEWPORT_IDS.FUSION.CORONAL, @@ -131,6 +138,9 @@ let viewportReferenceLineSlabThicknessControlsOn = [ VIEWPORT_IDS.CT.AXIAL, VIEWPORT_IDS.CT.SAGITTAL, VIEWPORT_IDS.CT.CORONAL, + VIEWPORT_IDS.PROSTATE.AXIAL, + VIEWPORT_IDS.PROSTATE.SAGITTAL, + VIEWPORT_IDS.PROSTATE.CORONAL, /*VIEWPORT_IDS.PT.AXIAL, VIEWPORT_IDS.PT.SAGITTAL, VIEWPORT_IDS.PT.CORONAL,*/ @@ -180,6 +190,10 @@ viewportColors[VIEWPORT_IDS.PT.AXIAL] = 'rgb(200, 0, 0)' viewportColors[VIEWPORT_IDS.PT.SAGITTAL] = 'rgb(200, 200, 0)' viewportColors[VIEWPORT_IDS.PT.CORONAL] = 'rgb(0, 200, 0)' +viewportColors[VIEWPORT_IDS.PROSTATE.AXIAL] = 'rgb(200, 0, 0)' +viewportColors[VIEWPORT_IDS.PROSTATE.SAGITTAL] = 'rgb(200, 200, 0)' +viewportColors[VIEWPORT_IDS.PROSTATE.CORONAL] = 'rgb(0, 200, 0)' + viewportColors[VIEWPORT_IDS.FUSION.AXIAL] = 'rgb(200, 0, 0)' viewportColors[VIEWPORT_IDS.FUSION.SAGITTAL] = 'rgb(200, 200, 0)' viewportColors[VIEWPORT_IDS.FUSION.CORONAL] = 'rgb(0, 200, 0)' @@ -245,6 +259,7 @@ function initToolGroups(toolConfiguration = {}) { const colorSceneToolGroup = ToolGroupManager.createToolGroup( TOOL_GROUP_UIDS.COLOR ) + const prostateSceneToolGroup = ToolGroupManager.createToolGroup(TOOL_GROUP_UIDS.PROSTATE) const fusionSceneToolGroup = ToolGroupManager.createToolGroup( TOOL_GROUP_UIDS.FUSION ) @@ -271,6 +286,7 @@ function initToolGroups(toolConfiguration = {}) { stackPTViewportToolGroup, stackDXViewportToolGroup, ctSceneToolGroup, + prostateSceneToolGroup, ptSceneToolGroup, fusionSceneToolGroup, ptMipSceneToolGroup, @@ -287,6 +303,7 @@ function addToolsToToolGroups({ stackCTViewportToolGroup, stackPTViewportToolGroup, stackDXViewportToolGroup, + prostateSceneToolGroup, ctSceneToolGroup, ptSceneToolGroup, fusionSceneToolGroup, @@ -335,7 +352,7 @@ function addToolsToToolGroups({ stackPTViewportToolGroup.setToolPassive('Probe') stackPTViewportToolGroup.setToolPassive('RectangleRoi') stackPTViewportToolGroup.setToolPassive('EllipticalRoi') - stackPTViewportToolGroup.setToolPassive('Crosshairs') + stackPTViewportToolGroup.setToolDisabled('Crosshairs') stackPTViewportToolGroup.setToolActive('StackScrollMouseWheel') stackPTViewportToolGroup.setToolActive('WindowLevel', { @@ -396,7 +413,7 @@ function addToolsToToolGroups({ stackCTViewportToolGroup.setToolPassive('Probe') stackCTViewportToolGroup.setToolPassive('RectangleRoi') stackCTViewportToolGroup.setToolPassive('EllipticalRoi') - stackCTViewportToolGroup.setToolPassive('Crosshairs') + stackCTViewportToolGroup.setToolDisabled('Crosshairs') stackCTViewportToolGroup.setToolPassive('Length') stackCTViewportToolGroup.setToolActive('StackScrollMouseWheel') @@ -459,7 +476,7 @@ function addToolsToToolGroups({ stackDXViewportToolGroup.setToolPassive('Probe') stackDXViewportToolGroup.setToolPassive('RectangleRoi') stackDXViewportToolGroup.setToolPassive('EllipticalRoi') - stackDXViewportToolGroup.setToolPassive('Crosshairs') + stackDXViewportToolGroup.setToolDisabled('Crosshairs') stackDXViewportToolGroup.setToolActive('StackScrollMouseWheel') stackDXViewportToolGroup.setToolActive('WindowLevel', { @@ -538,7 +555,7 @@ function addToolsToToolGroups({ ctSceneToolGroup.setToolPassive('Probe') ctSceneToolGroup.setToolPassive('RectangleRoi') ctSceneToolGroup.setToolPassive('EllipticalRoi') - ctSceneToolGroup.setToolPassive('Crosshairs') + ctSceneToolGroup.setToolDisabled('Crosshairs') ctSceneToolGroup.setToolActive('StackScrollMouseWheel') ctSceneToolGroup.setToolActive('WindowLevel', { @@ -564,6 +581,72 @@ function addToolsToToolGroups({ }) } + if (prostateSceneToolGroup) { + // Set up CT Scene tools + + // @TODO: This kills the volumeUID and tool configuration + prostateSceneToolGroup.addTool('WindowLevel', { + configuration: { volumeUID: prostateVolumeUID }, + }) + prostateSceneToolGroup.addTool('Length', {}) + prostateSceneToolGroup.addTool('Pan', {}) + prostateSceneToolGroup.addTool('Zoom', {}) + prostateSceneToolGroup.addTool('StackScrollMouseWheel', {}) + prostateSceneToolGroup.addTool('Bidirectional', { + configuration: { volumeUID: prostateVolumeUID }, + }) + prostateSceneToolGroup.addTool('Length', { + configuration: { volumeUID: prostateVolumeUID }, + }) + prostateSceneToolGroup.addTool('Probe', { + configuration: { volumeUID: prostateVolumeUID }, + }) + prostateSceneToolGroup.addTool('RectangleRoi', { + configuration: { volumeUID: prostateVolumeUID }, + }) + prostateSceneToolGroup.addTool('EllipticalRoi', { + configuration: { volumeUID: ctVolumeUID }, + }) + prostateSceneToolGroup.addTool('Crosshairs', { + configuration: { + getReferenceLineColor, + getReferenceLineControllable, + getReferenceLineDraggableRotatable, + getReferenceLineSlabThicknessControlsOn, + }, + }) + + prostateSceneToolGroup.setToolPassive('Bidirectional') + prostateSceneToolGroup.setToolPassive('Length') + prostateSceneToolGroup.setToolPassive('Probe') + prostateSceneToolGroup.setToolPassive('RectangleRoi') + prostateSceneToolGroup.setToolPassive('EllipticalRoi') + prostateSceneToolGroup.setToolDisabled('Crosshairs') + + prostateSceneToolGroup.setToolActive('StackScrollMouseWheel') + prostateSceneToolGroup.setToolActive('WindowLevel', { + bindings: [ + { + mouseButton: ToolBindings.Mouse.Primary, + }, + ], + }) + prostateSceneToolGroup.setToolActive('Pan', { + bindings: [ + { + mouseButton: ToolBindings.Mouse.Auxiliary, + }, + ], + }) + prostateSceneToolGroup.setToolActive('Zoom', { + bindings: [ + { + mouseButton: ToolBindings.Mouse.Secondary, + }, + ], + }) + } + if (ptSceneToolGroup) { // Set up PT Scene tools ptSceneToolGroup.addTool('RectangleScissors', { @@ -616,7 +699,7 @@ function addToolsToToolGroups({ ptSceneToolGroup.setToolPassive('RectangleRoi') ptSceneToolGroup.setToolPassive('EllipticalRoi') ptSceneToolGroup.setToolPassive('Bidirectional') - ptSceneToolGroup.setToolPassive('Crosshairs') + ptSceneToolGroup.setToolDisabled('Crosshairs') ptSceneToolGroup.setToolActive('StackScrollMouseWheel') ptSceneToolGroup.setToolActive('PetThreshold', { @@ -680,7 +763,7 @@ function addToolsToToolGroups({ fusionSceneToolGroup.setToolPassive('Probe') fusionSceneToolGroup.setToolPassive('RectangleRoi') fusionSceneToolGroup.setToolPassive('EllipticalRoi') - fusionSceneToolGroup.setToolPassive('Crosshairs') + fusionSceneToolGroup.setToolDisabled('Crosshairs') fusionSceneToolGroup.setToolActive('StackScrollMouseWheel') fusionSceneToolGroup.setToolActive('PetThreshold', { diff --git a/packages/docs/docs/concepts/tools.md b/packages/docs/docs/concepts/tools.md index 511a06d207..98f17eee65 100644 --- a/packages/docs/docs/concepts/tools.md +++ b/packages/docs/docs/concepts/tools.md @@ -129,6 +129,29 @@ ctSceneToolGroup.addTool('Zoom', {}) ctSceneToolGroup.addTool('Probe', {}) ``` +### Annotation sharing +The annotations in StackViewport and VolumeViewport are immediately shared. +You can activate both a Stack and a Volume viewport and draw annotations on +one while editing (modifying or moving) the other. This is accomplished by +providing the toolGroup of the VolumeViewports with the `VolumeUID`. + +When drawing a measurement: +-The tool gets the volume using volumeUID +- It checks which imageId of the volume the tool has been drawn on +- If in a stack viewport, we are at the same imageId, we render the tool + +```js +ctSceneToolGroup.addTool('Length', { + configuration: { volumeUID: ctVolumeUID }, +}) +ctStackToolGroup.addTool('Length') +``` + +<div style={{padding:"56.25% 0 0 0", position:"relative"}}> + <iframe src="https://player.vimeo.com/video/601943316?badge=0&autopause=0&player_id=0&app_id=58479&h=a6f3ee6e3d" frameBorder="0" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen style= {{ position:"absolute",top:0,left:0,width:"100%",height:"100%"}} title="measurement-report"></iframe> +</div> + + #### Dynamic tool statistics Cornerstone3D-Tools is capable of calculating dynamic statistics based on the modality of the volume being rendered. For instance, for CT volumes a `ProbeTool` will give Hounsfield Units and for PET it will calculate SUV stats. diff --git a/packages/docs/docs/tools-usage.md b/packages/docs/docs/tools-usage.md index d15114dfdf..c937265276 100644 --- a/packages/docs/docs/tools-usage.md +++ b/packages/docs/docs/tools-usage.md @@ -58,3 +58,58 @@ stackToolGroup.setToolActive('Probe', { bindings: [{ mouseButton: ToolBindings.Mouse.Primary }], }) ``` + + +## Crosshairs +Crosshairs enables cross-locating a point in 2 or 3 viewports. They can be +Active, Passive, Enabled, Disabled similar to other tools. + +```js +ctSceneToolGroup.addTool('Crosshairs', { + configuration: { + getReferenceLineColor, + getReferenceLineControllable, + getReferenceLineDraggableRotatable, + getReferenceLineSlabThicknessControlsOn, + }, +}) +``` + + +### Rotation +By clicking and dragging a rotation handle, you may change the view of the other viewports in the scene. + +### Slab Thickness +You can reformat a thick slab through the data. This feature computes a 2D thick view along the direction of the view from a 3D image. + + +In order to use the slab thickness you need to set the `blendMode` on the `Scene` to be `BlendMode.MAXIMUM_INTENSITY_BLEND`. + + +```js +await ctScene.setVolumes([ + { + volumeUID: ctVolumeUID, + blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND, + }, +]) +``` +### Configuration +Customization options include changing the colour of the crosshairs and determining whether or not to display the slabThickness and rotation handles. + +To familiarize yourself with these options, go to `initiToolGroups` in the demo folder. + +```js +ctSceneToolGroup.addTool('Crosshairs', { + configuration: { + getReferenceLineColor, + getReferenceLineControllable, + getReferenceLineDraggableRotatable, + getReferenceLineSlabThicknessControlsOn, + }, +}) +``` + +<div style={{padding:"56.25% 0 0 0", position:"relative"}}> + <iframe src="https://player.vimeo.com/video/601952835?badge=0&autopause=0&player_id=0&app_id=58479&h=abc1591622" frameBorder="0" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen style= {{ position:"absolute",top:0,left:0,width:"100%",height:"100%"}} title="measurement-report"></iframe> +</div> diff --git a/yarn.lock b/yarn.lock index be789190b9..1d7ac5ff8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3937,7 +3937,7 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -array.prototype.flat@^1.2.4: +array.prototype.flat@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13" integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==