UNPKG

@cornerstonejs/core

Version:
306 lines (305 loc) • 11.9 kB
import { ActorRenderMode } from '../../../types/index.js'; import clonePoint3 from '../../../utilities/clonePoint3.js'; import ResolvedViewportView from '../ResolvedViewportView.js'; import { canvasToWorldPlanarViewState, getCanvasCssDimensions, worldToCanvasPlanarViewState, } from './planarAdapterCoordinateTransforms.js'; import { derivePlanarPresentation, resolvePlanarICamera, } from './planarRenderCamera.js'; import { createPlanarCpuImageSliceBasis, createPlanarCpuVolumeSliceBasis, createPlanarImageSliceBasis, createPlanarVolumeSliceBasis, resolvePlanarVolumeImageIdIndex, shouldUsePlanarCpuVolumeSliceBasis, } from './planarSliceBasis.js'; import { createDefaultPlanarViewState, normalizePlanarViewState, } from './planarViewState.js'; import { normalizePlanarScale, } from './planarCameraScale.js'; export function resolvePlanarStackImageIdIndex(args) { const { fallbackImageIdIndex, viewState } = args; return viewState?.slice?.kind === 'stackIndex' ? viewState.slice.imageIdIndex : fallbackImageIdIndex; } function clonePoint2(point) { return [point[0], point[1]]; } class BasePlanarResolvedView extends ResolvedViewportView { get pan() { return clonePoint2(this.getPresentation().pan); } get zoom() { return this.getPresentation().zoom; } get scale() { return clonePoint2(this.getPresentation().scale); } get rotation() { return this.getPresentation().rotation; } getSliceBasis() { this.cachedSliceBasis ||= this.buildSliceBasis(); return this.cachedSliceBasis; } canvasToWorld(canvasPos) { return canvasToWorldPlanarViewState({ camera: this.requireResolvedICamera(), canvasHeight: this.state.canvasHeight, canvasPos, canvasWidth: this.state.canvasWidth, }); } worldToCanvas(worldPos) { return worldToCanvasPlanarViewState({ camera: this.requireResolvedICamera(), canvasHeight: this.state.canvasHeight, canvasWidth: this.state.canvasWidth, worldPos, }); } getFrameOfReferenceUID() { return this.state.frameOfReferenceUID; } withZoom(zoom, canvasPoint) { return this.withScale(zoom, canvasPoint); } withScale(scale, canvasPoint) { const nextScale = normalizePlanarScale(scale); if (!canvasPoint) { return this.cloneWithViewState({ ...this.state.viewState, displayArea: undefined, scale: nextScale, scaleMode: 'fit', }); } const worldPoint = this.canvasToWorld(canvasPoint); return this.cloneWithViewState({ ...this.state.viewState, anchorCanvas: [ canvasPoint[0] / Math.max(this.state.canvasWidth, 1), canvasPoint[1] / Math.max(this.state.canvasHeight, 1), ], anchorWorld: worldPoint, displayArea: undefined, scale: nextScale, scaleMode: 'fit', }); } withPan(pan) { const currentPan = this.pan; const [anchorX, anchorY] = this.state.viewState.anchorCanvas ?? [0.5, 0.5]; return this.cloneWithViewState({ ...this.state.viewState, anchorCanvas: [ anchorX + (pan[0] - currentPan[0]) / Math.max(this.state.canvasWidth, 1), anchorY + (pan[1] - currentPan[1]) / Math.max(this.state.canvasHeight, 1), ], displayArea: undefined, }); } flipHorizontal() { return this.cloneWithViewState({ ...this.state.viewState, flipHorizontal: !this.state.viewState.flipHorizontal, }); } flipVertical() { return this.cloneWithViewState({ ...this.state.viewState, flipVertical: !this.state.viewState.flipVertical, }); } indexToWorld(_index) { return; } buildICamera() { return resolvePlanarICamera({ camera: this.state.viewState, canvasHeight: this.state.canvasHeight, canvasWidth: this.state.canvasWidth, sliceBasis: this.getSliceBasis(), }); } getPresentation() { this.cachedPresentation ||= derivePlanarPresentation({ camera: this.state.viewState, canvasHeight: this.state.canvasHeight, canvasWidth: this.state.canvasWidth, sliceBasis: this.getSliceBasis(), }); return this.cachedPresentation; } requireResolvedICamera() { const viewState = this.toICamera(); if (!viewState.focalPoint || typeof viewState.parallelScale !== 'number' || !viewState.viewPlaneNormal || !viewState.viewUp) { throw new Error('[PlanarResolvedView] Failed to compute planar viewState'); } return { focalPoint: clonePoint3(viewState.focalPoint), parallelScale: viewState.parallelScale, presentationScale: viewState.presentationScale ? clonePoint2(viewState.presentationScale) : undefined, viewPlaneNormal: clonePoint3(viewState.viewPlaneNormal), viewUp: clonePoint3(viewState.viewUp), }; } createViewStateState(viewState) { return { ...this.state, viewState: normalizePlanarViewState(viewState), }; } } class PlanarStackResolvedView extends BasePlanarResolvedView { constructor(state) { super({ ...state, viewState: normalizePlanarViewState(state.viewState || createDefaultPlanarViewState()), }); } buildSliceBasis() { const args = { canvasHeight: this.state.canvasHeight, canvasWidth: this.state.canvasWidth, image: this.state.image, }; if (this.state.usePixelGridCenter) { return createPlanarCpuImageSliceBasis(args); } return createPlanarImageSliceBasis(args); } cloneWithViewState(viewState) { return new PlanarStackResolvedView(this.createViewStateState(viewState)); } } class PlanarVolumeResolvedView extends BasePlanarResolvedView { constructor(state) { const { sliceBasis, ...stateWithoutSliceBasis } = state; super({ ...stateWithoutSliceBasis, viewState: normalizePlanarViewState(state.viewState || createDefaultPlanarViewState()), }); this.cachedSliceBasis = sliceBasis; } indexToWorld(index) { return this.state.imageVolume.imageData?.indexToWorld(index); } buildSliceBasis() { const createSliceBasis = this.state.usePixelGridCenter ? createPlanarCpuVolumeSliceBasis : createPlanarVolumeSliceBasis; return createSliceBasis({ viewState: this.state.viewState, canvasHeight: this.state.canvasHeight, canvasWidth: this.state.canvasWidth, imageIdIndex: resolvePlanarVolumeImageIdIndex({ viewState: this.state.viewState, fallbackImageIdIndex: this.state.currentImageIdIndex, }), imageVolume: this.state.imageVolume, orientation: this.state.viewState.orientation, }).sliceBasis; } cloneWithViewState(viewState) { return new PlanarVolumeResolvedView(this.createViewStateState(viewState)); } } function getCurrentSliceIndex(rendering) { return rendering.currentImageIdIndex; } export function getPlanarViewStateCanvasDimensions(args) { const { renderContext, rendering } = args; if (rendering.renderMode === ActorRenderMode.CPU_IMAGE || rendering.renderMode === ActorRenderMode.CPU_VOLUME) { if (!renderContext.cpu) { throw new Error('[PlanarResolvedView] CPU render paths require a CPU canvas context'); } return getCanvasCssDimensions(renderContext.cpu.canvas); } if (!renderContext.vtk) { throw new Error('[PlanarResolvedView] VTK render paths require a VTK canvas context'); } return { canvasHeight: renderContext.vtk.canvas.clientHeight || renderContext.vtk.canvas.height, canvasWidth: renderContext.vtk.canvas.clientWidth || renderContext.vtk.canvas.width, }; } export function resolvePlanarViewportView(args) { const { data, frameOfReferenceUID, imageIds, canvasDimensions, renderContext, rendering, sliceIndex, } = args; if (!rendering) { return; } const { canvasHeight, canvasWidth } = canvasDimensions ?? getPlanarViewStateCanvasDimensions({ renderContext, rendering, }); const requestedViewState = args.viewState; if (rendering.renderMode === ActorRenderMode.CPU_IMAGE || rendering.renderMode === ActorRenderMode.VTK_IMAGE) { const image = (rendering.renderMode === ActorRenderMode.CPU_IMAGE ? rendering.enabledElement?.image : rendering.currentImage) || data?.image; if (!image) { return; } const currentImageIdIndex = resolvePlanarStackImageIdIndex({ fallbackImageIdIndex: getCurrentSliceIndex(rendering), viewState: requestedViewState, }); const imageIdCount = imageIds?.length ?? data?.imageIds.length; const maxImageIdIndex = typeof imageIdCount === 'number' && imageIdCount > 0 ? imageIdCount - 1 : Math.max(currentImageIdIndex, getCurrentSliceIndex(rendering), 0); return new PlanarStackResolvedView({ viewState: requestedViewState, canvasHeight, canvasWidth, currentImageIdIndex, frameOfReferenceUID, image, maxImageIdIndex, usePixelGridCenter: rendering.renderMode === ActorRenderMode.CPU_IMAGE, }); } if (rendering.renderMode === ActorRenderMode.CPU_VOLUME || rendering.renderMode === ActorRenderMode.VTK_VOLUME_SLICE) { const resolvedViewState = typeof sliceIndex === 'number' ? { ...requestedViewState, slice: { kind: 'stackIndex', imageIdIndex: sliceIndex, }, } : requestedViewState; const createSliceBasis = shouldUsePlanarCpuVolumeSliceBasis(rendering.dataPresentation?.interpolationType) ? createPlanarCpuVolumeSliceBasis : createPlanarVolumeSliceBasis; const resolvedImageIdIndex = typeof sliceIndex === 'number' ? sliceIndex : resolvePlanarVolumeImageIdIndex({ viewState: resolvedViewState, fallbackImageIdIndex: rendering.currentImageIdIndex, }); const { currentImageIdIndex, maxImageIdIndex, sliceBasis } = createSliceBasis({ viewState: resolvedViewState, canvasHeight, canvasWidth, imageIdIndex: resolvedImageIdIndex, imageVolume: rendering.imageVolume, orientation: resolvedViewState.orientation, }); return new PlanarVolumeResolvedView({ viewState: resolvedViewState, canvasHeight, canvasWidth, currentImageIdIndex, frameOfReferenceUID, imageVolume: rendering.imageVolume, maxImageIdIndex, sliceBasis, usePixelGridCenter: rendering.renderMode === ActorRenderMode.CPU_VOLUME && shouldUsePlanarCpuVolumeSliceBasis(rendering.dataPresentation?.interpolationType), }); } } export { BasePlanarResolvedView, PlanarStackResolvedView, PlanarVolumeResolvedView, };