UNPKG

@cornerstonejs/core

Version:
1,246 lines (1,245 loc) • 49.7 kB
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;