@kitware/vtk.js
Version:
Visualization Toolkit for the Web
368 lines (351 loc) • 16 kB
JavaScript
import { m as macro } from '../../../macros2.js';
import vtkBoundingBox from '../../../Common/DataModel/BoundingBox.js';
import vtkLine from '../../../Common/DataModel/Line.js';
import { k as add, l as normalize, s as subtract, d as dot, j as cross, m as multiplyAccumulate, w as multiplyScalar, X as signedAngleBetweenVectors } from '../../../Common/Core/Math/index.js';
import { getLineNames, getOtherLineName, updateState, boundPointOnPlane, getLinePlaneName, getLineInPlaneName, rotateVector } from './helpers.js';
import { InteractionMethodsName, ScrollingMethods, planeNameToViewType } from './Constants.js';
function widgetBehavior(publicAPI, model) {
model._isDragging = false;
let isScrolling = false;
let previousPosition;
macro.setGet(publicAPI, model, ['keepOrthogonality', {
type: 'object',
name: 'cursorStyles'
}]);
// Set default value for cursorStyles
publicAPI.setCursorStyles({
[InteractionMethodsName.TranslateCenter]: 'move',
[InteractionMethodsName.RotateLine]: 'alias',
[InteractionMethodsName.TranslateAxis]: 'pointer',
default: 'default'
});
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 = () => {
const cursorStyles = publicAPI.getCursorStyles();
if (cursorStyles) {
switch (publicAPI.getActiveInteraction()) {
case InteractionMethodsName.TranslateCenter:
model._apiSpecificRenderWindow.setCursor(cursorStyles.translateCenter);
break;
case InteractionMethodsName.RotateLine:
model._apiSpecificRenderWindow.setCursor(cursorStyles.rotateLine);
break;
case InteractionMethodsName.TranslateAxis:
model._apiSpecificRenderWindow.setCursor(cursorStyles.translateAxis);
break;
default:
model._apiSpecificRenderWindow.setCursor(cursorStyles.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.invokeInteractionEvent(publicAPI.getActiveInteraction());
}
}
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.invokeInteractionEvent(
// Force interaction mode because mouse cursor could be above rotation handle
InteractionMethodsName.TranslateCenter);
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()) {
const methodName = publicAPI.getActiveInteraction();
publicAPI[methodName](callData);
publicAPI.invokeInteractionEvent(methodName);
return macro.EVENT_ABORT;
}
return macro.VOID;
};
publicAPI.startInteraction = () => {
publicAPI.invokeStartInteractionEvent();
// When interacting, plane actor and lines must be re-rendered on other views
publicAPI.getViewWidgets().forEach(viewWidget => {
viewWidget.getInteractor().requestAnimation(publicAPI);
});
};
publicAPI.endInteraction = () => {
publicAPI.invokeEndInteractionEvent();
publicAPI.getViewWidgets().forEach(viewWidget => {
viewWidget.getInteractor().cancelAnimation(publicAPI);
});
};
publicAPI.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 = add(lineHandle.getOrigin(), lineHandle.getDirection(), []);
const currentLineVector = lineHandle.getDirection();
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 = subtract(worldCoords, previousPosition, []);
add(center, translation, newOrigin);
} else if (otherLineHandle) {
const otherLineVector = otherLineHandle.getDirection();
normalize(otherLineVector);
const axisTranslation = otherLineVector;
const dot$1 = dot(currentLineVector, otherLineVector);
// lines are colinear, translate along perpendicular axis from current line
if (dot$1 === 1 || dot$1 === -1) {
cross(currentLineVector, manipulator.getWidgetNormal(), axisTranslation);
}
const closestPoint = [];
worldCoords = manipulator.handleEvent(calldata, model._apiSpecificRenderWindow).worldCoords;
vtkLine.distanceToLine(worldCoords, lineHandle.getOrigin(), pointOnLine, closestPoint);
const translationVector = subtract(worldCoords, closestPoint, []);
const translationDistance = dot(translationVector, axisTranslation);
newOrigin = 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 = subtract(worldCoords, previousPosition, []);
previousPosition = worldCoords;
let newCenter = 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);
if (!worldCoords || !worldCoords.length) {
return;
}
const center = model.widgetState.getCenter();
const currentVectorToOrigin = [0, 0, 0];
subtract(worldCoords, center, currentVectorToOrigin);
normalize(currentVectorToOrigin);
const previousLineDirection = activeLineHandle.getDirection();
normalize(previousLineDirection);
const activePointName = publicAPI.getActiveRotationPointName();
if (activePointName === 'point1' || !activePointName && dot(currentVectorToOrigin, previousLineDirection) < 0) {
multiplyScalar(previousLineDirection, -1);
}
const radianAngle = 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
};
};
/**
* 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.setViewPlane = (viewType, normal, viewUp) => {
let newViewUp = viewUp;
if (newViewUp == null) {
newViewUp = model.widgetState.getPlanes()[viewType].viewUp;
}
model.widgetState.getPlanes()[viewType] = {
normal,
viewUp: newViewUp
};
updateState(model.widgetState, model._factory.getScaleInPixels(), model._factory.getRotationHandlePosition());
};
// --------------------------------------------------------------------------
// initialization
// --------------------------------------------------------------------------
}
export { widgetBehavior as default };