@acransac/vtk.js
Version:
Visualization Toolkit for the Web
305 lines (254 loc) • 9.48 kB
JavaScript
import macro from 'vtk.js/Sources/macro';
import vtkInteractorStyleTrackballCamera from 'vtk.js/Sources/Interaction/Style/InteractorStyleTrackballCamera';
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';
import { States } from 'vtk.js/Sources/Rendering/Core/InteractorStyle/Constants';
// ----------------------------------------------------------------------------
// vtkInteractorStyleImage methods
// ----------------------------------------------------------------------------
function vtkInteractorStyleImage(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkInteractorStyleImage');
// Public API methods
publicAPI.superHandleMouseMove = publicAPI.handleMouseMove;
publicAPI.handleMouseMove = (callData) => {
const pos = callData.position;
const renderer = callData.pokedRenderer;
switch (model.state) {
case States.IS_WINDOW_LEVEL:
publicAPI.windowLevel(renderer, pos);
publicAPI.invokeInteractionEvent({ type: 'InteractionEvent' });
break;
case States.IS_SLICE:
publicAPI.slice(renderer, pos);
publicAPI.invokeInteractionEvent({ type: 'InteractionEvent' });
break;
default:
break;
}
publicAPI.superHandleMouseMove(callData);
};
//----------------------------------------------------------------------------
publicAPI.superHandleLeftButtonPress = publicAPI.handleLeftButtonPress;
publicAPI.handleLeftButtonPress = (callData) => {
const pos = callData.position;
if (!callData.shiftKey && !callData.controlKey) {
model.windowLevelStartPosition[0] = pos.x;
model.windowLevelStartPosition[1] = pos.y;
// Get the last (the topmost) image
publicAPI.setCurrentImageNumber(model.currentImageNumber);
const property = model.currentImageProperty;
if (property) {
model.windowLevelInitial[0] = property.getColorWindow();
model.windowLevelInitial[1] = property.getColorLevel();
}
publicAPI.startWindowLevel();
} else if (model.interactionMode === 'IMAGE3D' && callData.shiftKey) {
// If shift is held down, do a rotation
publicAPI.startRotate();
} else if (
model.interactionMode === 'IMAGE_SLICING' &&
callData.controlKey
) {
// If ctrl is held down in slicing mode, slice the image
model.lastSlicePosition = pos.y;
publicAPI.startSlice();
} else {
// The rest of the button + key combinations remain the same
publicAPI.superHandleLeftButtonPress(callData);
}
};
//--------------------------------------------------------------------------
publicAPI.superHandleLeftButtonRelease = publicAPI.handleLeftButtonRelease;
publicAPI.handleLeftButtonRelease = () => {
switch (model.state) {
case States.IS_WINDOW_LEVEL:
publicAPI.endWindowLevel();
break;
case States.IS_SLICE:
publicAPI.endSlice();
break;
default:
publicAPI.superHandleLeftButtonRelease();
break;
}
};
//--------------------------------------------------------------------------
publicAPI.handleStartMouseWheel = (callData) => {
publicAPI.startSlice();
publicAPI.handleMouseWheel(callData);
};
//--------------------------------------------------------------------------
publicAPI.handleEndMouseWheel = () => {
publicAPI.endSlice();
};
//--------------------------------------------------------------------------
publicAPI.handleMouseWheel = (callData) => {
const camera = callData.pokedRenderer.getActiveCamera();
let distance = camera.getDistance();
distance += callData.spinY;
// clamp the distance to the clipping range
const range = camera.getClippingRange();
if (distance < range[0]) {
distance = range[0];
}
if (distance > range[1]) {
distance = range[1];
}
camera.setDistance(distance);
};
//----------------------------------------------------------------------------
publicAPI.windowLevel = (renderer, position) => {
model.windowLevelCurrentPosition[0] = position.x;
model.windowLevelCurrentPosition[1] = position.y;
const rwi = model.interactor;
if (model.currentImageProperty) {
const size = rwi.getView().getViewportSize(renderer);
const mWindow = model.windowLevelInitial[0];
const level = model.windowLevelInitial[1];
// Compute normalized delta
let dx =
((model.windowLevelCurrentPosition[0] -
model.windowLevelStartPosition[0]) *
4.0) /
size[0];
let dy =
((model.windowLevelStartPosition[1] -
model.windowLevelCurrentPosition[1]) *
4.0) /
size[1];
// Scale by current values
if (Math.abs(mWindow) > 0.01) {
dx *= mWindow;
} else {
dx *= mWindow < 0 ? -0.01 : 0.01;
}
if (Math.abs(level) > 0.01) {
dy *= level;
} else {
dy *= level < 0 ? -0.01 : 0.01;
}
// Abs so that direction does not flip
if (mWindow < 0.0) {
dx *= -1;
}
if (level < 0.0) {
dy *= -1;
}
// Compute new mWindow level
let newWindow = dx + mWindow;
const newLevel = level - dy;
if (newWindow < 0.01) {
newWindow = 0.01;
}
model.currentImageProperty.setColorWindow(newWindow);
model.currentImageProperty.setColorLevel(newLevel);
}
};
//----------------------------------------------------------------------------
publicAPI.slice = (renderer, position) => {
const rwi = model.interactor;
const dy = position.y - model.lastSlicePosition;
const camera = renderer.getActiveCamera();
const range = camera.getClippingRange();
let distance = camera.getDistance();
// scale the interaction by the height of the viewport
let viewportHeight = 0.0;
if (camera.getParallelProjection()) {
viewportHeight = camera.getParallelScale();
} else {
const angle = vtkMath.radiansFromDegrees(camera.getViewAngle());
viewportHeight = 2.0 * distance * Math.tan(0.5 * angle);
}
const size = rwi.getView().getViewportSize(renderer);
const delta = (dy * viewportHeight) / size[1];
distance += delta;
// clamp the distance to the clipping range
if (distance < range[0]) {
distance = range[0] + viewportHeight * 1e-3;
}
if (distance > range[1]) {
distance = range[1] - viewportHeight * 1e-3;
}
camera.setDistance(distance);
model.lastSlicePosition = position.y;
};
//----------------------------------------------------------------------------
// This is a way of dealing with images as if they were layers.
// It looks through the renderer's list of props and sets the
// interactor ivars from the Nth image that it finds. You can
// also use negative numbers, i.e. -1 will return the last image,
// -2 will return the second-to-last image, etc.
publicAPI.setCurrentImageNumber = (i) => {
if (i === null) {
return;
}
const renderer = model.interactor.getCurrentRenderer();
if (!renderer) {
return;
}
model.currentImageNumber = i;
function propMatch(j, prop, targetIndex) {
if (
prop.isA('vtkImageSlice') &&
j === targetIndex &&
prop.getPickable()
) {
return true;
}
return false;
}
const props = renderer.getViewProps();
let targetIndex = i;
if (i < 0) {
targetIndex += props.length;
}
let imageProp = null;
let foundImageProp = false;
for (let j = 0; j < props.length && !foundImageProp; j++) {
if (propMatch(j, props[j], targetIndex)) {
foundImageProp = true;
imageProp = props[j];
}
}
if (imageProp) {
publicAPI.setCurrentImageProperty(imageProp.getProperty());
}
};
//----------------------------------------------------------------------------
publicAPI.setCurrentImageProperty = (imageProperty) => {
model.currentImageProperty = imageProperty;
};
}
// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------
const DEFAULT_VALUES = {
windowLevelStartPosition: [0, 0],
windowLevelCurrentPosition: [0, 0],
lastSlicePosition: 0,
windowLevelInitial: [1.0, 0.5],
currentImageProperty: 0,
currentImageNumber: -1,
interactionMode: 'IMAGE2D',
xViewRightVector: [0, 1, 0],
xViewUpVector: [0, 0, -1],
yViewRightVector: [1, 0, 0],
yViewUpVector: [0, 0, -1],
zViewRightVector: [1, 0, 0],
zViewUpVector: [0, 1, 0],
};
// ----------------------------------------------------------------------------
export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);
// Inheritance
vtkInteractorStyleTrackballCamera.extend(publicAPI, model, initialValues);
// Create get-set macros
macro.setGet(publicAPI, model, ['interactionMode']);
// For more macro methods, see "Sources/macro.js"
// Object specific methods
vtkInteractorStyleImage(publicAPI, model);
}
// ----------------------------------------------------------------------------
export const newInstance = macro.newInstance(extend, 'vtkInteractorStyleImage');
// ----------------------------------------------------------------------------
export default { newInstance, extend };