@cornerstonejs/core
Version:
Cornerstone3D Core
206 lines (205 loc) • 7.45 kB
JavaScript
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,
};
}