UNPKG

@cornerstonejs/core

Version:
517 lines (516 loc) • 22.9 kB
import { mat4, vec3 } from 'gl-matrix'; import vtkPlane from '@kitware/vtk.js/Common/DataModel/Plane.js'; import cache from '../cache/cache.js'; import { EPSILON, MPR_CAMERA_VALUES, RENDERING_DEFAULTS } from '../constants/index.js'; import { OrientationAxis, Events } from '../enums/index.js'; import { actorIsA } from '../utilities/actorCheck.js'; import getClosestImageId from '../utilities/getClosestImageId.js'; import triggerEvent from '../utilities/triggerEvent.js'; import BaseVolumeViewport from './BaseVolumeViewport.js'; import setDefaultVolumeVOI from './helpers/setDefaultVolumeVOI.js'; import getImageSliceDataForVolumeViewport from '../utilities/getImageSliceDataForVolumeViewport.js'; import { transformCanvasToIJK } from '../utilities/transformCanvasToIJK.js'; import { transformIJKToCanvas } from '../utilities/transformIJKToCanvas.js'; import getVolumeViewportScrollInfo from '../utilities/getVolumeViewportScrollInfo.js'; import { calculateCameraPosition, getCameraVectors, } from './helpers/getCameraVectors.js'; import { getPlanarVolumeSliceNavigationState, getPlanarVolumeSlicePoint, } from './helpers/planarVolumeRendering.js'; class VolumeViewport extends BaseVolumeViewport { constructor(props) { super(props); this._useAcquisitionPlaneForViewPlane = false; this.getNumberOfSlices = () => { const { numberOfSlices } = getImageSliceDataForVolumeViewport(this) || {}; return numberOfSlices; }; this.resetCameraForResize = () => { return this.resetCamera({ resetPan: true, resetZoom: true, resetToCenter: true, resetRotation: false, suppressEvents: true, }); }; this.getCurrentImageIdIndex = (volumeId = this.getVolumeId(), useSlabThickness = true) => { if (!volumeId) { return 0; } const { currentStepIndex } = getVolumeViewportScrollInfo(this, volumeId, useSlabThickness); return currentStepIndex; }; this.getSliceIndex = () => { const { imageIndex } = getImageSliceDataForVolumeViewport(this) || {}; return imageIndex; }; this.getCurrentImageId = () => { const actorEntry = this.getDefaultActor(); if (!actorEntry || !actorIsA(actorEntry, 'vtkVolume')) { return; } const volume = cache.getVolume(this.getVolumeId()); if (!volume) { return; } const { viewPlaneNormal, focalPoint } = this.getCamera(); return getClosestImageId(volume, focalPoint, viewPlaneNormal); }; this.getSlicePlaneCoordinates = () => { const actorEntry = this.getDefaultActor(); if (!actorEntry?.actor) { console.warn('No image data found for calculating vtkPlanes.'); return []; } const volumeId = this.getVolumeId(); const imageVolume = cache.getVolume(volumeId); const camera = this.getCamera(); const { focalPoint, position, viewPlaneNormal } = camera; const { currentSliceIndex, sliceRange, spacingInNormalDirection } = getPlanarVolumeSliceNavigationState({ actor: actorEntry.actor, camera: { focalPoint, position, viewPlaneNormal, }, imageVolume, }); const numSlicesBackward = Math.round((sliceRange.current - sliceRange.min) / spacingInNormalDirection); const numSlicesForward = Math.round((sliceRange.max - sliceRange.current) / spacingInNormalDirection); const focalPoints = []; for (let i = -numSlicesBackward; i <= numSlicesForward; i++) { const { newFocalPoint: point } = getPlanarVolumeSlicePoint({ camera: { focalPoint, position, viewPlaneNormal, }, delta: i, sliceRange, spacingInNormalDirection, }); focalPoints.push({ sliceIndex: currentSliceIndex + i, point }); } return focalPoints; }; const { orientation } = this.options; if (orientation && orientation !== OrientationAxis.ACQUISITION) { this.applyViewOrientation(orientation); return; } this._useAcquisitionPlaneForViewPlane = true; } async setVolumes(volumeInputArray, immediate = false, suppressEvents = false) { const volumeId = volumeInputArray[0].volumeId; const firstImageVolume = cache.getVolume(volumeId); if (!firstImageVolume) { throw new Error(`imageVolume with id: ${volumeId} does not exist`); } if (this._useAcquisitionPlaneForViewPlane) { this._setViewPlaneToAcquisitionPlane(firstImageVolume); this._useAcquisitionPlaneForViewPlane = false; } else if (this.options.orientation && typeof this.options.orientation === 'string') { if (this.options.orientation.includes('_reformat')) { this._setViewPlaneToReformatOrientation(this.options.orientation, firstImageVolume); } } return super.setVolumes(volumeInputArray, immediate, suppressEvents); } async addVolumes(volumeInputArray, immediate = false, suppressEvents = false) { const firstImageVolume = cache.getVolume(volumeInputArray[0].volumeId); if (!firstImageVolume) { throw new Error(`imageVolume with id: ${firstImageVolume.volumeId} does not exist`); } if (this._useAcquisitionPlaneForViewPlane) { this._setViewPlaneToAcquisitionPlane(firstImageVolume); this._useAcquisitionPlaneForViewPlane = false; } else if (this.options.orientation && typeof this.options.orientation === 'string') { if (this.options.orientation.includes('_reformat')) { this._setViewPlaneToReformatOrientation(this.options.orientation, firstImageVolume); } } return super.addVolumes(volumeInputArray, immediate, suppressEvents); } jumpToWorld(worldPos) { let targetWorldPos = worldPos; const imageData = this.getImageData(); if (imageData?.imageData) { const bounds = imageData.imageData.getBounds(); targetWorldPos = [ Math.max(bounds[0], Math.min(bounds[1], worldPos[0])), Math.max(bounds[2], Math.min(bounds[3], worldPos[1])), Math.max(bounds[4], Math.min(bounds[5], worldPos[2])), ]; } const { focalPoint } = this.getCamera(); const delta = [0, 0, 0]; vec3.sub(delta, targetWorldPos, focalPoint); const camera = this.getCamera(); const normal = camera.viewPlaneNormal; const dotProd = vec3.dot(delta, normal); const projectedDelta = vec3.fromValues(normal[0], normal[1], normal[2]); vec3.scale(projectedDelta, projectedDelta, dotProd); if (Math.abs(projectedDelta[0]) > 1e-3 || Math.abs(projectedDelta[1]) > 1e-3 || Math.abs(projectedDelta[2]) > 1e-3) { const newFocalPoint = [0, 0, 0]; const newPosition = [0, 0, 0]; vec3.add(newFocalPoint, camera.focalPoint, projectedDelta); vec3.add(newPosition, camera.position, projectedDelta); this.setCamera({ focalPoint: newFocalPoint, position: newPosition, }); this.render(); } return true; } setOrientation(orientation, immediate = true, suppressEvents = false) { let viewPlaneNormal, viewUp; if (typeof orientation === 'string') { if (orientation === OrientationAxis.ACQUISITION) { ({ viewPlaneNormal, viewUp } = super._getAcquisitionPlaneOrientation()); } else if (orientation === OrientationAxis.REFORMAT) { ({ viewPlaneNormal, viewUp } = getCameraVectors(this, { useViewportNormal: true, })); } else if (orientation === OrientationAxis.AXIAL_REFORMAT || orientation === OrientationAxis.SAGITTAL_REFORMAT || orientation === OrientationAxis.CORONAL_REFORMAT) { let baseOrientation; if (orientation === OrientationAxis.AXIAL_REFORMAT) { baseOrientation = OrientationAxis.AXIAL; } else if (orientation === OrientationAxis.SAGITTAL_REFORMAT) { baseOrientation = OrientationAxis.SAGITTAL; } else { baseOrientation = OrientationAxis.CORONAL; } ({ viewPlaneNormal, viewUp } = getCameraVectors(this, { useViewportNormal: true, orientation: baseOrientation, })); } else if (MPR_CAMERA_VALUES[orientation]) { ({ viewPlaneNormal, viewUp } = MPR_CAMERA_VALUES[orientation]); } else { throw new Error(`Invalid orientation: ${orientation}. Use Enums.OrientationAxis instead.`); } this.setCamera({ viewPlaneNormal, viewUp, }); this.viewportProperties.orientation = orientation; this.resetCamera({ suppressEvents: true }); } else { ({ viewPlaneNormal, viewUp } = orientation); this.applyViewOrientation(orientation, true, suppressEvents); } if (immediate) { this.render(); } } setCameraClippingRange() { const activeCamera = this.getVtkActiveCamera(); if (!activeCamera) { console.warn('No active camera found'); return; } if (activeCamera.getParallelProjection()) { activeCamera.setClippingRange(-RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE, RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE); } else { activeCamera.setClippingRange(RENDERING_DEFAULTS.MINIMUM_SLAB_THICKNESS, RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE); } } _setViewPlaneToReformatOrientation(orientation, imageVolume) { let viewPlaneNormal, viewUp; if (imageVolume) { const { direction } = imageVolume; ({ viewPlaneNormal, viewUp } = calculateCameraPosition(direction.slice(0, 3), direction.slice(3, 6), direction.slice(6, 9), orientation)); } else { ({ viewPlaneNormal, viewUp } = this._getAcquisitionPlaneOrientation()); } this.setCamera({ viewPlaneNormal, viewUp, }); this.initialViewUp = viewUp; this.resetCamera(); } _setViewPlaneToAcquisitionPlane(imageVolume) { let viewPlaneNormal, viewUp; if (imageVolume) { const { direction } = imageVolume; viewPlaneNormal = direction.slice(6, 9).map((x) => -x); viewUp = direction.slice(3, 6).map((x) => -x); } else { ({ viewPlaneNormal, viewUp } = this._getAcquisitionPlaneOrientation()); } this.setCamera({ viewPlaneNormal, viewUp, }); this.initialViewUp = viewUp; this.resetCamera(); } getBlendMode(filterActorUIDs) { const actorEntries = this.getActors(); const actorForBlend = filterActorUIDs?.length > 0 ? actorEntries.find((actorEntry) => filterActorUIDs.includes(actorEntry.uid)) : actorEntries[0]; return (actorForBlend?.blendMode || actorForBlend?.actor.getMapper().getBlendMode()); } setBlendMode(blendMode, filterActorUIDs = [], immediate = false) { let actorEntries = this.getActors(); if (filterActorUIDs?.length > 0) { actorEntries = actorEntries.filter((actorEntry) => { return filterActorUIDs.includes(actorEntry.uid); }); } actorEntries.forEach((actorEntry) => { const { actor } = actorEntry; const mapper = actor.getMapper(); mapper.setBlendMode?.(blendMode); actorEntry.blendMode = blendMode; }); if (immediate) { this.render(); } } resetCamera(options) { const { resetPan = true, resetZoom = true, resetRotation = true, resetToCenter = true, suppressEvents = false, resetOrientation = true, resetAspectRatio = true, } = options || {}; const { orientation } = this.viewportProperties; if (orientation && resetOrientation) { this.applyViewOrientation(orientation, false); } super.resetCamera({ resetPan, resetZoom, resetToCenter, resetAspectRatio }); const activeCamera = this.getVtkActiveCamera(); const viewPlaneNormal = activeCamera.getViewPlaneNormal(); const focalPoint = activeCamera.getFocalPoint(); const actorEntries = this.getActors(); actorEntries.forEach((actorEntry) => { if (!actorEntry.actor) { return; } const mapper = actorEntry.actor.getMapper(); const vtkPlanes = mapper.getClippingPlanes(); if (vtkPlanes.length === 0 && !actorEntry?.clippingFilter) { const clipPlane1 = vtkPlane.newInstance(); const clipPlane2 = vtkPlane.newInstance(); const newVtkPlanes = [clipPlane1, clipPlane2]; let slabThickness = RENDERING_DEFAULTS.MINIMUM_SLAB_THICKNESS; if (actorEntry.slabThickness) { slabThickness = actorEntry.slabThickness; } this.setOrientationOfClippingPlanes(newVtkPlanes, slabThickness, viewPlaneNormal, focalPoint); mapper.addClippingPlane(clipPlane1); mapper.addClippingPlane(clipPlane2); } }); if (resetRotation && MPR_CAMERA_VALUES[this.viewportProperties.orientation] !== undefined) { const viewToReset = MPR_CAMERA_VALUES[this.viewportProperties.orientation]; this.setCameraNoEvent({ viewUp: viewToReset.viewUp, viewPlaneNormal: viewToReset.viewPlaneNormal, }); } if (!suppressEvents) { const eventDetail = { viewportId: this.id, camera: this.getCamera(), renderingEngineId: this.renderingEngineId, element: this.element, }; triggerEvent(this.element, Events.CAMERA_RESET, eventDetail); } return true; } setSlabThickness(slabThickness, filterActorUIDs = []) { if (slabThickness < 0.1) { slabThickness = RENDERING_DEFAULTS.MINIMUM_SLAB_THICKNESS; } let actorEntries = this.getActors(); if (filterActorUIDs?.length > 0) { actorEntries = actorEntries.filter((actorEntry) => { return filterActorUIDs.includes(actorEntry.uid); }); } actorEntries.forEach((actorEntry) => { if (actorIsA(actorEntry, 'vtkVolume')) { actorEntry.slabThickness = slabThickness; } }); const currentCamera = this.getCamera(); this.updateClippingPlanesForActors(currentCamera); this.triggerCameraModifiedEventIfNecessary(currentCamera, currentCamera); this.viewportProperties.slabThickness = slabThickness; } resetSlabThickness() { const actorEntries = this.getActors(); actorEntries.forEach((actorEntry) => { if (actorIsA(actorEntry, 'vtkVolume')) { actorEntry.slabThickness = RENDERING_DEFAULTS.MINIMUM_SLAB_THICKNESS; } }); const currentCamera = this.getCamera(); this.updateClippingPlanesForActors(currentCamera); this.triggerCameraModifiedEventIfNecessary(currentCamera, currentCamera); this.viewportProperties.slabThickness = undefined; } isInAcquisitionPlane() { const imageData = this.getImageData(); if (!imageData) { return false; } const { direction } = imageData; const { viewPlaneNormal } = this.getCamera(); const normalDirection = [direction[6], direction[7], direction[8]]; const TOLERANCE = 0.99; return (Math.abs(vec3.dot(viewPlaneNormal, normalDirection)) > TOLERANCE); } getSliceViewInfo() { const { width: canvasWidth, height: canvasHeight } = this.getCanvas(); const ijkOriginPoint = transformCanvasToIJK(this, [0, 0]); const ijkRowPoint = transformCanvasToIJK(this, [canvasWidth - 1, 0]); const ijkColPoint = transformCanvasToIJK(this, [0, canvasHeight - 1]); const ijkRowVec = vec3.sub(vec3.create(), ijkRowPoint, ijkOriginPoint); const ijkColVec = vec3.sub(vec3.create(), ijkColPoint, ijkOriginPoint); const ijkSliceVec = vec3.cross(vec3.create(), ijkRowVec, ijkColVec); vec3.normalize(ijkRowVec, ijkRowVec); vec3.normalize(ijkColVec, ijkColVec); vec3.normalize(ijkSliceVec, ijkSliceVec); const { dimensions } = this.getImageData(); const [sx, sy, sz] = dimensions; const ijkCorners = [ [0, 0, 0], [sx - 1, 0, 0], [0, sy - 1, 0], [sx - 1, sy - 1, 0], [0, 0, sz - 1], [sx - 1, 0, sz - 1], [0, sy - 1, sz - 1], [sx - 1, sy - 1, sz - 1], ]; const canvasCorners = ijkCorners.map((ijkCorner) => transformIJKToCanvas(this, ijkCorner)); const canvasAABB = canvasCorners.reduce((aabb, canvasPoint) => { aabb.minX = Math.min(aabb.minX, canvasPoint[0]); aabb.minY = Math.min(aabb.minY, canvasPoint[1]); aabb.maxX = Math.max(aabb.maxX, canvasPoint[0]); aabb.maxY = Math.max(aabb.maxY, canvasPoint[1]); return aabb; }, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }); const ijkTopLeft = transformCanvasToIJK(this, [ canvasAABB.minX, canvasAABB.minY, ]); const sliceToIndexMatrix = mat4.fromValues(ijkRowVec[0], ijkRowVec[1], ijkRowVec[2], 0, ijkColVec[0], ijkColVec[1], ijkColVec[2], 0, ijkSliceVec[0], ijkSliceVec[1], ijkSliceVec[2], 0, ijkTopLeft[0], ijkTopLeft[1], ijkTopLeft[2], 1); const ijkBottomRight = transformCanvasToIJK(this, [ canvasAABB.maxX, canvasAABB.maxY, ]); const ijkDiagonal = vec3.sub(vec3.create(), ijkBottomRight, ijkTopLeft); const indexToSliceMatrix = mat4.invert(mat4.create(), sliceToIndexMatrix); const { viewPlaneNormal } = this.getCamera(); const isOblique = viewPlaneNormal.filter((component) => Math.abs(component) > EPSILON) .length > 1; if (isOblique) { throw new Error('getSliceInfo is not supported for oblique views'); } const sliceAxis = viewPlaneNormal.findIndex((component) => Math.abs(component) > 1 - EPSILON); if (sliceAxis === -1) { throw new Error('Unable to determine slice axis'); } const sliceWidth = vec3.dot(ijkRowVec, ijkDiagonal) + 1; const sliceHeight = vec3.dot(ijkColVec, ijkDiagonal) + 1; return { sliceIndex: this.getSliceIndex(), width: sliceWidth, height: sliceHeight, slicePlane: sliceAxis, sliceToIndexMatrix, indexToSliceMatrix, }; } getCurrentSlicePixelData() { const { voxelManager } = this.getImageData(); const sliceData = voxelManager.getSliceData(this.getSliceViewInfo()); return sliceData; } getViewReference(viewRefSpecifier = {}) { const viewRef = super.getViewReference(viewRefSpecifier); if (!viewRef?.volumeId) { return viewRef; } const volume = cache.getVolume(viewRef.volumeId); viewRef.referencedImageId = getClosestImageId(volume, viewRef.cameraFocalPoint, viewRef.viewPlaneNormal); return viewRef; } resetProperties(volumeId) { this._resetProperties(volumeId); } _resetProperties(volumeId) { const volumeActor = volumeId ? this.getActor(volumeId) : this.getDefaultActor(); if (!volumeActor) { throw new Error(`No actor found for the given volumeId: ${volumeId}`); } if (volumeActor.slabThickness) { volumeActor.slabThickness = RENDERING_DEFAULTS.MINIMUM_SLAB_THICKNESS; this.viewportProperties.slabThickness = undefined; this.updateClippingPlanesForActors(this.getCamera()); } volumeId ||= this.getVolumeId(); const imageVolume = cache.getVolume(volumeId); if (!imageVolume) { throw new Error(`imageVolume with id: ${volumeId} does not exist in cache`); } setDefaultVolumeVOI(volumeActor.actor, imageVolume); this._restoreVolumeRenderingDefaults(volumeActor, volumeId); const eventDetails = { ...super.getVOIModifiedEventDetail(volumeId), }; const resetPan = true; const resetZoom = true; const resetToCenter = true; const resetCameraRotation = true; const resetAspectRatio = true; this.resetCamera({ resetPan, resetZoom, resetToCenter, resetCameraRotation, resetAspectRatio, }); triggerEvent(this.element, Events.VOI_MODIFIED, eventDetails); } getSlicesClippingPlanes() { const focalPoints = this.getSlicePlaneCoordinates(); const { viewPlaneNormal } = this.getCamera(); const slabThickness = RENDERING_DEFAULTS.MINIMUM_SLAB_THICKNESS; return focalPoints.map(({ point, sliceIndex }) => { const vtkPlanes = [vtkPlane.newInstance(), vtkPlane.newInstance()]; this.setOrientationOfClippingPlanes(vtkPlanes, slabThickness, viewPlaneNormal, point); return { sliceIndex, planes: vtkPlanes.map((plane) => ({ normal: plane.getNormal(), origin: plane.getOrigin(), })), }; }); } } export default VolumeViewport;