Skip to content

Commit

Permalink
feat: reset to center option for reset camera (#269)
Browse files Browse the repository at this point in the history
* fix: reset camera refactor to apply via setCamera

* initial implementation for resetToCenter

* fix: stack viewport reset camera

* feat: convert to gl matrix

* feat: add to examples

* fix build

* add review comments and fix test
  • Loading branch information
sedghi authored Oct 31, 2022
1 parent 6367722 commit 9539f6c
Show file tree
Hide file tree
Showing 12 changed files with 403 additions and 49 deletions.
13 changes: 10 additions & 3 deletions common/reviews/api/core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,8 @@ interface ICachedVolume {

// @public (undocumented)
interface ICamera {
// (undocumented)
clippingRange?: Point2;
// (undocumented)
flipHorizontal?: boolean;
// (undocumented)
Expand Down Expand Up @@ -1396,7 +1398,7 @@ interface IVolumeViewport extends IViewport {
// (undocumented)
removeVolumeActors(actorUIDs: Array<string>, immediate?: boolean): void;
// (undocumented)
resetCamera(resetPan?: boolean, resetZoom?: boolean): boolean;
resetCamera(resetPan?: boolean, resetZoom?: boolean, resetToCenter?: boolean): boolean;
// (undocumented)
setBlendMode(blendMode: BlendModes, filterActorUIDs?: Array<string>, immediate?: boolean): void;
// (undocumented)
Expand Down Expand Up @@ -1991,6 +1993,11 @@ export class Viewport implements IViewport {
// (undocumented)
_getEdges(bounds: Array<number>): Array<[number[], number[]]>;
// (undocumented)
_getFocalPointForResetCamera(centeredFocalPoint: Point3, previousCamera: ICamera, { resetPan, resetToCenter }: {
resetPan: boolean;
resetToCenter: boolean;
}): Point3;
// (undocumented)
getFrameOfReferenceUID: () => string;
// (undocumented)
getPan(): Point2;
Expand Down Expand Up @@ -2027,7 +2034,7 @@ export class Viewport implements IViewport {
// (undocumented)
reset(immediate?: boolean): void;
// (undocumented)
protected resetCamera(resetPan?: boolean, resetZoom?: boolean, storeAsInitialCamera?: boolean): boolean;
protected resetCamera(resetPan?: boolean, resetZoom?: boolean, resetToCenter?: boolean, storeAsInitialCamera?: boolean): boolean;
// (undocumented)
protected resetCameraNoEvent(): void;
// (undocumented)
Expand Down Expand Up @@ -2213,7 +2220,7 @@ export class VolumeViewport extends Viewport implements IVolumeViewport {
// (undocumented)
removeVolumeActors(actorUIDs: Array<string>, immediate?: boolean): void;
// (undocumented)
resetCamera(resetPan?: boolean, resetZoom?: boolean): boolean;
resetCamera(resetPan?: boolean, resetZoom?: boolean, resetToCenter?: boolean): boolean;
// (undocumented)
setBlendMode(blendMode: BlendModes, filterActorUIDs?: any[], immediate?: boolean): void;
// (undocumented)
Expand Down
7 changes: 6 additions & 1 deletion common/reviews/api/streaming-image-volume-loader.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ interface ICachedVolume {

// @public
interface ICamera {
clippingRange?: Point2;
flipHorizontal?: boolean;
flipVertical?: boolean;
focalPoint?: Point3;
Expand Down Expand Up @@ -1001,7 +1002,11 @@ interface IVolumeViewport extends IViewport {
hasImageURI: (imageURI: string) => boolean;
hasVolumeId: (volumeId: string) => boolean;
removeVolumeActors(actorUIDs: Array<string>, immediate?: boolean): void;
resetCamera(resetPan?: boolean, resetZoom?: boolean): boolean;
resetCamera(
resetPan?: boolean,
resetZoom?: boolean,
resetToCenter?: boolean
): boolean;
setBlendMode(
blendMode: BlendModes,
filterActorUIDs?: Array<string>,
Expand Down
7 changes: 6 additions & 1 deletion common/reviews/api/tools.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1905,6 +1905,7 @@ interface ICachedVolume {

// @public
interface ICamera {
clippingRange?: Point2;
flipHorizontal?: boolean;
flipVertical?: boolean;
focalPoint?: Point3;
Expand Down Expand Up @@ -2529,7 +2530,11 @@ interface IVolumeViewport extends IViewport {
hasImageURI: (imageURI: string) => boolean;
hasVolumeId: (volumeId: string) => boolean;
removeVolumeActors(actorUIDs: Array<string>, immediate?: boolean): void;
resetCamera(resetPan?: boolean, resetZoom?: boolean): boolean;
resetCamera(
resetPan?: boolean,
resetZoom?: boolean,
resetToCenter?: boolean
): boolean;
setBlendMode(
blendMode: BlendModes,
filterActorUIDs?: Array<string>,
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/RenderingEngine/StackViewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1875,8 +1875,10 @@ class StackViewport extends Viewport implements IStackViewport {
// without this
this.getVtkActiveCamera().roll(this.rotationCache);

// reset other properties
return super.resetCamera(resetPan, resetZoom);
// 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);
}

/**
Expand Down
125 changes: 92 additions & 33 deletions packages/core/src/RenderingEngine/Viewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Events from '../enums/Events';
import ViewportType from '../enums/ViewportType';
import renderingEngineCache from './renderingEngineCache';
import { triggerEvent, planar, isImageActor } from '../utilities';
import hasNaNValues from '../utilities/hasNaNValues';
import { RENDERING_DEFAULTS } from '../constants';
import type {
ICamera,
Expand Down Expand Up @@ -545,6 +546,7 @@ class Viewport implements IViewport {
protected resetCamera(
resetPan = true,
resetZoom = true,
resetToCenter = true,
storeAsInitialCamera = true
): boolean {
const renderer = this.getRenderer();
Expand Down Expand Up @@ -573,7 +575,6 @@ class Viewport implements IViewport {
// Reset the perspective zoom factors, otherwise subsequent zooms will cause
// the view angle to become very small and cause bad depth sorting.
// todo: parallel projection only
activeCamera.setViewAngle(90.0);

focalPoint[0] = (bounds[0] + bounds[1]) / 2.0;
focalPoint[1] = (bounds[2] + bounds[3]) / 2.0;
Expand Down Expand Up @@ -624,53 +625,53 @@ class Viewport implements IViewport {
radius = Math.sqrt(radius) * 0.5;

const distance = 1.1 * radius;
// const distance = radius / Math.sin(angle * 0.5)

// check view-up vector against view plane normal
if (Math.abs(vtkMath.dot(viewUp, viewPlaneNormal)) > 0.999) {
activeCamera.setViewUp(-viewUp[2], viewUp[0], viewUp[1]);
}

const focalPointToSet = resetPan ? focalPoint : previousCamera.focalPoint;
const viewUpToSet: Point3 =
Math.abs(vtkMath.dot(viewUp, viewPlaneNormal)) > 0.999
? [-viewUp[2], viewUp[0], viewUp[1]]
: viewUp;

activeCamera.setFocalPoint(
focalPointToSet[0],
focalPointToSet[1],
focalPointToSet[2]
const focalPointToSet = this._getFocalPointForResetCamera(
focalPoint,
previousCamera,
{ resetPan, resetToCenter }
);
activeCamera.setPosition(

const positionToSet: Point3 = [
focalPointToSet[0] + distance * viewPlaneNormal[0],
focalPointToSet[1] + distance * viewPlaneNormal[1],
focalPointToSet[2] + distance * viewPlaneNormal[2]
);
focalPointToSet[2] + distance * viewPlaneNormal[2],
];

renderer.resetCameraClippingRange(bounds);

if (resetZoom) {
activeCamera.setParallelScale(parallelScale);
}
const clippingRangeToUse: Point2 = [
-RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE,
RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE,
];

// update reasonable world to physical values
activeCamera.setPhysicalScale(radius);

// TODO: The PhysicalXXX stuff are used for VR only, do we need this?
activeCamera.setPhysicalTranslation(
-focalPointToSet[0],
-focalPointToSet[1],
-focalPointToSet[2]
);

activeCamera.setClippingRange(
-RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE,
RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE
);
this.setCamera({
parallelScale: resetZoom ? parallelScale : previousCamera.parallelScale,
focalPoint: focalPointToSet,
position: positionToSet,
viewAngle: 90,
viewUp: viewUpToSet,
clippingRange: clippingRangeToUse,
flipHorizontal: this.flipHorizontal ? false : undefined,
flipVertical: this.flipVertical ? false : undefined,
});

if (this.flipHorizontal || this.flipVertical) {
this.flip({ flipHorizontal: false, flipVertical: false });
}
const modifiedCamera = _cloneDeep(this.getCamera());

if (storeAsInitialCamera) {
this.setInitialCamera(this.getCamera());
this.setInitialCamera(modifiedCamera);
}

const RESET_CAMERA_EVENT = {
Expand All @@ -682,10 +683,7 @@ class Viewport implements IViewport {
// and do the right thing.
renderer.invokeEvent(RESET_CAMERA_EVENT);

this.triggerCameraModifiedEventIfNecessary(
previousCamera,
this.getCamera()
);
this.triggerCameraModifiedEventIfNecessary(previousCamera, modifiedCamera);

return true;
}
Expand Down Expand Up @@ -891,6 +889,7 @@ class Viewport implements IViewport {
viewAngle,
flipHorizontal,
flipVertical,
clippingRange,
} = cameraInterface;

if (flipHorizontal !== undefined || flipVertical !== undefined) {
Expand Down Expand Up @@ -925,6 +924,10 @@ class Viewport implements IViewport {
vtkCamera.setViewAngle(viewAngle);
}

if (clippingRange !== undefined) {
vtkCamera.setClippingRange(clippingRange);
}

// update clippingPlanes if volume viewports
const actorEntry = this.getDefaultActor();
if (actorEntry?.actor?.isA('vtkVolume')) {
Expand Down Expand Up @@ -1101,6 +1104,62 @@ class Viewport implements IViewport {
];
}

_getFocalPointForResetCamera(
centeredFocalPoint: Point3,
previousCamera: ICamera,
{ resetPan, resetToCenter }: { resetPan: boolean; resetToCenter: boolean }
): Point3 {
if (resetToCenter && resetPan) {
return centeredFocalPoint;
}

if (resetToCenter && !resetPan) {
return hasNaNValues(previousCamera.focalPoint)
? centeredFocalPoint
: previousCamera.focalPoint;
}

if (!resetToCenter && resetPan) {
// this is an interesting case that means the reset camera should not
// change the slice (default behavior is to go to the center of the
// image), and rather just reset the pan on the slice that is currently
// being viewed
const oldCamera = previousCamera;
const oldFocalPoint = oldCamera.focalPoint;
const oldViewPlaneNormal = oldCamera.viewPlaneNormal;

const vectorFromOldFocalPointToCenteredFocalPoint = vec3.create();
vec3.subtract(
vectorFromOldFocalPointToCenteredFocalPoint,
centeredFocalPoint,
oldFocalPoint
);

const distanceFromOldFocalPointToCenteredFocalPoint = vec3.dot(
vectorFromOldFocalPointToCenteredFocalPoint,
oldViewPlaneNormal
);

const newFocalPoint = vec3.create();
vec3.scaleAndAdd(
newFocalPoint,
centeredFocalPoint,
oldViewPlaneNormal,
-1 * distanceFromOldFocalPointToCenteredFocalPoint
);

return [newFocalPoint[0], newFocalPoint[1], newFocalPoint[2]];
}

if (!resetPan && !resetToCenter) {
// this means the reset camera should not change the slice and should not
// touch the pan either.
return hasNaNValues(previousCamera.focalPoint)
? centeredFocalPoint
: previousCamera.focalPoint;
}
}

/**
* Determines whether or not the 3D point position is inside the boundaries of the 3D imageData.
* @param point - 3D coordinate
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/RenderingEngine/VolumeViewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,8 +566,12 @@ class VolumeViewport extends Viewport implements IVolumeViewport {
/**
* Reset the camera for the volume viewport
*/
public resetCamera(resetPan = true, resetZoom = true): boolean {
super.resetCamera(resetPan, resetZoom);
public resetCamera(
resetPan = true,
resetZoom = true,
resetToCenter = true
): boolean {
super.resetCamera(resetPan, resetZoom, resetToCenter);
const activeCamera = this.getVtkActiveCamera();
// Set large numbers to ensure everything is always rendered
if (activeCamera.getParallelProjection()) {
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/types/ICamera.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Point3 from './Point3';
import Point2 from './Point2';

/**
* Camera Interface. See {@link https://kitware.github.io/vtk-examples/site/VTKBook/03Chapter3/#35-cameras} if you
Expand Down Expand Up @@ -28,6 +29,8 @@ interface ICamera {
flipHorizontal?: boolean;
/** flip Vertical */
flipVertical?: boolean;
/** clipping range */
clippingRange?: Point2;
}

export default ICamera;
6 changes: 5 additions & 1 deletion packages/core/src/types/IVolumeViewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,11 @@ export default interface IVolumeViewport extends IViewport {
/**
* Reset the camera for the volume viewport
*/
resetCamera(resetPan?: boolean, resetZoom?: boolean): boolean;
resetCamera(
resetPan?: boolean,
resetZoom?: boolean,
resetToCenter?: boolean
): boolean;
/**
* Sets the blendMode for actors of the viewport.
*/
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/utilities/hasNaNValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* A function that checks if there is a value in the array that is NaN.
* or if the input is a number it just checks if it is NaN.
* @param input - The input to check if it is NaN.
* @returns - True if the input is NaN, false otherwise.
*/
export default function hasNaNValues(input: number[] | number): boolean {
if (Array.isArray(input)) {
return input.some((value) => Number.isNaN(value));
}
return Number.isNaN(input);
}
Loading

0 comments on commit 9539f6c

Please sign in to comment.