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

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

  • 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)

  • 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

  • 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
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',
};

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 default {
ScrollingMethods,
InteractionMethodsName,
xyzToViewType,
viewTypeToXYZ,
};
behavior.js
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,
rotateVector,
updateState,
} from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/helpers';

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

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

// Reset "updateMethodName" attribute when no actors are selected
// Useful to update 'updateMethodeName' to the correct name which
// will be TranslateCenter by default
publicAPI.resetUpdateMethod = () => {
if (model.representations.length !== 0) {
model.representations[0].getSelectedState();
}
};

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

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

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

publicAPI.handleLeftButtonPress = (callData) => {
if (model.activeState && model.activeState.getActive()) {
isDragging = true;
const viewType = model.widgetState.getActiveViewType();
const currentPlaneNormal = model.widgetState.getPlanes()[viewType].normal;
model.planeManipulator.setOrigin(model.widgetState.getCenter());
model.planeManipulator.setNormal(currentPlaneNormal);

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 (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.invokeInternalInteractionEvent();
}
}
return macro.VOID;
};

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

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

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

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

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

publicAPI.invokeInternalInteractionEvent();
isScrolling = false;

return macro.EVENT_ABORT;
};

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

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

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

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

publicAPI.invokeInternalInteractionEvent = () => {
const methodName = model.activeState
? model.activeState.getUpdateMethodName()
: '';
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.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();

// 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] / dirProj[index]);

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

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

publicAPI[InteractionMethodsName.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[InteractionMethodsName.TranslateCenter] = (calldata) => {
let worldCoords = model.planeManipulator.handleEvent(
calldata,
model.openGLRenderWindow
);
worldCoords = publicAPI.getBoundedCenter(worldCoords);
model.activeState.setCenter(worldCoords);
updateState(model.widgetState);
};

publicAPI[InteractionMethodsName.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 radianAngle = vtkMath.signedAngleBetweenVectors(
previousVectorToOrigin,
currentVectorToOrigin,
planeNormal
);

publicAPI.rotateLineInView(activeLine, radianAngle);
};

/**
* Rotate a line by a specified angle
* @param {Line} line The line to rotate (e.g. getActiveLineState())
* @param {Number} radianAngle Applied angle in radian
*/
publicAPI.rotateLineInView = (line, radianAngle) => {
const viewType = line.getViewType();
const inViewType = line.getInViewType();
const planeNormal = model.widgetState.getPlanes()[inViewType].normal;
publicAPI.rotatePlane(viewType, radianAngle, planeNormal);

if (model.widgetState.getKeepOrthogonality()) {
const associatedLineName = getAssociatedLinesName(line.getName());
const associatedLine = model.widgetState[`get${associatedLineName}`]();
const associatedViewType = associatedLine.getViewType();
publicAPI.rotatePlane(associatedViewType, radianAngle, planeNormal);
}
updateState(model.widgetState);
};

/**
* 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
// --------------------------------------------------------------------------

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 vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder';

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

const EPSILON = 0.00001;

/**
* 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);
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);

const plane = vtkPlane.newInstance();
plane.setOrigin(...origin);
plane.setNormal(...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));

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

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

// 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 line axis
const xNormal = widgetState.getPlanes()[ViewTypes.YZ_PLANE].normal;
const yNormal = widgetState.getPlanes()[ViewTypes.XZ_PLANE].normal;
const zNormal = widgetState.getPlanes()[ViewTypes.XY_PLANE].normal;

const yzIntersectionLineAxis = vtkMath.cross(yNormal, zNormal, []);
const xzIntersectionLineAxis = vtkMath.cross(zNormal, xNormal, []);
const xyIntersectionLineAxis = vtkMath.cross(xNormal, yNormal, []);

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,
xyIntersectionLineAxis,
pdLength,
zRotationLength
);
updateLine(
widgetState.getAxisYinX(),
center,
xyIntersectionLineAxis,
pdLength,
zRotationLength
);

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

updateLine(
widgetState.getAxisXinZ(),
center,
xzIntersectionLineAxis,
pdLength,
yRotationLength
);
updateLine(
widgetState.getAxisZinX(),
center,
xzIntersectionLineAxis,
pdLength,
yRotationLength
);
}

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

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

/**
* Get the line name, constructs from the plane name and where the plane is displayed
* Example: planeName='X' rotatedPlaneName='Y', then the return values will be 'AxisXinY'
* @param {String} planeName Value between 'X', 'Y' and 'Z'
* @param {String} rotatedPlaneName Value between 'X', 'Y' and 'Z'
* @returns {String}
*/
export function getLineNameFromPlaneAndRotatedPlaneName(
planeName,
rotatedPlaneName
) {
return `Axis${planeName}in${rotatedPlaneName}`;
}

/**
* Extract the plane name from the line name
* Example: 'AxisXinY' will return 'X'
* @param {String} lineName Should be following this template : 'Axis_in_' with _ a character
* @returns {String} Value between 'X', 'Y' and 'Z' or null if an error occured
*/
export function getPlaneNameFromLineName(lineName) {
const match = lineName.match('([XYZ])in[XYZ]');
if (match) {
return match[1];
}
return null;
}

/**
* Get the orthogonal plane name of 'planeName' in a specific 'rotatedPlaneName'
* Example: planeName='X' on rotatedPlaneName='Z', then the associated plane name
* of 'X' plane is 'Y'
* @param {String} planeName
* @param {String} rotatedPlaneName
*/
export function getAssociatedPlaneName(planeName, rotatedPlaneName) {
const lineName = getLineNameFromPlaneAndRotatedPlaneName(
planeName,
rotatedPlaneName
);
const associatedLine = getAssociatedLinesName(lineName);
return getPlaneNameFromLineName(associatedLine);
}
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,
transformPlane,
} from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/helpers';
import { ViewTypes } from 'vtk.js/Sources/Widgets/Core/WidgetManager/Constants';

import { vec4, 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');

// --------------------------------------------------------------------------
// 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,
1,
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);
}

// --------------------------------------------------------------------------
// 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',
viewType: ViewTypes.XY_PLANE,
rotationEnabled: model.widgetState.getEnableRotation(),
},
},
];
case ViewTypes.XZ_PLANE:
return [
{
builder: vtkResliceCursorContextRepresentation,
labels: ['AxisXinY', 'AxisZinY'],
initialValues: {
axis1Name: 'AxisXinY',
axis2Name: 'AxisZinY',
viewType: ViewTypes.XZ_PLANE,
rotationEnabled: model.widgetState.getEnableRotation(),
},
},
];
case ViewTypes.YZ_PLANE:
return [
{
builder: vtkResliceCursorContextRepresentation,
labels: ['AxisYinX', 'AxisZinX'],
initialValues: {
axis1Name: 'AxisYinX',
axis2Name: 'AxisZinX',
viewType: ViewTypes.YZ_PLANE,
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.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.updateReslicePlane = (imageReslice, 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);

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

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 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, origin: o, point1: p1, point2: p2 };
};

/**
* 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.create();

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

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

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

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

const generateAxisXinY = () =>
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: 'viewType', initialValue: ViewTypes.YZ_PLANE })
.addField({ name: 'inViewType', initialValue: ViewTypes.XZ_PLANE })
.build();

const generateAxisXinZ = () =>
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: 'viewType', initialValue: ViewTypes.YZ_PLANE })
.addField({ name: 'inViewType', initialValue: ViewTypes.XY_PLANE })
.build();

const generateAxisYinX = () =>
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: 'viewType', initialValue: ViewTypes.XZ_PLANE })
.addField({ name: 'inViewType', initialValue: ViewTypes.YZ_PLANE })
.build();

const generateAxisYinZ = () =>
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: 'viewType', initialValue: ViewTypes.XZ_PLANE })
.addField({ name: 'inViewType', initialValue: ViewTypes.XY_PLANE })
.build();

const generateAxisZinX = () =>
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: 'viewType', initialValue: ViewTypes.XY_PLANE })
.addField({ name: 'inViewType', initialValue: ViewTypes.YZ_PLANE })
.build();

const generateAxisZinY = () =>
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: 'viewType', initialValue: ViewTypes.XY_PLANE })
.addField({ name: 'inViewType', initialValue: ViewTypes.XZ_PLANE })
.build();

export default function generateState() {
return vtkStateBuilder
.createBuilder()
.addStateFromInstance({
labels: ['AxisXinY'],
name: 'AxisXinY',
instance: generateAxisXinY(),
})
.addStateFromInstance({
labels: ['AxisXinZ'],
name: 'AxisXinZ',
instance: generateAxisXinZ(),
})
.addStateFromInstance({
labels: ['AxisYinX'],
name: 'AxisYinX',
instance: generateAxisYinX(),
})
.addStateFromInstance({
labels: ['AxisYinZ'],
name: 'AxisYinZ',
instance: generateAxisYinZ(),
})
.addStateFromInstance({
labels: ['AxisZinX'],
name: 'AxisZinX',
instance: generateAxisZinX(),
})
.addStateFromInstance({
labels: ['AxisZinY'],
name: 'AxisZinY',
instance: generateAxisZinY(),
})
.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: 'activeViewType', initialValue: null })
.addField({ name: 'lineThickness', initialValue: 2 })
.addField({ name: 'sphereRadius', initialValue: 5 })
.addField({ name: 'showCenter', initialValue: true })
.addField({
name: 'updateMethodName',
})
.addField({
name: 'planes',
initialValue: {
[ViewTypes.YZ_PLANE]: { normal: [1, 0, 0], viewUp: [0, 0, 1] },
[ViewTypes.XZ_PLANE]: { normal: [0, -1, 0], viewUp: [0, 0, 1] },
[ViewTypes.XY_PLANE]: { normal: [0, 0, -1], viewUp: [0, -1, 0] },
},
})
.addField({ name: 'enableRotation', initialValue: true })
.addField({ name: 'enableTranslation', initialValue: true })
.addField({ name: 'keepOrthogonality', initialValue: false })
.addField({
name: 'scrollingMethod',
initialValue: ScrollingMethods.MIDDLE_MOUSE_BUTTON,
})
.addField({ name: 'cameraOffsets', initialValue: {} })
.addField({ name: 'viewUpFromViewType', initialValue: {} })
.build();
}