@cornerstonejs/core
Version:
Cornerstone3D Core
1,092 lines • 57.5 kB
JavaScript
import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction.js';
import { vec2, vec3 } from 'gl-matrix';
import cache from '../cache/cache.js';
import { MPR_CAMERA_VALUES, RENDERING_DEFAULTS, VIEWPORT_PRESETS, } from '../constants/index.js';
import { Events, MetadataModules, OrientationAxis, ViewportStatus, VOILUTFunctionType, } from '../enums/index.js';
import ViewportType from '../enums/ViewportType.js';
import eventTarget from '../eventTarget.js';
import { getShouldUseCPURendering } from '../init.js';
import triggerEvent from '../utilities/triggerEvent.js';
import * as colormapUtils from '../utilities/colormap.js';
import invertRgbTransferFunction from '../utilities/invertRgbTransferFunction.js';
import createSigmoidRGBTransferFunction from '../utilities/createSigmoidRGBTransferFunction.js';
import transformWorldToIndex from '../utilities/transformWorldToIndex.js';
import { findMatchingColormap, updateOpacity as colormapUpdateOpacity, updateThreshold as colormapUpdateThreshold, updateOpacityMapping as colormapUpdateOpacityMapping, getOpacityState, } from '../utilities/colormap.js';
import getAcquisitionPlaneOrientation from '../utilities/getAcquisitionPlaneOrientation.js';
import { getTransferFunctionNodes, setTransferFunctionNodes, } from '../utilities/transferFunctionUtils.js';
import { createAndCacheVolume } from '../loaders/volumeLoader.js';
import resolveViewportVolumeId from './helpers/resolveViewportVolumeId.js';
import { getGenericViewportImageDisplaySet } from './GenericViewport/genericViewportDisplaySetAccess.js';
import createVolumeActor from './helpers/createVolumeActor.js';
import volumeNewImageEventDispatcher, { resetVolumeNewImageState, } from './helpers/volumeNewImageEventDispatcher.js';
import Viewport from './Viewport.js';
import vtkSlabCamera from './vtkClasses/vtkSlabCamera.js';
import getVolumeViewportScrollInfo from '../utilities/getVolumeViewportScrollInfo.js';
import { actorIsA, isImageActor } from '../utilities/actorCheck.js';
import snapFocalPointToSlice from '../utilities/snapFocalPointToSlice.js';
import getVoiFromSigmoidRGBTransferFunction from '../utilities/getVoiFromSigmoidRGBTransferFunction.js';
import isEqual, { isEqualAbs, isEqualNegative } from '../utilities/isEqual.js';
import applyPreset from '../utilities/applyPreset.js';
import uuidv4 from '../utilities/uuidv4.js';
import * as metaData from '../metaData.js';
import { getCameraVectors } from './helpers/getCameraVectors.js';
import { isContextPoolRenderingEngine } from './helpers/isContextPoolRenderingEngine.js';
import mprCameraValues from '../constants/mprCameraValues.js';
import { isInvalidNumber } from './helpers/isInvalidNumber.js';
import getVolumeViewReferenceId from '../utilities/getVolumeViewReferenceId.js';
import { createSharpeningRenderPass, createSmoothingRenderPass, } from './renderPasses/index.js';
class BaseVolumeViewport extends Viewport {
constructor(props) {
super(props);
this.useCPURendering = false;
this.sharpening = 0;
this.smoothing = 0;
this.perVolumeIdDefaultProperties = new Map();
this.viewportProperties = {};
this.volumeIds = new Set();
this.setRotation = (rotation) => {
const panFit = this.getPan(this.fitToCanvasCamera);
const pan = this.getPan();
const previousCamera = this.getCamera();
const panSub = vec2.sub([0, 0], panFit, pan);
this.setPan(panSub, false);
const { flipVertical } = this.getCamera();
const initialViewUp = flipVertical
? vec3.negate([0, 0, 0], this.initialViewUp)
: this.initialViewUp;
this.setCameraNoEvent({
viewUp: initialViewUp,
});
this.rotateCamera(rotation);
const afterPan = this.getPan();
const afterPanFit = this.getPan(this.fitToCanvasCamera);
const newCenter = vec2.sub([0, 0], afterPan, afterPanFit);
const newOffset = vec2.add([0, 0], panFit, newCenter);
this.setPan(newOffset, false);
if (this._suppressCameraModifiedEvents) {
return;
}
const camera = this.getCamera();
const eventDetail = {
previousCamera,
camera,
element: this.element,
viewportId: this.id,
renderingEngineId: this.renderingEngineId,
};
triggerEvent(this.element, Events.CAMERA_MODIFIED, eventDetail);
};
this.setSharpening = (sharpening) => {
this.sharpening = sharpening;
this.render();
};
this.setSmoothing = (smoothing) => {
this.smoothing = smoothing;
this.render();
};
this.getRenderPasses = () => {
if (!this.shouldUseCustomRenderPass()) {
return null;
}
const renderPasses = [];
try {
if (this.smoothing > 0) {
renderPasses.push(createSmoothingRenderPass(this.smoothing));
}
if (this.sharpening > 0) {
renderPasses.push(createSharpeningRenderPass(this.sharpening));
}
return renderPasses.length ? renderPasses : null;
}
catch (e) {
console.warn('Failed to create custom render passes:', e);
return null;
}
};
this.getDefaultProperties = (volumeId) => {
let volumeProperties;
if (volumeId !== undefined) {
volumeProperties = this.perVolumeIdDefaultProperties.get(volumeId);
}
if (volumeProperties !== undefined) {
return volumeProperties;
}
return {
...this.globalDefaultProperties,
};
};
this.getProperties = (volumeId) => {
const applicableVolumeActorInfo = this._getApplicableVolumeActor(volumeId);
if (!applicableVolumeActorInfo) {
return;
}
const { colormap: latestColormap, VOILUTFunction, interpolationType, invert, slabThickness, preset, } = this.viewportProperties;
volumeId ||= this.getVolumeId();
const volume = cache.getVolume(volumeId);
if (!volume) {
return null;
}
const volumeActorEntry = this.getActors().find((actorEntry) => {
return actorEntry.referencedId === volumeId;
});
if (!volumeActorEntry) {
return;
}
const volumeActor = volumeActorEntry.actor;
const cfun = volumeActor.getProperty().getRGBTransferFunction(0);
const [lower, upper] = this.viewportProperties?.VOILUTFunction === 'SIGMOID'
? getVoiFromSigmoidRGBTransferFunction(cfun)
: cfun.getRange();
const voiRange = { lower, upper };
const volumeColormap = this.getColormap(volumeId);
const colormap = volumeId && volumeColormap ? volumeColormap : latestColormap;
return {
colormap: colormap,
voiRange: voiRange,
VOILUTFunction: VOILUTFunction,
interpolationType: interpolationType,
invert: invert,
slabThickness: slabThickness,
preset,
sharpening: this.sharpening,
smoothing: this.smoothing,
};
};
this.getColormap = (volumeId) => {
const applicableVolumeActorInfo = this._getApplicableVolumeActor(volumeId);
if (!applicableVolumeActorInfo) {
return;
}
const { volumeActor } = applicableVolumeActorInfo;
const cfun = this._getOrCreateColorTransferFunction(volumeId);
const { nodes } = cfun.getState();
const RGBPoints = nodes.reduce((acc, node) => {
acc.push(node.x, node.r, node.g, node.b);
return acc;
}, []);
const matchedColormap = findMatchingColormap(RGBPoints, volumeActor) || {};
const { opacity, opacityMapping, threshold } = getOpacityState(volumeActor);
matchedColormap.opacity = opacity;
matchedColormap.opacityMapping = opacityMapping;
matchedColormap.threshold = threshold;
return matchedColormap;
};
this.getRotation = () => {
const { viewUp: currentViewUp, viewPlaneNormal, flipVertical, } = this.getCameraNoRotation();
const initialViewUp = flipVertical
? vec3.negate([0, 0, 0], this.initialViewUp)
: this.initialViewUp;
if (!initialViewUp) {
return 0;
}
const initialToCurrentViewUpAngle = (vec3.angle(initialViewUp, currentViewUp) * 180) / Math.PI;
const initialToCurrentViewUpCross = vec3.cross([0, 0, 0], initialViewUp, currentViewUp);
const normalDot = vec3.dot(initialToCurrentViewUpCross, viewPlaneNormal);
const value = normalDot >= 0
? initialToCurrentViewUpAngle
: (360 - initialToCurrentViewUpAngle) % 360;
return value;
};
this.getFrameOfReferenceUID = () => {
return this._FrameOfReferenceUID;
};
this.canvasToWorldTiled = (canvasPos) => {
const vtkCamera = this.getVtkActiveCamera();
vtkCamera.setIsPerformingCoordinateTransformation?.(true);
const renderer = this.getRenderer();
const displayCoords = this.getVtkDisplayCoordsTiled(canvasPos);
const offscreenMultiRenderWindow = this.getRenderingEngine().offscreenMultiRenderWindow;
const openGLRenderWindow = offscreenMultiRenderWindow.getOpenGLRenderWindow();
const worldCoord = openGLRenderWindow.displayToWorld(displayCoords[0], displayCoords[1], displayCoords[2], renderer);
vtkCamera.setIsPerformingCoordinateTransformation?.(false);
return [worldCoord[0], worldCoord[1], worldCoord[2]];
};
this.canvasToWorldContextPool = (canvasPos) => {
const vtkCamera = this.getVtkActiveCamera();
vtkCamera.setIsPerformingCoordinateTransformation?.(true);
const renderer = this.getRenderer();
const devicePixelRatio = window.devicePixelRatio || 1;
const { width, height } = this.canvas;
const aspectRatio = width / height;
const [xMin, yMin, xMax, yMax] = renderer.getViewport();
const viewportWidth = xMax - xMin;
const viewportHeight = yMax - yMin;
const canvasPosWithDPR = [
canvasPos[0] * devicePixelRatio,
canvasPos[1] * devicePixelRatio,
];
const normalizedDisplay = [
xMin + (canvasPosWithDPR[0] / width) * viewportWidth,
yMin + (1 - canvasPosWithDPR[1] / height) * viewportHeight,
0,
];
const projCoords = renderer.normalizedDisplayToProjection(normalizedDisplay[0], normalizedDisplay[1], normalizedDisplay[2]);
const viewCoords = renderer.projectionToView(projCoords[0], projCoords[1], projCoords[2], aspectRatio);
const worldCoord = renderer.viewToWorld(viewCoords[0], viewCoords[1], viewCoords[2]);
vtkCamera.setIsPerformingCoordinateTransformation?.(false);
return [worldCoord[0], worldCoord[1], worldCoord[2]];
};
this.getVtkDisplayCoordsTiled = (canvasPos) => {
const devicePixelRatio = window.devicePixelRatio || 1;
const canvasPosWithDPR = [
canvasPos[0] * devicePixelRatio,
canvasPos[1] * devicePixelRatio,
];
const offscreenMultiRenderWindow = this.getRenderingEngine().offscreenMultiRenderWindow;
const openGLRenderWindow = offscreenMultiRenderWindow.getOpenGLRenderWindow();
const size = openGLRenderWindow.getSize();
const displayCoord = [
canvasPosWithDPR[0] + this.sx,
canvasPosWithDPR[1] + this.sy,
];
displayCoord[1] = size[1] - displayCoord[1];
return [displayCoord[0], displayCoord[1], 0];
};
this.getVtkDisplayCoordsContextPool = (canvasPos) => {
const devicePixelRatio = window.devicePixelRatio || 1;
const canvasPosWithDPR = [
canvasPos[0] * devicePixelRatio,
canvasPos[1] * devicePixelRatio,
];
const renderer = this.getRenderer();
const { width, height } = this.canvas;
const [xMin, yMin, xMax, yMax] = renderer.getViewport();
const viewportWidth = xMax - xMin;
const viewportHeight = yMax - yMin;
const scaledX = (canvasPosWithDPR[0] / width) * viewportWidth * width;
const scaledY = (canvasPosWithDPR[1] / height) * viewportHeight * height;
const displayCoord = [scaledX, viewportHeight * height - scaledY];
return [displayCoord[0], displayCoord[1], 0];
};
this.worldToCanvasTiled = (worldPos) => {
const vtkCamera = this.getVtkActiveCamera();
vtkCamera.setIsPerformingCoordinateTransformation?.(true);
const renderer = this.getRenderer();
const offscreenMultiRenderWindow = this.getRenderingEngine().offscreenMultiRenderWindow;
const openGLRenderWindow = offscreenMultiRenderWindow.getOpenGLRenderWindow();
const size = openGLRenderWindow.getSize();
const displayCoord = openGLRenderWindow.worldToDisplay(...worldPos, renderer);
displayCoord[1] = size[1] - displayCoord[1];
const canvasCoord = [
displayCoord[0] - this.sx,
displayCoord[1] - this.sy,
];
const devicePixelRatio = window.devicePixelRatio || 1;
const canvasCoordWithDPR = [
canvasCoord[0] / devicePixelRatio,
canvasCoord[1] / devicePixelRatio,
];
vtkCamera.setIsPerformingCoordinateTransformation(false);
return canvasCoordWithDPR;
};
this.worldToCanvasContextPool = (worldPos) => {
const vtkCamera = this.getVtkActiveCamera();
vtkCamera.setIsPerformingCoordinateTransformation?.(true);
const renderer = this.getRenderer();
const { width, height } = this.canvas;
const aspectRatio = width / height;
const [xMin, yMin, xMax, yMax] = renderer.getViewport();
const viewportWidth = xMax - xMin;
const viewportHeight = yMax - yMin;
const viewCoords = renderer.worldToView(worldPos[0], worldPos[1], worldPos[2]);
const projCoords = renderer.viewToProjection(viewCoords[0], viewCoords[1], viewCoords[2], aspectRatio);
const normalizedDisplay = renderer.projectionToNormalizedDisplay(projCoords[0], projCoords[1], projCoords[2]);
const canvasNormalizedX = (normalizedDisplay[0] - xMin) / viewportWidth;
const canvasNormalizedY = (normalizedDisplay[1] - yMin) / viewportHeight;
const canvasX = canvasNormalizedX * width;
const canvasY = (1 - canvasNormalizedY) * height;
const devicePixelRatio = window.devicePixelRatio || 1;
const canvasCoordWithDPR = [
canvasX / devicePixelRatio,
canvasY / devicePixelRatio,
];
vtkCamera.setIsPerformingCoordinateTransformation(false);
return canvasCoordWithDPR;
};
this.hasImageURI = (imageURI) => {
const volumeActors = this.getActors().filter((actorEntry) => actorIsA(actorEntry, 'vtkVolume'));
return volumeActors.some(({ uid, referencedId }) => {
const volume = cache.getVolume(referencedId || uid);
if (!volume?.getImageIdIndex) {
return false;
}
return (volume.getImageIdIndex(imageURI) !== undefined ||
volume.getImageURIIndex(imageURI) !== undefined);
});
};
this.getImageIds = (volumeId) => {
const applicableVolumeActorInfo = this._getApplicableVolumeActor(volumeId);
if (!applicableVolumeActorInfo) {
throw new Error(`No actor found for the given volumeId: ${volumeId}`);
}
const volumeIdToUse = applicableVolumeActorInfo.volumeId;
const imageVolume = cache.getVolume(volumeIdToUse);
if (!imageVolume) {
throw new Error(`imageVolume with id: ${volumeIdToUse} does not exist in cache`);
}
return imageVolume.imageIds;
};
this.renderingPipelineFunctions = {
worldToCanvas: {
tiled: this.worldToCanvasTiled,
contextPool: this.worldToCanvasContextPool,
},
canvasToWorld: {
tiled: this.canvasToWorldTiled,
contextPool: this.canvasToWorldContextPool,
},
getVtkDisplayCoords: {
tiled: this.getVtkDisplayCoordsTiled,
contextPool: this.getVtkDisplayCoordsContextPool,
},
getRenderer: {
tiled: this.getRendererTiled,
contextPool: this.getRendererContextPool,
},
};
this.useCPURendering = getShouldUseCPURendering();
if (this.useCPURendering) {
throw new Error('VolumeViewports cannot be used whilst CPU Fallback Rendering is enabled.');
}
this._configureRenderingPipeline();
const renderer = this.getRenderer();
const camera = vtkSlabCamera.newInstance();
renderer.setActiveCamera(camera);
switch (this.type) {
case ViewportType.ORTHOGRAPHIC:
camera.setParallelProjection(true);
break;
case ViewportType.VOLUME_3D:
camera.setParallelProjection(true);
break;
case ViewportType.PERSPECTIVE:
camera.setParallelProjection(false);
break;
default:
throw new Error(`Unrecognized viewport type: ${this.type}`);
}
this.initializeVolumeNewImageEventDispatcher();
}
static get useCustomRenderingPipeline() {
return false;
}
isOrientationChangeable() {
return true;
}
getSliceViewInfo() {
throw new Error('Method not implemented.');
}
applyViewOrientation(orientation, resetCamera = true, suppressEvents = false) {
const { viewPlaneNormal, viewUp } = this._getOrientationVectors(orientation) || {};
if (!viewPlaneNormal || !viewUp) {
return;
}
const camera = this.getVtkActiveCamera();
camera.setDirectionOfProjection(-viewPlaneNormal[0], -viewPlaneNormal[1], -viewPlaneNormal[2]);
camera.setViewUpFrom(viewUp);
this.initialViewUp = viewUp;
if (resetCamera) {
const t = this;
t.resetCamera({
resetOrientation: false,
resetRotation: false,
suppressEvents,
});
}
}
initializeVolumeNewImageEventDispatcher() {
const volumeNewImageHandlerBound = volumeNewImageHandler.bind(this);
const volumeNewImageCleanUpBound = volumeNewImageCleanUp.bind(this);
function volumeNewImageHandler(event) {
const cameraEvent = event;
const { viewportId } = cameraEvent.detail;
if (viewportId !== this.id || this.isDisabled) {
return;
}
const viewportImageData = this.getImageData();
if (!viewportImageData) {
return;
}
volumeNewImageEventDispatcher(cameraEvent);
}
function volumeNewImageCleanUp(evt) {
const { viewportId } = evt.detail;
if (viewportId !== this.id) {
return;
}
this.element.removeEventListener(Events.CAMERA_MODIFIED, volumeNewImageHandlerBound);
eventTarget.removeEventListener(Events.ELEMENT_DISABLED, volumeNewImageCleanUpBound);
resetVolumeNewImageState(viewportId);
}
this.element.removeEventListener(Events.CAMERA_MODIFIED, volumeNewImageHandlerBound);
this.element.addEventListener(Events.CAMERA_MODIFIED, volumeNewImageHandlerBound);
eventTarget.addEventListener(Events.ELEMENT_DISABLED, volumeNewImageCleanUpBound);
}
setVOILUTFunction(voiLUTFunction, volumeId, suppressEvents) {
if (!Object.values(VOILUTFunctionType).includes(voiLUTFunction)) {
voiLUTFunction = VOILUTFunctionType.LINEAR;
}
const { voiRange } = this.getProperties();
this.setVOI(voiRange, volumeId, suppressEvents);
this.viewportProperties.VOILUTFunction = voiLUTFunction;
}
setColormap(colormap, volumeId, suppressEvents) {
const applicableVolumeActorInfo = this._getApplicableVolumeActor(volumeId);
if (!applicableVolumeActorInfo) {
return;
}
const { volumeActor } = applicableVolumeActorInfo;
const cfun = vtkColorTransferFunction.newInstance();
const colormapObj = colormapUtils.resolveColormap(colormap.name);
if (!colormapObj) {
throw new Error(`Colormap ${colormap} not found`);
}
const range = volumeActor
.getProperty()
.getRGBTransferFunction(0)
.getRange();
cfun.applyColorMap(colormapObj);
cfun.setMappingRange(range[0], range[1]);
volumeActor.getProperty().setRGBTransferFunction(0, cfun);
this.viewportProperties.colormap = colormap;
this._applyColormapOpacity(colormap, volumeActor);
if (!suppressEvents) {
const completeColormap = this.getColormap(volumeId);
const eventDetail = {
viewportId: this.id,
colormap: completeColormap,
volumeId,
};
triggerEvent(this.element, Events.VOI_MODIFIED, eventDetail);
triggerEvent(this.element, Events.COLORMAP_MODIFIED, eventDetail);
}
}
_applyColormapOpacity(colormap, volumeActor) {
const mapping = Array.isArray(colormap.opacityMapping)
? colormap.opacityMapping
: Array.isArray(colormap.opacity)
? colormap.opacity
: undefined;
const overall = typeof colormap.opacity === 'number' ? colormap.opacity : undefined;
if (mapping !== undefined) {
colormapUpdateOpacityMapping(volumeActor, mapping, overall);
}
else if (overall !== undefined) {
colormapUpdateOpacity(volumeActor, overall);
}
}
setOpacity(colormap, volumeId) {
const applicableVolumeActorInfo = this._getApplicableVolumeActor(volumeId);
if (!applicableVolumeActorInfo) {
return;
}
const { volumeActor } = applicableVolumeActorInfo;
this._applyColormapOpacity(colormap, volumeActor);
if (!this.viewportProperties.colormap) {
this.viewportProperties.colormap = {};
}
const { opacity, opacityMapping } = getOpacityState(volumeActor);
this.viewportProperties.colormap.opacity = opacity;
this.viewportProperties.colormap.opacityMapping = opacityMapping;
const matchedColormap = this.getColormap(volumeId);
const eventDetail = {
viewportId: this.id,
colormap: matchedColormap,
volumeId,
};
triggerEvent(this.element, Events.COLORMAP_MODIFIED, eventDetail);
}
setInvert(inverted, volumeId, suppressEvents) {
const applicableVolumeActorInfo = this._getApplicableVolumeActor(volumeId);
if (!applicableVolumeActorInfo) {
return;
}
const volumeIdToUse = applicableVolumeActorInfo.volumeId;
const cfun = this._getOrCreateColorTransferFunction(volumeIdToUse);
invertRgbTransferFunction(cfun);
this.viewportProperties.invert = inverted;
if (!suppressEvents) {
const eventDetail = {
...this.getVOIModifiedEventDetail(volumeIdToUse),
invertStateChanged: true,
};
triggerEvent(this.element, Events.VOI_MODIFIED, eventDetail);
}
}
getVOIModifiedEventDetail(volumeId) {
const applicableVolumeActorInfo = this._getApplicableVolumeActor(volumeId);
if (!applicableVolumeActorInfo) {
throw new Error(`No actor found for the given volumeId: ${volumeId}`);
}
const volumeActor = applicableVolumeActorInfo.volumeActor;
const transferFunction = volumeActor
.getProperty()
.getRGBTransferFunction(0);
const range = transferFunction.getMappingRange();
const matchedColormap = this.getColormap(volumeId);
const { VOILUTFunction, invert } = this.getProperties(volumeId);
return {
viewportId: this.id,
range: {
lower: range[0],
upper: range[1],
},
volumeId: applicableVolumeActorInfo.volumeId,
VOILUTFunction: VOILUTFunction,
colormap: matchedColormap,
invert,
};
}
_getOrCreateColorTransferFunction(volumeId) {
const applicableVolumeActorInfo = this._getApplicableVolumeActor(volumeId);
if (!applicableVolumeActorInfo) {
return null;
}
const { volumeActor } = applicableVolumeActorInfo;
const rgbTransferFunction = volumeActor
.getProperty()
.getRGBTransferFunction(0);
if (rgbTransferFunction) {
return rgbTransferFunction;
}
const newRGBTransferFunction = vtkColorTransferFunction.newInstance();
volumeActor.getProperty().setRGBTransferFunction(0, newRGBTransferFunction);
return newRGBTransferFunction;
}
setInterpolationType(interpolationType, volumeId) {
const applicableVolumeActorInfo = this._getApplicableVolumeActor(volumeId);
if (!applicableVolumeActorInfo) {
return;
}
const { volumeActor } = applicableVolumeActorInfo;
const volumeProperty = volumeActor.getProperty();
volumeProperty.setInterpolationType(interpolationType);
this.viewportProperties.interpolationType = interpolationType;
}
setVOI(voiRange, volumeId, suppressEvents = false) {
const applicableVolumeActorInfo = this._getApplicableVolumeActor(volumeId);
if (!applicableVolumeActorInfo) {
return;
}
const { volumeActor } = applicableVolumeActorInfo;
const volumeIdToUse = applicableVolumeActorInfo.volumeId;
const voiRangeToUse = voiRange;
if (typeof voiRangeToUse === 'undefined') {
throw new Error('voiRangeToUse is undefined, need to implement this in the new volume model');
}
if ([voiRangeToUse.lower, voiRangeToUse.upper].some(isInvalidNumber)) {
console.warn('VOI range contains invalid values, ignoring setVOI request', voiRangeToUse);
return;
}
const { VOILUTFunction } = this.getProperties(volumeIdToUse);
if (VOILUTFunction === VOILUTFunctionType.SAMPLED_SIGMOID) {
const cfun = createSigmoidRGBTransferFunction(voiRangeToUse);
volumeActor.getProperty().setRGBTransferFunction(0, cfun);
}
else {
const { lower, upper } = voiRangeToUse;
volumeActor
.getProperty()
.getRGBTransferFunction(0)
.setRange(lower, upper);
}
if (!suppressEvents) {
const eventDetail = {
...this.getVOIModifiedEventDetail(volumeIdToUse),
};
triggerEvent(this.element, Events.VOI_MODIFIED, eventDetail);
}
this.viewportProperties.voiRange = voiRangeToUse;
}
rotateCamera(rotation) {
const rotationToApply = rotation - this.getRotation();
this.getVtkActiveCamera().roll(-rotationToApply);
}
setDefaultProperties(ViewportProperties, volumeId) {
if (volumeId == null) {
this.globalDefaultProperties = ViewportProperties;
}
else {
this.perVolumeIdDefaultProperties.set(volumeId, ViewportProperties);
}
}
clearDefaultProperties(volumeId) {
if (volumeId == null) {
this.globalDefaultProperties = {};
this.resetProperties();
}
else {
this.perVolumeIdDefaultProperties.delete(volumeId);
this.resetToDefaultProperties(volumeId);
}
}
getViewReference(viewRefSpecifier = {}) {
const target = super.getViewReference(viewRefSpecifier);
const volumeId = this.getVolumeId(viewRefSpecifier);
if (viewRefSpecifier?.forFrameOfReference !== true) {
target.volumeId = volumeId;
}
if (typeof viewRefSpecifier?.sliceIndex !== 'number') {
return target;
}
const { viewPlaneNormal } = target;
const delta = viewRefSpecifier?.sliceIndex - this.getSliceIndex();
const { sliceRangeInfo } = getVolumeViewportScrollInfo(this, volumeId, true);
const { sliceRange, spacingInNormalDirection, camera } = sliceRangeInfo;
const { focalPoint, position } = camera;
const { newFocalPoint } = snapFocalPointToSlice(focalPoint, position, sliceRange, viewPlaneNormal, spacingInNormalDirection, delta);
target.cameraFocalPoint = newFocalPoint;
return target;
}
isReferenceViewable(viewRef, options) {
if (!viewRef.FrameOfReferenceUID) {
return false;
}
if (!super.isReferenceViewable(viewRef, options)) {
return false;
}
if (options?.withNavigation) {
const { referencedImageId } = viewRef;
return !referencedImageId || this.hasImageURI(referencedImageId);
}
const currentSliceIndex = this.getSliceIndex();
const { sliceIndex } = viewRef;
if (Array.isArray(sliceIndex)) {
return (sliceIndex[0] <= currentSliceIndex && currentSliceIndex <= sliceIndex[1]);
}
return sliceIndex === undefined || sliceIndex === currentSliceIndex;
}
scroll(delta = 1) {
const volumeId = this.getVolumeId();
const { sliceRangeInfo } = getVolumeViewportScrollInfo(this, volumeId, true);
if (!sliceRangeInfo) {
return;
}
const { sliceRange, spacingInNormalDirection, camera } = sliceRangeInfo;
const { focalPoint, viewPlaneNormal, position } = camera;
const { newFocalPoint, newPosition } = snapFocalPointToSlice(focalPoint, position, sliceRange, viewPlaneNormal, spacingInNormalDirection, delta);
this.setCamera({
focalPoint: newFocalPoint,
position: newPosition,
});
this.render();
}
setBestOrentation(inPlaneVector1, inPlaneVector2) {
if (!inPlaneVector1 && !inPlaneVector2) {
return;
}
const { viewPlaneNormal } = this.getCamera();
if (isCompatible(viewPlaneNormal, inPlaneVector2) &&
isCompatible(viewPlaneNormal, inPlaneVector1)) {
return;
}
const acquisition = this._getAcquisitionPlaneOrientation();
if (isCompatible(acquisition.viewPlaneNormal, inPlaneVector2) &&
isCompatible(acquisition.viewPlaneNormal, inPlaneVector1)) {
this.setOrientation(acquisition);
return;
}
for (const orientation of (Object.values(mprCameraValues))) {
if (isCompatible(orientation.viewPlaneNormal, inPlaneVector2) &&
isCompatible(orientation.viewPlaneNormal, inPlaneVector1)) {
this.setOrientation(orientation);
return;
}
}
const planeNormal = (vec3.cross(vec3.create(), inPlaneVector2 || acquisition.viewPlaneNormal, inPlaneVector1));
vec3.normalize(planeNormal, planeNormal);
this.setOrientation({ viewPlaneNormal: planeNormal });
}
setViewPlane(planeRestriction) {
const { point, inPlaneVector1, inPlaneVector2, FrameOfReferenceUID } = planeRestriction;
this.setBestOrentation(inPlaneVector1, inPlaneVector2);
const { focalPoint, viewPlaneNormal } = this.getCamera();
const deltaFocal = vec3.subtract(vec3.create(), point, focalPoint);
const alongNormal = vec3.dot(deltaFocal, viewPlaneNormal);
const deltaNormal = vec3.scaleAndAdd(vec3.create(), focalPoint, viewPlaneNormal, alongNormal);
this.setViewReference({
FrameOfReferenceUID,
cameraFocalPoint: deltaNormal,
viewPlaneNormal: viewPlaneNormal,
});
}
setViewReference(viewRef) {
if (!viewRef) {
return;
}
const volumeId = this.getVolumeId();
const { FrameOfReferenceUID: refFrameOfReference, cameraFocalPoint, referencedImageId, planeRestriction, viewPlaneNormal: refViewPlaneNormal, viewUp, } = viewRef;
let { sliceIndex } = viewRef;
if (planeRestriction && !refViewPlaneNormal) {
return this.setViewPlane(planeRestriction);
}
const { focalPoint, viewPlaneNormal, position } = this.getCamera();
const isNegativeNormal = isEqualNegative(viewPlaneNormal, refViewPlaneNormal);
const isSameNormal = isEqual(viewPlaneNormal, refViewPlaneNormal);
if (typeof sliceIndex === 'number' &&
volumeId !== undefined &&
viewRef.volumeId === volumeId &&
(isNegativeNormal || isSameNormal)) {
const { currentStepIndex, sliceRangeInfo, numScrollSteps } = getVolumeViewportScrollInfo(this, volumeId, true);
const { sliceRange, spacingInNormalDirection } = sliceRangeInfo;
if (isNegativeNormal) {
sliceIndex = numScrollSteps - sliceIndex - 1;
}
const delta = sliceIndex - currentStepIndex;
const { newFocalPoint, newPosition } = snapFocalPointToSlice(focalPoint, position, sliceRange, viewPlaneNormal, spacingInNormalDirection, delta);
this.setCamera({ focalPoint: newFocalPoint, position: newPosition });
}
else if (refFrameOfReference === this.getFrameOfReferenceUID()) {
if (refViewPlaneNormal && !isNegativeNormal && !isSameNormal) {
this.setOrientation({ viewPlaneNormal: refViewPlaneNormal, viewUp }, true, true);
this.setViewReference(viewRef);
return;
}
if (referencedImageId && this.isInAcquisitionPlane()) {
const imagePlaneModule = metaData.get(MetadataModules.IMAGE_PLANE, referencedImageId);
const { imagePositionPatient } = imagePlaneModule;
const { focalPoint } = this.getCamera();
const diffVector = vec3.subtract(vec3.create(), focalPoint, imagePositionPatient);
const projectedDistance = vec3.dot(diffVector, viewPlaneNormal);
const newImagePositionPatient = vec3.scaleAndAdd(vec3.create(), focalPoint, [-viewPlaneNormal[0], -viewPlaneNormal[1], -viewPlaneNormal[2]], projectedDistance);
const focalShift = vec3.subtract(vec3.create(), newImagePositionPatient, focalPoint);
const newPosition = vec3.add(vec3.create(), position, focalShift);
this.setCamera({
focalPoint: newImagePositionPatient,
position: newPosition,
});
this.render();
return;
}
if (cameraFocalPoint) {
const focalDelta = vec3.subtract([0, 0, 0], cameraFocalPoint, focalPoint);
const useNormal = refViewPlaneNormal ?? viewPlaneNormal;
const normalDot = vec3.dot(focalDelta, useNormal);
if (!isEqual(normalDot, 0)) {
vec3.scale(focalDelta, useNormal, normalDot);
}
const newFocal = vec3.add([0, 0, 0], focalPoint, focalDelta);
const newPosition = vec3.add([0, 0, 0], position, focalDelta);
this.setCamera({ focalPoint: newFocal, position: newPosition });
}
}
else {
throw new Error(`Incompatible view refs: ${refFrameOfReference}!==${this.getFrameOfReferenceUID()}`);
}
}
setThreshold(colormap, volumeId) {
const applicableVolumeActorInfo = this._getApplicableVolumeActor(volumeId);
if (!applicableVolumeActorInfo) {
return;
}
const { volumeActor } = applicableVolumeActorInfo;
colormapUpdateThreshold(volumeActor, colormap.threshold);
if (!this.viewportProperties.colormap) {
this.viewportProperties.colormap = {};
}
this.viewportProperties.colormap.threshold = colormap.threshold;
const matchedColormap = this.getColormap(volumeId);
const eventDetail = {
viewportId: this.id,
colormap: matchedColormap,
volumeId,
};
triggerEvent(this.element, Events.COLORMAP_MODIFIED, eventDetail);
}
setProperties({ voiRange, VOILUTFunction, invert, colormap, preset, interpolationType, slabThickness, sampleDistanceMultiplier, sharpening, smoothing, } = {}, volumeId, suppressEvents = false) {
if (this.globalDefaultProperties == null) {
this.setDefaultProperties({
voiRange,
VOILUTFunction,
invert,
colormap,
preset,
slabThickness,
sampleDistanceMultiplier,
});
}
if (invert !== undefined && this.viewportProperties.invert !== invert) {
this.setInvert(invert, volumeId, suppressEvents);
}
if (colormap?.name) {
this.setColormap(colormap, volumeId, suppressEvents);
}
if (colormap?.opacity != null || colormap?.opacityMapping != null) {
this.setOpacity(colormap, volumeId);
}
if (colormap?.threshold != null) {
this.setThreshold(colormap, volumeId);
}
if (voiRange !== undefined) {
this.setVOI(voiRange, volumeId, suppressEvents);
}
if (typeof interpolationType !== 'undefined') {
this.setInterpolationType(interpolationType);
}
if (VOILUTFunction !== undefined) {
this.setVOILUTFunction(VOILUTFunction, volumeId, suppressEvents);
}
if (preset !== undefined) {
this.setPreset(preset, volumeId, suppressEvents);
}
if (slabThickness !== undefined) {
this.setSlabThickness(slabThickness);
}
if (sampleDistanceMultiplier !== undefined) {
this.setSampleDistanceMultiplier(sampleDistanceMultiplier);
}
if (typeof sharpening !== 'undefined') {
this.setSharpening(sharpening);
}
if (typeof smoothing !== 'undefined') {
this.setSmoothing(smoothing);
}
}
shouldUseCustomRenderPass() {
return !this.useCPURendering;
}
_restoreVolumeRenderingDefaults(volumeActor, volumeId) {
if (!isImageActor(volumeActor)) {
return;
}
const properties = this.getDefaultProperties(volumeId);
if (properties?.preset || properties?.colormap) {
this.setProperties(properties, volumeId, true);
}
else {
const transferFunction = volumeActor.actor
.getProperty()
.getRGBTransferFunction(0);
setTransferFunctionNodes(transferFunction, this.initialTransferFunctionNodes);
}
}
resetToDefaultProperties(volumeId) {
const properties = this.globalDefaultProperties;
if (properties.colormap?.name) {
this.setColormap(properties.colormap, volumeId);
}
if (properties.colormap?.opacity != null) {
this.setOpacity(properties.colormap, volumeId);
}
if (properties.voiRange !== undefined) {
this.setVOI(properties.voiRange, volumeId);
}
if (properties.VOILUTFunction !== undefined) {
this.setVOILUTFunction(properties.VOILUTFunction, volumeId);
}
if (properties.invert !== undefined) {
this.setInvert(properties.invert, volumeId);
}
if (properties.slabThickness !== undefined) {
this.setSlabThickness(properties.slabThickness);
this.viewportProperties.slabThickness = properties.slabThickness;
}
if (properties.sampleDistanceMultiplier !== undefined) {
this.setSampleDistanceMultiplier(properties.sampleDistanceMultiplier);
}
if (properties.preset !== undefined) {
this.setPreset(properties.preset, volumeId, false);
}
this.render();
}
setPreset(presetNameOrObj, volumeId, suppressEvents) {
const applicableVolumeActorInfo = this._getApplicableVolumeActor(volumeId);
if (!applicableVolumeActorInfo) {
return;
}
const { volumeActor } = applicableVolumeActorInfo;
const viewportPreset = typeof presetNameOrObj === 'string'
? VIEWPORT_PRESETS.find((preset) => preset.name === presetNameOrObj)
: presetNameOrObj;
if (!viewportPreset) {
return;
}
applyPreset(volumeActor, viewportPreset);
this.viewportProperties.preset = viewportPreset.name;
this.render();
if (!suppressEvents) {
triggerEvent(this.element, Events.PRESET_MODIFIED, {
viewportId: this.id,
volumeId: applicableVolumeActorInfo.volumeId,
actor: volumeActor,
presetName: viewportPreset.name,
});
}
}
setSampleDistanceMultiplier(multiplier) { }
async setVolumes(volumeInputArray, immediate = false, suppressEvents = false) {
this.clearDisplaySets();
const volumeId = volumeInputArray[0].volumeId;
const firstImageVolume = cache.getVolume(volumeId);
if (!firstImageVolume) {
throw new Error(`imageVolume with id: ${volumeId} does not exist, you need to create/allocate the volume first`);
}
const FrameOfReferenceUID = firstImageVolume.metadata.FrameOfReferenceUID;
this._isValidVolumeInputArray(volumeInputArray, FrameOfReferenceUID);
this._FrameOfReferenceUID = FrameOfReferenceUID;
volumeInputArray.forEach((volumeInput) => {
this._addVolumeId(volumeInput.volumeId);
});
const volumeActors = [];
for (let i = 0; i < volumeInputArray.length; i++) {
const { volumeId, actorUID, slabThickness, ...rest } = volumeInputArray[i];
const actor = await createVolumeActor(volumeInputArray[i], this.element, this.id, suppressEvents);
const uid = actorUID || uuidv4();
volumeActors.push({
uid,
actor,
slabThickness,
referencedId: volumeId,
...rest,
});
}
this._setVolumeActors(volumeActors);
this.viewportStatus = ViewportStatus.PRE_RENDER;
this.initializeColorTransferFunction(volumeInputArray);
triggerEvent(this.element, Events.VOLUME_VIEWPORT_NEW_VOLUME, {
viewportId: this.id,
volumeActors,
});
if (immediate) {
this.render();
}
}
async setDisplaySets(...entries) {
await this.mountDisplaySets(entries, async (entry) => {
const dataSet = getGenericViewportImageDisplaySet(entry.displaySetId);
if (!dataSet?.imageIds?.length) {
throw new Error(`[VolumeViewport] No registered imageIds for display set ${entry.displaySetId}`);
}
const volumeId = resolveViewportVolumeId(dataSet.volumeId ?? entry.displaySetId);
if (!cache.getVolume(volumeId)) {
const volume = await createAndCacheVolume(volumeId, {
imageIds: dataSet.imageIds,
});
volume.load();
}
const volumeInput = {
volumeId,
...(entry.options ?? {}),
};
await this.setVolumes([volumeInput]);
});
}
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`);
}
const volumeActors = [];
this._isValidVolumeInputArray(volumeInputArray, this._FrameOfReferenceUID);
volumeInputArray.forEach((volumeInput) => {
this._addVolumeId(volumeInput.volumeId);
});
for (let i = 0; i < volumeInputArray.length; i++) {
const { volumeId, visibility, actorUID, slabThickness, ...rest } = volumeInputArray[i];
const actor = await createVolumeActor(volumeInputArray[i], this.element, this.id, suppressEvents);
if (!visibility) {
actor.setVisibility(false);
}
const uid = actorUID || uuidv4();
volumeActors.push({
uid,
actor,
slabThickness,
referencedId: volumeId,
...rest,
});
}
this.addActors(volumeActors);
this.initializeColorTransferFunction(volumeInputArray);
if (immediate) {
this.render();
}
}
removeVolumeActors(actorUIDs, immediate = false) {
this.removeActors(actorUIDs);
if (immediate) {
this.render();
}
}
setOrientation(_orientation, _immediate = true, _suppressEvents = false) {
console.warn('Method "setOrientation" needs implementation');
}
initializeColorTransferFunction(volumeInputArray) {
const selectedVolumeId = volumeInputArray[0].volumeId;
const colorTransferFunction = this._getOrCreateColorTransferFunction(selectedVolumeId);
if (!this.initialTransferFunctionNodes && colorTransferFunction) {
this.initialTransferFunctionNodes = getTransferFunctionNodes(colorTransferFunction);
}
}
_getApplicableVolumeActor(volumeId) {
const actorEntries = this.getActors();
if (!actorEntries?.length) {
return;
}
if (volumeId) {
const actorEntry = actorEntries.find((actor) => actor.referencedId === volumeId);
if (!actorEntry) {
return;
}
return {
volumeActor: actorEntry.actor,
volumeId,
actorUID: actorEntry.uid,
};
}
const defaultActorEntry = actorEntries[0];
return {
volumeActor: defaultActorEntry.actor,
volumeId: defaultActorEntry.referencedId,
actorUID: defaultActorEntry.uid,
};
}
async _isValidVolumeInputArray(volumeInputArray, FrameOfReferenceUID) {
const numVolumes = volumeInputArray.length;
for (let i = 1; i < numVolumes; i++) {
const imageVolume = cache.getVolume(volumeInputArray[i].volumeId);
if (FrameOfReferenceUID !== imageVolume.metadata.FrameOfReferenceUID) {
throw new Error(`Volumes being added to viewport ${this.id} do not share the same FrameOfReferenceUID. This is not yet supported`);
}
}
return true;
}
getBounds() {
const renderer = this.getRenderer();
const bounds = renderer.computeVisiblePropBounds();
return bounds;
}
flip(flipDirection) {
super.flip(flipDirection);
}
hasVolumeId(volumeId) {
return this.volumeIds.has(volumeId);
}
hasVolumeURI(volumeURI) {
for (const volumeId of this.volumeIds) {
if (volumeId.includes(volumeURI)) {
return true;
}
}
return false;
}
getImageData(volumeId) {
const defaultActor = this.getDefaultActor();
if (!defaultActor) {
return;
}
volumeId ||= this.getVolumeId();
const actorEntry = this.getActors()?.find((actor) => actor.referencedId === volumeId);
if (!actorEntry || !actorIsA(actorEntry, 'vtkVolume')) {
return;
}
const actor = actorEntry.actor;
const volume = cache.getVolume(volumeId);
const vtkImageData = actor.getMapper().getInputData();
return {
dimensions: vtkImageData.getDimensions(),
spacing: vtkImageData.getSpacing(),
origin: vtkImageData.getOrigin(),
direction: vtkImageData.getDirection(),
imageData: actor.getMapper().getInputData(),
meta