@cornerstonejs/core
Version:
Cornerstone3D Core
313 lines (312 loc) • 12.9 kB
JavaScript
import vtkPlaneFactory from '@kitware/vtk.js/Common/DataModel/Plane.js';
import { buildPlanarActorEntry } from './buildPlanarActorEntry.js';
import uuidv4 from '../../../utilities/uuidv4.js';
import { Events, ViewportType } from '../../../enums/index.js';
import eventTarget from '../../../eventTarget.js';
import createVolumeSliceActor from '../../helpers/createVolumeSliceActor.js';
import { ActorRenderMode } from '../../../types/index.js';
import { canvasToWorldContextPool, worldToCanvasContextPool, } from './planarAdapterCoordinateTransforms.js';
import { triggerPlanarVolumeNewImage } from './planarImageEvents.js';
import { applyPlanarICameraToActor, applyPlanarICameraToRenderer, } from './planarRenderCamera.js';
import { getPlanarRenderPathActiveSourceICamera, resolvePlanarRenderPathProjection, } from './planarRenderPathProjection.js';
import { applyPlanarVolumePresentation } from './planarVolumePresentation.js';
const SLICE_OVERLAY_DEPTH_EPSILON = 1e-4;
const PROJECTING_OVERLAY_RENDER_DELAY_MS = 240;
export class VtkVolumeSliceRenderPath {
async addData(ctx, data, options) {
const payload = data;
const imageVolume = payload.imageVolume;
const isSegmentationOverlay = options.role === 'overlay' && payload.reference?.kind === 'segmentation';
if (!imageVolume) {
throw new Error('[PlanarViewport] Volume rendering requires a prepared image volume');
}
const { actor } = await createVolumeSliceActor({
volumeId: payload.volumeId,
}, ctx.viewport.element, ctx.viewportId, true);
const mapper = actor.getMapper();
ctx.display.activateRenderMode(ActorRenderMode.VTK_VOLUME_SLICE);
ctx.vtk.renderer.addActor(actor);
const transferFunction = actor.getProperty().getRGBTransferFunction(0);
const defaultRange = transferFunction?.getRange?.();
const rendering = {
renderMode: ActorRenderMode.VTK_VOLUME_SLICE,
actorEntryUID: uuidv4(),
actor,
overlayOrder: getImageSliceOverlayOrder(ctx.vtk.renderer, actor),
imageVolume,
imageIds: payload.imageIds,
acquisitionOrientation: payload.acquisitionOrientation,
mapper,
currentImageIdIndex: payload.initialImageIdIndex ?? 0,
maxImageIdIndex: payload.imageIds.length - 1,
defaultVOIRange: defaultRange
? { lower: defaultRange[0], upper: defaultRange[1] }
: undefined,
dataPresentation: undefined,
isSegmentationOverlay,
};
let deferredProjectionRenderTimer = null;
const removeVolumeSubscriptions = subscribeToVolumeEvents(payload.volumeId, (eventType) => {
if (eventType === Events.IMAGE_VOLUME_MODIFIED) {
mapper.modified();
}
const isProjectingOverlay = rendering.isSegmentationOverlay &&
(rendering.dataPresentation?.slabThickness ?? 0) > 0;
if (!isProjectingOverlay) {
ctx.display.requestRender();
return;
}
if (deferredProjectionRenderTimer !== null) {
clearTimeout(deferredProjectionRenderTimer);
}
deferredProjectionRenderTimer = setTimeout(() => {
deferredProjectionRenderTimer = null;
ctx.display.requestRender();
}, PROJECTING_OVERLAY_RENDER_DELAY_MS);
});
rendering.removeStreamingSubscriptions = () => {
if (deferredProjectionRenderTimer !== null) {
clearTimeout(deferredProjectionRenderTimer);
deferredProjectionRenderTimer = null;
}
removeVolumeSubscriptions();
};
imageVolume.load(() => {
ctx.display.requestRender();
});
triggerPlanarVolumeNewImage(ctx, {
camera: ctx.viewport.getViewState(),
acquisitionOrientation: rendering.acquisitionOrientation,
imageIds: rendering.imageIds,
imageIdIndex: rendering.currentImageIdIndex,
maxImageIdIndex: rendering.maxImageIdIndex,
});
return {
rendering,
updateDataPresentation: (props) => {
this.updateDataPresentation(ctx, rendering, props);
},
applyViewState: (camera) => {
this.applyViewState(ctx, rendering, data.id, camera);
},
getFrameOfReferenceUID: () => {
return this.getFrameOfReferenceUID(rendering);
},
getActorEntry: (data) => {
const planarData = data;
return buildPlanarActorEntry(planarData, {
actor: rendering.actor,
mapper: rendering.mapper,
renderMode: ActorRenderMode.VTK_VOLUME_SLICE,
uid: rendering.actorEntryUID,
referencedIdFallback: planarData.volumeId,
});
},
getImageData: () => {
return this.getImageData(rendering);
},
render: () => {
this.render(ctx);
},
resize: () => {
this.resize(ctx, rendering, data.id);
},
removeData: () => {
this.removeData(ctx, rendering);
},
};
}
updateDataPresentation(ctx, rendering, props) {
rendering.dataPresentation = props;
applyPlanarVolumePresentation({
actor: rendering.actor,
defaultVOIRange: rendering.isSegmentationOverlay
? undefined
: rendering.defaultVOIRange,
mapper: rendering.mapper,
props: rendering.dataPresentation,
});
const activeSourceICamera = getPlanarRenderPathActiveSourceICamera(ctx);
updateVolumeSlicePlane(rendering.mapper, activeSourceICamera);
applyPlanarICameraToActor({
actor: rendering.actor,
activeSourceICamera,
});
updateVolumeSliceActorDepthOffset(rendering.actor, activeSourceICamera, rendering.overlayOrder);
ctx.vtk.renderer.resetCameraClippingRange();
}
applyViewState(ctx, rendering, dataId, cameraInput) {
const camera = cameraInput;
ctx.display.activateRenderMode(ActorRenderMode.VTK_VOLUME_SLICE);
this.syncRenderCamera(ctx, rendering, dataId, camera, true);
}
syncRenderCamera(ctx, rendering, dataId, camera, triggerImageEvent) {
const projection = resolvePlanarRenderPathProjection({
ctx,
dataId,
rendering,
viewState: camera,
});
if (!projection) {
return;
}
if (projection.isSourceBinding) {
applyPlanarICameraToRenderer({
renderer: ctx.vtk.renderer,
activeSourceICamera: projection.resolvedICamera,
});
}
applyPlanarICameraToActor({
actor: rendering.actor,
activeSourceICamera: projection.activeSourceICamera,
});
const imageIdIndexChanged = projection.currentImageIdIndex !== rendering.currentImageIdIndex;
const maxImageIdIndexChanged = projection.maxImageIdIndex !== rendering.maxImageIdIndex;
rendering.currentImageIdIndex = projection.currentImageIdIndex;
rendering.maxImageIdIndex = projection.maxImageIdIndex;
if (imageIdIndexChanged || maxImageIdIndexChanged) {
ctx.viewport.invalidateResolvedView();
}
updateVolumeSlicePlane(rendering.mapper, projection.activeSourceICamera);
updateVolumeSliceActorDepthOffset(rendering.actor, projection.activeSourceICamera, rendering.overlayOrder);
ctx.vtk.renderer.resetCameraClippingRange();
if (triggerImageEvent && imageIdIndexChanged) {
triggerPlanarVolumeNewImage(ctx, {
camera,
acquisitionOrientation: rendering.acquisitionOrientation,
imageIds: rendering.imageIds,
imageIdIndex: rendering.currentImageIdIndex,
maxImageIdIndex: rendering.maxImageIdIndex,
});
}
}
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 buildPlanarVolumeImageData(rendering.imageVolume);
}
render(ctx) {
ctx.display.requestRender();
}
resize(ctx, rendering, dataId) {
const camera = ctx.viewport.getViewState();
this.syncRenderCamera(ctx, rendering, dataId, camera, false);
ctx.display.requestRender();
}
removeData(ctx, rendering) {
const { actor, removeStreamingSubscriptions } = rendering;
removeStreamingSubscriptions?.();
ctx.vtk.renderer.removeActor(actor);
}
}
export class VtkVolumeSlicePath {
constructor() {
this.id = 'planar:vtk-volume-slice';
this.type = ViewportType.PLANAR_NEXT;
}
matches(data, options) {
return (data.type === 'image' &&
options.renderMode === ActorRenderMode.VTK_VOLUME_SLICE);
}
createRenderPath() {
return new VtkVolumeSliceRenderPath();
}
selectContext(rootContext) {
return {
viewportId: rootContext.viewportId,
renderingEngineId: rootContext.renderingEngineId,
type: rootContext.type,
viewport: rootContext.viewport,
renderPath: rootContext.renderPath,
view: rootContext.view,
display: rootContext.display,
vtk: rootContext.vtk,
};
}
}
function subscribeToVolumeEvents(volumeId, onProgress) {
const handleProgress = (evt) => {
const detail = evt.detail;
if (detail?.volumeId !== volumeId) {
return;
}
onProgress(evt.type);
};
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 ensureSlicePlane(mapper) {
const existingSlicePlane = mapper.getSlicePlane?.();
if (existingSlicePlane) {
return existingSlicePlane;
}
const slicePlane = vtkPlaneFactory.newInstance();
mapper.setSlicePlane(slicePlane);
return slicePlane;
}
function updateVolumeSlicePlane(mapper, activeSourceICamera) {
if (!activeSourceICamera?.focalPoint ||
!activeSourceICamera.viewPlaneNormal) {
return;
}
const slicePlane = ensureSlicePlane(mapper);
slicePlane.setOrigin(...activeSourceICamera.focalPoint);
slicePlane.setNormal(...activeSourceICamera.viewPlaneNormal);
}
function getImageSliceOverlayOrder(renderer, actor) {
const imageSliceActors = renderer
.getActors()
.filter((currentActor) => currentActor?.getClassName?.() === 'vtkImageSlice');
return Math.max(0, imageSliceActors.indexOf(actor));
}
function updateVolumeSliceActorDepthOffset(actor, activeSourceICamera, overlayOrder = 0) {
if (!activeSourceICamera?.viewPlaneNormal || overlayOrder <= 0) {
actor.setPosition(0, 0, 0);
return;
}
const [x, y, z] = activeSourceICamera.viewPlaneNormal;
const offset = overlayOrder * SLICE_OVERLAY_DEPTH_EPSILON;
actor.setPosition(x * offset, y * offset, z * offset);
}
function buildPlanarVolumeImageData(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,
};
}