UNPKG

@cornerstonejs/core

Version:
313 lines (312 loc) • 12.9 kB
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, }; }