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