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

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

  • image: vtkImage used to place the reslice cursor

  • activeViewType: Used in the behavior.js file in order to get the current view

  • sphereRadius: Manages the radius of the rotation points

  • Planes: Contains the normal and viewUp of the YZ_, XZ_ and XY_PLANE views (which is updated when a rotation is applied). It’s used to compute the resliced image

  • 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
import { ViewTypes } from 'vtk.js/Sources/Widgets/Core/WidgetManager/Constants';

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

// Note: These strings are used in ResliceCursorWidget/behavior.js
// as method's names
export const InteractionMethodsName = {
TranslateAxis: 'translateAxis',
RotateLine: 'rotateLine',
TranslateCenter: 'translateCenter',
TranslateCenterAndUpdatePlanes: 'translateCenterAndUpdatePlanes',
};

export const defaultViewUpFromViewType = {
[ViewTypes.YZ_PLANE]: [0, 0, 1], // Sagittal
[ViewTypes.XZ_PLANE]: [0, 0, 1], // Coronal
[ViewTypes.XY_PLANE]: [0, -1, 0], // Axial
};

export const xyzToViewType = [
ViewTypes.YZ_PLANE,
ViewTypes.XZ_PLANE,
ViewTypes.XY_PLANE,
];

export const viewTypeToXYZ = {
[ViewTypes.YZ_PLANE]: 0,
[ViewTypes.XZ_PLANE]: 1,
[ViewTypes.XY_PLANE]: 2,
};

export const planeNames = ['X', 'Y', 'Z'];

export const viewTypeToPlaneName = {
[ViewTypes.YZ_PLANE]: 'X',
[ViewTypes.XZ_PLANE]: 'Y',
[ViewTypes.XY_PLANE]: 'Z',
};

export const planeNameToViewType = {
X: ViewTypes.YZ_PLANE,
Y: ViewTypes.XZ_PLANE,
Z: ViewTypes.XY_PLANE,
};
export const lineNames = ['YinX', 'ZinX', 'XinY', 'ZinY', 'XinZ', 'YinZ'];

export default {
ScrollingMethods,
InteractionMethodsName,
xyzToViewType,
viewTypeToXYZ,
planeNames,
viewTypeToPlaneName,
planeNameToViewType,
lineNames,
};
behavior.js
import macro from 'vtk.js/Sources/macros';
import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox';
import vtkLine from 'vtk.js/Sources/Common/DataModel/Line';
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';

import {
boundPointOnPlane,
rotateVector,
updateState,
getOtherLineName,
getLinePlaneName,
getLineInPlaneName,
getLineNames,
} from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/helpers';

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

export default function widgetBehavior(publicAPI, model) {
model._isDragging = false;
let isScrolling = false;
let previousPosition;

macro.setGet(publicAPI, model, ['keepOrthogonality']);

publicAPI.setEnableTranslation = (enable) => {
model.representations[0].setPickable(enable); // line handle
model.representations[2].setPickable(enable); // center handle
};

publicAPI.setEnableRotation = (enable) => {
model.representations[1].setPickable(enable); // rotation handle
};

// FIXME: label information should be accessible from activeState instead of parent state.
publicAPI.getActiveInteraction = () => {
if (
model.widgetState
.getStatesWithLabel('rotation')
.includes(model.activeState)
) {
return InteractionMethodsName.RotateLine;
}
if (
model.widgetState.getStatesWithLabel('line').includes(model.activeState)
) {
return InteractionMethodsName.TranslateAxis;
}
if (
model.widgetState.getStatesWithLabel('center').includes(model.activeState)
) {
return InteractionMethodsName.TranslateCenter;
}
return null;
};

/**
* ActiveState can be RotationHandle or a LineHandle
* @returns 'YinX', 'ZinX', 'XinY', 'ZinY', 'XinZ' or 'YinZ'
*/
publicAPI.getActiveLineName = () =>
getLineNames(model.widgetState).find((lineName) =>
model.widgetState.getStatesWithLabel(lineName).includes(model.activeState)
);

// FIXME: label information should be accessible from activeState instead of parent state.
publicAPI.getActiveLineHandle = () =>
model.widgetState[`getAxis${publicAPI.getActiveLineName()}`]?.();

/**
* Return the line handle of the other line in the same view.
* @param {string} lineName name of the line (YinX, ZinX, XinY, ZinY, XinZ, YinZ)
* @returns ZinX if lineName == YinX, YinX if lineName == ZinX, ZinY if lineName == XinY...
*/
publicAPI.getOtherLineHandle = (lineName) =>
model.widgetState[
`getAxis${getOtherLineName(model.widgetState, lineName)}`
]?.();

// FIXME: label information should be accessible from activeState instead of parent state.
/**
* There are 2 rotation handles per axis: 'point0' and 'point1'.
* This function returns which rotation handle (point0 or point1) is currently active.
* ActiveState must be a RotationHandle.
* @returns 'point0', 'point1' or null if no point is active (e.g. line is being rotated)
*/
publicAPI.getActiveRotationPointName = () => {
if (
model.widgetState.getStatesWithLabel('point0').includes(model.activeState)
) {
return 'point0';
}
if (
model.widgetState.getStatesWithLabel('point1').includes(model.activeState)
) {
return 'point1';
}
return null;
};

publicAPI.startScrolling = (newPosition) => {
if (newPosition) {
previousPosition = newPosition;
}
isScrolling = true;
publicAPI.startInteraction();
};

publicAPI.endScrolling = () => {
isScrolling = false;
publicAPI.endInteraction();
};

publicAPI.updateCursor = () => {
switch (publicAPI.getActiveInteraction()) {
case InteractionMethodsName.TranslateCenter:
model._apiSpecificRenderWindow.setCursor('move');
break;
case InteractionMethodsName.RotateLine:
model._apiSpecificRenderWindow.setCursor('alias');
break;
case InteractionMethodsName.TranslateAxis:
model._apiSpecificRenderWindow.setCursor('pointer');
break;
default:
model._apiSpecificRenderWindow.setCursor('default');
break;
}
};

publicAPI.handleLeftButtonPress = (callData) => {
if (model.activeState && model.activeState.getActive()) {
model._isDragging = true;
const viewType = model.viewType;
const currentPlaneNormal = model.widgetState.getPlanes()[viewType].normal;
const manipulator =
model.activeState?.getManipulator?.() ?? model.manipulator;
manipulator.setWidgetOrigin(model.widgetState.getCenter());
manipulator.setWidgetNormal(currentPlaneNormal);
const { worldCoords } = manipulator.handleEvent(
callData,
model._apiSpecificRenderWindow
);
previousPosition = worldCoords;

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

return macro.EVENT_ABORT;
};

publicAPI.handleMouseMove = (callData) => {
if (model._isDragging) {
return publicAPI.handleEvent(callData);
}
if (isScrolling) {
if (previousPosition.y !== callData.position.y) {
const step = previousPosition.y - callData.position.y;
publicAPI.translateCenterOnPlaneDirection(step);
previousPosition = callData.position;

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

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

publicAPI.handleRightButtonPress = (calldata) => {
if (
model.widgetState.getScrollingMethod() ===
ScrollingMethods.RIGHT_MOUSE_BUTTON
) {
publicAPI.startScrolling(calldata.position);
}
};

publicAPI.handleRightButtonRelease = () => {
if (
model.widgetState.getScrollingMethod() ===
ScrollingMethods.RIGHT_MOUSE_BUTTON
) {
publicAPI.endScrolling();
}
};

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

publicAPI.handleMouseWheel = (calldata) => {
const step = calldata.spinY;
isScrolling = true;
publicAPI.translateCenterOnPlaneDirection(step);

publicAPI.invokeInternalInteractionEvent();
isScrolling = false;

return macro.EVENT_ABORT;
};

publicAPI.handleEndMouseWheel = () => {
publicAPI.endScrolling();
};

publicAPI.handleMiddleButtonPress = (calldata) => {
if (
model.widgetState.getScrollingMethod() ===
ScrollingMethods.MIDDLE_MOUSE_BUTTON
) {
publicAPI.startScrolling(calldata.position);
}
};

publicAPI.handleMiddleButtonRelease = () => {
if (
model.widgetState.getScrollingMethod() ===
ScrollingMethods.MIDDLE_MOUSE_BUTTON
) {
publicAPI.endScrolling();
}
};

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

publicAPI.invokeInternalInteractionEvent = () => {
const methodName = publicAPI.getActiveInteraction();
const computeFocalPointOffset =
methodName !== InteractionMethodsName.RotateLine;
const canUpdateFocalPoint =
methodName === InteractionMethodsName.RotateLine;
publicAPI.invokeInteractionEvent({
computeFocalPointOffset,
canUpdateFocalPoint,
});
};

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.translateCenterOnPlaneDirection = (nbSteps) => {
const dirProj = model.widgetState.getPlanes()[model.viewType].normal;

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

// Use Chebyshev norm
// https://math.stackexchange.com/questions/71423/what-is-the-term-for-the-projection-of-a-vector-onto-the-unit-cube
const absDirProj = dirProj.map((value) => Math.abs(value));
const index = absDirProj.indexOf(Math.max(...absDirProj));
const movingFactor =
(nbSteps * imageSpacing[index]) / Math.abs(dirProj[index]);

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

model.widgetState.setCenter(newCenter);
updateState(
model.widgetState,
model._factory.getScaleInPixels(),
model._factory.getRotationHandlePosition()
);
};

publicAPI[InteractionMethodsName.TranslateAxis] = (calldata) => {
const lineHandle = publicAPI.getActiveLineHandle();
const lineName = publicAPI.getActiveLineName();
const pointOnLine = vtkMath.add(
lineHandle.getOrigin(),
lineHandle.getDirection(),
[]
);
const currentLineVector = lineHandle.getDirection();
vtkMath.normalize(currentLineVector);

// Translate the current line along the other line
const otherLineHandle = publicAPI.getOtherLineHandle(lineName);
const center = model.widgetState.getCenter();
const manipulator =
model.activeState?.getManipulator?.() ?? model.manipulator;
let worldCoords = null;
let newOrigin = [];
if (model.activeState?.getManipulator?.()) {
worldCoords = manipulator.handleEvent(
calldata,
model._apiSpecificRenderWindow
).worldCoords;
const translation = vtkMath.subtract(worldCoords, previousPosition, []);
vtkMath.add(center, translation, newOrigin);
} else if (otherLineHandle) {
const otherLineVector = otherLineHandle.getDirection();
vtkMath.normalize(otherLineVector);
const axisTranslation = otherLineVector;

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

const closestPoint = [];
worldCoords = manipulator.handleEvent(
calldata,
model._apiSpecificRenderWindow
).worldCoords;
vtkLine.distanceToLine(
worldCoords,
lineHandle.getOrigin(),
pointOnLine,
closestPoint
);

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

newOrigin = vtkMath.multiplyAccumulate(
center,
axisTranslation,
translationDistance,
newOrigin
);
}
newOrigin = publicAPI.getBoundedCenter(newOrigin);
model.widgetState.setCenter(newOrigin);
updateState(
model.widgetState,
model._factory.getScaleInPixels(),
model._factory.getRotationHandlePosition()
);
previousPosition = worldCoords;
};

publicAPI.getBoundedCenter = (newCenter) => {
const oldCenter = model.widgetState.getCenter();
const imageBounds = model.widgetState.getImage().getBounds();

if (vtkBoundingBox.containsPoint(imageBounds, ...newCenter)) {
return newCenter;
}

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

publicAPI[InteractionMethodsName.TranslateCenter] = (calldata) => {
const manipulator =
model.activeState?.getManipulator?.() ?? model.manipulator;
const { worldCoords } = manipulator.handleEvent(
calldata,
model._apiSpecificRenderWindow
);
const translation = vtkMath.subtract(worldCoords, previousPosition, []);
previousPosition = worldCoords;
let newCenter = vtkMath.add(model.widgetState.getCenter(), translation, []);
newCenter = publicAPI.getBoundedCenter(newCenter);
model.widgetState.setCenter(newCenter);
updateState(
model.widgetState,
model._factory.getScaleInPixels(),
model._factory.getRotationHandlePosition()
);
};

publicAPI[InteractionMethodsName.RotateLine] = (calldata) => {
const activeLineHandle = publicAPI.getActiveLineHandle();
const manipulator =
model.activeState?.getManipulator?.() ?? model.manipulator;
const planeNormal = manipulator.getWidgetNormal();
const { worldCoords } = manipulator.handleEvent(
calldata,
model._apiSpecificRenderWindow
);

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

const previousLineDirection = activeLineHandle.getDirection();
vtkMath.normalize(previousLineDirection);
const activePointName = publicAPI.getActiveRotationPointName();
if (
activePointName === 'point1' ||
(!activePointName &&
vtkMath.dot(currentVectorToOrigin, previousLineDirection) < 0)
) {
vtkMath.multiplyScalar(previousLineDirection, -1);
}

const radianAngle = vtkMath.signedAngleBetweenVectors(
previousLineDirection,
currentVectorToOrigin,
planeNormal
);

publicAPI.rotateLineInView(publicAPI.getActiveLineName(), radianAngle);
};

/**
* Rotate a line by a specified angle
* @param {string} lineName The line name to rotate (e.g. YinX, ZinX, XinY, ZinY, XinZ, YinZ)
* @param {Number} radianAngle Applied angle in radian
*/
publicAPI.rotateLineInView = (lineName, radianAngle) => {
const viewType = planeNameToViewType[getLinePlaneName(lineName)];
const inViewType = planeNameToViewType[getLineInPlaneName(lineName)];
const planeNormal = model.widgetState.getPlanes()[inViewType].normal;
publicAPI.rotatePlane(viewType, radianAngle, planeNormal);

if (publicAPI.getKeepOrthogonality()) {
const otherLineName = getOtherLineName(model.widgetState, lineName);
const otherPlaneName = getLinePlaneName(otherLineName);
publicAPI.rotatePlane(
planeNameToViewType[otherPlaneName],
radianAngle,
planeNormal
);
}
updateState(
model.widgetState,
model._factory.getScaleInPixels(),
model._factory.getRotationHandlePosition()
);
};

/**
* Rotate a specified plane around an other specified plane.
* @param {ViewTypes} viewType Define which plane will be rotated
* @param {Number} radianAngle Applied angle in radian
* @param {vec3} planeNormal Define the axis to rotate around
*/
publicAPI.rotatePlane = (viewType, radianAngle, planeNormal) => {
const { normal, viewUp } = model.widgetState.getPlanes()[viewType];
const newNormal = rotateVector(normal, planeNormal, radianAngle);
const newViewUp = rotateVector(viewUp, planeNormal, radianAngle);

model.widgetState.getPlanes()[viewType] = {
normal: newNormal,
viewUp: newViewUp,
};
};

// --------------------------------------------------------------------------
// initialization
// --------------------------------------------------------------------------
}
cprBehavior.js
import resliceCursorBehavior from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/behavior';
import { InteractionMethodsName } from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/Constants';
import { updateState } from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/helpers';
import { ViewTypes } from 'vtk.js/Sources/Widgets/Core/WidgetManager/Constants';
import { vec3 } from 'gl-matrix';
// import macro from 'vtk.js/Sources/macros';

export default function widgetBehavior(publicAPI, model) {
// We inherit resliceCursorBehavior
resliceCursorBehavior(publicAPI, model);

const stretchPlaneName = 'Y';
const crossPlaneName = 'Z';

publicAPI.getActiveInteraction = () => {
if (
model.widgetState
.getStatesWithLabel(`lineIn${stretchPlaneName}`)
.includes(model.activeState)
) {
return InteractionMethodsName.TranslateCenterAndUpdatePlanes;
}
if (
model.widgetState
.getStatesWithLabel(`lineIn${crossPlaneName}`)
.includes(model.activeState) ||
model.widgetState
.getStatesWithLabel(`rotationIn${crossPlaneName}`)
.includes(model.activeState)
) {
return InteractionMethodsName.RotateLine;
}
return null;
};

publicAPI[InteractionMethodsName.TranslateCenterAndUpdatePlanes] = (
calldata
) => {
const manipulator =
model.activeState?.getManipulator?.() ?? model.manipulator;

const { worldCoords, worldDirection } = manipulator.handleEvent(
calldata,
model._apiSpecificRenderWindow
);
publicAPI.updateCenterAndPlanes(worldCoords, worldDirection);
};

publicAPI.updateCenterAndPlanes = (worldCoords, worldDirection) => {
// Update center
const newBoundedCenter = publicAPI.getBoundedCenter(worldCoords);
model.widgetState.setCenter(newBoundedCenter);

// Update planes if axes are given
if (worldDirection) {
const getAxis = (idx) =>
vec3.normalize([], worldDirection.slice(3 * idx, 3 * idx + 3));
const planes = model.widgetState.getPlanes();
Object.keys(planes).forEach((viewType) => {
switch (Number.parseInt(viewType, 10)) {
case ViewTypes.YZ_PLANE:
planes[viewType] = {
normal: getAxis(0),
viewUp: getAxis(2),
};
break;
case ViewTypes.XZ_PLANE:
planes[viewType] = {
normal: getAxis(1),
viewUp: getAxis(2),
};
break;
case ViewTypes.XY_PLANE:
planes[viewType] = {
normal: getAxis(2),
viewUp: getAxis(1),
};
break;
default:
break;
}
});
}

updateState(
model.widgetState,
model._factory.getScaleInPixels(),
model._factory.getRotationHandlePosition()
);
};

publicAPI.translateCenterOnPlaneDirection = (nbSteps) => {
const handleScroll = model._factory.getManipulator()?.handleScroll;
if (handleScroll) {
const { worldCoords, worldDirection } = handleScroll(nbSteps);
publicAPI.updateCenterAndPlanes(worldCoords, worldDirection);
}
};
}
helpers.js
import vtkBoundingBox, {
STATIC,
} from 'vtk.js/Sources/Common/DataModel/BoundingBox';
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 vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder';

import {
planeNames,
planeNameToViewType,
viewTypeToPlaneName,
} from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/Constants';

const EPSILON = 10e-7;

/**
* 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
* @return {Boolean} false if no bounds have been found, else true
*/
export function boundPlane(bounds, origin, p1, p2) {
const v1 = [];
vtkMath.subtract(p1, origin, v1);
vtkMath.normalize(v1);

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

const n = [0, 0, 1];
vtkMath.cross(v1, v2, n);
vtkMath.normalize(n);

// Inflate bounds in order to avoid precision error when cutting cubesource
const inflatedBounds = [...bounds];
const eps = [...n];
vtkMath.multiplyScalar(eps, EPSILON);
vtkBoundingBox.addBounds(
inflatedBounds,
bounds[0] + eps[0],
bounds[1] + eps[0],
bounds[2] + eps[1],
bounds[3] + eps[1],
bounds[4] + eps[2],
bounds[5] + eps[2]
);
vtkBoundingBox.addBounds(
inflatedBounds,
bounds[0] - eps[0],
bounds[1] - eps[0],
bounds[2] - eps[1],
bounds[3] - eps[1],
bounds[4] - eps[2],
bounds[5] - eps[2]
);

const plane = vtkPlane.newInstance();
plane.setOrigin(...origin);
plane.setNormal(...n);

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

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

const cutBounds = cutter.getOutputData();
if (cutBounds.getNumberOfPoints() === 0) {
return false;
}
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];
}
return true;
}
// 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));

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];
vtkBoundingBox.intersectBox(bounds, p1, dir12, out, tolerance);

return out;
}

/**
* Rotates a vector around another.
* @param {vec3} vectorToBeRotated Vector to rate
* @param {vec3} axis Axis to rotate around
* @param {Number} angle Angle in radian
* @returns The rotated vector
*/
export function rotateVector(vectorToBeRotated, axis, angle) {
const rotatedVector = [...vectorToBeRotated];
vtkMatrixBuilder.buildFromRadian().rotate(angle, axis).apply(rotatedVector);
return rotatedVector;
}

/**
* Return ['X', 'Y'] if there are only 2 planes defined in the widget state.
* Return ['X', 'Y', 'Z'] if there are 3 planes defined in the widget state.
* @param {object} widgetState the state of the widget
* @returns An array of plane names
*/
export function getPlaneNames(widgetState) {
return Object.keys(widgetState.getPlanes()).map(
(viewType) => viewTypeToPlaneName[viewType]
);
}

/**
* Return X if lineName == XinY|XinZ, Y if lineName == YinX|YinZ and Z otherwise
* @param {string} lineName name of the line (YinX, ZinX, XinY, ZinY, XinZ, YinZ)
*/
export function getLinePlaneName(lineName) {
return lineName[0];
}
/**
* Return X if lineName == YinX|ZinX, Y if lineName == XinY|ZinY and Z otherwise
* @param {string} lineName name of the line (YinX, ZinX, XinY, ZinY, XinZ, YinZ)
*/
export function getLineInPlaneName(lineName) {
return lineName[3];
}

/**
* Returns ['XinY', 'YinX'] if planes == ['X', 'Y']
* ['XinY', 'XinZ', 'YinX', 'YinZ', 'ZinX', 'ZinY'] if planes == ['X', 'Y', 'Z']
* @param {string} planes name of the planes (e.g. ['X', 'Y'])
*/
export function getPlanesLineNames(planes = planeNames) {
const lines = [];
planes.forEach((plane) => {
planes.forEach((inPlane) => {
if (plane !== inPlane) {
lines.push(`${plane}in${inPlane}`);
}
});
});
return lines;
}

export function getLineNames(widgetState) {
const planes = Object.keys(widgetState.getPlanes()).map(
(viewType) => viewTypeToPlaneName[viewType]
);
return getPlanesLineNames(planes);
}

/**
* Return ZinX if lineName == YinX, YinX if lineName == ZinX, ZinY if lineName == XinY...
* @param {string} lineName name of the line (YinX, ZinX, XinY, ZinY, XinZ, YinZ)
*/
export function getOtherLineName(widgetState, lineName) {
const linePlaneName = getLinePlaneName(lineName);
const lineInPlaneName = getLineInPlaneName(lineName);
const otherLineName = getPlaneNames(widgetState).find(
(planeName) => planeName !== linePlaneName && planeName !== lineInPlaneName
);
return `${otherLineName}in${lineInPlaneName}`;
}

// Compute the offset of the rotation handle origin
function computeRotationHandleOriginOffset(
axis,
rotationHandlePosition,
volumeDiagonalLength,
scaleInPixels
) {
// FIXME: p1 and p2 could be placed on the exact boundaries of the volume.
return vtkMath.multiplyScalar(
[...axis],
(rotationHandlePosition * (scaleInPixels ? 1 : volumeDiagonalLength)) / 2
);
}

// Update the reslice cursor state according to the three planes normals and the origin
export function updateState(
widgetState,
scaleInPixels,
rotationHandlePosition
) {
const planes = Object.keys(widgetState.getPlanes()).map(
(viewType) => viewTypeToPlaneName[viewType]
);
// Generates an object as such:
// axes = {'XY': cross(X, Y), 'YX': cross(X, Y), 'YZ': cross(Y, Z)...}
const axes = planes.reduce((res, plane) => {
planes
.filter((otherPlane) => plane !== otherPlane)
.forEach((otherPlane) => {
const cross = vtkMath.cross(
widgetState.getPlanes()[planeNameToViewType[plane]].normal,
widgetState.getPlanes()[planeNameToViewType[otherPlane]].normal,
[]
);
res[`${plane}${otherPlane}`] = cross;
res[`${otherPlane}${plane}`] = cross;
});
return res;
}, {});

const bounds = widgetState.getImage().getBounds();
const center = widgetState.getCenter();

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

widgetState.getCenterHandle().setOrigin(center);

getPlanesLineNames(planes).forEach((lineName) => {
const planeName = getLinePlaneName(lineName);
const inPlaneName = getLineInPlaneName(lineName);
const direction = axes[`${planeName}${inPlaneName}`];
widgetState[`getRotationHandle${lineName}0`]().setOrigin(center);
widgetState[`getRotationHandle${lineName}0`]()
.getManipulator()
?.setHandleOrigin(center);
widgetState[`getRotationHandle${lineName}0`]()
.getManipulator()
?.setHandleNormal(
widgetState.getPlanes()[planeNameToViewType[planeName]].normal
);
widgetState[`getRotationHandle${lineName}0`]().setOffset(
computeRotationHandleOriginOffset(
direction,
rotationHandlePosition,
pdLength,
scaleInPixels
)
);
widgetState[`getRotationHandle${lineName}1`]().setOrigin(center);
widgetState[`getRotationHandle${lineName}1`]()
.getManipulator()
?.setHandleOrigin(center);
widgetState[`getRotationHandle${lineName}1`]()
.getManipulator()
?.setHandleNormal(
widgetState.getPlanes()[planeNameToViewType[planeName]].normal
);
widgetState[`getRotationHandle${lineName}1`]().setOffset(
computeRotationHandleOriginOffset(
direction,
-rotationHandlePosition,
pdLength,
scaleInPixels
)
);
const lineHandle = widgetState[`getAxis${lineName}`]();
lineHandle.setOrigin(center);
lineHandle.getManipulator()?.setHandleOrigin(center);
lineHandle
.getManipulator()
?.setHandleNormal(
widgetState.getPlanes()[planeNameToViewType[planeName]].normal
);
const scale = vtkMath.normalize(direction);
const scale3 = lineHandle.getScale3();
scale3[2] = 2 * scale;
lineHandle.setScale3(scale3);
const right =
widgetState.getPlanes()[planeNameToViewType[inPlaneName]].normal;
const up = vtkMath.cross(direction, right, []);
lineHandle.setRight(right);
lineHandle.setUp(up);
lineHandle.setDirection(direction);
});
}

/**
* First rotate planeToTransform to match targetPlane normal.
* Then rotate around targetNormal to enforce targetViewUp "up" vector (i.e. Origin->p2 ).
* There is an infinite number of options to rotate a plane normal to another. Here we attempt to
* preserve Origin, P1 and P2 when rotating around targetPlane.
* @param {vtkPlaneSource} planeToTransform
* @param {vec3} targetOrigin Center of the plane
* @param {vec3} targetNormal Normal to state to the plane
* @param {vec3} viewType Vector that enforces view up
*/
export function transformPlane(
planeToTransform,
targetCenter,
targetNormal,
targetViewUp
) {
planeToTransform.setNormal(targetNormal);
const viewUp = vtkMath.subtract(
planeToTransform.getPoint2(),
planeToTransform.getOrigin(),
[]
);
const angle = vtkMath.signedAngleBetweenVectors(
viewUp,
targetViewUp,
targetNormal
);
planeToTransform.rotate(angle, targetNormal);
planeToTransform.setCenter(targetCenter);
}
index.js
import macro from 'vtk.js/Sources/macros';
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 vtkPlaneManipulator from 'vtk.js/Sources/Widgets/Manipulators/PlaneManipulator';
import vtkLineHandleRepresentation from 'vtk.js/Sources/Widgets/Representations/LineHandleRepresentation';
import vtkSphereHandleRepresentation from 'vtk.js/Sources/Widgets/Representations/SphereHandleRepresentation';

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,
transformPlane,
} from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/helpers';
import { viewTypeToPlaneName } from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/Constants';

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

import { mat4 } from 'gl-matrix';
import vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder';

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

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

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

model.methodsToLink = ['scaleInPixels'];

// --------------------------------------------------------------------------
// 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;
}

/**
* Compute the offset between display reslice cursor position and
* display focal point position
* This will be used to keep the same offset between reslice cursor
* center and focal point when needed.
*/
function computeFocalPointOffsetFromResliceCursorCenter(viewType, renderer) {
const worldFocalPoint = renderer.getActiveCamera().getFocalPoint();
const worldResliceCenter = model.widgetState.getCenter();

const view = renderer.getRenderWindow().getViews()[0];
const dims = view.getViewportSize(renderer);
const aspect = dims[0] / dims[1];
const displayFocalPoint = renderer.worldToNormalizedDisplay(
...worldFocalPoint,
aspect
);
const displayResliceCenter = renderer.worldToNormalizedDisplay(
...worldResliceCenter,
aspect
);

const newOffset = vtkMath.subtract(
displayFocalPoint,
displayResliceCenter,
[0, 0, 0]
);

const cameraOffsets = model.widgetState.getCameraOffsets();
cameraOffsets[viewType] = newOffset;

model.widgetState.setCameraOffsets(cameraOffsets);
}

function updateCamera(
renderer,
normal,
viewType,
resetFocalPoint,
keepCenterFocalDistance
) {
// 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]
);

let newFocalPoint = focalPoint;
if (resetFocalPoint) {
// intersect with the plane to get updated focal point
const intersection = vtkPlane.intersectWithLine(
focalPoint,
estimatedCameraPosition,
model.widgetState.getCenter(), // reslice cursor center
normal
);
newFocalPoint = intersection.x;
}

// Update the estimated focal point so that it will be at the same
// distance from the reslice center
if (keepCenterFocalDistance) {
const worldResliceCenter = model.widgetState.getCenter();

const view = renderer.getRenderWindow().getViews()[0];
const dims = view.getViewportSize(renderer);
const aspect = dims[0] / dims[1];
const displayResliceCenter = renderer.worldToNormalizedDisplay(
...worldResliceCenter,
aspect
);

const realOffset = model.widgetState.getCameraOffsets()[viewType];
const displayFocal = vtkMath.add(
displayResliceCenter,
realOffset,
[0, 0, 0]
);

const worldFocal = renderer.normalizedDisplayToWorld(
...displayFocal,
aspect
);

// Reproject focal point on slice in order to keep it on the
// same plane as the reslice cursor center
const intersection2 = vtkPlane.intersectWithLine(
worldFocal,
estimatedCameraPosition,
worldResliceCenter,
normal
);

newFocalPoint[0] = intersection2.x[0];
newFocalPoint[1] = intersection2.x[1];
newFocalPoint[2] = intersection2.x[2];
}

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]
);

// Don't clip away any part of the data.
// Renderer may not have yet actor bounds
const bounds = model.widgetState.getImage().getBounds();
if (resetFocalPoint) {
renderer.resetCamera(bounds);
}
renderer.resetCameraClippingRange(bounds);
}

/**
* Convenient function to return the widget for a given viewType
* @param {string} viewType
* @returns the widget instanced in the given viewType.
*/
function findWidgetForViewType(viewType) {
return publicAPI
.getViewIds()
.map((viewId) => publicAPI.getWidgetForView({ viewId }))
.find((widget) => widget.getViewType() === viewType);
}
/**
* Convenient function to return the ResliceCursorRepresentation for a given viewType
* @param {string} viewType
* @returns an array of 3 representations (for line handles, rotation handles, center handle)
* or an empty array if the widget has not yet been added to the view type.
*/
function findRepresentationsForViewType(viewType) {
const widgetForViewType = findWidgetForViewType(viewType);
return widgetForViewType ? widgetForViewType.getRepresentations() : [];
}

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

publicAPI.getRepresentationsForViewType = (viewType) => {
switch (viewType) {
case ViewTypes.XY_PLANE:
case ViewTypes.XZ_PLANE:
case ViewTypes.YZ_PLANE:
return [
{
builder: vtkLineHandleRepresentation,
labels: [`lineIn${viewTypeToPlaneName[viewType]}`],
initialValues: {
useActiveColor: false,
scaleInPixels: model.scaleInPixels,
},
},
{
builder: vtkSphereHandleRepresentation,
labels: [`rotationIn${viewTypeToPlaneName[viewType]}`],
initialValues: {
useActiveColor: false,
scaleInPixels: model.scaleInPixels,
},
},
{
builder: vtkSphereHandleRepresentation,
labels: ['center'],
initialValues: {
useActiveColor: false,
scaleInPixels: model.scaleInPixels,
},
},
];
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,
model.scaleInPixels,
model.rotationHandlePosition
);
};

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

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

publicAPI.updateCameraPoints = (
renderer,
viewType,
resetFocalPoint,
keepCenterFocalDistance,
computeFocalPointOffset
) => {
publicAPI.resetCamera(
renderer,
viewType,
resetFocalPoint,
keepCenterFocalDistance
);

if (computeFocalPointOffset) {
computeFocalPointOffsetFromResliceCursorCenter(viewType, renderer);
}
};

/**
*
* @param {*} renderer
* @param {*} viewType
* @param {*} resetFocalPoint Defines if the focal point is reset to the image center
* @param {*} keepCenterFocalDistance Defines if the estimated focal point has to be updated
* in order to keep the same distance to the center (according to the computed focal point
* shift)
*/
publicAPI.resetCamera = (
renderer,
viewType,
resetFocalPoint,
keepCenterFocalDistance
) => {
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 = publicAPI.getPlaneNormalFromViewType(viewType);

// ResetFocalPoint will reset focal point to the center of the image
const estimatedFocalPoint = resetFocalPoint ? center : focalPoint;

const estimatedCameraPosition = vtkMath.multiplyAccumulate(
estimatedFocalPoint,
normal,
distance,
[0, 0, 0]
);
renderer.getActiveCamera().setFocalPoint(...estimatedFocalPoint);
renderer.getActiveCamera().setPosition(...estimatedCameraPosition);
renderer
.getActiveCamera()
.setViewUp(model.widgetState.getPlanes()[viewType].viewUp);

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

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

const { normal, viewUp } = model.widgetState.getPlanes()[viewType];
// Adapt plane orientation in order to fit the correct viewUp
// so that the rotations will be more understandable than now.
transformPlane(planeSource, model.widgetState.getCenter(), normal, 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);
planeSource.setPoint2(...boundedP2);

return planeSource;
};

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

// TBD: use normal from planeSource ?
const { normal } = model.widgetState.getPlanes()[viewType];

const planeOrigin = planeSource.getOrigin();

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

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

const newResliceAxes = mat4.identity(new Float64Array(16));

for (let i = 0; i < 3; i++) {
newResliceAxes[i] = planeAxis1[i];
newResliceAxes[4 + i] = planeAxis2[i];
newResliceAxes[8 + i] = normal[i];
newResliceAxes[12 + i] = planeOrigin[i];
}

return newResliceAxes;
};

publicAPI.updateReslicePlane = (imageReslice, viewType) => {
// Calculate appropriate pixel spacing for the reslicing
const spacing = model.widgetState.getImage().getSpacing();

const planeSource = publicAPI.getPlaneSource(viewType);
const newResliceAxes = publicAPI.getResliceAxes(viewType);

const planeOrigin = planeSource.getOrigin();
const p1 = planeSource.getPoint1();
const planeAxis1 = vtkMath.subtract(p1, planeOrigin, []);
const planeSizeX = vtkMath.normalize(planeAxis1);

const p2 = planeSource.getPoint2();
const planeAxis2 = vtkMath.subtract(p2, planeOrigin, []);
const planeSizeY = vtkMath.normalize(planeAxis2);

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]);

// 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 origin = publicAPI.getWidgetState().getCenter();
const planeNormal = publicAPI.getPlaneNormalFromViewType(type);

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

return planeSource;
};

publicAPI.getPlaneNormalFromViewType = (viewType) =>
publicAPI.getWidgetState().getPlanes()[viewType].normal;

/**
* Returns the normals of the planes that are not viewType.
* @param {ViewType} viewType ViewType to extract other normals
*/
publicAPI.getOtherPlaneNormals = (viewType) =>
[ViewTypes.YZ_PLANE, ViewTypes.XZ_PLANE, ViewTypes.XY_PLANE]
.filter((vt) => vt !== viewType)
.map((vt) => publicAPI.getPlaneNormalFromViewType(vt));

/**
* Return the reslice cursor matrix built as such: [YZ, XZ, XY, center]
*/
publicAPI.getResliceMatrix = () => {
const resliceMatrix = mat4.identity(new Float64Array(16));

for (let i = 0; i < 3; i++) {
resliceMatrix[4 * i + 0] = publicAPI.getPlaneNormalFromViewType(
ViewTypes.YZ_PLANE
)[i];
resliceMatrix[4 * i + 1] = publicAPI.getPlaneNormalFromViewType(
ViewTypes.XZ_PLANE
)[i];
resliceMatrix[4 * i + 2] = publicAPI.getPlaneNormalFromViewType(
ViewTypes.XY_PLANE
)[i];
}

const origin = publicAPI.getWidgetState().getCenter();

const m = vtkMatrixBuilder
.buildFromRadian()
.translate(...origin)
.multiply(resliceMatrix)
.translate(...vtkMath.multiplyScalar([...origin], -1))
.getMatrix();

return m;
};

publicAPI.getDisplayScaleParams = () =>
[ViewTypes.YZ_PLANE, ViewTypes.XZ_PLANE, ViewTypes.XY_PLANE].reduce(
(res, viewType) => {
res[viewType] =
findRepresentationsForViewType(
viewType
)[0]?.getDisplayScaleParams?.();
return res;
},
{}
);
publicAPI.setScaleInPixels = macro.chain(
publicAPI.setScaleInPixels,
(scale) => {
publicAPI.getViewWidgets().forEach((w) => w.setScaleInPixels(scale));
updateState(
model.widgetState,
model.scaleInPixels,
model.rotationHandlePosition
);
}
);
}

// ----------------------------------------------------------------------------
/**
* Initializes the model.
* @param {*} initialValues optional object of member variables. initialValues.planes is an optional list of axis names (e.g. ['X', 'Y'])
* @returns the initial model object
*/
const defaultValues = (initialValues) => ({
behavior: widgetBehavior,
widgetState: stateGenerator(initialValues.planes),
rotationHandlePosition: 0.5,
scaleInPixels: true,
manipulator: vtkPlaneManipulator.newInstance(),
...initialValues,
});

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

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

vtkAbstractWidgetFactory.extend(publicAPI, model, initialValues);

macro.setGet(publicAPI, model, [
'scaleInPixels',
'rotationHandlePosition',
'manipulator',
]);

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,
planeNames,
planeNameToViewType,
} from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/Constants';

const defaultPlanes = {
X: {
normal: [1, 0, 0],
viewUp: [0, 0, 1],
color3: [255, 0, 0],
},
Y: {
normal: [0, -1, 0],
viewUp: [0, 0, 1],
color3: [0, 255, 0],
},
Z: {
normal: [0, 0, -1],
viewUp: [0, -1, 0],
color3: [0, 0, 255],
},
};

const viewsColor3 = {
X: [255, 0, 0], // red
Y: [0, 255, 0], // green
Z: [0, 0, 255], // blue
};

export default function generateState(planes = planeNames) {
const state = vtkStateBuilder
.createBuilder()
.addField({ name: 'center', initialValue: [0, 0, 0] })
.addField({ name: 'image', initialValue: null })
.addField({ name: 'activeViewType', initialValue: null })
.addField({
name: 'planes',
initialValue: planes.reduce(
(res, planeName) => ({
...res,
[planeNameToViewType[planeName]]: {
normal: defaultPlanes[planeName].normal,
viewUp: defaultPlanes[planeName].viewUp,
},
}),
{}
),
})
.addField({
name: 'scrollingMethod',
initialValue: ScrollingMethods.MIDDLE_MOUSE_BUTTON,
})
.addField({ name: 'cameraOffsets', initialValue: {} })
.addField({ name: 'viewUpFromViewType', initialValue: {} })
.addStateFromMixin({
labels: ['handles', 'sphere', 'center'],
mixins: ['origin', 'color3', 'scale1', 'visible', 'manipulator'],
name: 'centerHandle',
initialValues: {
scale1: 30,
color3: [255, 255, 255],
},
});

planes.reduce(
(viewState, view) =>
planes
.filter((v) => v !== view)
.reduce((axisState, axis) => {
// Line handle
axisState.addStateFromMixin({
labels: ['handles', 'line', `lineIn${view}`, `${axis}in${view}`],
mixins: [
'origin',
'color3',
'scale3', // scale3[2] will be automatically overwritten
'orientation',
'visible',
'manipulator',
],
name: `axis${axis}in${view}`,
initialValues: {
scale3: [4, 4, 4],
color3: viewsColor3[axis],
},
});
// Rotation handle
for (let rotationHandle = 0; rotationHandle < 2; ++rotationHandle) {
axisState.addStateFromMixin({
labels: [
'handles',
'sphere',
'rotation',
`rotationIn${view}`,
`${axis}in${view}`,
`point${rotationHandle}`,
],
mixins: ['origin', 'color3', 'scale1', 'visible', 'manipulator'],
name: `rotationHandle${axis}in${view}${rotationHandle}`,
initialValues: {
scale1: 30,
color3: viewsColor3[axis],
},
});
}
return axisState;
}, viewState),
state
);
return state.build();
}