UNPKG

@cornerstonejs/core

Version:
1,169 lines • 88.3 kB
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData.js'; import extendedVtkCamera from './vtkClasses/extendedVtkCamera.js'; import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction.js'; import vtkImageMapper from '@kitware/vtk.js/Rendering/Core/ImageMapper.js'; import vtkImageSlice from '@kitware/vtk.js/Rendering/Core/ImageSlice.js'; import { mat4, vec2, vec3 } from 'gl-matrix'; import eventTarget from '../eventTarget.js'; import * as metaData from '../metaData.js'; import { getImageDataMetadata as getImageDataMetadataUtil } from '../utilities/getImageDataMetadata.js'; import { coreLog } from '../utilities/logger.js'; import { getGenericViewportImageDisplaySet } from './GenericViewport/genericViewportDisplaySetAccess.js'; import { actorIsA, isImageActor } from '../utilities/actorCheck.js'; import * as colormapUtils from '../utilities/colormap.js'; import { getTransferFunctionNodes, setTransferFunctionNodes, } from '../utilities/transferFunctionUtils.js'; import * as windowLevelUtil from '../utilities/windowLevel.js'; import createLinearRGBTransferFunction from '../utilities/createLinearRGBTransferFunction.js'; import createSigmoidRGBTransferFunction from '../utilities/createSigmoidRGBTransferFunction.js'; import { updateVTKImageDataWithCornerstoneImage } from '../utilities/updateVTKImageDataWithCornerstoneImage.js'; import triggerEvent from '../utilities/triggerEvent.js'; import { isEqual } from '../utilities/isEqual.js'; import invertRgbTransferFunction from '../utilities/invertRgbTransferFunction.js'; import imageRetrieveMetadataProvider from '../utilities/imageRetrieveMetadataProvider.js'; import imageIdToURI from '../utilities/imageIdToURI.js'; import getVOIRangeFromWindowLevel from '../utilities/getVOIRangeFromWindowLevel.js'; import Viewport from './Viewport.js'; import drawImageSync from './helpers/cpuFallback/drawImageSync.js'; import { resolveCPUFallbackColormap } from './helpers/cpuFallback/colors/index.js'; import { getImagePlaneModule } from '../utilities/buildMetadata.js'; import { createEmptyVTKImageData } from './helpers/planarImageRendering.js'; import { Events, InterpolationType, MetadataModules, RequestType, VOILUTFunctionType, ViewportStatus, } from '../enums/index.js'; import { loadAndCacheImage } from '../loaders/imageLoader.js'; import imageLoadPoolManager from '../requestPool/imageLoadPoolManager.js'; import calculateTransform from './helpers/cpuFallback/rendering/calculateTransform.js'; import canvasToPixel from './helpers/cpuFallback/rendering/canvasToPixel.js'; import getDefaultViewport from './helpers/cpuFallback/rendering/getDefaultViewport.js'; import pixelToCanvas from './helpers/cpuFallback/rendering/pixelToCanvas.js'; import { cssToDevicePixels, deviceToCssPixels, } from './helpers/cpuFallback/rendering/cssPixelConversion.js'; import resize from './helpers/cpuFallback/rendering/resize.js'; import cache from '../cache/cache.js'; import { getShouldUseCPURendering } from '../init.js'; import { createProgressive } from '../loaders/ProgressiveRetrieveImages.js'; import correctShift from './helpers/cpuFallback/rendering/correctShift.js'; import resetCamera from './helpers/cpuFallback/rendering/resetCamera.js'; import { Transform } from './helpers/cpuFallback/rendering/transform.js'; import uuidv4 from '../utilities/uuidv4.js'; import getSpacingInNormalDirection from '../utilities/getSpacingInNormalDirection.js'; import getClosestImageId from '../utilities/getClosestImageId.js'; import { adjustInitialViewUp } from '../utilities/adjustInitialViewUp.js'; import { isContextPoolRenderingEngine } from './helpers/isContextPoolRenderingEngine.js'; import { createSharpeningRenderPass, createSmoothingRenderPass, } from './renderPasses/index.js'; const log = coreLog.getLogger('RenderingEngine', 'StackViewport'); class StackViewport extends Viewport { constructor(props) { super(props); this.imageIds = []; this.imageKeyToIndexMap = new Map(); this.currentImageIdIndex = 0; this.targetImageIdIndex = 0; this.imagesLoader = this; this.globalDefaultProperties = {}; this.perImageIdDefaultProperties = new Map(); this.voiUpdatedWithSetProperties = false; this.sharpening = 0; this.smoothing = 0; this.invert = false; this.initialInvert = false; this.initialTransferFunctionNodes = null; this.stackInvalidated = false; this._publishCalibratedEvent = false; this.updateRenderingPipeline = () => { this._configureRenderingPipeline(); }; 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.resize = () => { if (this.useCPURendering) { this._resizeCPU(); } }; this._resizeCPU = () => { if (this._cpuFallbackEnabledElement.viewport) { resize(this._cpuFallbackEnabledElement); } }; this.getFrameOfReferenceUID = (sliceIndex) => this.getImagePlaneReferenceData(sliceIndex)?.FrameOfReferenceUID; this.getCornerstoneImage = () => this.csImage; this.createActorMapper = (imageData) => { const mapper = vtkImageMapper.newInstance(); mapper.setInputData(imageData); const actor = vtkImageSlice.newInstance(); actor.setMapper(mapper); if (imageData.getPointData().getScalars().getNumberOfComponents() > 1) { actor.getProperty().setIndependentComponents(false); } return actor; }; this.getNumberOfSlices = () => { return this.imageIds.length; }; this.getDefaultProperties = (imageId) => { let imageProperties; if (imageId !== undefined) { imageProperties = this.perImageIdDefaultProperties.get(imageId); } if (imageProperties !== undefined) { return imageProperties; } return { ...this.globalDefaultProperties, }; }; this.getProperties = () => { const { colormap, voiRange, VOILUTFunction, interpolationType, invert, voiUpdatedWithSetProperties, } = this; return { colormap, voiRange, VOILUTFunction, interpolationType, invert, isComputedVOI: !voiUpdatedWithSetProperties, sharpening: this.sharpening, smoothing: this.smoothing, }; }; this.resetCameraForResize = () => { return this.resetCamera({ resetPan: true, resetZoom: true, resetToCenter: true, resetAspectRatio: true, suppressEvents: true, }); }; this.getRotationCPU = () => { const { viewport } = this._cpuFallbackEnabledElement; return viewport.rotation; }; this.getRotationGPU = () => { const { viewUp: currentViewUp, viewPlaneNormal, flipVertical, flipHorizontal, } = this.getCameraNoRotation(); const adjustedViewUp = adjustInitialViewUp(this.initialViewUp, flipHorizontal, flipVertical, viewPlaneNormal); const angleRad = vec3.angle(adjustedViewUp, currentViewUp); const initialToCurrentViewUpAngle = (angleRad * 180) / Math.PI; const initialToCurrentViewUpCross = vec3.cross(vec3.create(), adjustedViewUp, currentViewUp); const normalDot = vec3.dot(initialToCurrentViewUpCross, viewPlaneNormal); return normalDot >= 0 ? initialToCurrentViewUpAngle : (360 - initialToCurrentViewUpAngle) % 360; }; this.setRotation = (rotation) => { const previousCamera = this.getCamera(); if (this.useCPURendering) { this.setRotationCPU(rotation); } else { this.setRotationGPU(rotation); } 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.renderImageObject = (image) => { this._setCSImage(image); const renderFn = this.useCPURendering ? this._updateToDisplayImageCPU : this._updateActorToDisplayImageId; renderFn.call(this, image); }; this._setCSImage = (image) => { image.isPreScaled = image.preScale?.scaled; this.csImage = image; }; this.canvasToWorldCPU = (canvasPos, worldPos = [0, 0, 0]) => { if (!this._cpuFallbackEnabledElement.image) { return; } const [px, py] = canvasToPixel(this._cpuFallbackEnabledElement, cssToDevicePixels(canvasPos)); const { origin, spacing, direction } = this.getImageData(); const iVector = direction.slice(0, 3); const jVector = direction.slice(3, 6); vec3.scaleAndAdd(worldPos, origin, iVector, px * spacing[0]); vec3.scaleAndAdd(worldPos, worldPos, jVector, py * spacing[1]); return worldPos; }; this.worldToCanvasCPU = (worldPos) => { const { spacing, direction, origin } = this.getImageData(); const iVector = direction.slice(0, 3); const jVector = direction.slice(3, 6); const diff = vec3.subtract(vec3.create(), worldPos, origin); const indexPoint = [ vec3.dot(diff, iVector) / spacing[0], vec3.dot(diff, jVector) / spacing[1], ]; const canvasPoint = pixelToCanvas(this._cpuFallbackEnabledElement, indexPoint); return deviceToCssPixels(canvasPoint); }; this.canvasToWorldGPUContextPool = (canvasPos) => { const renderer = this.getRenderer(); const vtkCamera = this.getVtkActiveCamera(); const crange = vtkCamera.getClippingRange(); const distance = vtkCamera.getDistance(); vtkCamera.setClippingRange(distance, distance + 0.1); const devicePixelRatio = window.devicePixelRatio || 1; const { width, height } = this.canvas; const aspectRatio = width / height; const canvasPosWithDPR = [ canvasPos[0] * devicePixelRatio, canvasPos[1] * devicePixelRatio, ]; const viewport = renderer.getViewport(); const [xStart, yStart, xEnd, yEnd] = viewport; const viewportWidth = xEnd - xStart; const viewportHeight = yEnd - yStart; const normalizedDisplay = [ xStart + (canvasPosWithDPR[0] / width) * viewportWidth, yStart + (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.setClippingRange(crange[0], crange[1]); return [worldCoord[0], worldCoord[1], worldCoord[2]]; }; this.canvasToWorldGPUTiled = (canvasPos) => { const renderer = this.getRenderer(); const vtkCamera = this.getVtkActiveCamera(); const crange = vtkCamera.getClippingRange(); const distance = vtkCamera.getDistance(); vtkCamera.setClippingRange(distance, distance + 0.1); const offscreenMultiRenderWindow = this.getRenderingEngine().offscreenMultiRenderWindow; const openGLRenderWindow = offscreenMultiRenderWindow.getOpenGLRenderWindow(); const size = openGLRenderWindow.getSize(); const devicePixelRatio = window.devicePixelRatio || 1; const canvasPosWithDPR = [ canvasPos[0] * devicePixelRatio, canvasPos[1] * devicePixelRatio, ]; const displayCoord = [ canvasPosWithDPR[0] + this.sx, canvasPosWithDPR[1] + this.sy, ]; displayCoord[1] = size[1] - displayCoord[1]; const worldCoord = openGLRenderWindow.displayToWorld(displayCoord[0], displayCoord[1], 0, renderer); vtkCamera.setClippingRange(crange[0], crange[1]); return [worldCoord[0], worldCoord[1], worldCoord[2]]; }; this.worldToCanvasGPUContextPool = (worldPos) => { const renderer = this.getRenderer(); const vtkCamera = this.getVtkActiveCamera(); const crange = vtkCamera.getClippingRange(); const distance = vtkCamera.getDistance(); vtkCamera.setClippingRange(distance, distance + 0.1); const devicePixelRatio = window.devicePixelRatio || 1; const { width, height } = this.canvas; const aspectRatio = width / height; 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 viewport = renderer.getViewport(); const [xStart, yStart, xEnd, yEnd] = viewport; const viewportWidth = xEnd - xStart; const viewportHeight = yEnd - yStart; const canvasX = ((normalizedDisplay[0] - xStart) / viewportWidth) * width; const canvasY = (1 - (normalizedDisplay[1] - yStart) / viewportHeight) * height; vtkCamera.setClippingRange(crange[0], crange[1]); const canvasCoordWithDPR = [ canvasX / devicePixelRatio, canvasY / devicePixelRatio, ]; return canvasCoordWithDPR; }; this.worldToCanvasGPUTiled = (worldPos) => { const renderer = this.getRenderer(); const vtkCamera = this.getVtkActiveCamera(); const crange = vtkCamera.getClippingRange(); const distance = vtkCamera.getDistance(); vtkCamera.setClippingRange(distance, distance + 0.1); 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, ]; vtkCamera.setClippingRange(crange[0], crange[1]); const devicePixelRatio = window.devicePixelRatio || 1; const canvasCoordWithDPR = [ canvasCoord[0] / devicePixelRatio, canvasCoord[1] / devicePixelRatio, ]; return canvasCoordWithDPR; }; this.getCurrentImageIdIndex = () => { return this.currentImageIdIndex; }; this.getSliceIndex = () => { return this.currentImageIdIndex; }; this.getTargetImageIdIndex = () => { return this.targetImageIdIndex; }; this.getImageIds = () => { return this.imageIds; }; this.getCurrentImageId = (index = this.getCurrentImageIdIndex()) => { return this.imageIds[index]; }; this.hasImageId = (imageId) => { return this.imageKeyToIndexMap.has(imageId); }; this.hasImageURI = (imageURI) => { return this.imageKeyToIndexMap.has(imageURI); }; this.customRenderViewportToCanvas = () => { if (!this.useCPURendering) { throw new Error('Custom cpu rendering pipeline should only be hit in CPU rendering mode'); } if (this._cpuFallbackEnabledElement.image) { drawImageSync(this._cpuFallbackEnabledElement, this.cpuRenderingInvalidated); this.cpuRenderingInvalidated = false; } else { this.fillWithBackgroundColor(); } return { canvas: this.canvas, element: this.element, viewportId: this.id, renderingEngineId: this.renderingEngineId, viewportStatus: this.viewportStatus, }; }; this.renderingPipelineFunctions = { getImageData: { cpu: this.getImageDataCPU, gpu: this.getImageDataGPU, }, setColormap: { cpu: this.setColormapCPU, gpu: this.setColormapGPU, }, getCamera: { cpu: this.getCameraCPU, gpu: super.getCamera, }, setCamera: { cpu: this.setCameraCPU, gpu: super.setCamera, }, getPan: { cpu: this.getPanCPU, gpu: super.getPan, }, setPan: { cpu: this.setPanCPU, gpu: super.setPan, }, getZoom: { cpu: this.getZoomCPU, gpu: super.getZoom, }, setZoom: { cpu: this.setZoomCPU, gpu: super.setZoom, }, getAspectRatio: { cpu: this.getAspectRatioCPU, gpu: super.getAspectRatio, }, setAspectRatio: { cpu: this.setAspectRatioCPU, gpu: super.setAspectRatio, }, setVOI: { cpu: this.setVOICPU, gpu: this.setVOIGPU, }, getRotation: { cpu: this.getRotationCPU, gpu: this.getRotationGPU, }, setInterpolationType: { cpu: this.setInterpolationTypeCPU, gpu: this.setInterpolationTypeGPU, }, setInvertColor: { cpu: this.setInvertColorCPU, gpu: this.setInvertColorGPU, }, resetCamera: { cpu: (options = {}) => { const { resetPan = true, resetZoom = true } = options; this.resetCameraCPU({ resetPan, resetZoom }); return true; }, gpu: (options = {}) => { const { resetPan = true, resetZoom = true, resetAspectRatio = true, } = options; this.resetCameraGPU({ resetPan, resetZoom, resetAspectRatio }); return true; }, }, canvasToWorld: { cpu: this.canvasToWorldCPU, gpu: { tiled: this.canvasToWorldGPUTiled, contextPool: this.canvasToWorldGPUContextPool, }, }, worldToCanvas: { cpu: this.worldToCanvasCPU, gpu: { tiled: this.worldToCanvasGPUTiled, contextPool: this.worldToCanvasGPUContextPool, }, }, getRenderer: { cpu: () => this.getCPUFallbackError('getRenderer'), gpu: { tiled: this.getRendererTiled, contextPool: this.getRendererContextPool, }, }, getDefaultActor: { cpu: () => this.getCPUFallbackError('getDefaultActor'), gpu: super.getDefaultActor, }, getActors: { cpu: () => this.getCPUFallbackError('getActors'), gpu: super.getActors, }, getActor: { cpu: () => this.getCPUFallbackError('getActor'), gpu: super.getActor, }, setActors: { cpu: () => this.getCPUFallbackError('setActors'), gpu: super.setActors, }, addActors: { cpu: () => this.getCPUFallbackError('addActors'), gpu: super.addActors, }, addActor: { cpu: () => this.getCPUFallbackError('addActor'), gpu: super.addActor, }, removeAllActors: { cpu: () => this.getCPUFallbackError('removeAllActors'), gpu: super.removeAllActors, }, unsetColormap: { cpu: this.unsetColormapCPU, gpu: this.unsetColormapGPU, }, }; this.scaling = {}; this.modality = null; this.useCPURendering = getShouldUseCPURendering(); this._configureRenderingPipeline(); if (this.useCPURendering) { this._resetCPUFallbackElement(); } else { this._resetGPUViewport(); } this.currentImageIdIndex = 0; this.targetImageIdIndex = 0; this.resetCamera(); this.initializeElementDisabledHandler(); } setUseCPURendering(value) { this.useCPURendering = value; this._configureRenderingPipeline(value); } static get useCustomRenderingPipeline() { return getShouldUseCPURendering(); } _configureRenderingPipeline(value) { const isContextPool = isContextPoolRenderingEngine(); this.useCPURendering = value ?? getShouldUseCPURendering(); for (const key in this.renderingPipelineFunctions) { if (Object.prototype.hasOwnProperty.call(this.renderingPipelineFunctions, key)) { const functions = this.renderingPipelineFunctions[key]; if (this.useCPURendering) { this[key] = functions.cpu; } else { if (typeof functions.gpu === 'object' && functions.gpu.tiled && functions.gpu.contextPool) { this[key] = isContextPool ? functions.gpu.contextPool : functions.gpu.tiled; } else { this[key] = functions.gpu; } } } } if (this.useCPURendering) { this._resetCPUFallbackElement(); } else { this._resetGPUViewport(); } } _resetCPUFallbackElement() { this._cpuFallbackEnabledElement = { canvas: this.canvas, renderingTools: {}, transform: new Transform(), viewport: { rotation: 0, aspectRatio: [1, 1] }, }; } _resetGPUViewport() { const renderer = this.getRenderer(); const camera = extendedVtkCamera.newInstance(); renderer.setActiveCamera(camera); const viewPlaneNormal = [0, 0, -1]; this.initialViewUp = [0, -1, 0]; camera.setDirectionOfProjection(-viewPlaneNormal[0], -viewPlaneNormal[1], -viewPlaneNormal[2]); camera.setViewUp(...this.initialViewUp); camera.setParallelProjection(true); camera.setThicknessFromFocalPoint(0.1); camera.setFreezeFocalPoint(true); } shouldUseCustomRenderPass() { return !this.useCPURendering; } initializeElementDisabledHandler() { eventTarget.addEventListener(Events.ELEMENT_DISABLED, function elementDisabledHandler() { clearTimeout(this.debouncedTimeout); eventTarget.removeEventListener(Events.ELEMENT_DISABLED, elementDisabledHandler); }); } getImageDataGPU() { const defaultActor = this.getDefaultActor(); if (!defaultActor) { return; } if (!isImageActor(defaultActor)) { return; } const { actor } = defaultActor; const vtkImageData = actor.getMapper().getInputData(); const csImage = this.csImage; return { dimensions: vtkImageData.getDimensions(), spacing: vtkImageData.getSpacing(), origin: vtkImageData.getOrigin(), direction: vtkImageData.getDirection(), get scalarData() { return csImage?.voxelManager.getScalarData(); }, imageData: actor.getMapper().getInputData(), metadata: { Modality: this.modality, FrameOfReferenceUID: this.getFrameOfReferenceUID(), }, scaling: this.scaling, hasPixelSpacing: this.hasPixelSpacing, calibration: { ...csImage?.calibration, ...this.calibration }, preScale: { ...csImage?.preScale, }, voxelManager: csImage?.voxelManager, }; } getImageDataCPU() { const { metadata } = this._cpuFallbackEnabledElement; if (!metadata) { return; } const spacing = metadata.spacing; const csImage = this.csImage; return { dimensions: metadata.dimensions, spacing, origin: metadata.origin, direction: metadata.direction, metadata: { Modality: this.modality, FrameOfReferenceUID: this.getFrameOfReferenceUID(), }, scaling: this.scaling, imageData: { getDirection: () => metadata.direction, getDimensions: () => metadata.dimensions, getScalarData: () => this.cpuImagePixelData, getSpacing: () => spacing, worldToIndex: (point) => { const canvasPoint = this.worldToCanvasCPU(point); const pixelCoord = canvasToPixel(this._cpuFallbackEnabledElement, cssToDevicePixels(canvasPoint)); return [pixelCoord[0], pixelCoord[1], 0]; }, indexToWorld: (point, destPoint) => { const canvasPoint = pixelToCanvas(this._cpuFallbackEnabledElement, [ point[0], point[1], ]); return this.canvasToWorldCPU(deviceToCssPixels(canvasPoint), destPoint); }, }, scalarData: this.cpuImagePixelData, hasPixelSpacing: this.hasPixelSpacing, calibration: { ...csImage?.calibration, ...this.calibration }, preScale: { ...csImage?.preScale, }, voxelManager: csImage?.voxelManager, }; } calibrateIfNecessary(imageId, imagePlaneModule) { const calibration = metaData.get('calibratedPixelSpacing', imageId); const isUpdated = this.calibration !== calibration; const scale = calibration?.scale; this.hasPixelSpacing = scale > 0 || (!imagePlaneModule.usingDefaultValues && imagePlaneModule.rowPixelSpacing > 0); imagePlaneModule.calibration = calibration; if (!isUpdated) { return imagePlaneModule; } this.calibration = calibration; this._publishCalibratedEvent = true; this._calibrationEvent = { scale, calibration, }; return imagePlaneModule; } setDefaultProperties(ViewportProperties, imageId) { if (imageId == null) { this.globalDefaultProperties = ViewportProperties; } else { this.perImageIdDefaultProperties.set(imageId, ViewportProperties); if (this.getCurrentImageId() === imageId) { this.setProperties(ViewportProperties); } } } clearDefaultProperties(imageId) { if (imageId == null) { this.globalDefaultProperties = {}; this.resetProperties(); } else { this.perImageIdDefaultProperties.delete(imageId); this.resetToDefaultProperties(); } } setProperties({ colormap, voiRange, VOILUTFunction, invert, interpolationType, sharpening, smoothing, } = {}, suppressEvents = false) { this.viewportStatus = this.csImage ? ViewportStatus.PRE_RENDER : ViewportStatus.LOADING; this.globalDefaultProperties = { colormap: this.globalDefaultProperties.colormap ?? colormap, voiRange: this.globalDefaultProperties.voiRange ?? voiRange, VOILUTFunction: this.globalDefaultProperties.VOILUTFunction ?? VOILUTFunction, invert: this.globalDefaultProperties.invert ?? invert, interpolationType: this.globalDefaultProperties.interpolationType ?? interpolationType, sharpening: this.globalDefaultProperties.sharpening ?? sharpening, smoothing: this.globalDefaultProperties.smoothing ?? smoothing, }; if (typeof colormap !== 'undefined') { this.setColormap(colormap); } if (typeof voiRange !== 'undefined') { const voiUpdatedWithSetProperties = true; this.setVOI(voiRange, { suppressEvents, voiUpdatedWithSetProperties }); } if (typeof VOILUTFunction !== 'undefined') { this.setVOILUTFunction(VOILUTFunction, suppressEvents); } if (typeof invert !== 'undefined') { this.setInvertColor(invert); } if (typeof interpolationType !== 'undefined') { this.setInterpolationType(interpolationType); } if (typeof sharpening !== 'undefined') { this.setSharpening(sharpening); } if (typeof smoothing !== 'undefined') { this.setSmoothing(smoothing); } } resetProperties() { this.cpuRenderingInvalidated = true; this.voiUpdatedWithSetProperties = false; this.viewportStatus = ViewportStatus.PRE_RENDER; this.fillWithBackgroundColor(); if (this.useCPURendering) { this._cpuFallbackEnabledElement.renderingTools = {}; } this._resetProperties(); this.render(); } _resetProperties() { let voiRange; if (this._isCurrentImagePTPrescaled()) { voiRange = this._getDefaultPTPrescaledVOIRange(); } else { voiRange = this._getVOIRangeForCurrentImage(); } this.setVOI(voiRange); this.setInvertColor(this.initialInvert); this.setInterpolationType(InterpolationType.LINEAR); if (!this.useCPURendering) { const transferFunction = this.getTransferFunction(); setTransferFunctionNodes(transferFunction, this.initialTransferFunctionNodes); const nodes = getTransferFunctionNodes(transferFunction); const RGBPoints = nodes.reduce((acc, node) => { acc.push(node[0], node[1], node[2], node[3]); return acc; }, []); const defaultActor = this.getDefaultActor(); const matchedColormap = colormapUtils.findMatchingColormap(RGBPoints, defaultActor.actor); this.setColormap(matchedColormap); } } resetToDefaultProperties() { this.cpuRenderingInvalidated = true; this.viewportStatus = ViewportStatus.PRE_RENDER; this.fillWithBackgroundColor(); if (this.useCPURendering) { this._cpuFallbackEnabledElement.renderingTools = {}; } const currentImageId = this.getCurrentImageId(); const properties = this.perImageIdDefaultProperties.get(currentImageId) || this.globalDefaultProperties; if (properties.colormap?.name) { this.setColormap(properties.colormap); } let voiRange; if (properties.voiRange == undefined) { voiRange = this._getVOIRangeForCurrentImage(); } else { voiRange = properties.voiRange; } this.setVOI(voiRange); this.setInterpolationType(InterpolationType.LINEAR); this.setInvertColor(false); this.render(); } _getVOIFromCache() { let voiRange; if (this.voiUpdatedWithSetProperties) { voiRange = this.voiRange; } else if (this._isCurrentImagePTPrescaled()) { voiRange = this._getDefaultPTPrescaledVOIRange(); } else { voiRange = this._getVOIRangeForCurrentImage() ?? this.voiRange; } return voiRange; } _setPropertiesFromCache() { const voiRange = this._getVOIFromCache(); const { colormap, VOILUTFunction, interpolationType, invert, sharpening, smoothing, } = this.getProperties(); if (typeof VOILUTFunction !== 'undefined') { this.setVOILUTFunction(VOILUTFunction, true); } this.setVOI(voiRange); if (typeof colormap !== 'undefined') { this.setColormap(colormap); this.invert = false; } this.setInterpolationType(interpolationType); this.setInvertColor(invert); if (typeof sharpening !== 'undefined') { this.setSharpening(sharpening); } if (typeof smoothing !== 'undefined') { this.setSmoothing(smoothing); } } getCameraCPU() { const { metadata, viewport } = this._cpuFallbackEnabledElement; if (!metadata) { return {}; } const { direction } = metadata; const viewPlaneNormal = direction.slice(6, 9).map((x) => -x); let viewUp = direction.slice(3, 6).map((x) => -x); if (viewport.rotation) { const rotationMatrix = mat4.fromRotation(mat4.create(), (viewport.rotation * Math.PI) / 180, viewPlaneNormal); viewUp = vec3.transformMat4(vec3.create(), viewUp, rotationMatrix); } const canvasCenter = [ this.element.clientWidth / 2, this.element.clientHeight / 2, ]; const canvasCenterWorld = this.canvasToWorld(canvasCenter); const topLeftWorld = this.canvasToWorld([0, 0]); const bottomLeftWorld = this.canvasToWorld([0, this.element.clientHeight]); const parallelScale = vec3.distance(topLeftWorld, bottomLeftWorld) / 2; return { parallelProjection: true, focalPoint: canvasCenterWorld, position: [0, 0, 0], parallelScale, scale: viewport.scale, viewPlaneNormal: [ viewPlaneNormal[0], viewPlaneNormal[1], viewPlaneNormal[2], ], aspectRatio: viewport.aspectRatio ?? [1, 1], viewUp: [viewUp[0], viewUp[1], viewUp[2]], flipHorizontal: this.flipHorizontal, flipVertical: this.flipVertical, }; } setAspectRatioForViewport(aspectRatio, isFitViewportAfterStretch = true) { const { viewport, image } = this._cpuFallbackEnabledElement; if (!isFitViewportAfterStretch) { viewport.aspectRatio = aspectRatio; return; } const { clientWidth, clientHeight } = this.element; const { rowPixelSpacing, columnPixelSpacing, width, height } = image; const getRatioValue = ([x, y]) => x / y; const oldRatioValue = getRatioValue(viewport.aspectRatio || [1, 1]); const newRatioValue = getRatioValue(aspectRatio); const canvasRatio = clientWidth / clientHeight; const baseHeight = clientHeight * rowPixelSpacing * 0.5; const calculateFitParallelScale = (rVal) => { const effectiveWidth = width * columnPixelSpacing * rVal; const effectiveHeight = height * rowPixelSpacing; if (effectiveWidth / effectiveHeight > canvasRatio) { return baseHeight / (clientWidth / effectiveWidth); } const fitPScale = effectiveHeight * 0.5; return rVal < 1 ? fitPScale / rVal : fitPScale; }; const ratioFactor = calculateFitParallelScale(newRatioValue) / calculateFitParallelScale(oldRatioValue); viewport.aspectRatio = aspectRatio; if (viewport.parallelScale) { viewport.parallelScale *= ratioFactor; viewport.scale = baseHeight / viewport.parallelScale; } } setCameraCPU(cameraInterface) { const { viewport, image } = this._cpuFallbackEnabledElement; const previousCamera = this.getCameraCPU(); const { focalPoint, parallelScale, scale, flipHorizontal, flipVertical, aspectRatio, isFitViewportAfterStretch, } = cameraInterface; const { clientHeight } = this.element; if (focalPoint) { const focalPointCanvas = this.worldToCanvasCPU(focalPoint); const focalPointPixel = canvasToPixel(this._cpuFallbackEnabledElement, cssToDevicePixels(focalPointCanvas)); const prevFocalPointCanvas = this.worldToCanvasCPU(previousCamera.focalPoint); const prevFocalPointPixel = canvasToPixel(this._cpuFallbackEnabledElement, cssToDevicePixels(prevFocalPointCanvas)); const deltaPixel = vec2.create(); vec2.subtract(deltaPixel, vec2.fromValues(focalPointPixel[0], focalPointPixel[1]), vec2.fromValues(prevFocalPointPixel[0], prevFocalPointPixel[1])); const shift = correctShift({ x: deltaPixel[0], y: deltaPixel[1] }, viewport); viewport.translation.x -= shift.x; viewport.translation.y -= shift.y; } if (parallelScale) { const { rowPixelSpacing } = image; const scale = (clientHeight * rowPixelSpacing * 0.5) / parallelScale; viewport.scale = scale; viewport.parallelScale = parallelScale; } if (scale) { const { rowPixelSpacing } = image; viewport.scale = scale; viewport.parallelScale = (clientHeight * rowPixelSpacing * 0.5) / scale; } if (aspectRatio) { this.setAspectRatioForViewport(aspectRatio, isFitViewportAfterStretch); } if (flipHorizontal !== undefined || flipVertical !== undefined) { this.setFlipCPU({ flipHorizontal, flipVertical }); } this._cpuFallbackEnabledElement.transform = calculateTransform(this._cpuFallbackEnabledElement); const eventDetail = { previousCamera, camera: this.getCamera(), element: this.element, viewportId: this.id, renderingEngineId: this.renderingEngineId, }; triggerEvent(this.element, Events.CAMERA_MODIFIED, eventDetail); } getPanCPU() { const { viewport } = this._cpuFallbackEnabledElement; return [viewport.translation?.x ?? 0, viewport.translation?.y ?? 0]; } setPanCPU(pan) { const camera = this.getCameraCPU(); this.setCameraCPU({ ...camera, focalPoint: [...pan.map((p) => -p), 0], }); } getZoomCPU() { const { viewport } = this._cpuFallbackEnabledElement; return viewport.scale; } setZoomCPU(zoom) { const camera = this.getCameraCPU(); this.setCameraCPU({ ...camera, scale: zoom }); } getAspectRatioCPU() { const { aspectRatio } = this.getCameraCPU(); return aspectRatio ?? this.options?.aspectRatio ?? [1, 1]; } setAspectRatioCPU(value, isFitViewportAfterStretch = true, storeAsInitialCamera = false) { const camera = this.getCameraCPU(); if (storeAsInitialCamera) { this.options.aspectRatio = value; } this.setCameraCPU({ ...camera, aspectRatio: value, isFitViewportAfterStretch, }); } setFlipCPU({ flipHorizontal, flipVertical }) { const { viewport } = this._cpuFallbackEnabledElement; if (flipHorizontal !== undefined) { viewport.hflip = flipHorizontal; this.flipHorizontal = viewport.hflip; } if (flipVertical !== undefined) { viewport.vflip = flipVertical; this.flipVertical = viewport.vflip; } } setVOILUTFunction(voiLUTFunction, suppressEvents) { if (this.useCPURendering) { throw new Error('VOI LUT function is not supported in CPU rendering'); } const newVOILUTFunction = this._getValidVOILUTFunction(voiLUTFunction); let forceRecreateLUTFunction = false; if (this.VOILUTFunction !== newVOILUTFunction) { forceRecreateLUTFunction = true; } this.VOILUTFunction = newVOILUTFunction; const { voiRange } = this.getProperties(); this.setVOI(voiRange, { suppressEvents, forceRecreateLUTFunction }); } setRotationCPU(rotation) { const { viewport } = this._cpuFallbackEnabledElement; viewport.rotation = rotation; } setRotationGPU(rotation) { const panFit = this.getPan(this.fitToCanvasCamera); const pan = this.getPan(); const panSub = vec2.sub([0, 0], panFit, pan); this.setPan(panSub, false); const { flipVertical, flipHorizontal, viewPlaneNormal } = this.getCamera(); const adjustedViewUp = adjustInitialViewUp(this.initialViewUp, flipHorizontal, flipVertical, viewPlaneNormal); this.setCameraNoEvent({ viewUp: adjustedViewUp, }); this.getVtkActiveCamera().roll(-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); } setInterpolationTypeGPU(interpolationType) { const defaultActor = this.getDefaultActor(); if (!defaultActor) { return; } if (!isImageActor(defaultActor)) { return; } const { actor } = defaultActor; const volumeProperty = actor.getProperty(); volumeProperty.setInterpolationType(interpolationType); this.interpolationType = interpolationType; } setInterpolationTypeCPU(interpolationType) { const { viewport } = this._cpuFallbackEnabledElement; viewport.pixelReplication = interpolationType === InterpolationType.LINEAR ? false : true; this.interpolationType = interpolationType; } setInvertColorCPU(invert) { const { viewport } = this._cpuFallbackEnabledElement; if (!viewport) { return; } viewport.invert = invert; this.invert = invert; } setInvertColorGPU(invert) { const defaultActor = this.getDefaultActor(); if (!defaultActor) { return; } if (!isImageActor(defaultActor)) { return; } if (actorIsA(defaultActor, 'vtkVolume')) { const volumeActor = defaultActor.actor; const tfunc = volumeActor.getProperty().getRGBTransferFunction(0); if ((!this.invert && invert) || (this.invert && !invert)) { invertRgbTransferFunction(tfunc); } this.invert = invert; } else if (actorIsA(defaultActor, 'vtkImageSlice')) { const imageSliceActor = defaultActor.actor; const tfunc = imageSliceActor.getProperty().getRGBTransferFunction(0); if ((!this.invert && invert) || (this.invert && !invert)) { invertRgbTransferFunction(tfunc); } this.invert = invert; } } setVOICPU(voiRange, options = {}) { const { suppressEvents = false } = options; const { viewport, image } = this._cpuFallbackEnabledElement; if (!viewport || !image) { return; } if (typeof voiRange === 'undefined') { const { windowWidth: ww, windowCenter: wc } = image; const wwToUse = Array.isArray(ww) ? ww[0] : ww; const wcToUse = Array.isArray(wc) ? wc[0] : wc; viewport.voi = { windowWidth: wwToUse, windowCenter: wcToUse, voiLUTFunction: image.voiLUTFunction, }; const { lower, upper } = getVOIRangeFromWindowLevel(wwToUse, wcToUse, image.voiLUTFunction); voiRange = { lower, upper }; } else { const { lower, upper } = voiRange; const { windowCenter, windowWidth } = windowLevelUtil.toWindowLevel(lower, upper); if (!viewport.voi) { viewport.voi = { windowWidth: 0, windowCenter: 0, voiLUTFunction: image.voiLUTFunction, }; } viewport.voi.windowWidth = windowWidth; viewport.voi.windowCenter = windowCenter; } this.voiRange = voiRange; const eventDetail = { viewportId: this.id, range: voiRange, }; if (!suppressEvents) { triggerEvent(this.element, Events.VOI_MODIFIED, eventDetail); } } getTransferFunction() { const defaultActor = this.getDefaultActor(); if (!defaultActor) { return; } if (!isImageActor(defaultActor)) { return; } const imageActor = defaultActor.actor; return imageActor.getProperty().getRGBTransferFunction(0); } setVOIGPU(voiRange, options = {}) { const { suppressEvents = false, forceRecreateLUTFunction = false, voiUpdatedWithSetProperties = false, } = options; if (voiRange && this.voiRange && this.voiRange.lower === voiRange.lower && this.voiRange.upper === voiRange.upper && !forceRecreateLUTFunction && !this.stackInvalidated) { return; } const defaultActor = this.getDefaultActor(); if (!defaultActor) { return; } if (!isImageActor(defaultActor)) { return; } const imageActor = defaultActor.actor; let voiRangeToUse = voiRange; if (typeof voiRangeToUse === 'undefined') { const imageData = imageActor.getMapper().getInputData(); const range = imageData.getPointData().getScalars().getRange(); const maxVoiRange = { lower: range[0], upper: range[1] }; voiRangeToUse = maxVoiRange; } imageActor.getProperty().setUseLookupTableScalarRange(true); let transferFunction = imageActor.getProperty().getRGBTransferFunction(0); const isSigmoidTFun = this.VOILUTFunction === VOILUTFunctionType.SAMPLED_SIGMOID; if (isSigmoidTFun || !transferFunction || forceRecreateLUTFunction) { const transferFunctionCreator = isSigmoidTFun ? createSigmoidRGBTransferFunction : createLinearRGBTransferFunction; transferFunction = transferFunctionCreator(voiRangeToUse); if (this.invert) { invertRgbTransferFunction(transferFunction); } imageActor.getProperty().setRGBTransferFunction(0, transferFunction); this.initialTransferFunctionNodes = getTransferFunctionNodes(transferFunction); } if (!isSigmoidTFun) { transferFunction.setRange(voiRangeToUse.lower, voiRangeToUse.upper); } this.voiRange = voiRangeToUse; if (!this.voiUpdatedWithSetProperties) { this.voiUpdatedWithSetProperties = voiUpdatedWithSetProperties; } if (suppressEvents) { return; } const eventDetail = { viewportId: this.id, range: voiRangeToUse, VOILUTFunction: this.VOILUTFunction, }; triggerEvent(this.element, Events.VOI_MODIFIED, eventDetail); } _addScalingToViewport(imageId