UNPKG

@cornerstonejs/core

Version:
1,092 lines • 57.5 kB
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