ResliceCursorWidget

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

getWidgetState(object)

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

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

These sub states contain :

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

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

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

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

  • image: vtkImage used to place the reslice cursor

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

  • sphereRadius: Manages the radius of the rotation points

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

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

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

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

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

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

behavior.js
import { mat4 } from 'gl-matrix';

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

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

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

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

publicAPI.invokeStartInteractionEvent();

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

return macro.EVENT_ABORT;
};

publicAPI.handleMouseMove = (callData) => {
if (isDragging && model.pickable) {
return publicAPI.handleEvent(callData);
}
return macro.VOID;
};

publicAPI.handleLeftButtonRelease = () => {
if (isDragging) {
publicAPI.invokeEndInteractionEvent();
publicAPI.getViewWidgets().forEach((viewWidget) => {
viewWidget.getInteractor().cancelAnimation(publicAPI);
});
}
isDragging = false;
model.widgetState.deactivate();
};

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

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

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

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

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

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

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

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

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

publicAPI.translateCenter = (calldata) => {
const worldCoords = model.planeManipulator.handleEvent(
calldata,
model.openGLRenderWindow
);

model.activeState.setCenter(worldCoords);
updateState(model.widgetState);
};

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

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

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

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

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

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

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

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

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

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

model.planeManipulator = vtkPlaneManipulator.newInstance();
}
helpers.js
import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox';
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';

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

// Project point (inPoint) to the bounds of the image according to a plane
// defined by two vectors (v1, v2)
export function boundPoint(inPoint, v1, v2, bounds) {
const absT1 = v1.map((val) => Math.abs(val));
const absT2 = v2.map((val) => Math.abs(val));
const epsilon = 0.00001;

let o1 = 0.0;
let o2 = 0.0;

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

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

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

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

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

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

return outPoint;
}

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

export function getViewPlaneNameFromViewType(viewType) {
switch (viewType) {
case ViewTypes.SAGITTAL:
return 'X';
case ViewTypes.CORONAL:
return 'Y';
case ViewTypes.AXIAL:
return 'Z';
default:
return '';
}
}

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

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

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

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

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

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

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

updateLine(
widgetState.getAxisXinZ(),
center,
newYAxis,
pdLength,
yRotationLength
);
updateLine(
widgetState.getAxisZinX(),
center,
newYAxis,
pdLength,
yRotationLength
);
}
index.js
import macro from 'vtk.js/Sources/macro';
import vtkAbstractWidgetFactory from 'vtk.js/Sources/Widgets/Core/AbstractWidgetFactory';
import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox';
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 {
boundPoint,
updateState,
getViewPlaneNameFromViewType,
} from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/helpers';
import { ViewTypes } from 'vtk.js/Sources/Widgets/Core/WidgetManager/Constants';

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

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

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

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

model.methodsToLink = ['activeColor', 'useActiveColor', 'opacity'];

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

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
}
// Add a small offset to force the recomputation of the 3 plane points (origin
// point1 and point2) after setting the normal in function updateReslicePlane
// Else, the three points won't be in adequation with the set normal
if (offset[0] === 0 && offset[1] === 0 && offset[2] === 0) {
offset[0] += 0.0000001;
offset[1] += 0.0000001;
offset[2] += 0.0000001;
}

// 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.CORONAL) {
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.AXIAL) {
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.SAGITTAL) {
planeSource.setOrigin(
center[0],
bounds[2] + offset[1],
bounds[4] + offset[2]
);
planeSource.setPoint1(
center[0],
bounds[3] - offset[1],
bounds[4] + offset[2]
);
planeSource.setPoint2(
center[0],
bounds[2] + offset[1],
bounds[5] - offset[2]
);
}
return planeSource;
}

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

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

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

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

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

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

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

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

// Renderer may not have yet actor bounds
let rendererBounds = renderer.computeVisiblePropBounds();
const bounds = model.widgetState.getImage().getBounds();
const bboxObj = vtkBoundingBox.newInstance({ bounds });
bboxObj.addBounds(rendererBounds);
rendererBounds = bboxObj.getBounds();

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

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

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

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

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

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

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

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

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

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

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

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

renderer.getActiveCamera().setFocalPoint(...estimatedFocalPoint);
renderer.getActiveCamera().setPosition(...estimatedCameraPosition);

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

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

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

const planeSource = computeReslicePlaneOrigin(viewType);
planeSource.setNormal(...plane.getNormal());
planeSource.setCenter(...plane.getOrigin());

let o = planeSource.getOrigin();

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

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

// Clip to bounds
const boundedOrigin = boundPoint(
planeSource.getOrigin(),
planeAxis1,
planeAxis2,
model.widgetState.getImage().getBounds()
);

const boundedP1 = boundPoint(
planeSource.getPoint1(),
planeAxis1,
planeAxis2,
model.widgetState.getImage().getBounds()
);

const boundedP2 = boundPoint(
planeSource.getPoint2(),
planeAxis1,
planeAxis2,
model.widgetState.getImage().getBounds()
);

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

o = planeSource.getOrigin();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

return modified;
};

publicAPI.getPlaneSourceFromViewType = (type) => {
const planeSource = vtkPlaneSource.newInstance();
const widgetState = publicAPI.getWidgetState();
const origin = widgetState.getCenter();
planeSource.setOrigin(origin);
let normal = [];
switch (type) {
case ViewTypes.AXIAL: {
normal = widgetState.getZPlaneNormal();
break;
}
case ViewTypes.CORONAL: {
normal = widgetState.getYPlaneNormal();
break;
}
case ViewTypes.SAGITTAL: {
normal = widgetState.getXPlaneNormal();
break;
}
default:
break;
}

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

return planeSource;
};
}

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

const DEFAULT_VALUES = {};

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

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

vtkAbstractWidgetFactory.extend(publicAPI, model, initialValues);

vtkResliceCursorWidget(publicAPI, model);
}

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

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

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

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

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

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

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

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

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