ResliceCursorWidget

This class represents a reslice cursor that can be used to perform interactive thick slab MPR’s through data. It consists of two cross sectional hairs. The hairs may have a hole in the center. These may be translated or rotated independent of each other in the view. The result is used to reslice the data along these cross sections. This allows the user to perform multi-planar thin or thick reformat of the data on an image view, rather than a 3D.

getWidgetState(object)

Get the object that contains all the attributes used to update the representation and made computation for reslicing. The state contains:

  • Six sub states which define the representation of all lines in all views. For example, the axis X is drawn in the Y and the Z view. Then, if we want to access to the attributes of the axis X, then we can call : state.getAxisXinY() and state.getAxisXinZ().

These sub states contain :

* two points which define the lines
* two rotation points which define the center of the rotation points
* the color of the line
* the name of the line (for example 'AxisXinY')
* the name of the plane (X)
  • Center: The center of the six lines

  • Opacity: Update the opacity of the lines/rotation points actors

  • activeLineState: Used in the behavior.js file in order to get the attribute of the selected line

  • activeRotationPointName: Used in the behavior.js file in order to get the selected rotation point

  • image: vtkImage used to place the reslice cursor

  • activeViewName: Used in the behavior.js file in order to get the correct view attributes (X, Y, Z)

  • sphereRadius: Manages the radius of the rotation points

  • showCenter: Defines if the reslice cursor center is displayed or not. If not, it’s still possible to move the center. The cursor mouse will be turned into ‘move’ cursor when you can translate the center.

  • updateMethodName: Used in the behavior.js in order to know which actions is going to be applied (translation, axisTransltaion, rotation)

  • {X, Y, Z}PlaneNormal: Contains the normal of the {X, Y, Z} plane (which is updated when a rotation is applied). It’s used to compute the resliced image

  • enableRotation: if false, then remove the rotation points and disable the line rotation

  • enableTranslation: if false, disable the translation of the axis

  • keepOrthogonality: if false, then rotation are totally free. Else, if one axis is rotated, then the associated one if rotating the same axis in order to keep orthogonality

  • scrollingMethod: Define which mouse button is used to scroll (use Contants.js):

    • MIDDLE_MOUSE_BUTTON : Default
    • LEFT_MOUSE_BUTTON
    • RIGHT_MOUSE_BUTTON

setCenter

You can manually set the center of the reslice cursor by calling this method with an array of three value. That can be useful if you want to implement a simple click which moves the center.
If you want to add the previous feature, then you’ll have to defined the

renderer[axis].widgetInstance.onWidgetChange(() => {
renderer
// No need to update plane nor refresh when interaction
// is on current view. Plane can't be changed with interaction on current
// view. Refreshs happen automatically with `animation`.
.filter((_, index) => index !== axis)
.forEach((viewer) => {
// update widget
});
});

Source

Constants.js
export const ScrollingMethods = {
MIDDLE_MOUSE_BUTTON: 0,
LEFT_MOUSE_BUTTON: 1,
RIGHT_MOUSE_BUTTON: 2,
};

export default {
ScrollingMethods,
};
behavior.js
import { mat4 } from 'gl-matrix';

import macro from 'vtk.js/Sources/macro';
import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox';
import vtkLine from 'vtk.js/Sources/Common/DataModel/Line';
import vtkPlaneManipulator from 'vtk.js/Sources/Widgets/Manipulators/PlaneManipulator';
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';

import {
boundPointOnPlane,
getAssociatedLinesName,
updateState,
} from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/helpers';

import { ScrollingMethods } from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/Constants';

export default function widgetBehavior(publicAPI, model) {
let isDragging = null;
let isScrolling = false;

publicAPI.updateCursor = () => {
switch (model.activeState.getUpdateMethodName()) {
case 'translateCenter':
model.openGLRenderWindow.setCursor('move');
break;
case 'rotateLine':
model.openGLRenderWindow.setCursor('alias');
break;
case 'translateAxis':
model.openGLRenderWindow.setCursor('pointer');
break;
default:
model.openGLRenderWindow.setCursor('default');
break;
}
};

publicAPI.handleLeftButtonPress = (callData) => {
if (model.activeState && model.activeState.getActive()) {
isDragging = true;
const viewName = model.widgetState.getActiveViewName();
const currentPlaneNormal = model.widgetState[
`get${viewName}PlaneNormal`
]();
model.planeManipulator.setOrigin(model.widgetState.getCenter());
model.planeManipulator.setNormal(currentPlaneNormal);

publicAPI.startInteraction();
} else if (
model.widgetState.getScrollingMethod() ===
ScrollingMethods.LEFT_MOUSE_BUTTON
) {
isScrolling = true;
model.previousPosition = callData.position;
publicAPI.startInteraction();
} else {
return macro.VOID;
}

return macro.EVENT_ABORT;
};

publicAPI.handleMouseMove = (callData) => {
if (isDragging && model.pickable) {
return publicAPI.handleEvent(callData);
}
if (isScrolling) {
if (model.previousPosition.y !== callData.position.y) {
const step = model.previousPosition.y - callData.position.y;
publicAPI.translateCenterOnCurrentDirection(
step,
callData.pokedRenderer
);
model.previousPosition = callData.position;

publicAPI.invokeInteractionEvent();
}
}
return macro.VOID;
};

publicAPI.handleLeftButtonRelease = () => {
if (isDragging || isScrolling) {
publicAPI.endInteraction();
}
isDragging = false;
isScrolling = false;
model.widgetState.deactivate();
};

publicAPI.handleRightButtonPress = (calldata) => {
if (
model.widgetState.getScrollingMethod() ===
ScrollingMethods.RIGHT_MOUSE_BUTTON
) {
model.previousPosition = calldata.position;
isScrolling = true;
publicAPI.startInteraction();
}
};

publicAPI.handleRightButtonRelease = (calldata) => {
if (
model.widgetState.getScrollingMethod() ===
ScrollingMethods.RIGHT_MOUSE_BUTTON
) {
isScrolling = false;
publicAPI.endInteraction();
}
};

publicAPI.handleStartMouseWheel = (callData) => {
publicAPI.startInteraction();
};

publicAPI.handleMouseWheel = (calldata) => {
const step = calldata.spinY;
publicAPI.translateCenterOnCurrentDirection(step, calldata.pokedRenderer);

publicAPI.invokeInteractionEvent();

return macro.EVENT_ABORT;
};

publicAPI.handleEndMouseWheel = (calldata) => {
publicAPI.endInteraction();
};

publicAPI.handleMiddleButtonPress = (calldata) => {
if (
model.widgetState.getScrollingMethod() ===
ScrollingMethods.MIDDLE_MOUSE_BUTTON
) {
isScrolling = true;
model.previousPosition = calldata.position;
publicAPI.startInteraction();
}
};

publicAPI.handleMiddleButtonRelease = (calldata) => {
if (
model.widgetState.getScrollingMethod() ===
ScrollingMethods.MIDDLE_MOUSE_BUTTON
) {
isScrolling = false;
publicAPI.endInteraction();
}
};

publicAPI.handleEvent = (callData) => {
if (model.activeState.getActive()) {
publicAPI[model.activeState.getUpdateMethodName()](callData);
publicAPI.invokeInteractionEvent();
return macro.EVENT_ABORT;
}
return macro.VOID;
};

publicAPI.startInteraction = () => {
publicAPI.invokeStartInteractionEvent();
// When interacting, plane actor and lines must be re-rendered on other views
publicAPI.getViewWidgets().forEach((viewWidget) => {
viewWidget.getInteractor().requestAnimation(publicAPI);
});
};

publicAPI.endInteraction = () => {
publicAPI.invokeEndInteractionEvent();
publicAPI.getViewWidgets().forEach((viewWidget) => {
viewWidget.getInteractor().cancelAnimation(publicAPI);
});
};

publicAPI.translateCenterOnCurrentDirection = (nbSteps, renderer) => {
const dirProj = renderer
.getRenderWindow()
.getRenderers()[0]
.getActiveCamera()
.getDirectionOfProjection();

// Direction of the projection is the inverse of what we want
const direction = vtkMath.multiplyScalar(dirProj, -1);

const oldCenter = model.widgetState.getCenter();
const image = model.widgetState.getImage();
const imageSpacing = image.getSpacing();

// Define the potentially new center
let newCenter = [
oldCenter[0] + nbSteps * direction[0] * imageSpacing[0],
oldCenter[1] + nbSteps * direction[1] * imageSpacing[1],
oldCenter[2] + nbSteps * direction[2] * imageSpacing[2],
];
newCenter = publicAPI.getBoundedCenter(newCenter);

model.widgetState.setCenter(newCenter);
updateState(model.widgetState);
};

publicAPI.translateAxis = (calldata) => {
const stateLine = model.widgetState.getActiveLineState();
const worldCoords = model.planeManipulator.handleEvent(
calldata,
model.openGLRenderWindow
);

const point1 = stateLine.getPoint1();
const point2 = stateLine.getPoint2();

// Translate the current line along the other line
const otherLineName = getAssociatedLinesName(stateLine.getName());
const otherLine = model.widgetState[`get${otherLineName}`]();
const otherLineVector = vtkMath.subtract(
otherLine.getPoint2(),
otherLine.getPoint1(),
[]
);
vtkMath.normalize(otherLineVector);
const axisTranslation = otherLineVector;

const currentLineVector = vtkMath.subtract(point2, point1, [0, 0, 0]);
vtkMath.normalize(currentLineVector);

const dot = vtkMath.dot(currentLineVector, otherLineVector);
// lines are colinear, translate along perpendicular axis from current line
if (dot === 1 || dot === -1) {
vtkMath.cross(
currentLineVector,
model.planeManipulator.getNormal(),
axisTranslation
);
}

const closestPoint = [];
vtkLine.distanceToLine(worldCoords, point1, point2, closestPoint);

const translationVector = vtkMath.subtract(worldCoords, closestPoint, []);
const translationDistance = vtkMath.dot(translationVector, axisTranslation);

const center = model.widgetState.getCenter();
let newOrigin = vtkMath.multiplyAccumulate(
center,
axisTranslation,
translationDistance,
[0, 0, 0]
);
newOrigin = publicAPI.getBoundedCenter(newOrigin);
model.widgetState.setCenter(newOrigin);
updateState(model.widgetState);
};

publicAPI.getBoundedCenter = (newCenter) => {
const oldCenter = model.widgetState.getCenter();
const imageBounds = model.widgetState.getImage().getBounds();
const bounds = vtkBoundingBox.newInstance({ bounds: imageBounds });

if (bounds.containsPoint(...newCenter)) {
return newCenter;
}

return boundPointOnPlane(newCenter, oldCenter, imageBounds);
};

publicAPI.translateCenter = (calldata) => {
let worldCoords = model.planeManipulator.handleEvent(
calldata,
model.openGLRenderWindow
);
worldCoords = publicAPI.getBoundedCenter(worldCoords);
model.activeState.setCenter(worldCoords);
updateState(model.widgetState);
};

publicAPI.rotateLine = (calldata) => {
const activeLine = model.widgetState.getActiveLineState();
const planeNormal = model.planeManipulator.getNormal();
const worldCoords = model.planeManipulator.handleEvent(
calldata,
model.openGLRenderWindow
);

const center = model.widgetState.getCenter();
const previousWorldPosition = activeLine[
`get${model.widgetState.getActiveRotationPointName()}`
]();

const previousVectorToOrigin = [0, 0, 0];
vtkMath.subtract(previousWorldPosition, center, previousVectorToOrigin);
vtkMath.normalize(previousVectorToOrigin);

const currentVectorToOrigin = [0, 0, 0];
vtkMath.subtract(worldCoords, center, currentVectorToOrigin);
vtkMath.normalize(currentVectorToOrigin);

const rotationAngle = vtkMath.angleBetweenVectors(
previousVectorToOrigin,
currentVectorToOrigin
);

// Define the direction of the rotation
const cross = [0, 0, 0];
vtkMath.cross(currentVectorToOrigin, previousVectorToOrigin, cross);
vtkMath.normalize(cross);

const sign = vtkMath.dot(cross, planeNormal) > 0 ? -1 : 1;

const matrix = mat4.create();
mat4.translate(matrix, matrix, center);
mat4.rotate(matrix, matrix, rotationAngle * sign, planeNormal);
mat4.translate(matrix, matrix, [-center[0], -center[1], -center[2]]);

// Rotate associated line's plane normal
const planeName = activeLine.getPlaneName();
const normal = model.widgetState[`get${planeName}PlaneNormal`]();
const newNormal = vtkMath.rotateVector(
normal,
planeNormal,
rotationAngle * sign
);
model.widgetState[`set${planeName}PlaneNormal`](newNormal);

if (model.widgetState.getKeepOrthogonality()) {
const associatedLineName = getAssociatedLinesName(activeLine.getName());
const associatedLine = model.widgetState[`get${associatedLineName}`]();
const planeName2 = associatedLine.getPlaneName();
const normal2 = model.widgetState[`get${planeName2}PlaneNormal`]();
const newNormal2 = vtkMath.rotateVector(
normal2,
planeNormal,
rotationAngle * sign
);
model.widgetState[`set${planeName2}PlaneNormal`](newNormal2);
}
updateState(model.widgetState);
};

// --------------------------------------------------------------------------
// initialization
// --------------------------------------------------------------------------

model.planeManipulator = vtkPlaneManipulator.newInstance();
}
helpers.js
import vtkBoundingBox, {
STATIC,
} from 'vtk.js/Sources/Common/DataModel/BoundingBox';
import vtkBox from 'vtk.js/Sources/Common/DataModel/Box';
import vtkCubeSource from 'vtk.js/Sources/Filters/Sources/CubeSource';
import vtkCutter from 'vtk.js/Sources/Filters/Core/Cutter';
import vtkPlane from 'vtk.js/Sources/Common/DataModel/Plane';
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';

import { ViewTypes } from 'vtk.js/Sources/Widgets/Core/WidgetManager/Constants';

/**
* Fit the plane defined by origin, p1, p2 onto the bounds.
* Plane is untouched if does not intersect bounds.
* @param {Array} bounds
* @param {Array} origin
* @param {Array} p1
* @param {Array} p2
*/
export function boundPlane(bounds, origin, p1, p2) {
const v1 = [];
vtkMath.subtract(p1, origin, v1);

const v2 = [];
vtkMath.subtract(p2, origin, v2);

const n = [0, 0, 1];
vtkMath.cross(v1, v2, n);
const plane = vtkPlane.newInstance();
plane.setOrigin(origin);
plane.setNormal(n);
vtkMath.normalize(v1);
vtkMath.normalize(v2);
vtkMath.normalize(n);

const cubeSource = vtkCubeSource.newInstance();
cubeSource.setBounds(bounds);

const cutter = vtkCutter.newInstance();
cutter.setCutFunction(plane);
cutter.setInputConnection(cubeSource.getOutputPort());

const cutBounds = cutter.getOutputData();
if (cutBounds.getNumberOfPoints() === 0) {
return;
}
const localBounds = STATIC.computeLocalBounds(
cutBounds.getPoints(),
v1,
v2,
n
);
for (let i = 0; i < 3; i += 1) {
origin[i] =
localBounds[0] * v1[i] + localBounds[2] * v2[i] + localBounds[4] * n[i];
p1[i] =
localBounds[1] * v1[i] + localBounds[2] * v2[i] + localBounds[4] * n[i];
p2[i] =
localBounds[0] * v1[i] + localBounds[3] * v2[i] + localBounds[4] * n[i];
}
}
// Project point (inPoint) to the bounds of the image according to a plane
// defined by two vectors (v1, v2)
export function boundPoint(inPoint, v1, v2, bounds) {
const absT1 = v1.map((val) => Math.abs(val));
const absT2 = v2.map((val) => Math.abs(val));
const epsilon = 0.00001;

let o1 = 0.0;
let o2 = 0.0;

for (let i = 0; i < 3; i++) {
let axisOffset = 0;

const useT1 = absT1[i] > absT2[i];
const t = useT1 ? v1 : v2;
const absT = useT1 ? absT1 : absT2;

if (inPoint[i] < bounds[i * 2]) {
axisOffset = absT[i] > epsilon ? (bounds[2 * i] - inPoint[i]) / t[i] : 0;
} else if (inPoint[i] > bounds[2 * i + 1]) {
axisOffset =
absT[i] > epsilon ? (bounds[2 * i + 1] - inPoint[i]) / t[i] : 0;
}

if (useT1) {
if (Math.abs(axisOffset) > Math.abs(o1)) {
o1 = axisOffset;
}
} else if (Math.abs(axisOffset) > Math.abs(o2)) {
o2 = axisOffset;
}
}

const outPoint = [inPoint[0], inPoint[1], inPoint[2]];

if (o1 !== 0.0) {
vtkMath.multiplyAccumulate(outPoint, v1, o1, outPoint);
}
if (o2 !== 0.0) {
vtkMath.multiplyAccumulate(outPoint, v2, o2, outPoint);
}

return outPoint;
}

// Compute the intersection between p1 and p2 on bounds
export function boundPointOnPlane(p1, p2, bounds) {
const dir12 = [0, 0, 0];
vtkMath.subtract(p2, p1, dir12);

const out = [0, 0, 0];
const tolerance = [0, 0, 0];
vtkBox.intersectBox(bounds, p1, dir12, out, tolerance);

return out;
}

// Get name of the line in the same plane as the input
export function getAssociatedLinesName(lineName) {
switch (lineName) {
case 'AxisXinY':
return 'AxisZinY';
case 'AxisXinZ':
return 'AxisYinZ';
case 'AxisYinX':
return 'AxisZinX';
case 'AxisYinZ':
return 'AxisXinZ';
case 'AxisZinX':
return 'AxisYinX';
case 'AxisZinY':
return 'AxisXinY';
default:
return '';
}
}

export function getViewPlaneNameFromViewType(viewType) {
switch (viewType) {
case ViewTypes.YZ_PLANE:
return 'X';
case ViewTypes.XZ_PLANE:
return 'Y';
case ViewTypes.XY_PLANE:
return 'Z';
default:
return '';
}
}

// Update the extremities and the rotation point coordinate of the line
function updateLine(lineState, center, axis, lineLength, rotationLength) {
const p1 = [
center[0] - lineLength * axis[0],
center[1] - lineLength * axis[1],
center[2] - lineLength * axis[2],
];
const p2 = [
center[0] + lineLength * axis[0],
center[1] + lineLength * axis[1],
center[2] + lineLength * axis[2],
];
const rotationP1 = [
center[0] - rotationLength * axis[0],
center[1] - rotationLength * axis[1],
center[2] - rotationLength * axis[2],
];
const rotationP2 = [
center[0] + rotationLength * axis[0],
center[1] + rotationLength * axis[1],
center[2] + rotationLength * axis[2],
];

lineState.setPoint1(p1);
lineState.setPoint2(p2);
lineState.setRotationPoint1(rotationP1);
lineState.setRotationPoint2(rotationP2);
}

// Update the reslice cursor state according to the three planes normals and the origin
export function updateState(widgetState) {
// Compute axis
const xNormal = widgetState.getXPlaneNormal();
const yNormal = widgetState.getYPlaneNormal();
const zNormal = widgetState.getZPlaneNormal();
const newXAxis = [];
const newYAxis = [];
const newZAxis = [];
vtkMath.cross(xNormal, yNormal, newZAxis);
vtkMath.cross(yNormal, zNormal, newXAxis);
vtkMath.cross(zNormal, xNormal, newYAxis);

const bounds = widgetState.getImage().getBounds();
const center = widgetState.getCenter();
// Factor used to define where the rotation point will be displayed
// according to the plane size where there will be visible
const factor = 0.5 * 0.85;
const xRotationLength = (bounds[1] - bounds[0]) * factor;
const yRotationLength = (bounds[3] - bounds[2]) * factor;
const zRotationLength = (bounds[5] - bounds[4]) * factor;

// Length of the principal diagonal.
const pdLength = 20 * 0.5 * vtkBoundingBox.getDiagonalLength(bounds);

updateLine(
widgetState.getAxisXinY(),
center,
newZAxis,
pdLength,
zRotationLength
);
updateLine(
widgetState.getAxisYinX(),
center,
newZAxis,
pdLength,
zRotationLength
);

updateLine(
widgetState.getAxisYinZ(),
center,
newXAxis,
pdLength,
xRotationLength
);
updateLine(
widgetState.getAxisZinY(),
center,
newXAxis,
pdLength,
xRotationLength
);

updateLine(
widgetState.getAxisXinZ(),
center,
newYAxis,
pdLength,
yRotationLength
);
updateLine(
widgetState.getAxisZinX(),
center,
newYAxis,
pdLength,
yRotationLength
);
}
index.js
import macro from 'vtk.js/Sources/macro';
import vtkAbstractWidgetFactory from 'vtk.js/Sources/Widgets/Core/AbstractWidgetFactory';
import vtkPlane from 'vtk.js/Sources/Common/DataModel/Plane';
import vtkPlaneSource from 'vtk.js/Sources/Filters/Sources/PlaneSource';
import vtkResliceCursorContextRepresentation from 'vtk.js/Sources/Widgets/Representations/ResliceCursorContextRepresentation';

import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';

import widgetBehavior from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/behavior';
import stateGenerator from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/state';

import {
boundPlane,
updateState,
getViewPlaneNameFromViewType,
} from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/helpers';
import { ViewTypes } from 'vtk.js/Sources/Widgets/Core/WidgetManager/Constants';

import { vec4, mat4 } from 'gl-matrix';

const VTK_INT_MAX = 2147483647;
const { vtkErrorMacro } = macro;
const viewUpFromViewType = {};

// ----------------------------------------------------------------------------
// Factory
// ----------------------------------------------------------------------------

function vtkResliceCursorWidget(publicAPI, model) {
model.classHierarchy.push('vtkResliceCursorWidget');

// --------------------------------------------------------------------------
// Private methods
// --------------------------------------------------------------------------

/**
* Compute the origin of the reslice plane prior to transformations
* It does not take into account the current view normal. (always axis aligned)
* @param {*} viewType axial, coronal or sagittal
*/
function computeReslicePlaneOrigin(viewType) {
const bounds = model.widgetState.getImage().getBounds();

const center = publicAPI.getWidgetState().getCenter();
const imageCenter = model.widgetState.getImage().getCenter();

// Offset based on the center of the image and how far from it the
// reslice cursor is. This allows us to capture the whole image even
// if we resliced in awkward places.
const offset = [];
for (let i = 0; i < 3; i++) {
offset[i] = -Math.abs(center[i] - imageCenter[i]);
offset[i] *= 2; // give us room
}

// Now set the size of the plane based on the location of the cursor so as to
// at least completely cover the viewed region
const planeSource = vtkPlaneSource.newInstance();

if (viewType === ViewTypes.XZ_PLANE) {
planeSource.setOrigin(
bounds[0] + offset[0],
center[1],
bounds[4] + offset[2]
);
planeSource.setPoint1(
bounds[1] - offset[0],
center[1],
bounds[4] + offset[2]
);
planeSource.setPoint2(
bounds[0] + offset[0],
center[1],
bounds[5] - offset[2]
);
} else if (viewType === ViewTypes.XY_PLANE) {
planeSource.setOrigin(
bounds[0] + offset[0],
bounds[2] + offset[1],
center[2]
);
planeSource.setPoint1(
bounds[1] - offset[0],
bounds[2] + offset[1],
center[2]
);
planeSource.setPoint2(
bounds[0] + offset[0],
bounds[3] - offset[1],
center[2]
);
} else if (viewType === ViewTypes.YZ_PLANE) {
planeSource.setOrigin(
center[0],
bounds[2] + offset[1],
bounds[4] + offset[2]
);
planeSource.setPoint1(
center[0],
bounds[3] - offset[1],
bounds[4] + offset[2]
);
planeSource.setPoint2(
center[0],
bounds[2] + offset[1],
bounds[5] - offset[2]
);
}
return planeSource;
}

function updateCamera(renderer, normal) {
// When the reslice plane is changed, update the camera to look at the
// normal to the reslice plane.

const focalPoint = renderer.getActiveCamera().getFocalPoint();

const distance = renderer.getActiveCamera().getDistance();

const estimatedCameraPosition = vtkMath.multiplyAccumulate(
focalPoint,
normal,
distance,
[0, 0, 0]
);

// intersect with the plane to get updated focal point
const intersection = vtkPlane.intersectWithLine(
focalPoint,
estimatedCameraPosition,
model.widgetState.getCenter(),
normal
);
const newFocalPoint = intersection.x;

renderer
.getActiveCamera()
.setFocalPoint(newFocalPoint[0], newFocalPoint[1], newFocalPoint[2]);

const newCameraPosition = vtkMath.multiplyAccumulate(
newFocalPoint,
normal,
distance,
[0, 0, 0]
);

renderer
.getActiveCamera()
.setPosition(
newCameraPosition[0],
newCameraPosition[1],
newCameraPosition[2]
);

// Renderer may not have yet actor bounds
const bounds = model.widgetState.getImage().getBounds();

// Don't clip away any part of the data.
renderer.resetCamera(bounds);
renderer.resetCameraClippingRange(bounds);
}

// --------------------------------------------------------------------------
// initialization
// --------------------------------------------------------------------------

model.behavior = widgetBehavior;
model.widgetState = stateGenerator();

publicAPI.getRepresentationsForViewType = (viewType) => {
switch (viewType) {
case ViewTypes.XY_PLANE:
return [
{
builder: vtkResliceCursorContextRepresentation,
labels: ['AxisXinZ', 'AxisYinZ'],
initialValues: {
axis1Name: 'AxisXinZ',
axis2Name: 'AxisYinZ',
viewName: 'Z',
rotationEnabled: model.widgetState.getEnableRotation(),
},
},
];
case ViewTypes.XZ_PLANE:
return [
{
builder: vtkResliceCursorContextRepresentation,
labels: ['AxisXinY', 'AxisZinY'],
initialValues: {
axis1Name: 'AxisXinY',
axis2Name: 'AxisZinY',
viewName: 'Y',
rotationEnabled: model.widgetState.getEnableRotation(),
},
},
];
case ViewTypes.YZ_PLANE:
return [
{
builder: vtkResliceCursorContextRepresentation,
labels: ['AxisYinX', 'AxisZinX'],
initialValues: {
axis1Name: 'AxisYinX',
axis2Name: 'AxisZinX',
viewName: 'X',
rotationEnabled: model.widgetState.getEnableRotation(),
},
},
];
case ViewTypes.DEFAULT:
case ViewTypes.GEOMETRY:
case ViewTypes.SLICE:
case ViewTypes.VOLUME:
default:
return [];
}
};

publicAPI.setImage = (image) => {
model.widgetState.setImage(image);
const center = image.getCenter();
model.widgetState.setCenter(center);
updateState(model.widgetState);
};

publicAPI.setCenter = (center) => {
model.widgetState.setCenter(center);
updateState(model.widgetState);
publicAPI.modified();
};

// --------------------------------------------------------------------------
// Methods
// --------------------------------------------------------------------------

publicAPI.resetCamera = (renderer, viewType) => {
const viewName = getViewPlaneNameFromViewType(viewType);

const center = model.widgetState.getImage().getCenter();
const focalPoint = renderer.getActiveCamera().getFocalPoint();
const position = renderer.getActiveCamera().getPosition();

// Distance is preserved
const distance = Math.sqrt(
vtkMath.distance2BetweenPoints(position, focalPoint)
);

const normal = model.widgetState[`get${viewName}PlaneNormal`]();

const estimatedFocalPoint = center;
const estimatedCameraPosition = vtkMath.multiplyAccumulate(
estimatedFocalPoint,
normal,
distance,
[0, 0, 0]
);

renderer.getActiveCamera().setFocalPoint(...estimatedFocalPoint);
renderer.getActiveCamera().setPosition(...estimatedCameraPosition);
renderer.getActiveCamera().setViewUp(viewUpFromViewType[viewType]);

// Project focalPoint onto image plane and preserve distance
updateCamera(renderer, normal);
};

publicAPI.updateReslicePlane = (imageReslice, viewType) => {
const plane = publicAPI.getPlaneSourceFromViewType(viewType);

// Calculate appropriate pixel spacing for the reslicing
const spacing = model.widgetState.getImage().getSpacing();

// Compute original (i.e. before rotation) plane (i.e. origin, p1, p2)
// centered on cursor center.
const planeSource = computeReslicePlaneOrigin(viewType);

// Apply rotation onto plane (i.e. origin, p1, p2)
planeSource.setNormal(...plane.getNormal());
// TBD: isn't it a no-op ?
planeSource.setCenter(...plane.getOrigin());

// TODO: orient plane on volume.

// Compute view up to configure camera later on
const bottomLeftPoint = planeSource.getOrigin();
const topLeftPoint = planeSource.getPoint2();
const viewUp = vtkMath.subtract(topLeftPoint, bottomLeftPoint, [0, 0, 0]);
vtkMath.normalize(viewUp);
viewUpFromViewType[viewType] = viewUp;

// Clip to bounds
const boundedOrigin = [...planeSource.getOrigin()];
const boundedP1 = [...planeSource.getPoint1()];
const boundedP2 = [...planeSource.getPoint2()];
boundPlane(
model.widgetState.getImage().getBounds(),
boundedOrigin,
boundedP1,
boundedP2
);

planeSource.setOrigin(boundedOrigin);
planeSource.setPoint1(boundedP1[0], boundedP1[1], boundedP1[2]);
planeSource.setPoint2(boundedP2[0], boundedP2[1], boundedP2[2]);

const o = planeSource.getOrigin();

const p1 = planeSource.getPoint1();
const planeAxis1 = [];
vtkMath.subtract(p1, o, planeAxis1);

const p2 = planeSource.getPoint2();
const planeAxis2 = [];
vtkMath.subtract(p2, o, planeAxis2);

// The x,y dimensions of the plane
const planeSizeX = vtkMath.normalize(planeAxis1);
const planeSizeY = vtkMath.normalize(planeAxis2);
const normal = planeSource.getNormal();

const newResliceAxes = mat4.create();
mat4.identity(newResliceAxes);

for (let i = 0; i < 3; i++) {
newResliceAxes[4 * i + 0] = planeAxis1[i];
newResliceAxes[4 * i + 1] = planeAxis2[i];
newResliceAxes[4 * i + 2] = normal[i];
}

const spacingX =
Math.abs(planeAxis1[0] * spacing[0]) +
Math.abs(planeAxis1[1] * spacing[1]) +
Math.abs(planeAxis1[2] * spacing[2]);

const spacingY =
Math.abs(planeAxis2[0] * spacing[0]) +
Math.abs(planeAxis2[1] * spacing[1]) +
Math.abs(planeAxis2[2] * spacing[2]);

const planeOrigin = [...planeSource.getOrigin(), 1.0];
const originXYZW = [];
const newOriginXYZW = [];

vec4.transformMat4(originXYZW, planeOrigin, newResliceAxes);
mat4.transpose(newResliceAxes, newResliceAxes);
vec4.transformMat4(newOriginXYZW, originXYZW, newResliceAxes);

newResliceAxes[4 * 3 + 0] = newOriginXYZW[0];
newResliceAxes[4 * 3 + 1] = newOriginXYZW[1];
newResliceAxes[4 * 3 + 2] = newOriginXYZW[2];

// Compute a new set of resliced extents
let extentX = 0;
let extentY = 0;

// Pad extent up to a power of two for efficient texture mapping
// make sure we're working with valid values
const realExtentX =
spacingX === 0 ? Number.MAX_SAFE_INTEGER : planeSizeX / spacingX;

// Sanity check the input data:
// * if realExtentX is too large, extentX will wrap
// * if spacingX is 0, things will blow up.

const value = VTK_INT_MAX >> 1; // eslint-disable-line no-bitwise

if (realExtentX > value) {
vtkErrorMacro(
'Invalid X extent: ',
realExtentX,
' on view type : ',
viewType
);
extentX = 0;
} else {
extentX = 1;
while (extentX < realExtentX) {
extentX <<= 1; // eslint-disable-line no-bitwise
}
}

// make sure extentY doesn't wrap during padding
const realExtentY =
spacingY === 0 ? Number.MAX_SAFE_INTEGER : planeSizeY / spacingY;

if (realExtentY > value) {
vtkErrorMacro(
'Invalid Y extent:',
realExtentY,
' on view type : ',
viewType
);
extentY = 0;
} else {
extentY = 1;
while (extentY < realExtentY) {
extentY <<= 1; // eslint-disable-line no-bitwise
}
}

const outputSpacingX = extentX === 0 ? 1.0 : planeSizeX / extentX;
const outputSpacingY = extentY === 0 ? 1.0 : planeSizeY / extentY;

let modified = imageReslice.setResliceAxes(newResliceAxes);
modified =
imageReslice.setOutputSpacing([outputSpacingX, outputSpacingY, 1]) ||
modified;
modified =
imageReslice.setOutputOrigin([
0.5 * outputSpacingX,
0.5 * outputSpacingY,
0,
]) || modified;
modified =
imageReslice.setOutputExtent([0, extentX - 1, 0, extentY - 1, 0, 0]) ||
modified;

return modified;
};

/**
* Returns a plane source with origin at cursor center and
* normal from the view.
* @param {ViewType} type: Axial, Coronal or Sagittal
*/
publicAPI.getPlaneSourceFromViewType = (type) => {
const planeSource = vtkPlaneSource.newInstance();
const widgetState = publicAPI.getWidgetState();
const origin = widgetState.getCenter();
planeSource.setOrigin(origin);
let normal = [];
switch (type) {
case ViewTypes.XY_PLANE: {
normal = widgetState.getZPlaneNormal();
break;
}
case ViewTypes.XZ_PLANE: {
normal = widgetState.getYPlaneNormal();
break;
}
case ViewTypes.YZ_PLANE: {
normal = widgetState.getXPlaneNormal();
break;
}
default:
break;
}

planeSource.setNormal(normal);
planeSource.setOrigin(origin);

return planeSource;
};
}

// ----------------------------------------------------------------------------

const DEFAULT_VALUES = {};

// ----------------------------------------------------------------------------

export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);

vtkAbstractWidgetFactory.extend(publicAPI, model, initialValues);

vtkResliceCursorWidget(publicAPI, model);
}

// ----------------------------------------------------------------------------

export const newInstance = macro.newInstance(extend, 'vtkResliceCursorWidget');

// ----------------------------------------------------------------------------

export default { newInstance, extend };
state.js
import vtkStateBuilder from 'vtk.js/Sources/Widgets/Core/StateBuilder';
import { ScrollingMethods } from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/Constants';

const factor = 1;
const rotationFactor = 1;
const axisXColor = [1, 0, 0];
const axisYColor = [0, 1, 0];
const axisZColor = [0, 0, 1];

const axisXinY = vtkStateBuilder
.createBuilder()
.addField({ name: 'point1', initialValue: [0, 0, -factor] })
.addField({ name: 'point2', initialValue: [0, 0, factor] })
.addField({ name: 'rotationPoint1', initialValue: [0, 0, -rotationFactor] })
.addField({ name: 'rotationPoint2', initialValue: [0, 0, rotationFactor] })
.addField({ name: 'color', initialValue: axisXColor })
.addField({ name: 'name', initialValue: 'AxisXinY' })
.addField({ name: 'planeName', initialValue: 'X' })
.build();
const axisXinZ = vtkStateBuilder
.createBuilder()
.addField({ name: 'point1', initialValue: [0, -factor, 0] })
.addField({ name: 'point2', initialValue: [0, factor, 0] })
.addField({ name: 'rotationPoint1', initialValue: [0, -rotationFactor, 0] })
.addField({ name: 'rotationPoint2', initialValue: [0, rotationFactor, 0] })
.addField({ name: 'color', initialValue: axisXColor })
.addField({ name: 'name', initialValue: 'AxisXinZ' })
.addField({ name: 'planeName', initialValue: 'X' })
.build();

const axisYinX = vtkStateBuilder
.createBuilder()
.addField({ name: 'point1', initialValue: [0, 0, -factor] })
.addField({ name: 'point2', initialValue: [0, 0, factor] })
.addField({ name: 'rotationPoint1', initialValue: [0, 0, -rotationFactor] })
.addField({ name: 'rotationPoint2', initialValue: [0, 0, rotationFactor] })
.addField({ name: 'color', initialValue: axisYColor })
.addField({ name: 'name', initialValue: 'AxisYinX' })
.addField({ name: 'planeName', initialValue: 'Y' })
.build();
const axisYinZ = vtkStateBuilder
.createBuilder()
.addField({ name: 'point1', initialValue: [-factor, 0, 0] })
.addField({ name: 'point2', initialValue: [factor, 0, 0] })
.addField({ name: 'rotationPoint1', initialValue: [-rotationFactor, 0, 0] })
.addField({ name: 'rotationPoint2', initialValue: [rotationFactor, 0, 0] })
.addField({ name: 'color', initialValue: axisYColor })
.addField({ name: 'name', initialValue: 'AxisYinZ' })
.addField({ name: 'planeName', initialValue: 'Y' })
.build();

const axisZinX = vtkStateBuilder
.createBuilder()
.addField({ name: 'point1', initialValue: [0, -factor, 0] })
.addField({ name: 'point2', initialValue: [0, factor, 0] })
.addField({ name: 'rotationPoint1', initialValue: [0, -rotationFactor, 0] })
.addField({ name: 'rotationPoint2', initialValue: [0, rotationFactor, 0] })
.addField({ name: 'color', initialValue: axisZColor })
.addField({ name: 'name', initialValue: 'AxisZinX' })
.addField({ name: 'planeName', initialValue: 'Z' })
.build();
const axisZinY = vtkStateBuilder
.createBuilder()
.addField({ name: 'point1', initialValue: [-factor, 0, 0] })
.addField({ name: 'point2', initialValue: [factor, 0, 0] })
.addField({ name: 'rotationPoint1', initialValue: [-rotationFactor, 0, 0] })
.addField({ name: 'rotationPoint2', initialValue: [rotationFactor, 0, 0] })
.addField({ name: 'color', initialValue: axisZColor })
.addField({ name: 'name', initialValue: 'AxisZinY' })
.addField({ name: 'planeName', initialValue: 'Z' })
.build();

export default function generateState() {
return vtkStateBuilder
.createBuilder()
.addStateFromInstance({
labels: ['AxisXinY'],
name: 'AxisXinY',
instance: axisXinY,
})
.addStateFromInstance({
labels: ['AxisXinZ'],
name: 'AxisXinZ',
instance: axisXinZ,
})
.addStateFromInstance({
labels: ['AxisYinX'],
name: 'AxisYinX',
instance: axisYinX,
})
.addStateFromInstance({
labels: ['AxisYinZ'],
name: 'AxisYinZ',
instance: axisYinZ,
})
.addStateFromInstance({
labels: ['AxisZinX'],
name: 'AxisZinX',
instance: axisZinX,
})
.addStateFromInstance({
labels: ['AxisZinY'],
name: 'AxisZinY',
instance: axisZinY,
})
.addField({ name: 'center', initialValue: [0, 0, 0] })
.addField({ name: 'opacity', initialValue: 1 })
.addField({ name: 'activeLineState', initialValue: null })
.addField({ name: 'activeRotationPointName', initialValue: null })
.addField({ name: 'image', initialValue: null })
.addField({ name: 'activeViewName', initialValue: '' })
.addField({ name: 'lineThickness', initialValue: 2 })
.addField({ name: 'sphereRadius', initialValue: 5 })
.addField({ name: 'showCenter', initialValue: true })
.addField({
name: 'updateMethodName',
})
.addField({ name: 'XPlaneNormal', initialValue: [1, 0, 0] })
.addField({ name: 'YPlaneNormal', initialValue: [0, -1, 0] })
.addField({ name: 'ZPlaneNormal', initialValue: [0, 0, 1] })
.addField({ name: 'enableRotation', initialValue: true })
.addField({ name: 'enableTranslation', initialValue: true })
.addField({ name: 'keepOrthogonality', initialValue: false })
.addField({
name: 'scrollingMethod',
initialValue: ScrollingMethods.MIDDLE_MOUSE_BUTTON,
})
.build();
}