@cornerstonejs/core
Version:
Cornerstone3D Core
1,246 lines (1,245 loc) • 49.7 kB
JavaScript
import vtkMatrixBuilder from '@kitware/vtk.js/Common/Core/MatrixBuilder.js';
import vtkMath from '@kitware/vtk.js/Common/Core/Math.js';
import vtkPlane from '@kitware/vtk.js/Common/DataModel/Plane.js';
import { vec2, vec3 } from 'gl-matrix';
import Events from '../enums/Events.js';
import ViewportStatus from '../enums/ViewportStatus.js';
import ViewportType from '../enums/ViewportType.js';
import renderingEngineCache from './renderingEngineCache.js';
import { actorIsA, isImageActor } from '../utilities/actorCheck.js';
import triggerEvent from '../utilities/triggerEvent.js';
import * as planar from '../utilities/planar.js';
import isEqual from '../utilities/isEqual.js';
import hasNaNValues from '../utilities/hasNaNValues.js';
import { RENDERING_DEFAULTS } from '../constants/index.js';
import { InterpolationType } from '../enums/index.js';
import { deepClone } from '../utilities/deepClone.js';
import { updatePlaneRestriction } from '../utilities/updatePlaneRestriction.js';
import { getCubeSizeInView } from '../utilities/getPlaneCubeIntersectionDimensions.js';
import { getConfiguration } from '../init.js';
class Viewport {
constructor(props) {
this.insetImageMultiplier = getConfiguration().rendering
?.useLegacyCameraFOV
? 1.1
: 1;
this.flipHorizontal = false;
this.flipVertical = false;
this.viewportStatus = ViewportStatus.NO_DATA;
this._displaySets = [];
this._suppressCameraModifiedEvents = false;
this.hasPixelSpacing = true;
this.getProperties = () => ({});
this.setRotation = (_rotation) => {
};
this.viewportWidgets = new Map();
this.addWidget = (widgetId, widget) => {
this.viewportWidgets.set(widgetId, widget);
};
this.getWidget = (id) => {
return this.viewportWidgets.get(id);
};
this.getWidgets = () => {
return Array.from(this.viewportWidgets.values());
};
this.getRenderPasses = () => {
return null;
};
this.removeWidgets = () => {
const widgets = this.getWidgets();
widgets.forEach((widget) => {
if (widget.getEnabled()) {
widget.setEnabled(false);
}
if (widget.getActor && widget.getRenderer) {
const actor = widget.getActor();
const renderer = widget.getRenderer();
if (renderer && actor) {
renderer.removeActor(actor);
}
}
});
};
this.id = props.id;
this.renderingEngineId = props.renderingEngineId;
this.type = props.type;
this.element = props.element;
this.canvas = props.canvas;
this.sx = props.sx;
this.sy = props.sy;
this.sWidth = props.sWidth;
this.sHeight = props.sHeight;
this._actors = new Map();
this.element.setAttribute('data-viewport-uid', this.id);
this.element.setAttribute('data-rendering-engine-uid', this.renderingEngineId);
this.defaultOptions = deepClone(props.defaultOptions);
this.suppressEvents = props.defaultOptions.suppressEvents
? props.defaultOptions.suppressEvents
: false;
this.options = deepClone(props.defaultOptions);
this.isDisabled = false;
}
static get useCustomRenderingPipeline() {
return false;
}
getUseCustomRenderingPipeline() {
return this.constructor.useCustomRenderingPipeline;
}
resizeForRenderingEngine({ keepCamera = true, } = {}) {
if (typeof this.resize === 'function') {
this.resize();
}
const previousCamera = keepCamera ? this.getCamera() : undefined;
this.resetCamera();
if (previousCamera) {
this.setCamera(previousCamera);
}
}
isOrientationChangeable() {
return false;
}
dispose() {
}
destroy() {
this.dispose();
}
setRendered() {
if (this.viewportStatus === ViewportStatus.NO_DATA ||
this.viewportStatus === ViewportStatus.LOADING) {
return;
}
this.viewportStatus = ViewportStatus.RENDERED;
}
setNeedsRender() {
this.viewportStatus = ViewportStatus.NEEDS_RENDER;
}
setColorTransform(voiRange, averageWhite) {
let feFilter = null;
if (!voiRange && !averageWhite) {
return;
}
const white = averageWhite || [255, 255, 255];
const maxWhite = Math.max(...white);
const scaleWhite = white.map((c) => maxWhite / c);
const { lower = 0, upper = 255 } = voiRange || {};
const wlScale = (upper - lower + 1) / 255;
const wlDelta = lower / 255;
feFilter = `url('data:image/svg+xml,\
<svg xmlns="http://www.w3.org/2000/svg">\
<filter id="colour" color-interpolation-filters="linearRGB">\
<feColorMatrix type="matrix" \
values="\
${scaleWhite[0] * wlScale} 0 0 0 ${wlDelta} \
0 ${scaleWhite[1] * wlScale} 0 0 ${wlDelta} \
0 0 ${scaleWhite[2] * wlScale} 0 ${wlDelta} \
0 0 0 1 0" />\
</filter>\
</svg>#colour')`;
return feFilter;
}
getRenderingEngine() {
return renderingEngineCache.get(this.renderingEngineId);
}
getRenderer() {
const renderingEngine = this.getRenderingEngine();
if (!renderingEngine || renderingEngine.hasBeenDestroyed) {
throw new Error('Rendering engine has been destroyed');
}
return renderingEngine.offscreenMultiRenderWindow?.getRenderer(this.id);
}
render() {
const renderingEngine = this.getRenderingEngine();
renderingEngine.renderViewport(this.id);
}
setOptions(options, immediate = false) {
this.options = deepClone(options);
if (this.options?.displayArea) {
this.setDisplayArea(this.options?.displayArea);
}
if (immediate) {
this.render();
}
}
reset(immediate = false) {
this.options = deepClone(this.defaultOptions);
if (immediate) {
this.render();
}
}
getSliceViewInfo() {
throw new Error('Method not implemented.');
}
flip({ flipHorizontal, flipVertical }) {
const imageData = this.getDefaultImageData();
if (!imageData) {
return;
}
const camera = this.getCamera();
const { viewPlaneNormal, viewUp, focalPoint, position } = camera;
const viewRight = vec3.cross(vec3.create(), viewPlaneNormal, viewUp);
let viewUpToSet = vec3.copy(vec3.create(), viewUp);
const viewPlaneNormalToSet = vec3.negate(vec3.create(), viewPlaneNormal);
const distance = vec3.distance(position, focalPoint);
const dimensions = imageData.getDimensions();
const middleIJK = getVolumeCenterIJK(dimensions, imageData.getDirection(), viewPlaneNormal);
const idx = [middleIJK[0], middleIJK[1], middleIJK[2]];
const centeredFocalPoint = imageData.indexToWorld(idx, vec3.create());
const resetFocalPoint = this._getFocalPointForResetCamera(centeredFocalPoint, camera, { resetPan: true, resetToCenter: false });
const panDir = vec3.subtract(vec3.create(), focalPoint, resetFocalPoint);
const panValue = vec3.length(panDir);
const getPanDir = (mirrorVec) => {
const panDirMirror = vec3.scale(vec3.create(), mirrorVec, 2 * vec3.dot(panDir, mirrorVec));
vec3.subtract(panDirMirror, panDirMirror, panDir);
vec3.normalize(panDirMirror, panDirMirror);
return panDirMirror;
};
if (flipHorizontal) {
const panDirMirror = getPanDir(viewUpToSet);
const newFocalPoint = vec3.scaleAndAdd(vec3.create(), resetFocalPoint, panDirMirror, panValue);
const newPosition = vec3.scaleAndAdd(vec3.create(), newFocalPoint, viewPlaneNormalToSet, distance);
this.setCamera({
viewPlaneNormal: viewPlaneNormalToSet,
position: newPosition,
focalPoint: newFocalPoint,
});
this.flipHorizontal = !this.flipHorizontal;
}
if (flipVertical) {
viewUpToSet = vec3.negate(viewUpToSet, viewUp);
const panDirMirror = getPanDir(viewRight);
const newFocalPoint = vec3.scaleAndAdd(vec3.create(), resetFocalPoint, panDirMirror, panValue);
const newPosition = vec3.scaleAndAdd(vec3.create(), newFocalPoint, viewPlaneNormalToSet, distance);
this.setCamera({
focalPoint: newFocalPoint,
viewPlaneNormal: viewPlaneNormalToSet,
viewUp: viewUpToSet,
position: newPosition,
});
this.flipVertical = !this.flipVertical;
}
this.render();
}
getDefaultImageData() {
const actorEntry = this.getDefaultActor();
if (actorEntry && isImageActor(actorEntry)) {
return actorEntry.actor.getMapper().getInputData();
}
}
getDefaultActor() {
return this.getActors()[0];
}
getActors() {
return Array.from(this._actors.values());
}
getActorUIDs() {
return Array.from(this._actors.keys());
}
getActor(actorUID) {
return this._actors.get(actorUID);
}
getImageActor(volumeId) {
const actorEntries = this.getActors();
let actorEntry = actorEntries[0];
if (volumeId) {
actorEntry = actorEntries.find((a) => a.referencedId === volumeId);
}
if (!actorEntry || !isImageActor(actorEntry)) {
return null;
}
const actor = actorEntry.actor;
return actor;
}
getActorUIDByIndex(index) {
const actor = this.getActors()[index];
if (actor) {
return actor.uid;
}
}
getActorByIndex(index) {
return this.getActors()[index];
}
setActors(actors) {
const currentActors = this.getActors();
this.removeAllActors();
this.addActors(actors, { resetCamera: true });
triggerEvent(this.element, Events.ACTORS_CHANGED, {
viewportId: this.id,
removedActors: currentActors,
addedActors: actors,
currentActors: actors,
});
}
_removeActor(actorUID) {
const actorEntry = this.getActor(actorUID);
if (!actorEntry) {
console.warn(`Actor ${actorUID} does not exist in ${this.id}, can't remove`);
return;
}
const renderer = this.getRenderer();
renderer.removeActor(actorEntry.actor);
this._actors.delete(actorUID);
return actorEntry;
}
removeActors(actorUIDs) {
const removedActors = [];
actorUIDs.forEach((actorUID) => {
const removedActor = this._removeActor(actorUID);
if (removedActor) {
removedActors.push(removedActor);
}
});
const currentActors = this.getActors();
triggerEvent(this.element, Events.ACTORS_CHANGED, {
viewportId: this.id,
removedActors,
addedActors: [],
currentActors,
});
}
addActors(actors, options = {}) {
const { resetCamera = false } = options;
const renderingEngine = this.getRenderingEngine();
if (!renderingEngine || renderingEngine.hasBeenDestroyed) {
console.warn('Viewport::addActors::Rendering engine has not been initialized or has been destroyed');
return;
}
actors.forEach((actor) => {
this.addActor(actor);
});
if (!resetCamera) {
const prevViewPresentation = this.getViewPresentation();
const prevViewRef = this.getViewReference();
this.resetCamera();
this.setViewReference(prevViewRef);
this.setViewPresentation(prevViewPresentation);
}
else {
this.resetCamera();
}
triggerEvent(this.element, Events.ACTORS_CHANGED, {
viewportId: this.id,
removedActors: [],
addedActors: actors,
currentActors: this.getActors(),
});
}
addActor(actorEntry) {
const { uid: actorUID, actor } = actorEntry;
const renderingEngine = this.getRenderingEngine();
if (!renderingEngine || renderingEngine.hasBeenDestroyed) {
console.warn(`Cannot add actor UID of ${actorUID} Rendering Engine has been destroyed`);
return;
}
if (!actorUID || !actor) {
throw new Error('Actors should have uid and vtk Actor properties');
}
if (this.getActor(actorUID)) {
console.warn(`Actor ${actorUID} already exists for this viewport`);
return;
}
const renderer = this.getRenderer();
renderer?.addActor(actor);
this._actors.set(actorUID, Object.assign({}, actorEntry));
this.updateCameraClippingPlanesAndRange();
triggerEvent(this.element, Events.ACTORS_CHANGED, {
viewportId: this.id,
removedActors: [],
addedActors: [actorEntry],
currentActors: this.getActors(),
});
}
removeAllActors() {
const currentActors = this.getActors();
this.getRenderer()?.removeAllViewProps();
this._actors = new Map();
triggerEvent(this.element, Events.ACTORS_CHANGED, {
viewportId: this.id,
removedActors: currentActors,
addedActors: [],
currentActors: [],
});
return;
}
resetCameraNoEvent() {
const savedValue = this._suppressCameraModifiedEvents;
this._suppressCameraModifiedEvents = true;
this.resetCamera();
this._suppressCameraModifiedEvents = savedValue;
}
setCameraNoEvent(camera) {
const savedValue = this._suppressCameraModifiedEvents;
this._suppressCameraModifiedEvents = true;
this.setCamera(camera);
this._suppressCameraModifiedEvents = savedValue;
}
_getViewImageDataIntersections(imageData, focalPoint, normal) {
const A = normal[0];
const B = normal[1];
const C = normal[2];
const D = A * focalPoint[0] + B * focalPoint[1] + C * focalPoint[2];
const bounds = imageData.getBounds();
const edges = this._getEdges(bounds);
const intersections = [];
for (const edge of edges) {
const [[x0, y0, z0], [x1, y1, z1]] = edge;
if (A * (x1 - x0) + B * (y1 - y0) + C * (z1 - z0) === 0) {
continue;
}
const intersectionPoint = planar.linePlaneIntersection([x0, y0, z0], [x1, y1, z1], [A, B, C, D]);
if (this._isInBounds(intersectionPoint, bounds)) {
intersections.push(intersectionPoint);
}
}
return intersections;
}
setInterpolationType(_interpolationType, _arg) {
}
setDisplayArea(displayArea, suppressEvents = false) {
if (!displayArea) {
return;
}
const { storeAsInitialCamera, type: areaType } = displayArea;
if (storeAsInitialCamera) {
this.options.displayArea = displayArea;
}
const { _suppressCameraModifiedEvents } = this;
this._suppressCameraModifiedEvents = true;
this.setCamera(this.fitToCanvasCamera);
if (areaType === 'SCALE') {
this.setDisplayAreaScale(displayArea);
}
else {
this.setInterpolationType(this.getProperties()?.interpolationType ?? InterpolationType.LINEAR);
this.setDisplayAreaFit(displayArea);
}
if (storeAsInitialCamera) {
this.initialCamera = this.getCamera();
}
this._suppressCameraModifiedEvents = _suppressCameraModifiedEvents;
if (!suppressEvents && !_suppressCameraModifiedEvents) {
const eventDetail = {
viewportId: this.id,
displayArea: displayArea,
storeAsInitialCamera: storeAsInitialCamera,
};
triggerEvent(this.element, Events.DISPLAY_AREA_MODIFIED, eventDetail);
this.setCamera(this.getCamera());
}
}
setDisplayAreaScale(displayArea) {
const { scale = 1 } = displayArea;
const canvas = this.canvas;
const height = canvas.height;
const width = canvas.width;
if (height < 8 || width < 8) {
return;
}
const imageData = this.getDefaultImageData();
const spacingWorld = imageData.getSpacing();
const spacing = spacingWorld[1];
this.setInterpolationType(InterpolationType.NEAREST);
this.setCamera({ parallelScale: (height * spacing) / (2 * scale) });
delete displayArea.imageArea;
this.setDisplayAreaFit(displayArea);
const { focalPoint, position, viewUp, viewPlaneNormal } = this.getCamera();
const focalChange = vec3.create();
if (canvas.height % 2) {
vec3.scaleAndAdd(focalChange, focalChange, viewUp, scale * 0.5 * spacing);
}
if (canvas.width % 2) {
const viewRight = vec3.cross(vec3.create(), viewUp, viewPlaneNormal);
vec3.scaleAndAdd(focalChange, focalChange, viewRight, scale * 0.5 * spacing);
}
if (!focalChange[0] && !focalChange[1] && !focalChange[2]) {
return;
}
this.setCamera({
focalPoint: vec3.add(vec3.create(), focalPoint, focalChange),
position: vec3.add(vec3.create(), position, focalChange),
});
}
setDisplayAreaFit(displayArea) {
const { imageArea, imageCanvasPoint } = displayArea;
const devicePixelRatio = window?.devicePixelRatio || 1;
const imageData = this.getDefaultImageData();
if (!imageData) {
return;
}
const canvasWidth = this.sWidth / devicePixelRatio;
const canvasHeight = this.sHeight / devicePixelRatio;
const dimensions = imageData.getDimensions();
const canvasZero = this.worldToCanvas(imageData.indexToWorld([0, 0, 0]));
const canvasEdge = this.worldToCanvas(imageData.indexToWorld([
dimensions[0],
dimensions[1],
dimensions[2],
]));
const canvasImage = [
Math.abs(canvasEdge[0] - canvasZero[0]),
Math.abs(canvasEdge[1] - canvasZero[1]),
];
const [imgWidth, imgHeight] = canvasImage;
let zoom = this.getZoom() / this.insetImageMultiplier;
if (imageArea) {
const [areaX, areaY] = imageArea;
const currentScale = Math.max(Math.abs(imgWidth / canvasWidth), Math.abs(imgHeight / canvasHeight));
const requireX = Math.abs((areaX * imgWidth) / canvasWidth);
const requireY = Math.abs((areaY * imgHeight) / canvasHeight);
const initZoom = this.getZoom();
const fitZoom = this.getZoom(this.fitToCanvasCamera);
const absZoom = requireX > requireY ? currentScale / requireX : currentScale / requireY;
const applyZoom = (absZoom * initZoom) / fitZoom;
zoom = applyZoom;
this.setZoom(this.insetImageMultiplier * zoom, false);
}
if (imageCanvasPoint) {
const { imagePoint, canvasPoint = imagePoint || [0.5, 0.5] } = imageCanvasPoint;
const [canvasX, canvasY] = canvasPoint;
const canvasPanX = canvasWidth * (canvasX - 0.5);
const canvasPanY = canvasHeight * (canvasY - 0.5);
const [imageX, imageY] = imagePoint || canvasPoint;
const useZoom = zoom;
const imagePanX = this.insetImageMultiplier * useZoom * imgWidth * (0.5 - imageX);
const imagePanY = this.insetImageMultiplier * useZoom * imgHeight * (0.5 - imageY);
const newPositionX = imagePanX + canvasPanX;
const newPositionY = imagePanY + canvasPanY;
const deltaPoint2 = [newPositionX, newPositionY];
vec2.add(deltaPoint2, deltaPoint2, this.getPan());
this.setPan(deltaPoint2, false);
}
}
getDisplayArea() {
return this.options?.displayArea;
}
resetCamera(options) {
const { resetPan = true, resetZoom = true, resetToCenter = true, storeAsInitialCamera = true, resetAspectRatio = true, } = options || {};
const renderer = this.getRenderer();
this.setCameraNoEvent({
flipHorizontal: false,
flipVertical: false,
});
const previousCamera = this.getCamera();
let bounds;
const defaultActor = this.getDefaultActor();
if (defaultActor && isImageActor(defaultActor)) {
const imageData = defaultActor.actor.getMapper().getInputData();
bounds = imageData.getBounds();
}
else {
bounds = renderer.computeVisiblePropBounds();
}
const focalPoint = [0, 0, 0];
const imageData = this.getDefaultImageData();
const activeCamera = this.getVtkActiveCamera();
const viewPlaneNormal = activeCamera.getViewPlaneNormal();
const viewUp = activeCamera.getViewUp();
focalPoint[0] = (bounds[0] + bounds[1]) / 2.0;
focalPoint[1] = (bounds[2] + bounds[3]) / 2.0;
focalPoint[2] = (bounds[4] + bounds[5]) / 2.0;
if (imageData) {
const dimensions = imageData.getDimensions();
const middleIJK = getVolumeCenterIJK(dimensions, imageData.getDirection(), viewPlaneNormal);
const idx = [middleIJK[0], middleIJK[1], middleIJK[2]];
imageData.indexToWorld(idx, focalPoint);
}
const { widthWorld, heightWorld } = imageData
? getCubeSizeInView(imageData, viewPlaneNormal, viewUp)
: this._getWorldDistanceViewUpAndViewRight(bounds, viewUp, viewPlaneNormal);
const canvasSize = [this.sWidth, this.sHeight];
const boundsAspectRatio = widthWorld / heightWorld;
const canvasAspectRatio = canvasSize[0] / canvasSize[1];
const scaleFactor = boundsAspectRatio / canvasAspectRatio;
const parallelScale = scaleFactor < 1
? (this.insetImageMultiplier * heightWorld) / 2
: (this.insetImageMultiplier * heightWorld * scaleFactor) / 2;
const radius = Viewport.boundsRadius(bounds) *
(this.type === ViewportType.VOLUME_3D ? 10 : 1);
const distance = this.insetImageMultiplier * radius;
const viewUpToSet = Math.abs(vtkMath.dot(viewUp, viewPlaneNormal)) > 0.999
? [-viewUp[2], viewUp[0], viewUp[1]]
: viewUp;
const focalPointToSet = this._getFocalPointForResetCamera(focalPoint, previousCamera, { resetPan, resetToCenter });
const positionToSet = [
focalPointToSet[0] + distance * viewPlaneNormal[0],
focalPointToSet[1] + distance * viewPlaneNormal[1],
focalPointToSet[2] + distance * viewPlaneNormal[2],
];
renderer.resetCameraClippingRange(bounds);
const clippingRangeToUse = [
-RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE,
RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE,
];
activeCamera.setPhysicalScale(radius);
activeCamera.setPhysicalTranslation(-focalPointToSet[0], -focalPointToSet[1], -focalPointToSet[2]);
const targetAspectRatio = resetAspectRatio
? this.options?.aspectRatio || [1, 1]
: this.getAspectRatio();
this.setCamera({
parallelScale: resetZoom ? parallelScale : previousCamera.parallelScale,
focalPoint: focalPointToSet,
position: positionToSet,
viewAngle: 90,
viewUp: viewUpToSet,
clippingRange: clippingRangeToUse,
aspectRatio: targetAspectRatio,
isFitViewportAfterStretch: false,
});
const modifiedCamera = this.getCamera();
this.setFitToCanvasCamera(this.getCamera());
if (storeAsInitialCamera) {
this.setInitialCamera(modifiedCamera);
}
if (resetZoom) {
this.setZoom(1, storeAsInitialCamera);
}
const RESET_CAMERA_EVENT = {
type: 'ResetCameraEvent',
renderer,
};
renderer.invokeEvent(RESET_CAMERA_EVENT);
this.triggerCameraModifiedEventIfNecessary(previousCamera, modifiedCamera);
if (imageData &&
this.options.displayArea &&
resetZoom &&
resetPan &&
resetToCenter) {
this.setDisplayArea(this.options.displayArea);
}
return true;
}
setInitialCamera(camera) {
this.initialCamera = camera;
}
setFitToCanvasCamera(camera) {
this.fitToCanvasCamera = camera;
}
getPan(initialCamera = this.initialCamera) {
if (!initialCamera) {
return [0, 0];
}
const activeCamera = this.getVtkActiveCamera();
const focalPoint = activeCamera.getFocalPoint();
const zero3 = this.canvasToWorld([0, 0]);
const initialCanvasFocal = this.worldToCanvas(vec3.subtract([0, 0, 0], initialCamera.focalPoint, zero3));
const currentCanvasFocal = this.worldToCanvas(vec3.subtract([0, 0, 0], focalPoint, zero3));
const result = vec2.subtract([0, 0], initialCanvasFocal, currentCanvasFocal);
return result;
}
getCurrentImageIdIndex() {
throw new Error('Not implemented');
}
getSliceIndex() {
throw new Error('Not implemented');
}
getImageData() {
throw new Error('Not implemented');
}
getViewReferenceId(_specifier) {
return null;
}
setPan(pan, storeAsInitialCamera = false) {
const previousCamera = this.getCamera();
const { focalPoint, position } = previousCamera;
const zero3 = this.canvasToWorld([0, 0]);
const delta2 = vec2.subtract([0, 0], pan, this.getPan());
if (Math.abs(delta2[0]) < 1 &&
Math.abs(delta2[1]) < 1 &&
!storeAsInitialCamera) {
return;
}
const delta = vec3.subtract(vec3.create(), this.canvasToWorld(delta2), zero3);
const newFocal = vec3.subtract(vec3.create(), focalPoint, delta);
const newPosition = vec3.subtract(vec3.create(), position, delta);
this.setCamera({
...previousCamera,
focalPoint: newFocal,
position: newPosition,
}, storeAsInitialCamera);
}
getZoom(compareCamera = this.initialCamera) {
if (!compareCamera) {
return 1;
}
const activeCamera = this.getVtkActiveCamera();
const { parallelScale: initialParallelScale } = compareCamera;
return initialParallelScale / activeCamera.getParallelScale();
}
setZoom(value, storeAsInitialCamera = false) {
const camera = this.getCamera();
const { parallelScale: initialParallelScale } = this.initialCamera;
const parallelScale = initialParallelScale / value;
if (camera.parallelScale === parallelScale && !storeAsInitialCamera) {
return;
}
this.setCamera({
...camera,
parallelScale,
}, storeAsInitialCamera);
}
getAspectRatio() {
const { aspectRatio } = this.getCamera();
return aspectRatio ?? this.options?.aspectRatio ?? [1, 1];
}
setAspectRatio(value, isFitViewportAfterStretch = true, storeAsInitialCamera = false) {
const camera = this.getCamera();
if (storeAsInitialCamera) {
this.options.aspectRatio = value;
}
this.setCamera({
...camera,
aspectRatio: value,
isFitViewportAfterStretch,
}, storeAsInitialCamera);
}
_getFocalPointForViewPlaneReset(imageData) {
const { focalPoint, viewPlaneNormal: normal } = this.getCamera();
const intersections = this._getViewImageDataIntersections(imageData, focalPoint, normal);
let x = 0;
let y = 0;
let z = 0;
intersections.forEach(([point_x, point_y, point_z]) => {
x += point_x;
y += point_y;
z += point_z;
});
const newFocalPoint = [
x / intersections.length,
y / intersections.length,
z / intersections.length,
];
return newFocalPoint;
}
getCanvas() {
return this.canvas;
}
getVtkActiveCamera() {
const renderer = this.getRenderer();
if (!renderer) {
console.warn('No renderer found for the viewport');
return null;
}
return renderer.getActiveCamera();
}
getCameraNoRotation() {
const vtkCamera = this.getVtkActiveCamera();
const sanitizeVector = (vector, defaultValue) => {
return vector.some((v) => isNaN(v)) ? defaultValue : vector;
};
const viewUp = sanitizeVector([...vtkCamera.getViewUp()], [0, 1, 0]);
const viewPlaneNormal = sanitizeVector([...vtkCamera.getViewPlaneNormal()], [0, 0, -1]);
const position = sanitizeVector([...vtkCamera.getPosition()], [0, 0, 1]);
const focalPoint = sanitizeVector([...vtkCamera.getFocalPoint()], [0, 0, 0]);
return {
viewUp,
viewPlaneNormal,
position,
focalPoint,
parallelProjection: vtkCamera.getParallelProjection(),
parallelScale: vtkCamera.getParallelScale(),
viewAngle: vtkCamera.getViewAngle(),
flipHorizontal: this.flipHorizontal,
flipVertical: this.flipVertical,
aspectRatio: vtkCamera.getAspectRatio(),
};
}
getCamera() {
const camera = this.getCameraNoRotation();
return {
...camera,
rotation: this.getRotation(),
};
}
setAspectRatioForVTKCamera(aspectRatio, isFitViewportAfterStretch = true) {
const vtkCamera = this.getVtkActiveCamera();
if (!isFitViewportAfterStretch) {
vtkCamera.setAspectRatio(aspectRatio);
return;
}
const currentAspect = vtkCamera.getAspectRatio() || [1, 1];
if (currentAspect[0] === aspectRatio[0] &&
currentAspect[1] === aspectRatio[1]) {
return;
}
const getRatioValue = ([x, y]) => x / y;
const oldRatioValue = getRatioValue(currentAspect);
const newRatioValue = getRatioValue(aspectRatio);
vtkCamera.setAspectRatio(aspectRatio);
const imageData = this.getDefaultImageData();
if (!imageData)
return;
const getFitScale = (rVal) => {
const { widthWorld, heightWorld } = getCubeSizeInView(imageData, vtkCamera.getViewPlaneNormal(), vtkCamera.getViewUp());
const canvasAspectRatio = this.sWidth / this.sHeight;
const effectiveWidth = widthWorld * rVal;
const effectiveImageRatio = effectiveWidth / heightWorld;
let fitScale = effectiveImageRatio > canvasAspectRatio
? effectiveWidth / (2 * canvasAspectRatio)
: heightWorld / 2;
if (rVal < 1)
fitScale /= rVal;
return fitScale;
};
const ratioFactor = getFitScale(newRatioValue) / getFitScale(oldRatioValue);
vtkCamera.setParallelScale(vtkCamera.getParallelScale() * ratioFactor);
if (this.initialCamera?.parallelScale) {
this.initialCamera = {
...this.initialCamera,
parallelScale: this.initialCamera.parallelScale * ratioFactor,
};
}
if (this.fitToCanvasCamera?.parallelScale) {
this.fitToCanvasCamera = {
...this.fitToCanvasCamera,
parallelScale: this.fitToCanvasCamera.parallelScale * ratioFactor,
};
}
}
setCamera(cameraInterface, storeAsInitialCamera = false) {
const vtkCamera = this.getVtkActiveCamera();
const previousCamera = this.getCamera();
const updatedCamera = Object.assign({}, previousCamera, cameraInterface);
const { viewUp, viewPlaneNormal, position, focalPoint, parallelScale, viewAngle, flipHorizontal, flipVertical, clippingRange, aspectRatio, isFitViewportAfterStretch, } = cameraInterface;
if (flipHorizontal !== undefined) {
const flipH = (flipHorizontal && !this.flipHorizontal) ||
(!flipHorizontal && this.flipHorizontal);
if (flipH) {
this.flip({ flipHorizontal: flipH });
}
}
if (flipVertical !== undefined) {
const flipV = (flipVertical && !this.flipVertical) ||
(!flipVertical && this.flipVertical);
if (flipV) {
this.flip({ flipVertical: flipV });
}
}
if (viewUp !== undefined) {
vtkCamera.setViewUp(viewUp);
}
if (viewPlaneNormal !== undefined) {
vtkCamera.setDirectionOfProjection(-viewPlaneNormal[0], -viewPlaneNormal[1], -viewPlaneNormal[2]);
}
if (position !== undefined) {
vtkCamera.setPosition(...position);
}
if (focalPoint !== undefined) {
vtkCamera.setFocalPoint(...focalPoint);
}
if (parallelScale !== undefined) {
vtkCamera.setParallelScale(parallelScale);
}
if (viewAngle !== undefined) {
vtkCamera.setViewAngle(viewAngle);
}
if (clippingRange !== undefined) {
vtkCamera.setClippingRange(clippingRange);
}
if (aspectRatio) {
this.setAspectRatioForVTKCamera(aspectRatio, isFitViewportAfterStretch);
}
const prevFocalPoint = previousCamera.focalPoint;
const prevViewUp = previousCamera.viewUp;
if ((prevFocalPoint && focalPoint) || (prevViewUp && viewUp)) {
const currentViewPlaneNormal = vtkCamera.getViewPlaneNormal();
const currentViewUp = vtkCamera.getViewUp();
let cameraModifiedOutOfPlane = false;
let viewUpHasChanged = false;
if (focalPoint) {
const deltaCamera = [
focalPoint[0] - prevFocalPoint[0],
focalPoint[1] - prevFocalPoint[1],
focalPoint[2] - prevFocalPoint[2],
];
cameraModifiedOutOfPlane =
Math.abs(vtkMath.dot(deltaCamera, currentViewPlaneNormal)) > 0;
}
if (viewUp) {
viewUpHasChanged = !isEqual(currentViewUp, prevViewUp);
}
if (cameraModifiedOutOfPlane || viewUpHasChanged) {
const actorEntry = this.getDefaultActor();
if (!actorEntry?.actor) {
return;
}
if (!actorIsA(actorEntry, 'vtkActor')) {
this.updateClippingPlanesForActors(updatedCamera);
}
if (actorIsA(actorEntry, 'vtkImageSlice') ||
this.type === ViewportType.VOLUME_3D) {
const renderer = this.getRenderer();
renderer.resetCameraClippingRange();
}
}
}
if (storeAsInitialCamera) {
this.setInitialCamera(updatedCamera);
}
this.triggerCameraModifiedEventIfNecessary(previousCamera, this.getCamera());
}
triggerCameraModifiedEventIfNecessary(previousCamera, updatedCamera) {
if (!this._suppressCameraModifiedEvents && !this.suppressEvents) {
const eventDetail = {
previousCamera,
camera: updatedCamera,
element: this.element,
viewportId: this.id,
renderingEngineId: this.renderingEngineId,
};
triggerEvent(this.element, Events.CAMERA_MODIFIED, eventDetail);
}
}
updateCameraClippingPlanesAndRange() {
const currentCamera = this.getCamera();
this.updateClippingPlanesForActors(currentCamera);
this.getRenderer().resetCameraClippingRange();
}
async updateClippingPlanesForActors(updatedCamera) {
const actorEntries = this.getActors();
actorEntries.map((actorEntry) => {
if (!actorEntry.actor) {
return;
}
const mapper = actorEntry.actor.getMapper();
let vtkPlanes = actorEntry?.clippingFilter
? actorEntry?.clippingFilter.getClippingPlanes()
: mapper.getClippingPlanes();
if (vtkPlanes.length === 0 && actorEntry?.clippingFilter) {
vtkPlanes = [vtkPlane.newInstance(), vtkPlane.newInstance()];
}
let slabThickness = RENDERING_DEFAULTS.MINIMUM_SLAB_THICKNESS;
if (actorEntry.slabThickness) {
slabThickness = actorEntry.slabThickness;
}
const { viewPlaneNormal, focalPoint } = updatedCamera;
this.setOrientationOfClippingPlanes(vtkPlanes, slabThickness, viewPlaneNormal, focalPoint);
triggerEvent(this.element, Events.CLIPPING_PLANES_UPDATED, {
actorEntry,
focalPoint,
vtkPlanes,
viewport: this,
});
});
}
setOrientationOfClippingPlanes(vtkPlanes, slabThickness, viewPlaneNormal, focalPoint) {
if (vtkPlanes.length < 2) {
return;
}
const scaledDistance = [
viewPlaneNormal[0],
viewPlaneNormal[1],
viewPlaneNormal[2],
];
vtkMath.multiplyScalar(scaledDistance, slabThickness);
vtkPlanes[0].setNormal(viewPlaneNormal);
const newOrigin1 = [0, 0, 0];
vtkMath.subtract(focalPoint, scaledDistance, newOrigin1);
vtkPlanes[0].setOrigin(newOrigin1);
vtkPlanes[1].setNormal(-viewPlaneNormal[0], -viewPlaneNormal[1], -viewPlaneNormal[2]);
const newOrigin2 = [0, 0, 0];
vtkMath.add(focalPoint, scaledDistance, newOrigin2);
vtkPlanes[1].setOrigin(newOrigin2);
}
getClippingPlanesForActor(actorEntry) {
if (!actorEntry) {
actorEntry = this.getDefaultActor();
}
if (!actorEntry.actor) {
throw new Error('Invalid actor entry: Actor is undefined');
}
const mapper = actorEntry.actor.getMapper();
let vtkPlanes = actorEntry?.clippingFilter
? actorEntry?.clippingFilter.getClippingPlanes()
: mapper.getClippingPlanes();
if (vtkPlanes.length === 0 && actorEntry?.clippingFilter) {
vtkPlanes = [vtkPlane.newInstance(), vtkPlane.newInstance()];
}
return vtkPlanes;
}
_getWorldDistanceViewUpAndViewRight(bounds, viewUp, viewPlaneNormal) {
const viewUpCorners = this._getCorners(bounds);
const viewRightCorners = this._getCorners(bounds);
const viewRight = vec3.cross(vec3.create(), viewUp, viewPlaneNormal);
let transform = vtkMatrixBuilder
.buildFromDegree()
.identity()
.rotateFromDirections(viewUp, [1, 0, 0]);
viewUpCorners.forEach((pt) => transform.apply(pt));
let minY = Infinity;
let maxY = -Infinity;
for (let i = 0; i < 8; i++) {
const y = viewUpCorners[i][0];
if (y > maxY) {
maxY = y;
}
if (y < minY) {
minY = y;
}
}
transform = vtkMatrixBuilder
.buildFromDegree()
.identity()
.rotateFromDirections([viewRight[0], viewRight[1], viewRight[2]], [1, 0, 0]);
viewRightCorners.forEach((pt) => transform.apply(pt));
let minX = Infinity;
let maxX = -Infinity;
for (let i = 0; i < 8; i++) {
const x = viewRightCorners[i][0];
if (x > maxX) {
maxX = x;
}
if (x < minX) {
minX = x;
}
}
return { widthWorld: maxX - minX, heightWorld: maxY - minY };
}
getViewReference(viewRefSpecifier) {
const { focalPoint: cameraFocalPoint, viewPlaneNormal, viewUp, } = this.getCamera();
const FrameOfReferenceUID = this.getFrameOfReferenceUID();
const target = {
FrameOfReferenceUID,
cameraFocalPoint,
viewPlaneNormal,
viewUp,
sliceIndex: viewRefSpecifier?.sliceIndex ?? this.getSliceIndex(),
planeRestriction: {
FrameOfReferenceUID,
point: viewRefSpecifier?.points?.[0] || cameraFocalPoint,
inPlaneVector1: viewUp,
inPlaneVector2: (vec3.cross(vec3.create(), viewUp, viewPlaneNormal)),
},
};
if (viewRefSpecifier?.points) {
updatePlaneRestriction(viewRefSpecifier.points, target.planeRestriction);
}
return target;
}
isPlaneViewable(planeRestriction, options) {
if (planeRestriction.FrameOfReferenceUID !== this.getFrameOfReferenceUID()) {
return false;
}
const { focalPoint, viewPlaneNormal } = this.getCamera();
const { point, inPlaneVector1, inPlaneVector2 } = planeRestriction;
if (options?.withOrientation) {
return true;
}
if (inPlaneVector1 &&
!isEqual(0, vec3.dot(viewPlaneNormal, inPlaneVector1))) {
return false;
}
if (inPlaneVector2 &&
!isEqual(0, vec3.dot(viewPlaneNormal, inPlaneVector2))) {
return false;
}
if (options?.withNavigation) {
return true;
}
const pointVector = vec3.sub(vec3.create(), point, focalPoint);
return isEqual(0, vec3.dot(pointVector, viewPlaneNormal));
}
isReferenceViewable(viewRef, options) {
if (viewRef.planeRestriction) {
return this.isPlaneViewable(viewRef.planeRestriction, options);
}
if (viewRef.FrameOfReferenceUID &&
viewRef.FrameOfReferenceUID !== this.getFrameOfReferenceUID()) {
return false;
}
const { viewPlaneNormal } = viewRef;
const camera = this.getCamera();
if (viewPlaneNormal &&
!isEqual(viewPlaneNormal, camera.viewPlaneNormal) &&
!isEqual(vec3.negate(camera.viewPlaneNormal, camera.viewPlaneNormal), viewPlaneNormal)) {
return options?.withOrientation;
}
return true;
}
getViewPresentation(viewPresSel = {
rotation: true,
displayArea: true,
zoom: true,
pan: true,
aspectRatio: true,
flipHorizontal: true,
flipVertical: true,
}) {
const target = {};
const { rotation, displayArea, zoom, pan, flipHorizontal, flipVertical } = viewPresSel;
if (rotation) {
target.rotation = this.getRotation();
}
if (displayArea) {
target.displayArea = this.getDisplayArea();
}
const initZoom = this.getZoom();
if (zoom) {
target.zoom = initZoom;
}
const currentAspectRatio = this.getAspectRatio();
target.aspectRatio = currentAspectRatio;
if (pan) {
const currentPan = this.getPan();
const [aspectX, aspectY] = currentAspectRatio;
const normalizedPanX = currentPan[0] / (initZoom * aspectX);
const normalizedPanY = currentPan[1] / (initZoom * aspectY);
target.pan = [normalizedPanX, normalizedPanY];
}
if (flipHorizontal) {
target.flipHorizontal = this.flipHorizontal;
}
if (flipVertical) {
target.flipVertical = this.flipVertical;
}
return target;
}
setViewReference(_viewRef) {
}
setViewPresentation(viewPres) {
if (!viewPres) {
return;
}
const { displayArea, zoom = this.getZoom(), pan, aspectRatio = this.getAspectRatio(), rotation, flipHorizontal = this.flipHorizontal, flipVertical = this.flipVertical, } = viewPres;
if (displayArea !== this.getDisplayArea()) {
this.setDisplayArea(displayArea);
}
this.setZoom(zoom);
this.setAspectRatio(aspectRatio);
if (pan) {
const [aspectX, aspectY] = aspectRatio;
this.setPan([pan[0] * zoom * aspectX, pan[1] * zoom * aspectY]);
}
if (flipHorizontal !== undefined &&
flipHorizontal !== this.flipHorizontal) {
this.flip({ flipHorizontal });
}
if (flipVertical !== undefined && flipVertical !== this.flipVertical) {
this.flip({ flipVertical });
}
if (rotation >= 0) {
this.setRotation(rotation);
}
}
_getCorners(bounds) {
return [
[bounds[0], bounds[2], bounds[4]],
[bounds[0], bounds[2], bounds[5]],
[bounds[0], bounds[3], bounds[4]],
[bounds[0], bounds[3], bounds[5]],
[bounds[1], bounds[2], bounds[4]],
[bounds[1], bounds[2], bounds[5]],
[bounds[1], bounds[3], bounds[4]],
[bounds[1], bounds[3], bounds[5]],
];
}
_getFocalPointForResetCamera(centeredFocalPoint, previousCamera, { resetPan = true, resetToCenter = true }) {
if (resetToCenter && resetPan) {
return centeredFocalPoint;
}
if (resetToCenter && !resetPan) {
return hasNaNValues(previousCamera.focalPoint)
? centeredFocalPoint
: previousCamera.focalPoint;
}
if (!resetToCenter && resetPan) {
const oldCamera = previousCamera;
const oldFocalPoint = oldCamera.focalPoint;
const oldViewPlaneNormal = oldCamera.viewPlaneNormal;
const vectorFromOldFocalPointToCenteredFocalPoint = vec3.subtract(vec3.create(), centeredFocalPoint, oldFocalPoint);
const distanceFromOldFocalPointToCenteredFocalPoint = vec3.dot(vectorFromOldFocalPointToCenteredFocalPoint, oldViewPlaneNormal);
const newFocalPoint = vec3.scaleAndAdd(vec3.create(), centeredFocalPoint, oldViewPlaneNormal, -1 * distanceFromOldFocalPointToCenteredFocalPoint);
return [newFocalPoint[0], newFocalPoint[1], newFocalPoint[2]];
}
if (!resetPan && !resetToCenter) {
return hasNaNValues(previousCamera.focalPoint)
? centeredFocalPoint
: previousCamera.focalPoint;
}
}
_isInBounds(point, bounds) {
const [xMin, xMax, yMin, yMax, zMin, zMax] = bounds;
const [x, y, z] = point;
if (x < xMin || x > xMax || y < yMin || y > yMax || z < zMin || z > zMax) {
return false;
}
return true;
}
_getEdges(bounds) {
const [p1, p2, p3, p4, p5, p6, p7, p8] = this._getCorners(bounds);
return [
[p1, p2],
[p1, p5],
[p1, p3],
[p2, p4],
[p2, p6],
[p3, p4],
[p3, p7],
[p4, p8],
[p5, p7],
[p5, p6],
[p6, p8],
[p7, p8],
];
}
static boundsRadius(bounds) {
const w1 = (bounds[1] - bounds[0]) ** 2;
const w2 = (bounds[3] - bounds[2]) ** 2;
const w3 = (bounds[5] - bounds[4]) ** 2;
const radius = Math.sqrt(w1 + w2 + w3 || 1) * 0.5;
return radius;
}
setDataList(_entries) {
throw new Error('Unsupported operation setDataList');
}
setDisplaySets(...entries) {
this._displaySets = entries;
}
async mountDisplaySets(entries, load) {
const [entry] = entries;
if (!entry?.displaySetId) {
throw new Error(`${this.constructor.name}: setDisplaySets requires a displaySetId`);
}
await load(entry);
this._displaySets = [entry];
}
getDisplaySets() {
return this._displaySets;
}
clearDisplaySets() {
this._displaySets = [];
}
}
Viewport.CameraViewPresentation = {
rotation: true,
pan: true,
zoom: true,
aspectRatio: true,
displayArea: true,
};
Viewport.TransferViewPresentation = {
windowLevel: true,
paletteLut: true,
};
export function getVolumeCenterIJK(dimensions, direction, viewPlaneNormal) {
const ijkAxes = [
[direction[0], direction[1], direction[2]],
[direction[3], direction[4], direction[5]],
[direction[6], direction[7], direction[8]],
];
const normal = [
viewPlaneNormal[0],
viewPlaneNormal[1],
viewPlaneNormal[2],
];
let sliceAxis = 0;
let maxDot = -1;
for (let i = 0; i < 3; i++) {
const dot = Math.abs(vec3.dot(ijkAxes[i], normal));
if (dot > maxDot) {
maxDot = dot;
sliceAxis = i;
}
}
const isAxisAligned = Math.abs(maxDot - 1) < 1e-3;
return dimensions.map((d, i) => i === sliceAxis && isAxisAligned ? Math.floor(d / 2) : (d - 1) / 2);
}
export default Viewport;