Skip to content

Commit

Permalink
fix: resetCamera and annotations for flipped viewports (#278)
Browse files Browse the repository at this point in the history
* fix: remove applyTx since flip is not on actor anymore

* fix: make annotations appear on flipeed viewports

* fix: reset camera for flipped viewports

* fix build

* fix test

* fix: tests

* fix build
  • Loading branch information
sedghi authored Nov 4, 2022
1 parent b195727 commit cabefce
Show file tree
Hide file tree
Showing 13 changed files with 135 additions and 85 deletions.
8 changes: 5 additions & 3 deletions common/reviews/api/core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ const colormapsData: CPUFallbackColormapsData;
declare namespace CONSTANTS {
export {
colormapsData as CPU_COLORMAPS,
RENDERING_DEFAULTS
RENDERING_DEFAULTS,
EPSILON
}
}
export { CONSTANTS }
Expand Down Expand Up @@ -425,6 +426,9 @@ declare namespace Enums {
}
export { Enums }

// @public (undocumented)
const EPSILON = 0.001;

// @public (undocumented)
export enum EVENTS {
// (undocumented)
Expand Down Expand Up @@ -1957,8 +1961,6 @@ export class Viewport implements IViewport {
// (undocumented)
addActors(actors: Array<ActorEntry>, resetCameraPanAndZoom?: boolean): void;
// (undocumented)
protected applyFlipTx: (worldPos: Point3) => Point3;
// (undocumented)
readonly canvas: HTMLCanvasElement;
// (undocumented)
canvasToWorld: (canvasPos: Point2) => Point3;
Expand Down
8 changes: 6 additions & 2 deletions common/reviews/api/tools.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1634,6 +1634,9 @@ function filterAnnotationsWithinSlice(annotations: Annotations, camera: Types_2.
// @public (undocumented)
function filterViewportsWithFrameOfReferenceUID(viewports: Array<Types_2.IStackViewport | Types_2.IVolumeViewport>, FrameOfReferenceUID: string): Array<Types_2.IStackViewport | Types_2.IVolumeViewport>;

// @public (undocumented)
function filterViewportsWithParallelNormals(viewports: any, camera: any, EPS?: number): any;

// @public (undocumented)
function filterViewportsWithToolEnabled(viewports: Array<Types_2.IStackViewport | Types_2.IVolumeViewport>, toolName: string): Array<Types_2.IStackViewport | Types_2.IVolumeViewport>;

Expand Down Expand Up @@ -1837,7 +1840,7 @@ function getToolGroupsWithSegmentation(segmentationId: string): string[];
function getToolState(element: HTMLDivElement): CINETypes.ToolData | undefined;

// @public (undocumented)
function getViewportIdsWithToolToRender(element: HTMLDivElement, toolName: string, requireSameOrientation?: boolean): string[];
function getViewportIdsWithToolToRender(element: HTMLDivElement, toolName: string, requireParallelNormals?: boolean): string[];

// @public (undocumented)
function getViewportSpecificAnnotationManager(element?: Types_2.IEnabledElement | HTMLDivElement): FrameOfReferenceSpecificAnnotationManager;
Expand Down Expand Up @@ -4394,7 +4397,8 @@ declare namespace viewportFilters {
export {
filterViewportsWithToolEnabled,
filterViewportsWithFrameOfReferenceUID,
getViewportIdsWithToolToRender
getViewportIdsWithToolToRender,
filterViewportsWithParallelNormals
}
}

Expand Down
8 changes: 3 additions & 5 deletions packages/core/src/RenderingEngine/StackViewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2165,7 +2165,7 @@ class StackViewport extends Viewport implements IStackViewport {
// The y axis display coordinates are inverted with respect to canvas coords
displayCoord[1] = size[1] - displayCoord[1];

let worldCoord = openGLRenderWindow.displayToWorld(
const worldCoord = openGLRenderWindow.displayToWorld(
displayCoord[0],
displayCoord[1],
0,
Expand All @@ -2175,9 +2175,7 @@ class StackViewport extends Viewport implements IStackViewport {
// set clipping range back to original to be able
vtkCamera.setClippingRange(crange[0], crange[1]);

worldCoord = this.applyFlipTx(worldCoord);

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

private worldToCanvasGPU = (worldPos: Point3) => {
Expand All @@ -2199,7 +2197,7 @@ class StackViewport extends Viewport implements IStackViewport {
offscreenMultiRenderWindow.getOpenGLRenderWindow();
const size = openGLRenderWindow.getSize();
const displayCoord = openGLRenderWindow.worldToDisplay(
...this.applyFlipTx(worldPos),
...worldPos,
renderer
);

Expand Down
95 changes: 44 additions & 51 deletions packages/core/src/RenderingEngine/Viewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,24 +174,6 @@ class Viewport implements IViewport {
}
}

protected applyFlipTx = (worldPos: Point3): Point3 => {
const actorEntry = this.getDefaultActor();

if (!actorEntry || !actorEntry.actor) {
return worldPos;
}

const actor = actorEntry.actor;
const mat = actor.getMatrix();

const newPos = vec3.create();
const matT = mat4.create();
mat4.transpose(matT, mat);
vec3.transformMat4(newPos, worldPos, matT);

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

/**
* Flip the viewport on horizontal or vertical axis, this method
* works with vtk-js backed rendering pipeline.
Expand All @@ -207,29 +189,6 @@ class Viewport implements IViewport {
return;
}

let flipH = false;
let flipV = false;

if (
typeof flipHorizontal !== 'undefined' &&
((flipHorizontal && !this.flipHorizontal) ||
(!flipHorizontal && this.flipHorizontal))
) {
flipH = true;
}

if (
typeof flipVertical !== 'undefined' &&
((flipVertical && !this.flipVertical) ||
(!flipVertical && this.flipVertical))
) {
flipV = true;
}

if (!flipH && !flipV) {
return;
}

const { viewPlaneNormal, viewUp, focalPoint, position } = this.getCamera();

const viewRight = vec3.create();
Expand Down Expand Up @@ -275,7 +234,7 @@ class Viewport implements IViewport {
// Flipping horizontal mean that the camera should move
// to the other side of the image but looking at the
// same direction and same focal point
if (flipH) {
if (flipHorizontal) {
// we need to apply the pan value to the new focal point but in the direction
// that is mirrored on the viewUp for the flip horizontal and
// viewRight for the flip vertical
Expand Down Expand Up @@ -303,11 +262,13 @@ class Viewport implements IViewport {
position: newPosition as Point3,
focalPoint: newFocalPoint as Point3,
});

this.flipHorizontal = !this.flipHorizontal;
}

// Flipping vertical mean that the camera should negate the view up
// and also move to the other side of the image but looking at the
if (flipV) {
if (flipVertical) {
viewUpToSet = vec3.negate(viewUpToSet, viewUp);

// we need to apply the pan value to the new focal point but in the direction
Expand All @@ -332,6 +293,8 @@ class Viewport implements IViewport {
viewUp: viewUpToSet as Point3,
position: newPosition as Point3,
});

this.flipVertical = !this.flipVertical;
}

this.render();
Expand Down Expand Up @@ -579,6 +542,15 @@ class Viewport implements IViewport {
storeAsInitialCamera = true
): boolean {
const renderer = this.getRenderer();

// fix the flip right away, since we rely on the viewPlaneNormal and
// viewUp for later. Basically, we need to flip back if flipHorizontal
// is true or flipVertical is true
this.setCamera({
flipHorizontal: false,
flipVertical: false,
});

const previousCamera = _cloneDeep(this.getCamera());

const bounds = renderer.computeVisiblePropBounds();
Expand Down Expand Up @@ -693,8 +665,6 @@ class Viewport implements IViewport {
viewAngle: 90,
viewUp: viewUpToSet,
clippingRange: clippingRangeToUse,
flipHorizontal: this.flipHorizontal ? false : undefined,
flipVertical: this.flipVertical ? false : undefined,
});

const modifiedCamera = _cloneDeep(this.getCamera());
Expand Down Expand Up @@ -886,8 +856,8 @@ class Viewport implements IViewport {
return {
viewUp: <Point3>vtkCamera.getViewUp(),
viewPlaneNormal: <Point3>vtkCamera.getViewPlaneNormal(),
position: <Point3>this.applyFlipTx(vtkCamera.getPosition() as Point3),
focalPoint: <Point3>this.applyFlipTx(vtkCamera.getFocalPoint() as Point3),
position: <Point3>vtkCamera.getPosition(),
focalPoint: <Point3>vtkCamera.getFocalPoint(),
parallelProjection: vtkCamera.getParallelProjection(),
parallelScale: vtkCamera.getParallelScale(),
viewAngle: vtkCamera.getViewAngle(),
Expand Down Expand Up @@ -921,8 +891,31 @@ class Viewport implements IViewport {
clippingRange,
} = cameraInterface;

if (flipHorizontal !== undefined || flipVertical !== undefined) {
this.flip({ flipHorizontal, flipVertical });
// Note: Flip camera should be two separate calls since
// for flip, we need to flip the viewportNormal, and if
// flipHorizontal, and flipVertical are both true, that would
// the logic would be incorrect. So instead, we handle flip Horizontal
// and flipVertical separately.
if (flipHorizontal !== undefined) {
// flip if not flipped but requested to flip OR if flipped but requested to
// not flip
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) {
Expand All @@ -938,11 +931,11 @@ class Viewport implements IViewport {
}

if (position !== undefined) {
vtkCamera.setPosition(...this.applyFlipTx(position));
vtkCamera.setPosition(...position);
}

if (focalPoint !== undefined) {
vtkCamera.setFocalPoint(...this.applyFlipTx(focalPoint));
vtkCamera.setFocalPoint(...focalPoint);
}

if (parallelScale !== undefined) {
Expand Down
13 changes: 5 additions & 8 deletions packages/core/src/RenderingEngine/VolumeViewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,14 @@ import type {
} from '../types';
import type { ViewportInput } from '../types/IViewport';
import type IVolumeViewport from '../types/IVolumeViewport';
import { RENDERING_DEFAULTS } from '../constants';
import { RENDERING_DEFAULTS, EPSILON } from '../constants';
import { Events, BlendModes, OrientationAxis } from '../enums';
import eventTarget from '../eventTarget';
import type { vtkSlabCamera as vtkSlabCameraType } from './vtkClasses/vtkSlabCamera';
import { imageIdToURI, triggerEvent } from '../utilities';
import { VoiModifiedEventDetail } from '../types/EventTypes';
import deepFreeze from '../utilities/deepFreeze';

const EPSILON = 1e-3;

const ORIENTATION = {
axial: {
viewPlaneNormal: <Point3>[0, 0, -1],
Expand Down Expand Up @@ -784,7 +782,7 @@ class VolumeViewport extends Viewport implements IVolumeViewport {
const vtkCamera = this.getVtkActiveCamera() as vtkSlabCameraType;

/**
* NOTE: this is necessary because we want the coordinate trasformation
* NOTE: this is necessary because we want the coordinate transformation
* respect to the view plane (plane orthogonal to the camera and passing to
* the focal point).
*
Expand Down Expand Up @@ -826,7 +824,7 @@ class VolumeViewport extends Viewport implements IVolumeViewport {
// The y axis display coordinates are inverted with respect to canvas coords
displayCoord[1] = size[1] - displayCoord[1];

let worldCoord = openGLRenderWindow.displayToWorld(
const worldCoord = openGLRenderWindow.displayToWorld(
displayCoord[0],
displayCoord[1],
0,
Expand All @@ -835,8 +833,7 @@ class VolumeViewport extends Viewport implements IVolumeViewport {

vtkCamera.setIsPerformingCoordinateTransformation(false);

worldCoord = this.applyFlipTx(worldCoord);
return worldCoord;
return [worldCoord[0], worldCoord[1], worldCoord[2]];
};

/**
Expand Down Expand Up @@ -881,7 +878,7 @@ class VolumeViewport extends Viewport implements IVolumeViewport {
offscreenMultiRenderWindow.getOpenGLRenderWindow();
const size = openGLRenderWindow.getSize();
const displayCoord = openGLRenderWindow.worldToDisplay(
...this.applyFlipTx(worldPos),
...worldPos,
renderer
);

Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/constants/epsilon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const EPSILON = 1e-3;

export default EPSILON;
3 changes: 2 additions & 1 deletion packages/core/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import CPU_COLORMAPS from './cpuColormaps';
import RENDERING_DEFAULTS from './rendering';
import EPSILON from './epsilon';

export { CPU_COLORMAPS, RENDERING_DEFAULTS };
export { CPU_COLORMAPS, RENDERING_DEFAULTS, EPSILON };
2 changes: 1 addition & 1 deletion packages/tools/examples/crossHairs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const toolGroupId = 'MY_TOOLGROUP_ID';
// ======== Set up page ======== //
setTitleAndDescription(
'Crosshairs',
'Here we demonstrate crosshairs linking three orthogonal views of the same data'
'Here we demonstrate crosshairs linking three orthogonal views of the same data. You can select the blend mode that will be used if you modify the slab thickness of the crosshairs by dragging the control points.'
);

const size = '500px';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { vec3 } from 'gl-matrix';
import { utilities as csUtils } from '@cornerstonejs/core';
import { CONSTANTS } from '@cornerstonejs/core';
import type { Types } from '@cornerstonejs/core';
import { Annotations, Annotation } from '../../types';

const { EPSILON } = CONSTANTS;

const PARALLEL_THRESHOLD = 1 - EPSILON;

/**
* given some `Annotations`, and the slice defined by the camera's normal
* direction and the spacing in the normal, filter the `Annotations` which
Expand All @@ -19,13 +23,28 @@ export default function filterAnnotationsWithinSlice(
spacingInNormalDirection: number
): Annotations {
const { viewPlaneNormal } = camera;
const annotationsWithSameNormal = annotations.filter((td: Annotation) => {
const annotationViewPlaneNormal = td.metadata.viewPlaneNormal;
return csUtils.isEqual(annotationViewPlaneNormal, viewPlaneNormal);
});

// The reason we use parallel normals instead of actual orientation is that
// flipped action is done through camera API, so we can't rely on the
// orientation (viewplaneNormal and viewUp) since even the same image and
// same slice if flipped will have different orientation, but still rendering
// the same slice. Instead, we choose to use the parallel normals to filter
// the annotations and later we fine tune it with the annotation within slice
// logic down below.
const annotationsWithParallelNormals = annotations.filter(
(td: Annotation) => {
const annotationViewPlaneNormal = td.metadata.viewPlaneNormal;

const isParallel =
Math.abs(vec3.dot(viewPlaneNormal, annotationViewPlaneNormal)) >
PARALLEL_THRESHOLD;

return annotationViewPlaneNormal && isParallel;
}
);

// No in plane annotations.
if (!annotationsWithSameNormal.length) {
if (!annotationsWithParallelNormals.length) {
return [];
}

Expand All @@ -37,7 +56,7 @@ export default function filterAnnotationsWithinSlice(

const annotationsWithinSlice = [];

for (const annotation of annotationsWithSameNormal) {
for (const annotation of annotationsWithParallelNormals) {
const data = annotation.data;
const point = data.handles.points[0];

Expand Down
Loading

0 comments on commit cabefce

Please sign in to comment.