@acransac/vtk.js
Version:
Visualization Toolkit for the Web
607 lines (509 loc) • 17.7 kB
JavaScript
import macro from 'vtk.js/Sources/macro';
import {
BehaviorCategory,
ShapeBehavior,
} from 'vtk.js/Sources/Widgets/Widgets3D/ShapeWidget/Constants';
import vtkLabelRepresentation from 'vtk.js/Sources/Interaction/Widgets/LabelRepresentation';
import { vec3 } from 'gl-matrix';
const { vtkErrorMacro } = macro;
const EPSILON = 1e-6;
function makeBoundsFromPoints(point1, point2) {
return [
Math.min(point1[0], point2[0]),
Math.max(point1[0], point2[0]),
Math.min(point1[1], point2[1]),
Math.max(point1[1], point2[1]),
Math.min(point1[2], point2[2]),
Math.max(point1[2], point2[2]),
];
}
export default function widgetBehavior(publicAPI, model) {
model.classHierarchy.push('vtkShapeWidgetProp');
const superClass = { ...publicAPI };
// --------------------------------------------------------------------------
// Display 2D
// --------------------------------------------------------------------------
publicAPI.setDisplayCallback = (callback) =>
model.representations[0].setDisplayCallback(callback);
// --------------------------------------------------------------------------
// Public methods
// --------------------------------------------------------------------------
publicAPI.setModifierBehavior = (behavior) => {
Object.assign(model.modifierBehavior, behavior);
};
publicAPI.isBehaviorActive = (category, flag) =>
Object.keys(model.keysDown).some(
(key) =>
model.keysDown[key] &&
model.modifierBehavior[key] &&
model.modifierBehavior[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) &&
model.modifierBehavior.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
) ||
model.modifierBehavior.None[BehaviorCategory.PLACEMENT] ===
ShapeBehavior[BehaviorCategory.PLACEMENT].DRAG;
publicAPI.setVisibleOnFocus = (visibleOnFocus) => {
model.visibleOnFocus = visibleOnFocus;
};
publicAPI.setLabelTextCallback = (callback) => {
model.labelTextCallback = callback;
};
publicAPI.setResetAfterPointPlacement = (reset) => {
model.resetAfterPointPlacement = reset;
};
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;
if (model.useHandles) {
model.point2Handle.setVisible(true);
}
publicAPI.updateShapeBounds();
if (model.visibleOnFocus) {
model.shapeHandle.setVisible(true);
}
}
};
publicAPI.placePoint2 = (point2) => {
if (model.hasFocus) {
model.point2 = point2;
model.point2Handle.setOrigin(model.point2);
publicAPI.updateShapeBounds();
if (model.resetAfterPointPlacement) {
publicAPI.reset();
} else {
publicAPI.loseFocus();
}
}
};
publicAPI.setPixelScale = (pixelScale) => {
model.pixelScale = pixelScale;
publicAPI.updateHandlesSize();
};
publicAPI.updateHandlesSize = () => {
if (model.pixelScale !== null) {
const scale =
model.pixelScale *
vec3.distance(
model.openGLRenderWindow.displayToWorld(0, 0, 0, model.renderer),
model.openGLRenderWindow.displayToWorld(1, 0, 0, model.renderer)
);
model.point1Handle.setScale1(scale);
model.point2Handle.setScale1(scale);
}
};
publicAPI.setVisibility = (visibility) => {
if (model.useHandles) {
superClass.setVisibility(visibility);
} else {
model.shapeHandle.setVisible(visibility);
}
if (model.label) {
if (visibility) {
model.label.setContainer(model.interactor.getContainer());
} else {
model.label.setContainer(null);
}
}
publicAPI.updateRepresentationForRender();
model.interactor.render();
};
publicAPI.getLabel = () => model.label;
// --------------------------------------------------------------------------
// 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);
};
publicAPI.setCorners = (point1, point2) => {
if (model.label && model.labelTextCallback) {
const bounds = makeBoundsFromPoints(point1, point2);
const screenPoint1 = model.openGLRenderWindow.worldToDisplay(
...point1,
model.renderer
);
const screenPoint2 = model.openGLRenderWindow.worldToDisplay(
...point2,
model.renderer
);
const screenBounds = [
screenPoint1[0],
screenPoint2[0],
screenPoint1[1],
screenPoint2[1],
screenPoint1[2],
screenPoint2[2],
];
model.labelTextCallback(bounds, screenBounds, model.label);
}
};
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');
}
}
};
/*
* 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 = () => {
if (!model.hasFocus) {
model.point1Handle.setVisible(false);
}
model.shapeHandle.setVisible(false);
model.point1 = null;
model.point2 = null;
if (model.label) {
model.label.setLabelText('');
}
model.point1Handle.setOrigin(model.point2Handle.getOrigin());
model.point2Handle.setVisible(false);
model.point2Handle.deactivate();
if (model.hasFocus) {
model.point1Handle.activate();
model.activeState = model.point1Handle;
} else {
model.point1Handle.deactivate();
model.activeState = null;
}
publicAPI.updateShapeBounds();
};
// --------------------------------------------------------------------------
// Interactor events
// --------------------------------------------------------------------------
publicAPI.handleMouseMove = (callData) => {
if (
!model.activeState ||
!model.activeState.getActive() ||
!model.pickable ||
!model.manipulator
) {
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);
model.shapeHandle.setUp(up);
model.shapeHandle.setRight(right);
model.shapeHandle.setDirection(normal);
model.manipulator.setNormal(normal);
}
const worldCoords = model.manipulator.handleEvent(
callData,
model.openGLRenderWindow
);
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();
}
} else if (model.useHandles && model.isDragging) {
if (model.activeState === model.point1Handle) {
model.point1Handle.setOrigin(worldCoords);
model.point1 = worldCoords;
} else {
model.point2Handle.setOrigin(worldCoords);
model.point2 = worldCoords;
}
publicAPI.updateShapeBounds();
publicAPI.invokeInteractionEvent();
}
return model.hasFocus ? macro.EVENT_ABORT : macro.VOID;
};
// --------------------------------------------------------------------------
// Left click: Add point / End interaction
// --------------------------------------------------------------------------
publicAPI.handleLeftButtonPress = (e) => {
if (
!model.activeState ||
!model.activeState.getActive() ||
!model.pickable
) {
return macro.VOID;
}
if (model.hasFocus) {
if (!model.point1) {
publicAPI.placePoint1(model.point1Handle.getOrigin());
publicAPI.invokeStartInteractionEvent();
} else {
publicAPI.placePoint2(model.point2Handle.getOrigin());
publicAPI.invokeInteractionEvent();
publicAPI.invokeEndInteractionEvent();
}
return macro.EVENT_ABORT;
}
if (
model.point1 &&
(model.activeState === model.point1Handle ||
model.activeState === model.point2Handle)
) {
model.isDragging = true;
model.openGLRenderWindow.setCursor('grabbing');
model.interactor.requestAnimation(publicAPI);
publicAPI.invokeStartInteractionEvent();
return macro.EVENT_ABORT;
}
return macro.VOID;
};
// --------------------------------------------------------------------------
// Left release: Maybe end interaction
// --------------------------------------------------------------------------
publicAPI.handleLeftButtonRelease = (e) => {
if (model.isDragging) {
model.isDragging = false;
model.openGLRenderWindow.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.openGLRenderWindow.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) {
model.point2 = model.point2Handle.getOrigin();
publicAPI.updateShapeBounds();
if (publicAPI.isDraggingEnabled()) {
const distance = vec3.squaredDistance(model.point1, model.point2);
const maxDistance = 100;
if (distance > maxDistance || publicAPI.isDraggingForced()) {
publicAPI.invokeInteractionEvent();
publicAPI.invokeEndInteractionEvent();
if (model.resetAfterPointPlacement) {
publicAPI.reset();
} else {
publicAPI.loseFocus();
}
}
}
}
return macro.EVENT_ABORT;
};
// --------------------------------------------------------------------------
// Register key presses/releases
// --------------------------------------------------------------------------
publicAPI.handleKeyDown = ({ key }) => {
if (key === 'Escape') {
if (model.hasFocus) {
publicAPI.invokeEndInteractionEvent();
publicAPI.reset();
publicAPI.loseFocus();
}
} else {
model.keysDown[key] = true;
}
if (model.hasFocus) {
if (model.point1) {
model.point2 = model.point2Handle.getOrigin();
publicAPI.updateShapeBounds();
}
}
};
publicAPI.handleKeyUp = ({ key }) => {
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();
if (!model.label) {
model.label = vtkLabelRepresentation.newInstance();
}
model.label.setRenderer(model.renderer);
model.label.buildRepresentation();
model.renderer.addViewProp(model.label);
model.label.setContainer(model.interactor.getContainer());
model.point1Handle.activate();
model.activeState = model.point1Handle;
if (model.useHandles) {
model.point1Handle.setVisible(true);
}
model.shapeHandle.setVisible(false);
model.interactor.requestAnimation(publicAPI);
}
publicAPI.updateHandlesSize();
model.hasFocus = true;
};
// --------------------------------------------------------------------------
publicAPI.loseFocus = () => {
if (model.hasFocus) {
if (model.visibleOnFocus && !model.useHandles) {
model.shapeHandle.setVisible(false);
}
model.interactor.cancelAnimation(publicAPI);
}
if (model.label && !model.useHandles) {
model.label.setContainer(null);
}
if (!model.useHandles || !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();
model.hasFocus = false;
};
}