@cornerstonejs/core
Version:
Cornerstone3D Core
333 lines (332 loc) • 14.5 kB
JavaScript
import { mat4, vec2, vec3 } from 'gl-matrix';
import vtkPlaneFactory from '@kitware/vtk.js/Common/DataModel/Plane.js';
import { RENDERING_DEFAULTS } from '../../../constants/index.js';
import { applyPlanarViewFlip, normalizePlanarRotation, rotatePlanarViewUp, } from './planarViewPresentation.js';
import { getSafeCanvasDimension, normalizePoint3 } from './planarMath.js';
import setVtkCameraClippingRange from '../setVtkCameraClippingRange.js';
import { MIN_PLANAR_SCALE, normalizePlanarScale, normalizePlanarScaleMode, } from './planarCameraScale.js';
const MIN_DISPLAY_AREA_VALUE = 1e-3;
const MIN_SCALE_RATIO = 1e-6;
function getSliceCanvasDimensionsAtFit(args) {
const { canvasHeight, canvasWidth, sliceBasis } = args;
const safeCanvasWidth = getSafeCanvasDimension(canvasWidth);
const safeCanvasHeight = getSafeCanvasDimension(canvasHeight);
const safeFitParallelScale = Math.max(sliceBasis.fitParallelScale, MIN_DISPLAY_AREA_VALUE);
const worldHeightAtFit = safeFitParallelScale * 2;
const worldWidthAtFit = worldHeightAtFit * (safeCanvasWidth / safeCanvasHeight);
return [
(Math.max(sliceBasis.sliceWidthWorld, MIN_DISPLAY_AREA_VALUE) /
Math.max(worldWidthAtFit, MIN_DISPLAY_AREA_VALUE)) *
safeCanvasWidth,
(Math.max(sliceBasis.sliceHeightWorld, MIN_DISPLAY_AREA_VALUE) /
Math.max(worldHeightAtFit, MIN_DISPLAY_AREA_VALUE)) *
safeCanvasHeight,
];
}
function deriveDisplayAreaPresentation(args) {
const { canvasHeight, canvasWidth, camera, scale, sliceBasis } = args;
const displayArea = camera?.displayArea;
if (!displayArea) {
return;
}
const safeCanvasWidth = getSafeCanvasDimension(canvasWidth);
const safeCanvasHeight = getSafeCanvasDimension(canvasHeight);
const [imageWidthAtFit, imageHeightAtFit] = getSliceCanvasDimensionsAtFit({
canvasHeight,
canvasWidth,
sliceBasis,
});
let resolvedScale = [...scale];
if (displayArea.type === 'SCALE' && displayArea.scale !== undefined) {
resolvedScale = normalizePlanarScale(displayArea.scale);
}
else if (displayArea.imageArea) {
const [areaX, areaY] = displayArea.imageArea;
const safeAreaX = Math.max(Math.abs(areaX), MIN_DISPLAY_AREA_VALUE);
const safeAreaY = Math.max(Math.abs(areaY), MIN_DISPLAY_AREA_VALUE);
const fitScaleX = safeCanvasWidth /
Math.max(safeAreaX * imageWidthAtFit, MIN_DISPLAY_AREA_VALUE);
const fitScaleY = safeCanvasHeight /
Math.max(safeAreaY * imageHeightAtFit, MIN_DISPLAY_AREA_VALUE);
const scaleMode = normalizePlanarScaleMode(displayArea.scaleMode ?? camera?.scaleMode);
if (scaleMode === 'absolute') {
resolvedScale = [fitScaleX * scale[0], fitScaleY * scale[1]];
}
else {
const uniformFitScale = scaleMode === 'fitWidth'
? fitScaleX
: scaleMode === 'fitHeight'
? fitScaleY
: Math.min(fitScaleX, fitScaleY);
resolvedScale = [uniformFitScale * scale[0], uniformFitScale * scale[1]];
}
}
let pan = [0, 0];
if (displayArea.imageCanvasPoint) {
const { imagePoint, canvasPoint = imagePoint || [0.5, 0.5] } = displayArea.imageCanvasPoint;
const [canvasX, canvasY] = canvasPoint;
const [imageX, imageY] = imagePoint || canvasPoint;
pan = [
resolvedScale[0] * imageWidthAtFit * (0.5 - imageX) +
safeCanvasWidth * (canvasX - 0.5),
resolvedScale[1] * imageHeightAtFit * (0.5 - imageY) +
safeCanvasHeight * (canvasY - 0.5),
];
}
return {
pan,
scale: resolvedScale,
zoom: resolvedScale[1],
};
}
export function projectAnchorWorldToCurrentPlane(anchorWorld, planePoint, planeNormal) {
const normalizedPlaneNormal = normalizePoint3(planeNormal, [0, 0, 1]);
const delta = vec3.subtract(vec3.create(), anchorWorld, planePoint);
const distance = vec3.dot(delta, normalizedPlaneNormal);
return vec3.scaleAndAdd(vec3.create(), anchorWorld, normalizedPlaneNormal, -distance);
}
export function derivePlanarPresentation(args) {
const { sliceBasis, camera, canvasHeight, canvasWidth } = args;
const scale = normalizePlanarScale(camera?.scale);
const rotation = normalizePlanarRotation(camera?.rotation ?? 0);
const anchorCanvas = vec2.clone((camera?.anchorCanvas ?? [0.5, 0.5]));
const safeCanvasWidth = getSafeCanvasDimension(canvasWidth);
const safeCanvasHeight = getSafeCanvasDimension(canvasHeight);
const viewPlaneNormal = normalizePoint3(sliceBasis.viewPlaneNormal, [0, 0, 1]);
const rotatedViewUp = normalizePoint3(rotatePlanarViewUp({
rotation,
viewPlaneNormal,
viewUp: sliceBasis.viewUp,
}), [0, -1, 0]);
const { viewPlaneNormal: flippedViewPlaneNormal, viewUp: flippedViewUp } = applyPlanarViewFlip({
flipHorizontal: camera?.flipHorizontal,
flipVertical: camera?.flipVertical,
viewPlaneNormal,
viewUp: rotatedViewUp,
});
let right = vec3.cross(vec3.create(), flippedViewUp, flippedViewPlaneNormal);
if (!vec3.length(right)) {
right = [1, 0, 0];
}
else {
right = vec3.normalize(vec3.create(), right);
}
const effectiveAnchorWorld = camera?.anchorWorld
? projectAnchorWorldToCurrentPlane(camera.anchorWorld, sliceBasis.sliceCenterWorld, viewPlaneNormal)
: sliceBasis.sliceCenterWorld;
const [scaleX, scaleY] = scale;
const parallelScale = sliceBasis.fitParallelScale / scaleY;
const worldHeight = parallelScale * 2;
const worldWidth = worldHeight * (safeCanvasWidth / safeCanvasHeight);
const effectiveWorldWidth = worldWidth * (scaleY / scaleX);
const displayAreaPresentation = deriveDisplayAreaPresentation({
sliceBasis,
camera,
canvasWidth,
canvasHeight,
scale,
});
const deltaWorld = vec3.subtract(vec3.create(), sliceBasis.sliceCenterWorld, effectiveAnchorWorld);
const panFromAnchorWorld = [
(vec3.dot(deltaWorld, right) * safeCanvasWidth) /
effectiveWorldWidth,
(-vec3.dot(deltaWorld, flippedViewUp) *
safeCanvasHeight) /
worldHeight,
];
const panFromAnchorCanvas = [
(anchorCanvas[0] - 0.5) * safeCanvasWidth,
(anchorCanvas[1] - 0.5) * safeCanvasHeight,
];
const pan = displayAreaPresentation?.pan ?? [
panFromAnchorWorld[0] + panFromAnchorCanvas[0],
panFromAnchorWorld[1] + panFromAnchorCanvas[1],
];
return {
pan,
flipHorizontal: camera?.flipHorizontal === true,
flipVertical: camera?.flipVertical === true,
scale: displayAreaPresentation?.scale ?? scale,
zoom: displayAreaPresentation?.zoom ?? scale[1],
rotation,
};
}
function getResolvedPanOffset(args) {
const { sliceBasis, canvasWidth, canvasHeight, flipHorizontal, flipVertical, pan = [0, 0], rotation, scale = [1, 1], } = args;
const viewPlaneNormal = normalizePoint3(sliceBasis.viewPlaneNormal);
const rotatedViewUp = normalizePoint3(rotatePlanarViewUp({
rotation,
viewPlaneNormal,
viewUp: sliceBasis.viewUp,
}));
const { viewPlaneNormal: flippedViewPlaneNormal, viewUp: flippedViewUp } = applyPlanarViewFlip({
flipHorizontal,
flipVertical,
viewPlaneNormal,
viewUp: rotatedViewUp,
});
let right = vec3.cross(vec3.create(), flippedViewUp, flippedViewPlaneNormal);
if (vec3.length(right) === 0) {
right = vec3.fromValues(1, 0, 0);
}
right = vec3.normalize(vec3.create(), right);
const [scaleX, scaleY] = normalizePlanarScale(scale);
const [panX, panY] = pan;
const parallelScale = sliceBasis.fitParallelScale / scaleY;
const safeCanvasWidth = getSafeCanvasDimension(canvasWidth);
const safeCanvasHeight = getSafeCanvasDimension(canvasHeight);
const worldHeight = parallelScale * 2;
const worldWidth = worldHeight * (safeCanvasWidth / safeCanvasHeight);
const effectiveWorldWidth = worldWidth * (scaleY / scaleX);
const deltaWorld = vec3.create();
vec3.scaleAndAdd(deltaWorld, deltaWorld, right, (panX * effectiveWorldWidth) / safeCanvasWidth);
vec3.scaleAndAdd(deltaWorld, deltaWorld, flippedViewUp, (-panY * worldHeight) / safeCanvasHeight);
return {
deltaWorld: deltaWorld,
parallelScale,
presentationScale: [scaleX, scaleY],
viewPlaneNormal: flippedViewPlaneNormal,
viewUp: flippedViewUp,
};
}
export function resolvePlanarICamera(args) {
const { sliceBasis, camera, canvasHeight, canvasWidth } = args;
const presentation = derivePlanarPresentation({
sliceBasis,
camera,
canvasWidth,
canvasHeight,
});
const { deltaWorld, parallelScale, presentationScale, viewPlaneNormal, viewUp, } = getResolvedPanOffset({
sliceBasis,
canvasWidth,
canvasHeight,
flipHorizontal: presentation.flipHorizontal,
flipVertical: presentation.flipVertical,
pan: presentation.pan,
rotation: presentation.rotation,
scale: presentation.scale,
});
const focalPoint = vec3.subtract(vec3.create(), sliceBasis.sliceCenterWorld, deltaWorld);
return {
focalPoint,
parallelProjection: true,
parallelScale,
presentationScale,
scale: presentation.scale,
scaleMode: normalizePlanarScaleMode(camera?.scaleMode),
position: vec3.scaleAndAdd(vec3.create(), focalPoint, viewPlaneNormal, sliceBasis.cameraDistance),
flipHorizontal: presentation.flipHorizontal,
flipVertical: presentation.flipVertical,
viewPlaneNormal,
viewUp,
};
}
export function applyPlanarICameraToRenderer(args) {
const { renderer, activeSourceICamera } = args;
if (!activeSourceICamera?.focalPoint ||
!activeSourceICamera.position ||
!activeSourceICamera.viewPlaneNormal ||
!activeSourceICamera.viewUp ||
typeof activeSourceICamera.parallelScale !== 'number') {
return;
}
const vtkCamera = renderer.getActiveCamera();
vtkCamera.setParallelProjection(true);
vtkCamera.setDirectionOfProjection(-activeSourceICamera.viewPlaneNormal[0], -activeSourceICamera.viewPlaneNormal[1], -activeSourceICamera.viewPlaneNormal[2]);
vtkCamera.setParallelScale(activeSourceICamera.parallelScale);
vtkCamera.setFocalPoint(...activeSourceICamera.focalPoint);
vtkCamera.setPosition(...activeSourceICamera.position);
vtkCamera.setViewUp(...activeSourceICamera.viewUp);
return activeSourceICamera;
}
function getPlanarICameraRight(activeSourceICamera) {
const viewUp = normalizePoint3(activeSourceICamera.viewUp, [0, -1, 0]);
const viewPlaneNormal = normalizePoint3(activeSourceICamera.viewPlaneNormal, [0, 0, 1]);
const right = vec3.cross(vec3.create(), viewUp, viewPlaneNormal);
if (vec3.length(right) < MIN_SCALE_RATIO) {
return [1, 0, 0];
}
return vec3.normalize(vec3.create(), right);
}
export function createPlanarPresentationScaleMatrix(activeSourceICamera) {
const matrix = mat4.create();
if (!activeSourceICamera?.focalPoint ||
!activeSourceICamera.viewPlaneNormal ||
!activeSourceICamera.viewUp) {
return matrix;
}
const [scaleX, scaleY] = normalizePlanarScale(activeSourceICamera.presentationScale);
const ratioX = Math.max(scaleX / scaleY, MIN_PLANAR_SCALE);
if (Math.abs(ratioX - 1) < MIN_SCALE_RATIO) {
return matrix;
}
const right = getPlanarICameraRight(activeSourceICamera);
const ratioDelta = ratioX - 1;
const [rx, ry, rz] = right;
const linear = [
1 + ratioDelta * rx * rx,
ratioDelta * rx * ry,
ratioDelta * rx * rz,
ratioDelta * ry * rx,
1 + ratioDelta * ry * ry,
ratioDelta * ry * rz,
ratioDelta * rz * rx,
ratioDelta * rz * ry,
1 + ratioDelta * rz * rz,
];
const [cx, cy, cz] = activeSourceICamera.focalPoint;
const transformedCenter = [
linear[0] * cx + linear[3] * cy + linear[6] * cz,
linear[1] * cx + linear[4] * cy + linear[7] * cz,
linear[2] * cx + linear[5] * cy + linear[8] * cz,
];
matrix[0] = linear[0];
matrix[1] = linear[1];
matrix[2] = linear[2];
matrix[4] = linear[3];
matrix[5] = linear[4];
matrix[6] = linear[5];
matrix[8] = linear[6];
matrix[9] = linear[7];
matrix[10] = linear[8];
matrix[12] = cx - transformedCenter[0];
matrix[13] = cy - transformedCenter[1];
matrix[14] = cz - transformedCenter[2];
return matrix;
}
export function applyPlanarICameraToActor(args) {
const { actor, activeSourceICamera } = args;
actor?.setUserMatrix?.(createPlanarPresentationScaleMatrix(activeSourceICamera));
}
export function setPlanarVolumeCameraClippingRange(renderer) {
setVtkCameraClippingRange(renderer.getActiveCamera());
}
function ensureClippingPlanes(mapper) {
const existingClippingPlanes = mapper.getClippingPlanes();
if (existingClippingPlanes.length >= 2) {
return existingClippingPlanes;
}
mapper.addClippingPlane(vtkPlaneFactory.newInstance());
mapper.addClippingPlane(vtkPlaneFactory.newInstance());
return mapper.getClippingPlanes();
}
export function updatePlanarVolumeClippingPlanes(args) {
const { camera, mapper, slabThickness = RENDERING_DEFAULTS.MINIMUM_SLAB_THICKNESS, } = args;
const clippingPlanes = ensureClippingPlanes(mapper);
const scaledDistance = camera.viewPlaneNormal.map((value) => value * slabThickness);
const clipPlane1Origin = [
camera.focalPoint[0] - scaledDistance[0],
camera.focalPoint[1] - scaledDistance[1],
camera.focalPoint[2] - scaledDistance[2],
];
const clipPlane2Origin = [
camera.focalPoint[0] + scaledDistance[0],
camera.focalPoint[1] + scaledDistance[1],
camera.focalPoint[2] + scaledDistance[2],
];
clippingPlanes[0].setNormal(...camera.viewPlaneNormal);
clippingPlanes[0].setOrigin(...clipPlane1Origin);
clippingPlanes[1].setNormal(-camera.viewPlaneNormal[0], -camera.viewPlaneNormal[1], -camera.viewPlaneNormal[2]);
clippingPlanes[1].setOrigin(...clipPlane2Origin);
}