UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

368 lines (351 loc) 16 kB
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 };