UNPKG

@cornerstonejs/core

Version:
206 lines (205 loc) • 7.45 kB
import '@kitware/vtk.js/Rendering/Profiles/Volume.js'; import { Events, ViewportType } from '../../../enums/index.js'; import eventTarget from '../../../eventTarget.js'; import createLinearRGBTransferFunction from '../../../utilities/createLinearRGBTransferFunction.js'; import invertRgbTransferFunction from '../../../utilities/invertRgbTransferFunction.js'; import { updateOpacity as updateVolumeOpacity } from '../../../utilities/colormap.js'; import uuidv4 from '../../../utilities/uuidv4.js'; import createVolumeActor from '../../helpers/createVolumeActor.js'; import { canvasToWorldContextPool, worldToCanvasContextPool, } from '../../helpers/vtkCanvasCoordinateTransforms.js'; import applyVolume3DCamera from './applyVolume3DCamera.js'; import { getInitialVolume3DCamera } from './vtkVolume3DInitialCamera.js'; import setVtkCameraClippingRange from '../setVtkCameraClippingRange.js'; export class VtkVolume3DRenderPath { async addData(ctx, data, options) { const payload = data; const hadVolume = ctx.vtk.renderer.getVolumes().length > 0; const actor = await createVolumeActor({ volumeId: payload.volumeId, }, ctx.viewport.element, ctx.viewportId, true); const mapper = actor.getMapper(); ctx.vtk.renderer.addVolume(actor); if (!hadVolume) { const initialCamera = getInitialVolume3DCamera(ctx, payload.imageVolume); if (initialCamera) { applyCamera(ctx, initialCamera); } } setCameraClippingRange(ctx); const defaultRange = actor .getProperty() .getRGBTransferFunction(0) .getRange(); const rendering = { renderMode: 'vtkVolume3d', actorEntryUID: uuidv4(), actor, defaultVOIRange: defaultRange ? { lower: defaultRange[0], upper: defaultRange[1] } : undefined, imageVolume: payload.imageVolume, mapper, removeStreamingSubscriptions: subscribeToVolumeEvents(payload.volumeId, () => { ctx.display.requestRender(); }), }; return { rendering, updateDataPresentation: (props) => { this.updateDataPresentation(rendering, props); }, applyViewState: (camera) => { this.applyViewState(ctx, camera); }, getFrameOfReferenceUID: () => { return this.getFrameOfReferenceUID(rendering); }, getImageData: () => { return this.getImageData(rendering); }, render: () => { this.render(ctx); }, resize: () => { this.resize(ctx); }, removeData: () => { this.removeData(ctx, rendering); }, }; } updateDataPresentation(rendering, props) { applyDataPresentation(rendering, props); } applyViewState(ctx, camera) { applyCamera(ctx, camera); } canvasToWorld(ctx, canvasPos) { return canvasToWorldContextPool({ canvas: ctx.vtk.canvas, renderer: ctx.vtk.renderer, canvasPos, }); } worldToCanvas(ctx, worldPos) { return worldToCanvasContextPool({ canvas: ctx.vtk.canvas, renderer: ctx.vtk.renderer, worldPos, }); } getFrameOfReferenceUID(rendering) { return rendering.imageVolume.metadata?.FrameOfReferenceUID; } getImageData(rendering) { return buildVolumeImageData(rendering.imageVolume); } render(ctx) { ctx.display.requestRender(); } resize(ctx) { ctx.display.requestRender(); } removeData(ctx, rendering) { const { actor, removeStreamingSubscriptions } = rendering; removeStreamingSubscriptions?.(); ctx.vtk.renderer.removeVolume(actor); } } export class VtkVolume3DPath { constructor() { this.id = 'volume3d:vtk-volume'; this.type = ViewportType.VOLUME_3D_NEXT; } matches(data, options) { return data.type === 'image' && options.renderMode === 'vtkVolume3d'; } createRenderPath() { return new VtkVolume3DRenderPath(); } selectContext(rootContext) { return rootContext; } } function subscribeToVolumeEvents(volumeId, onProgress) { const handleProgress = (evt) => { const detail = evt.detail; if (detail?.volumeId !== volumeId) { return; } onProgress(); }; eventTarget.addEventListener(Events.IMAGE_VOLUME_MODIFIED, handleProgress); eventTarget.addEventListener(Events.IMAGE_VOLUME_LOADING_COMPLETED, handleProgress); return () => { eventTarget.removeEventListener(Events.IMAGE_VOLUME_MODIFIED, handleProgress); eventTarget.removeEventListener(Events.IMAGE_VOLUME_LOADING_COMPLETED, handleProgress); }; } function applyDataPresentation(rendering, props) { const { actor, defaultVOIRange } = rendering; const property = actor.getProperty(); const voiRange = props?.voiRange ?? defaultVOIRange; actor.setVisibility(props?.visible === false ? false : true); if (props?.opacity !== undefined) { updateVolumeOpacity(actor, props.opacity); } if (!voiRange) { return; } const transferFunction = createLinearRGBTransferFunction(voiRange); if (props?.invert) { invertRgbTransferFunction(transferFunction); } property.setRGBTransferFunction(0, transferFunction); if (props?.interpolationType !== undefined) { property.setInterpolationType(props.interpolationType); } if (props?.sampleDistanceMultiplier !== undefined) { applySampleDistanceMultiplier(rendering.mapper, props.sampleDistanceMultiplier); } } function applyCamera(ctx, camera) { applyVolume3DCamera(ctx, camera); if (camera && camera.clippingRange === undefined) { setCameraClippingRange(ctx); } } function applySampleDistanceMultiplier(mapper, multiplier) { const imageData = mapper.getInputData?.(); if (!imageData) { return; } const spacing = imageData.getSpacing(); const defaultSampleDistance = (spacing[0] + spacing[1] + spacing[2]) / 6; const safeMultiplier = Number.isFinite(multiplier) ? Math.max(multiplier, 0.001) : 1; mapper.setSampleDistance(defaultSampleDistance * safeMultiplier); } function setCameraClippingRange(ctx) { setVtkCameraClippingRange(ctx.vtk.renderer.getActiveCamera()); } function buildVolumeImageData(imageVolume) { const vtkImageData = imageVolume.imageData; if (!vtkImageData) { return; } return { dimensions: vtkImageData.getDimensions(), spacing: vtkImageData.getSpacing(), origin: vtkImageData.getOrigin(), direction: vtkImageData.getDirection(), imageData: vtkImageData, metadata: { Modality: imageVolume.metadata?.Modality, FrameOfReferenceUID: imageVolume.metadata?.FrameOfReferenceUID, }, get scalarData() { return imageVolume.voxelManager?.getScalarData(); }, scaling: imageVolume.scaling, hasPixelSpacing: imageVolume.hasPixelSpacing, voxelManager: imageVolume.voxelManager, }; }