UNPKG

@cornerstonejs/core

Version:
333 lines (332 loc) • 14.5 kB
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); }