@cornerstonejs/core
Version:
Cornerstone3D Core
517 lines (516 loc) • 22.9 kB
JavaScript
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;