Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 30 additions & 9 deletions packages/core/src/RenderingEngine/StackViewport.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
import type { vtkImageData as vtkImageDataType } from '@kitware/vtk.js/Common/DataModel/ImageData';
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
import vtkCamera from '@kitware/vtk.js/Rendering/Core/Camera';
import extendedVtkCamera from './vtkClasses/extendedVtkCamera';
import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction';
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
import vtkImageMapper from '@kitware/vtk.js/Rendering/Core/ImageMapper';
Expand Down Expand Up @@ -285,7 +285,7 @@ class StackViewport extends Viewport {

private _resetGPUViewport() {
const renderer = this.getRenderer();
const camera = vtkCamera.newInstance();
const camera = extendedVtkCamera.newInstance();
renderer.setActiveCamera(camera);

const viewPlaneNormal = [0, 0, -1] as Point3;
Expand Down Expand Up @@ -350,6 +350,7 @@ class StackViewport extends Viewport {
resetZoom?: boolean;
resetToCenter?: boolean;
suppressEvents?: boolean;
resetAspectRatio?: boolean;
}) => boolean;

/**
Expand Down Expand Up @@ -848,6 +849,7 @@ class StackViewport extends Viewport {
resetPan: true,
resetZoom: true,
resetToCenter: true,
resetAspectRatio: true,
suppressEvents: true,
});
};
Expand Down Expand Up @@ -1045,8 +1047,14 @@ class StackViewport extends Viewport {
const { viewport, image } = this._cpuFallbackEnabledElement;
const previousCamera = this.getCameraCPU();

const { focalPoint, parallelScale, scale, flipHorizontal, flipVertical } =
cameraInterface;
const {
focalPoint,
parallelScale,
scale,
flipHorizontal,
flipVertical,
aspectRatio,
} = cameraInterface;

const { clientHeight } = this.element;

Expand Down Expand Up @@ -2683,7 +2691,7 @@ class StackViewport extends Viewport {
});
}

private resetCameraGPU({ resetPan, resetZoom }): boolean {
private resetCameraGPU({ resetPan, resetZoom, resetAspectRatio }): boolean {
// Todo: we need to make the rotation a camera properties so that
// we can reset it there, right now it is not possible to reset the rotation
// without this
Expand All @@ -2699,7 +2707,12 @@ class StackViewport extends Viewport {
// For stack Viewport we since we have only one slice
// it should be enough to reset the camera to the center of the image
const resetToCenter = true;
return super.resetCamera({ resetPan, resetZoom, resetToCenter });
return super.resetCamera({
resetPan,
resetZoom,
resetToCenter,
resetAspectRatio,
});
}

/**
Expand Down Expand Up @@ -3608,10 +3621,18 @@ class StackViewport extends Viewport {
return true;
},
gpu: (
options: { resetPan?: boolean; resetZoom?: boolean } = {}
options: {
resetPan?: boolean;
resetZoom?: boolean;
resetAspectRatio?: boolean;
} = {}
): boolean => {
const { resetPan = true, resetZoom = true } = options;
this.resetCameraGPU({ resetPan, resetZoom });
const {
resetPan = true,
resetZoom = true,
resetAspectRatio = true,
} = options;
this.resetCameraGPU({ resetPan, resetZoom, resetAspectRatio });
return true;
},
},
Expand Down
61 changes: 57 additions & 4 deletions packages/core/src/RenderingEngine/Viewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import type vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper';
import { deepClone } from '../utilities/deepClone';
import { updatePlaneRestriction } from '../utilities/updatePlaneRestriction';
import { getConfiguration } from '../init';
import type { extendedVtkCamera } from './vtkClasses/extendedVtkCamera';

/**
* An object representing a single viewport, which is a camera
Expand All @@ -71,6 +72,7 @@ class Viewport {
rotation: true,
pan: true,
zoom: true,
aspectRatio: true,
displayArea: true,
};

Expand Down Expand Up @@ -1057,12 +1059,14 @@ class Viewport {
resetZoom?: boolean;
resetToCenter?: boolean;
storeAsInitialCamera?: boolean;
resetAspectRatio?: boolean;
}): boolean {
const {
resetPan = true,
resetZoom = true,
resetToCenter = true,
storeAsInitialCamera = true,
resetAspectRatio = true,
} = options || {};

const renderer = this.getRenderer();
Expand Down Expand Up @@ -1208,6 +1212,7 @@ class Viewport {
viewAngle: 90,
viewUp: viewUpToSet,
clippingRange: clippingRangeToUse,
aspectRatio: resetAspectRatio && [1, 1],
});

const modifiedCamera = this.getCamera();
Expand Down Expand Up @@ -1391,6 +1396,36 @@ class Viewport {
);
}

/**
* Returns the current aspect ratio of the viewport as a 2D point `[widthRatio, heightRatio]`.
*
* @returns The current aspect ratio `[widthRatio, heightRatio]`
* based on the active camera settings.
*/
public getAspectRatio(): Point2 {
const { aspectRatio } = this.getCamera();
return aspectRatio;
}

/**
* Sets the aspect ratio of the viewport using the provided 2D point `[widthRatio, heightRatio]`.
*
* @param value - The aspect ratio to set as `[widthRatio, heightRatio]`.
* @param storeAsInitialCamera - Whether to store the updated camera state as the initial camera.
* Defaults to `false`.
*/
public setAspectRatio(value: Point2, storeAsInitialCamera = false): void {
const camera = this.getCamera();

this.setCamera(
{
...camera,
aspectRatio: value,
},
storeAsInitialCamera
);
}

/**
* Because the focalPoint is always in the centre of the viewport,
* we must do planar computations if the frame (image "slice") is to be preserved.
Expand Down Expand Up @@ -1454,7 +1489,7 @@ class Viewport {
}

protected getCameraNoRotation(): ICamera {
const vtkCamera = this.getVtkActiveCamera();
const vtkCamera = this.getVtkActiveCamera() as extendedVtkCamera;

// Helper function to replace NaN vectors with defaults
const sanitizeVector = (vector: Point3, defaultValue: Point3): Point3 => {
Expand Down Expand Up @@ -1489,6 +1524,7 @@ class Viewport {
viewAngle: vtkCamera.getViewAngle(),
flipHorizontal: this.flipHorizontal,
flipVertical: this.flipVertical,
aspectRatio: vtkCamera.getAspectRatio(),
};
}

Expand All @@ -1515,7 +1551,7 @@ class Viewport {
cameraInterface: ICamera,
storeAsInitialCamera = false
): void {
const vtkCamera = this.getVtkActiveCamera();
const vtkCamera = this.getVtkActiveCamera() as extendedVtkCamera;
const previousCamera = this.getCamera();
const updatedCamera = Object.assign({}, previousCamera, cameraInterface);
const {
Expand All @@ -1528,6 +1564,7 @@ class Viewport {
flipHorizontal,
flipVertical,
clippingRange,
aspectRatio,
} = cameraInterface;

// Note: Flip camera should be two separate calls since
Expand Down Expand Up @@ -1589,6 +1626,10 @@ class Viewport {
vtkCamera.setClippingRange(clippingRange);
}

if (aspectRatio) {
vtkCamera.setAspectRatio(aspectRatio);
}

// update clipping range only if focal point changed of a new actor is added
const prevFocalPoint = previousCamera.focalPoint;
const prevViewUp = previousCamera.viewUp;
Expand Down Expand Up @@ -1983,6 +2024,7 @@ class Viewport {
displayArea: true,
zoom: true,
pan: true,
aspectRatio: true,
flipHorizontal: true,
flipVertical: true,
}
Expand All @@ -2002,9 +2044,17 @@ class Viewport {
if (zoom) {
target.zoom = initZoom;
}
const currentAspectRatio = this.getAspectRatio();
target.aspectRatio = currentAspectRatio;
if (pan) {
target.pan = this.getPan();
vec2.scale(target.pan, target.pan, 1 / initZoom);
const currentPan = this.getPan();
const [aspectX, aspectY] = currentAspectRatio;

// Normalize pan to remove effect of zoom and stretch
const normalizedPanX = currentPan[0] / (initZoom * aspectX);
const normalizedPanY = currentPan[1] / (initZoom * aspectY);

target.pan = [normalizedPanX, normalizedPanY] as Point2;
}

if (flipHorizontal) {
Expand Down Expand Up @@ -2036,6 +2086,7 @@ class Viewport {
displayArea,
zoom = this.getZoom(),
pan,
aspectRatio = this.getAspectRatio(),
rotation,
flipHorizontal = this.flipHorizontal,
flipVertical = this.flipVertical,
Expand All @@ -2048,6 +2099,8 @@ class Viewport {
this.setPan(vec2.scale([0, 0], pan, zoom) as Point2);
}

this.setAspectRatio(aspectRatio);

// flip operation requires another re-render to take effect, so unfortunately
// right now if the view presentation requires a flip, it will flicker. The
// correct way to handle this is to wait for camera and flip together and then
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/RenderingEngine/VolumeViewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,12 +381,13 @@ class VolumeViewport extends BaseVolumeViewport {
resetToCenter = true,
suppressEvents = false,
resetOrientation = true,
resetAspectRatio = true,
} = options || {};
const { orientation } = this.viewportProperties;
if (orientation && resetOrientation) {
this.applyViewOrientation(orientation, false);
}
super.resetCamera({ resetPan, resetZoom, resetToCenter });
super.resetCamera({ resetPan, resetZoom, resetToCenter, resetAspectRatio });

const activeCamera = this.getVtkActiveCamera();
const viewPlaneNormal = activeCamera.getViewPlaneNormal() as Point3;
Expand Down Expand Up @@ -821,11 +822,13 @@ class VolumeViewport extends BaseVolumeViewport {
const resetZoom = true;
const resetToCenter = true;
const resetCameraRotation = true;
const resetAspectRatio = true;
this.resetCamera({
resetPan,
resetZoom,
resetToCenter,
resetCameraRotation,
resetAspectRatio,
});

triggerEvent(this.element, Events.VOI_MODIFIED, eventDetails);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { vec3 } from 'gl-matrix';

const EPSILON = 1e-6;

/**
* Determine which projection-matrix indices to multiply for canvas X and Y scaling.
* For each anatomical axis stretch work as follows
* axial => stretch in anterior-posterior
* sagittal => stretch in superior-inferior
* coronal => stretch in superior-inferior
* @param {Array<number>} viewUp - [ux, uy, uz] camera viewUp (patient-space)
* @param {Array<number>} viewPlaneNormal - [dx, dy, dz] camera viewPlaneNormal (patient-space)
* @returns {{ idxX: number, idxY: number}}
*/
export function getProjectionScaleIndices(viewUp, viewPlaneNormal) {
const up = vec3.normalize(vec3.create(), viewUp);
const vpn = vec3.normalize(vec3.create(), viewPlaneNormal);

// Image axes in patient space
// Right-hand coordinate system: imageX = up × vpn
const imageX = vec3.create();
vec3.cross(imageX, up, vpn);
if (vec3.length(imageX) < EPSILON) {
// Fallback: if up and vpn are nearly parallel, create an orthogonal axis
const tmp =
Math.abs(up[0]) < 1 / Math.sqrt(2) // Use 45 degree to find the nearest isometric axis
? vec3.fromValues(1, 0, 0)
: vec3.fromValues(0, 1, 0);
vec3.cross(imageX, tmp, up);
}
vec3.normalize(imageX, imageX);
const imageY = up;

// Determine anatomical orientation (axial/sagittal/coronal)
const absVpn = [Math.abs(vpn[0]), Math.abs(vpn[1]), Math.abs(vpn[2])];
let orientation = 'axial';
if (absVpn[0] === Math.max(...absVpn)) {
orientation = 'sagittal';
} else if (absVpn[1] === Math.max(...absVpn)) {
orientation = 'coronal';
}

// Determine which anatomical axis to stretch
const AY = vec3.fromValues(0, 1, 0); // A-P
const AZ = vec3.fromValues(0, 0, 1); // S-I
const target = orientation === 'axial' ? AY : AZ;

// Which image axis (X or Y) aligns better with target?
const scoreX = Math.abs(vec3.dot(imageX, target));
const scoreY = Math.abs(vec3.dot(imageY, target));

// Choose projection-matrix diagonal indices
// 0 - scale X (horizontal)
// 5 - scale Y (vertical)
let [idxX, idxY] = [0, 5];
// If target aligns with horizontal, swap so vertical scaling follows rotation
if (scoreX > scoreY) {
[idxX, idxY] = [5, 0];
}

return { idxX, idxY };
}
2 changes: 2 additions & 0 deletions packages/core/src/RenderingEngine/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import setVolumesForViewports from './setVolumesForViewports';
import addVolumesToViewports from './addVolumesToViewports';
import volumeNewImageEventDispatcher from './volumeNewImageEventDispatcher';
import addImageSlicesToViewports from './addImageSlicesToViewports';
import { getProjectionScaleIndices } from './getProjectionScaleIndices';

export {
createVolumeActor,
Expand All @@ -13,4 +14,5 @@ export {
addVolumesToViewports,
addImageSlicesToViewports,
volumeNewImageEventDispatcher,
getProjectionScaleIndices,
};
Loading
Loading