@acransac/vtk.js
Version:
Visualization Toolkit for the Web
343 lines (298 loc) • 11.2 kB
JavaScript
import Constants from 'vtk.js/Sources/Widgets/Widgets3D/LineWidget/Constants';
import macro from 'vtk.js/Sources/macro';
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math/';
import {
calculateTextPosition,
updateTextPosition,
getNumberOfPlacedHandles,
isHandlePlaced,
getPoint,
} from 'vtk.js/Sources/Widgets/Widgets3D/LineWidget/helpers';
const { ShapeType } = Constants;
// Total number of points to place
const MAX_POINTS = 2;
const handleGetters = ['getHandle1', 'getHandle2', 'getMoveHandle'];
export default function widgetBehavior(publicAPI, model) {
model.classHierarchy.push('vtkLineWidgetProp');
/**
* Returns the handle at the handleIndex'th index.
* @param {number} handleIndex 0, 1 or 2
*/
publicAPI.getHandle = (handleIndex) =>
model.widgetState[handleGetters[handleIndex]]();
publicAPI.isPlaced = () =>
getNumberOfPlacedHandles(model.widgetState) === MAX_POINTS;
// --------------------------------------------------------------------------
// Interactor event
// --------------------------------------------------------------------------
function ignoreKey(e) {
return e.altKey || e.controlKey || e.shiftKey;
}
function updateCursor() {
model.isDragging = true;
model.openGLRenderWindow.setCursor('grabbing');
model.interactor.requestAnimation(publicAPI);
}
// --------------------------------------------------------------------------
// Text methods
// --------------------------------------------------------------------------
/**
* check for handle 2 position in comparison to handle 1 position
* and sets text offset to not overlap on the line representation
*/
function getOffsetDirectionForTextPosition() {
const pos1 = publicAPI.getHandle(0).getOrigin();
const pos2 = publicAPI.getHandle(1).getOrigin();
let dySign = 1;
if (pos1[0] <= pos2[0]) {
dySign = pos1[1] <= pos2[1] ? 1 : -1;
} else {
dySign = pos1[1] <= pos2[1] ? -1 : 1;
}
return dySign;
}
/**
* place SVGText on line according to both handle positions
* which purpose is to never have text representation overlapping
* on PolyLine representation
* */
publicAPI.placeText = () => {
const dySign = getOffsetDirectionForTextPosition();
const textPropsCp = { ...model.representations[3].getTextProps() };
textPropsCp.dy = dySign * Math.abs(textPropsCp.dy);
model.representations[3].setTextProps(textPropsCp);
model.interactor.render();
};
publicAPI.setText = (text) => {
model.widgetState.getText().setText(text);
model.interactor.render();
};
// --------------------------------------------------------------------------
// Handle positioning methods
// --------------------------------------------------------------------------
// Handle utilities ---------------------------------------------------------
function getLineDirection(p1, p2) {
const dir = vtkMath.subtract(p1, p2, []);
vtkMath.normalize(dir);
return dir;
}
// Handle orientation & rotation ---------------------------------------------------------
function computeMousePosition(p1, callData) {
const displayMousePos = publicAPI.computeWorldToDisplay(
model.renderer,
...p1
);
const worldMousePos = publicAPI.computeDisplayToWorld(
model.renderer,
callData.position.x,
callData.position.y,
displayMousePos[2]
);
return worldMousePos;
}
/**
* Returns the handle orientation to match the direction vector of the polyLine from one tip to another
* @param {number} handleIndex 0 for handle1, 1 for handle2
* @param {object} callData if specified, uses mouse position as 2nd point
*/
function getHandleOrientation(handleIndex, callData = null) {
const point1 = getPoint(handleIndex, model.widgetState);
const point2 = callData
? computeMousePosition(point1, callData)
: getPoint(1 - handleIndex, model.widgetState);
return getLineDirection(point1, point2);
}
/**
* Orient handle
* @param {number} handleIndex 0, 1 or 2
* @param {object} callData optional, see getHandleOrientation for details.
*/
function updateHandleOrientation(handleIndex, callData = null) {
const orientation = getHandleOrientation(Math.min(1, handleIndex));
model.representations[handleIndex].setOrientation(orientation);
}
publicAPI.updateHandleOrientations = () => {
updateHandleOrientation(0);
updateHandleOrientation(1);
updateHandleOrientation(2);
};
publicAPI.rotateHandlesToFaceCamera = () => {
model.representations[0].setViewMatrix(
Array.from(model.camera.getViewMatrix())
);
model.representations[1].setViewMatrix(
Array.from(model.camera.getViewMatrix())
);
};
// Handles visibility ---------------------------------------------------------
publicAPI.setMoveHandleVisibility = (visibility) => {
model.representations[2].setVisibilityFlagArray([visibility, visibility]);
model.widgetState.getMoveHandle().setVisible(visibility);
model.representations[2].updateActorVisibility();
};
/**
* Set actor visibility to true unless it is a NONE handle
* and uses state visibility variable for the displayActor visibility to
* allow pickable handles even when they are not displayed on screen
* @param handle : the handle state object
* @param handleNb : the handle number according to its label in widget state
*/
publicAPI.updateHandleVisibility = (handleIndex) => {
const handle = publicAPI.getHandle(handleIndex);
const visibility =
handle.getVisible() && isHandlePlaced(handleIndex, model.widgetState);
model.representations[handleIndex].setVisibilityFlagArray([
visibility,
visibility && handle.getShape() !== ShapeType.NONE,
]);
model.representations[handleIndex].updateActorVisibility();
model.interactor.render();
};
/**
* Called when placing a point from the first time.
* @param {number} handleIndex
*/
publicAPI.placeHandle = (handleIndex) => {
const handle = publicAPI.getHandle(handleIndex);
handle.setOrigin(...model.widgetState.getMoveHandle().getOrigin());
publicAPI.updateHandleOrientations();
publicAPI.rotateHandlesToFaceCamera();
model.widgetState.getText().setOrigin(calculateTextPosition(model));
publicAPI.updateHandleVisibility(handleIndex);
if (handleIndex === 0) {
// For the line (handle1, handle2, moveHandle) to be displayed
// correctly, handle2 origin must be valid.
publicAPI
.getHandle(1)
.setOrigin(...model.widgetState.getMoveHandle().getOrigin());
// Now that handle2 has a valid origin, hide it
publicAPI.updateHandleVisibility(1);
model.widgetState
.getMoveHandle()
.setShape(publicAPI.getHandle(1).getShape());
}
if (handleIndex === 1) {
publicAPI.placeText();
publicAPI.setMoveHandleVisibility(false);
}
};
// --------------------------------------------------------------------------
// Left press: Select handle to drag
// --------------------------------------------------------------------------
publicAPI.handleLeftButtonPress = (e) => {
if (
!model.activeState ||
!model.activeState.getActive() ||
!model.pickable ||
ignoreKey(e)
) {
return macro.VOID;
}
if (
model.activeState === model.widgetState.getMoveHandle() &&
getNumberOfPlacedHandles(model.widgetState) === 0
) {
publicAPI.placeHandle(0);
} else if (
model.widgetState.getMoveHandle().getActive() &&
getNumberOfPlacedHandles(model.widgetState) === 1
) {
publicAPI.placeHandle(1);
} else {
updateCursor();
}
publicAPI.invokeStartInteractionEvent();
return macro.EVENT_ABORT;
};
// --------------------------------------------------------------------------
// Mouse move: Drag selected handle / Handle follow the mouse
// --------------------------------------------------------------------------
publicAPI.handleMouseMove = (callData) => {
if (model.hasFocus && publicAPI.isPlaced() && !model.isDragging) {
publicAPI.loseFocus();
return macro.VOID;
}
if (
model.pickable &&
model.manipulator &&
model.activeState &&
model.activeState.getActive() &&
!ignoreKey(callData)
) {
const worldCoords = model.manipulator.handleEvent(
callData,
model.openGLRenderWindow
);
if (
// is placing first or second handle
model.activeState === model.widgetState.getMoveHandle() ||
// is dragging already placed first or second handle
model.isDragging
) {
model.activeState.setOrigin(worldCoords);
publicAPI.updateHandleOrientations();
updateTextPosition(model);
publicAPI.invokeInteractionEvent();
return macro.EVENT_ABORT;
}
}
return macro.VOID;
};
// --------------------------------------------------------------------------
// Left release: Finish drag / Create new handle
// --------------------------------------------------------------------------
publicAPI.handleLeftButtonRelease = () => {
// After dragging a point or placing all points
if (
model.activeState &&
model.activeState.getActive() &&
(model.isDragging || publicAPI.isPlaced())
) {
// Recompute offsets
publicAPI.placeText();
model.widgetState.deactivate();
model.widgetState.getMoveHandle().deactivate();
model.activeState = null;
model.interactor.cancelAnimation(publicAPI);
model.openGLRenderWindow.setCursor('pointer');
model.hasFocus = false;
publicAPI.invokeEndInteractionEvent();
model.widgetManager.enablePicking();
model.interactor.render();
}
if (
model.isDragging === false &&
(!model.activeState || !model.activeState.getActive())
) {
publicAPI.rotateHandlesToFaceCamera();
}
model.isDragging = false;
};
// --------------------------------------------------------------------------
// Focus API - moveHandle follow mouse when widget has focus
// --------------------------------------------------------------------------
publicAPI.grabFocus = () => {
if (!model.hasFocus && !publicAPI.isPlaced()) {
model.activeState = model.widgetState.getMoveHandle();
model.activeState.setShape(publicAPI.getHandle(0).getShape());
publicAPI.setMoveHandleVisibility(true);
model.activeState.activate();
model.interactor.requestAnimation(publicAPI);
publicAPI.invokeStartInteractionEvent();
}
model.hasFocus = true;
};
// --------------------------------------------------------------------------
publicAPI.loseFocus = () => {
if (model.hasFocus) {
model.interactor.cancelAnimation(publicAPI);
publicAPI.invokeEndInteractionEvent();
}
model.widgetState.deactivate();
model.widgetState.getMoveHandle().deactivate();
model.activeState = null;
model.hasFocus = false;
model.widgetManager.enablePicking();
model.interactor.render();
};
}