@kitware/vtk.js
Version:
Visualization Toolkit for the Web
482 lines (453 loc) • 20.4 kB
JavaScript
import { m as macro } from '../../../macros2.js';
import { f as vtkMath } from '../../../Common/Core/Math/index.js';
import vtkBoundingBox from '../../../Common/DataModel/BoundingBox.js';
import vtkPlane from '../../../Common/DataModel/Plane.js';
import { ShapeBehavior, BehaviorCategory, TextPosition } from './Constants.js';
import { boundPlane } from '../ResliceCursorWidget/helpers.js';
import { vec3 } from 'gl-matrix';
const {
vtkErrorMacro
} = macro;
const EPSILON = 1e-6;
function widgetBehavior(publicAPI, model) {
model.classHierarchy.push('vtkShapeWidgetProp');
model._isDragging = false;
model.keysDown = {};
const superClass = {
...publicAPI
};
// --------------------------------------------------------------------------
// Display 2D
// --------------------------------------------------------------------------
publicAPI.setDisplayCallback = callback => model.representations[0].setDisplayCallback(callback);
publicAPI.setText = text => {
model.widgetState.getText().setText(text);
// Recompute position
model._interactor.render();
};
// --------------------------------------------------------------------------
// Public methods
// --------------------------------------------------------------------------
publicAPI.setResetAfterPointPlacement = model._factory.setResetAfterPointPlacement;
publicAPI.getResetAfterPointPlacement = model._factory.getResetAfterPointPlacement;
publicAPI.setModifierBehavior = model._factory.setModifierBehavior;
publicAPI.getModifierBehavior = model._factory.getModifierBehavior;
publicAPI.isBehaviorActive = (category, flag) => Object.keys(model.keysDown).some(key => model.keysDown[key] && publicAPI.getModifierBehavior()[key] && publicAPI.getModifierBehavior()[key][category] === flag);
publicAPI.isOppositeBehaviorActive = (category, flag) => Object.values(ShapeBehavior[category]).some(flagToTry => flag !== flagToTry && publicAPI.isBehaviorActive(category, flagToTry));
publicAPI.getActiveBehaviorFromCategory = category => Object.values(ShapeBehavior[category]).find(flag => publicAPI.isBehaviorActive(category, flag) || !publicAPI.isOppositeBehaviorActive(category, flag) && publicAPI.getModifierBehavior().None[category] === flag);
publicAPI.isRatioFixed = () => publicAPI.getActiveBehaviorFromCategory(BehaviorCategory.RATIO) === ShapeBehavior[BehaviorCategory.RATIO].FIXED;
publicAPI.isDraggingEnabled = () => {
const behavior = publicAPI.getActiveBehaviorFromCategory(BehaviorCategory.PLACEMENT);
return behavior === ShapeBehavior[BehaviorCategory.PLACEMENT].DRAG || behavior === ShapeBehavior[BehaviorCategory.PLACEMENT].CLICK_AND_DRAG;
};
publicAPI.isDraggingForced = () => publicAPI.isBehaviorActive(BehaviorCategory.PLACEMENT, ShapeBehavior[BehaviorCategory.PLACEMENT].DRAG) || publicAPI.getModifierBehavior().None[BehaviorCategory.PLACEMENT] === ShapeBehavior[BehaviorCategory.PLACEMENT].DRAG;
publicAPI.getPoint1 = () => model.point1;
publicAPI.getPoint2 = () => model.point2;
publicAPI.setPoints = (point1, point2) => {
model.point1 = point1;
model.point2 = point2;
model.point1Handle.setOrigin(model.point1);
model.point2Handle.setOrigin(model.point2);
publicAPI.updateShapeBounds();
};
// This method is to be called to place the first point
// for the first time. It is not inlined so that
// the user can specify himself where the first point
// is right after publicAPI.grabFocus() without waiting
// for interactions.
publicAPI.placePoint1 = point => {
if (model.hasFocus) {
publicAPI.setPoints(point, point);
model.point1Handle.deactivate();
model.point2Handle.activate();
model.activeState = model.point2Handle;
model.point2Handle.setVisible(true);
model.widgetState.getText().setVisible(true);
publicAPI.updateShapeBounds();
model.shapeHandle.setVisible(true);
}
};
publicAPI.placePoint2 = point2 => {
if (model.hasFocus) {
model.point2 = point2;
model.point2Handle.setOrigin(model.point2);
publicAPI.updateShapeBounds();
}
};
// --------------------------------------------------------------------------
// Private methods
// --------------------------------------------------------------------------
publicAPI.makeSquareFromPoints = (point1, point2) => {
const diagonal = [0, 0, 0];
vec3.subtract(diagonal, point2, point1);
const dir = model.shapeHandle.getDirection();
const right = model.shapeHandle.getRight();
const up = model.shapeHandle.getUp();
const dirComponent = vec3.dot(diagonal, dir);
let rightComponent = vec3.dot(diagonal, right);
let upComponent = vec3.dot(diagonal, up);
const absRightComponent = Math.abs(rightComponent);
const absUpComponent = Math.abs(upComponent);
if (absRightComponent < EPSILON) {
rightComponent = upComponent;
} else if (absUpComponent < EPSILON) {
upComponent = rightComponent;
} else if (absRightComponent > absUpComponent) {
upComponent = upComponent / absUpComponent * absRightComponent;
} else {
rightComponent = rightComponent / absRightComponent * absUpComponent;
}
return [point1[0] + rightComponent * right[0] + upComponent * up[0] + dirComponent * dir[0], point1[1] + rightComponent * right[1] + upComponent * up[1] + dirComponent * dir[1], point1[2] + rightComponent * right[2] + upComponent * up[2] + dirComponent * dir[2]];
};
const getCornersFromRadius = (center, pointOnCircle) => {
const radius = vec3.distance(center, pointOnCircle);
const up = model.shapeHandle.getUp();
const right = model.shapeHandle.getRight();
const point1 = [center[0] + (up[0] - right[0]) * radius, center[1] + (up[1] - right[1]) * radius, center[2] + (up[2] - right[2]) * radius];
const point2 = [center[0] + (right[0] - up[0]) * radius, center[1] + (right[1] - up[1]) * radius, center[2] + (right[2] - up[2]) * radius];
return {
point1,
point2
};
};
const getCornersFromDiameter = (point1, point2) => {
const center = [0.5 * (point1[0] + point2[0]), 0.5 * (point1[1] + point2[1]), 0.5 * (point1[2] + point2[2])];
return getCornersFromRadius(center, point1);
};
// TODO: move to ShapeWidget/index.js
publicAPI.getBounds = () => model.point1 && model.point2 ? vtkBoundingBox.addPoints(vtkBoundingBox.reset([]), [model.point1, model.point2]) : vtkMath.uninitializeBounds([]);
// To be reimplemented by subclass
publicAPI.setCorners = (point1, point2) => {
publicAPI.updateTextPosition(point1, point2);
};
publicAPI.updateShapeBounds = () => {
if (model.point1 && model.point2) {
const point1 = [...model.point1];
let point2 = [...model.point2];
if (publicAPI.isRatioFixed()) {
point2 = publicAPI.makeSquareFromPoints(point1, point2);
}
switch (publicAPI.getActiveBehaviorFromCategory(BehaviorCategory.POINTS)) {
case ShapeBehavior[BehaviorCategory.POINTS].CORNER_TO_CORNER:
{
publicAPI.setCorners(point1, point2);
break;
}
case ShapeBehavior[BehaviorCategory.POINTS].CENTER_TO_CORNER:
{
const diagonal = [0, 0, 0];
vec3.subtract(diagonal, point1, point2);
vec3.add(point1, point1, diagonal);
publicAPI.setCorners(point1, point2);
break;
}
case ShapeBehavior[BehaviorCategory.POINTS].RADIUS:
{
const points = getCornersFromRadius(point1, point2);
publicAPI.setCorners(points.point1, points.point2);
break;
}
case ShapeBehavior[BehaviorCategory.POINTS].DIAMETER:
{
const points = getCornersFromDiameter(point1, point2);
publicAPI.setCorners(points.point1, points.point2);
break;
}
default:
// This should never be executed
vtkErrorMacro('vtk internal error');
}
}
};
const computePositionVector = (textPosition, minPoint, maxPoint) => {
const positionVector = [0, 0, 0];
switch (textPosition) {
case TextPosition.MIN:
break;
case TextPosition.MAX:
vtkMath.subtract(maxPoint, minPoint, positionVector);
break;
case TextPosition.CENTER:
default:
vtkMath.subtract(maxPoint, minPoint, positionVector);
vtkMath.multiplyScalar(positionVector, 0.5);
break;
}
return positionVector;
};
const computeTextPosition = function (worldBounds, textPosition) {
let worldMargin = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
const viewPlaneOrigin = vtkBoundingBox.getCenter(worldBounds);
const viewPlaneNormal = model._renderer.getActiveCamera().getDirectionOfProjection();
const viewUp = model._renderer.getActiveCamera().getViewUp();
const positionMargin = Array.isArray(worldMargin) ? [...worldMargin] : [worldMargin, worldMargin, viewPlaneOrigin ? worldMargin : 0];
// Map bounds from world positions to display positions
const minPoint = model._apiSpecificRenderWindow.worldToDisplay(...vtkBoundingBox.getMinPoint(worldBounds), model._renderer);
const maxPoint = model._apiSpecificRenderWindow.worldToDisplay(...vtkBoundingBox.getMaxPoint(worldBounds), model._renderer);
const displayBounds = vtkBoundingBox.addPoints(vtkBoundingBox.reset([]), [minPoint, maxPoint]);
let planeOrigin = [];
let p1 = [];
let p2 = [];
let p3 = [];
// If we are in a 2D projection
if (viewPlaneOrigin && viewPlaneNormal && viewUp && vtkBoundingBox.intersectPlane(displayBounds, viewPlaneOrigin, viewPlaneNormal)) {
// Map plane origin from world positions to display positions
const displayPlaneOrigin = model._apiSpecificRenderWindow.worldToDisplay(...viewPlaneOrigin, model._renderer);
// Map plane normal from world positions to display positions
const planeNormalPoint = vtkMath.add(viewPlaneOrigin, viewPlaneNormal, []);
const displayPlaneNormalPoint = model._apiSpecificRenderWindow.worldToDisplay(...planeNormalPoint, model._renderer);
const displayPlaneNormal = vtkMath.subtract(displayPlaneNormalPoint, displayPlaneOrigin, []);
// Project view plane into bounding box
const largeDistance = 10 * vtkBoundingBox.getDiagonalLength(displayBounds);
vtkPlane.projectPoint(vtkBoundingBox.getCenter(displayBounds), displayPlaneOrigin, displayPlaneNormal, planeOrigin);
const planeU = vtkMath.cross(viewUp, displayPlaneNormal, []);
vtkMath.normalize(planeU); // u
vtkMath.normalize(viewUp); // v
vtkMath.normalize(displayPlaneNormal); // w
vtkMath.multiplyAccumulate(planeOrigin, viewUp, -largeDistance, planeOrigin);
vtkMath.multiplyAccumulate(planeOrigin, planeU, -largeDistance, planeOrigin);
p1 = vtkMath.multiplyAccumulate(planeOrigin, planeU, 2 * largeDistance, []);
p2 = vtkMath.multiplyAccumulate(planeOrigin, viewUp, 2 * largeDistance, []);
p3 = planeOrigin;
boundPlane(displayBounds, planeOrigin, p1, p2);
} else {
planeOrigin = [displayBounds[0], displayBounds[2], displayBounds[4]];
p1 = [displayBounds[1], displayBounds[2], displayBounds[4]];
p2 = [displayBounds[0], displayBounds[3], displayBounds[4]];
p3 = [displayBounds[0], displayBounds[2], displayBounds[5]];
}
// Compute horizontal, vertical and depth position
const u = computePositionVector(textPosition[0], planeOrigin, p1);
const v = computePositionVector(textPosition[1], planeOrigin, p2);
const w = computePositionVector(textPosition[2], planeOrigin, p3);
const finalPosition = planeOrigin;
vtkMath.add(finalPosition, u, finalPosition);
vtkMath.add(finalPosition, v, finalPosition);
vtkMath.add(finalPosition, w, finalPosition);
vtkMath.add(finalPosition, positionMargin, finalPosition);
return model._apiSpecificRenderWindow.displayToWorld(...finalPosition, model._renderer);
};
publicAPI.updateTextPosition = (point1, point2) => {
const bounds = vtkBoundingBox.addPoints(vtkBoundingBox.reset([]), [point1, point2]);
const screenPosition = computeTextPosition(bounds, model.widgetState.getTextPosition(), model.widgetState.getTextWorldMargin());
const textHandle = model.widgetState.getText();
textHandle.setOrigin(screenPosition);
};
/*
* If the widget has the focus, this method reset the widget
* to it's state just after it grabbed the focus. Otherwise
* it resets the widget to its state before it grabbed the focus.
*/
publicAPI.reset = () => {
model.point1 = null;
model.point2 = null;
model.widgetState.getText().setVisible(false);
model.point1Handle.setOrigin(null);
model.point2Handle.setOrigin(null);
model.shapeHandle.setOrigin(null);
model.shapeHandle.setVisible(false);
model.point2Handle.setVisible(false);
model.point2Handle.deactivate();
if (model.hasFocus) {
model.point1Handle.activate();
model.activeState = model.point1Handle;
} else {
model.point1Handle.setVisible(false);
model.point1Handle.deactivate();
model.activeState = null;
}
publicAPI.updateShapeBounds();
};
// --------------------------------------------------------------------------
// Interactor events
// --------------------------------------------------------------------------
publicAPI.handleMouseMove = callData => {
const manipulator = model.activeState?.getManipulator?.() ?? model.manipulator;
if (!manipulator || !model.activeState || !model.activeState.getActive() || !model.pickable || !model.dragable) {
return macro.VOID;
}
if (!model.point2) {
// Update orientation to match the camera's plane
// if the corners are not yet placed
const normal = model._camera.getDirectionOfProjection();
const up = model._camera.getViewUp();
const right = [];
vec3.cross(right, up, normal);
vtkMath.normalize(right);
vec3.cross(up, normal, right);
vtkMath.normalize(up);
model.shapeHandle.setUp(up);
model.shapeHandle.setRight(right);
model.shapeHandle.setDirection(normal);
}
const {
worldCoords,
worldDelta
} = manipulator.handleEvent(callData, model._apiSpecificRenderWindow);
if (!worldCoords.length) {
return macro.VOID;
}
if (model.hasFocus) {
if (!model.point1) {
model.point1Handle.setOrigin(worldCoords);
} else {
model.point2Handle.setOrigin(worldCoords);
model.point2 = worldCoords;
publicAPI.updateShapeBounds();
publicAPI.invokeInteractionEvent();
}
} else if (model._isDragging) {
if (model.activeState === model.point1Handle) {
vtkMath.add(model.point1Handle.getOrigin(), worldDelta, model.point1);
model.point1Handle.setOrigin(model.point1);
} else {
vtkMath.add(model.point2Handle.getOrigin(), worldDelta, model.point2);
model.point2Handle.setOrigin(model.point2);
}
publicAPI.updateShapeBounds();
publicAPI.invokeInteractionEvent();
}
return model.hasFocus || model._isDragging ? macro.EVENT_ABORT : macro.VOID;
};
// --------------------------------------------------------------------------
// Left click: Add point / End interaction
// --------------------------------------------------------------------------
publicAPI.handleLeftButtonPress = e => {
const manipulator = model.activeState?.getManipulator?.() ?? model.manipulator;
if (!model.activeState || !model.activeState.getActive() || !model.pickable || !manipulator) {
return macro.VOID;
}
const {
worldCoords
} = manipulator.handleEvent(e, model._apiSpecificRenderWindow);
if (model.hasFocus) {
if (!model.point1) {
model.point1Handle.setOrigin(worldCoords);
publicAPI.placePoint1(model.point1Handle.getOrigin());
publicAPI.invokeStartInteractionEvent();
} else {
model.point2Handle.setOrigin(worldCoords);
publicAPI.placePoint2(model.point2Handle.getOrigin());
publicAPI.invokeInteractionEvent();
publicAPI.invokeEndInteractionEvent();
if (publicAPI.getResetAfterPointPlacement()) {
publicAPI.reset();
} else {
publicAPI.loseFocus();
}
}
return macro.EVENT_ABORT;
}
if (model.point1 && (model.activeState === model.point1Handle || model.activeState === model.point2Handle) && model.dragable) {
model._isDragging = true;
model._apiSpecificRenderWindow.setCursor('grabbing');
model._interactor.requestAnimation(publicAPI);
}
publicAPI.invokeStartInteractionEvent();
return macro.EVENT_ABORT;
};
// --------------------------------------------------------------------------
// Left release: Maybe end interaction
// --------------------------------------------------------------------------
publicAPI.handleLeftButtonRelease = e => {
if (model._isDragging) {
model._isDragging = false;
model._apiSpecificRenderWindow.setCursor('pointer');
model.widgetState.deactivate();
model._interactor.cancelAnimation(publicAPI);
publicAPI.invokeEndInteractionEvent();
return macro.EVENT_ABORT;
}
if (!model.hasFocus || !model.pickable) {
return macro.VOID;
}
const viewSize = model._apiSpecificRenderWindow.getSize();
if (e.position.x < 0 || e.position.x > viewSize[0] - 1 || e.position.y < 0 || e.position.y > viewSize[1] - 1) {
return macro.VOID;
}
if (model.point1) {
publicAPI.placePoint2(model.point2Handle.getOrigin());
if (publicAPI.isDraggingEnabled()) {
const distance = vec3.squaredDistance(model.point1, model.point2);
const maxDistance = 100;
if (distance > maxDistance || publicAPI.isDraggingForced()) {
publicAPI.invokeInteractionEvent();
publicAPI.invokeEndInteractionEvent();
if (publicAPI.getResetAfterPointPlacement()) {
publicAPI.reset();
} else {
publicAPI.loseFocus();
}
}
}
}
return macro.EVENT_ABORT;
};
// --------------------------------------------------------------------------
// Register key presses/releases
// --------------------------------------------------------------------------
publicAPI.handleKeyDown = _ref => {
let {
key
} = _ref;
if (key === 'Escape') {
if (model.hasFocus) {
publicAPI.reset();
publicAPI.loseFocus();
publicAPI.invokeEndInteractionEvent();
}
} else {
model.keysDown[key] = true;
}
if (model.hasFocus) {
if (model.point1) {
model.point2 = model.point2Handle.getOrigin();
publicAPI.updateShapeBounds();
}
}
};
publicAPI.handleKeyUp = _ref2 => {
let {
key
} = _ref2;
model.keysDown[key] = false;
if (model.hasFocus) {
if (model.point1) {
model.point2 = model.point2Handle.getOrigin();
publicAPI.updateShapeBounds();
}
}
};
// --------------------------------------------------------------------------
// Focus API - follow mouse when widget has focus
// --------------------------------------------------------------------------
publicAPI.grabFocus = () => {
if (!model.hasFocus) {
publicAPI.reset();
model.point1Handle.activate();
model.activeState = model.point1Handle;
model.point1Handle.setVisible(true);
model.shapeHandle.setVisible(false);
model._interactor.requestAnimation(publicAPI);
}
superClass.grabFocus();
};
// --------------------------------------------------------------------------
publicAPI.loseFocus = () => {
if (model.hasFocus) {
model._interactor.cancelAnimation(publicAPI);
}
if (!model.point1) {
model.point1Handle.setVisible(false);
model.point2Handle.setVisible(false);
}
model.widgetState.deactivate();
model.point1Handle.deactivate();
model.point2Handle.deactivate();
model.activeState = null;
model._interactor.render();
model._widgetManager.enablePicking();
superClass.loseFocus();
};
}
export { widgetBehavior as default };