@acransac/vtk.js
Version:
Visualization Toolkit for the Web
1,093 lines (975 loc) • 33.2 kB
JavaScript
import macro from 'vtk.js/Sources/macro';
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';
import Constants from 'vtk.js/Sources/Rendering/Core/RenderWindowInteractor/Constants';
const { Device, Input } = Constants;
const {
vtkWarningMacro,
vtkErrorMacro,
normalizeWheel,
vtkOnceErrorMacro,
} = macro;
// ----------------------------------------------------------------------------
// Global methods
// ----------------------------------------------------------------------------
const deviceInputMap = {
'OpenVR Gamepad': [
Input.TrackPad,
Input.Trigger,
Input.Grip,
Input.ApplicationMenu,
],
};
const handledEvents = [
'StartAnimation',
'Animation',
'EndAnimation',
'MouseEnter',
'MouseLeave',
'StartMouseMove',
'MouseMove',
'EndMouseMove',
'LeftButtonPress',
'LeftButtonRelease',
'MiddleButtonPress',
'MiddleButtonRelease',
'RightButtonPress',
'RightButtonRelease',
'KeyPress',
'KeyDown',
'KeyUp',
'StartMouseWheel',
'MouseWheel',
'EndMouseWheel',
'StartPinch',
'Pinch',
'EndPinch',
'StartPan',
'Pan',
'EndPan',
'StartRotate',
'Rotate',
'EndRotate',
'Button3D',
'Move3D',
'StartPointerLock',
'EndPointerLock',
'StartInteractionEvent',
'InteractionEvent',
'EndInteractionEvent',
];
function preventDefault(event) {
event.stopPropagation();
event.preventDefault();
return false;
}
// ----------------------------------------------------------------------------
// vtkRenderWindowInteractor methods
// ----------------------------------------------------------------------------
function vtkRenderWindowInteractor(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkRenderWindowInteractor');
// Initialize list of requesters
const animationRequesters = new Set();
// track active event listeners to handle simultaneous button tracking
let activeListenerCount = 0;
// Public API methods
//----------------------------------------------------------------------
publicAPI.start = () => {
// Let the compositing handle the event loop if it wants to.
// if (publicAPI.HasObserver(vtkCommand::StartEvent) && !publicAPI.HandleEventLoop) {
// publicAPI.invokeEvent({ type: 'StartEvent' });
// return;
// }
// As a convenience, initialize if we aren't initialized yet.
if (!model.initialized) {
publicAPI.initialize();
if (!model.initialized) {
return;
}
}
// Pass execution to the subclass which will run the event loop,
// this will not return until TerminateApp is called.
publicAPI.startEventLoop();
};
//----------------------------------------------------------------------
publicAPI.setRenderWindow = (aren) => {
vtkErrorMacro(
'you want to call setView(view) instead of setRenderWindow on a vtk.js interactor'
);
};
//----------------------------------------------------------------------
publicAPI.setInteractorStyle = (style) => {
if (model.interactorStyle !== style) {
if (model.interactorStyle != null) {
model.interactorStyle.setInteractor(null);
}
model.interactorStyle = style;
if (model.interactorStyle != null) {
if (model.interactorStyle.getInteractor() !== publicAPI) {
model.interactorStyle.setInteractor(publicAPI);
}
}
}
};
//---------------------------------------------------------------------
publicAPI.initialize = () => {
model.initialized = true;
publicAPI.enable();
publicAPI.render();
};
publicAPI.enable = () => publicAPI.setEnabled(true);
publicAPI.disable = () => publicAPI.setEnabled(false);
publicAPI.startEventLoop = () => vtkWarningMacro('empty event loop');
function updateCurrentRenderer(x, y) {
model.currentRenderer = publicAPI.findPokedRenderer(x, y);
}
publicAPI.getCurrentRenderer = () => {
if (model.currentRenderer) {
return model.currentRenderer;
}
updateCurrentRenderer(0, 0);
return model.currentRenderer;
};
function getScreenEventPositionFor(source) {
const bounds = model.container.getBoundingClientRect();
const canvas = model.view.getCanvas();
const scaleX = canvas.width / bounds.width;
const scaleY = canvas.height / bounds.height;
const position = {
x: scaleX * (source.clientX - bounds.left),
y: scaleY * (bounds.height - source.clientY + bounds.top),
z: 0,
};
updateCurrentRenderer(position.x, position.y);
return position;
}
function getTouchEventPositionsFor(touches) {
const positions = {};
for (let i = 0; i < touches.length; i++) {
const touch = touches[i];
positions[touch.identifier] = getScreenEventPositionFor(touch);
}
return positions;
}
function getModifierKeysFor(event) {
return {
controlKey: event.ctrlKey,
altKey: event.altKey,
shiftKey: event.shiftKey,
};
}
function getKeysFor(event) {
const modifierKeys = getModifierKeysFor(event);
const keys = {
key: event.key,
keyCode: event.charCode,
...modifierKeys,
};
return keys;
}
function interactionRegistration(addListeners, force = false) {
const rootElm = document;
const method = addListeners ? 'addEventListener' : 'removeEventListener';
const invMethod = addListeners ? 'removeEventListener' : 'addEventListener';
if (!force && !addListeners && activeListenerCount > 0) {
--activeListenerCount;
}
// only add/remove listeners when there are no registered listeners
if (!activeListenerCount || force) {
activeListenerCount = 0;
if (model.container) {
model.container[invMethod]('mousemove', publicAPI.handleMouseMove);
}
rootElm[method]('mouseup', publicAPI.handleMouseUp);
rootElm[method]('mousemove', publicAPI.handleMouseMove);
rootElm[method]('touchend', publicAPI.handleTouchEnd, false);
rootElm[method]('touchcancel', publicAPI.handleTouchEnd, false);
rootElm[method]('touchmove', publicAPI.handleTouchMove, false);
}
if (!force && addListeners) {
++activeListenerCount;
}
}
publicAPI.bindEvents = (container) => {
model.container = container;
container.addEventListener('contextmenu', preventDefault);
// container.addEventListener('click', preventDefault); // Avoid stopping event propagation
container.addEventListener('wheel', publicAPI.handleWheel);
container.addEventListener('DOMMouseScroll', publicAPI.handleWheel);
container.addEventListener('mouseenter', publicAPI.handleMouseEnter);
container.addEventListener('mouseleave', publicAPI.handleMouseLeave);
container.addEventListener('mousemove', publicAPI.handleMouseMove);
container.addEventListener('mousedown', publicAPI.handleMouseDown);
document
.querySelector('body')
.addEventListener('keypress', publicAPI.handleKeyPress);
document
.querySelector('body')
.addEventListener('keydown', publicAPI.handleKeyDown);
document
.querySelector('body')
.addEventListener('keyup', publicAPI.handleKeyUp);
document.addEventListener(
'pointerlockchange',
publicAPI.handlePointerLockChange
);
container.addEventListener('touchstart', publicAPI.handleTouchStart, false);
};
publicAPI.unbindEvents = () => {
// force unbinding listeners
interactionRegistration(false, true);
model.container.removeEventListener('contextmenu', preventDefault);
// model.container.removeEventListener('click', preventDefault); // Avoid stopping event propagation
model.container.removeEventListener('wheel', publicAPI.handleWheel);
model.container.removeEventListener(
'DOMMouseScroll',
publicAPI.handleWheel
);
model.container.removeEventListener(
'mouseenter',
publicAPI.handleMouseEnter
);
model.container.removeEventListener(
'mouseleave',
publicAPI.handleMouseLeave
);
model.container.removeEventListener('mousemove', publicAPI.handleMouseMove);
model.container.removeEventListener('mousedown', publicAPI.handleMouseDown);
document
.querySelector('body')
.removeEventListener('keypress', publicAPI.handleKeyPress);
document
.querySelector('body')
.removeEventListener('keydown', publicAPI.handleKeyDown);
document
.querySelector('body')
.removeEventListener('keyup', publicAPI.handleKeyUp);
document.removeEventListener(
'pointerlockchange',
publicAPI.handlePointerLockChange
);
model.container.removeEventListener(
'touchstart',
publicAPI.handleTouchStart
);
model.container = null;
};
publicAPI.handleKeyPress = (event) => {
const data = getKeysFor(event);
publicAPI.keyPressEvent(data);
};
publicAPI.handleKeyDown = (event) => {
const data = getKeysFor(event);
publicAPI.keyDownEvent(data);
};
publicAPI.handleKeyUp = (event) => {
const data = getKeysFor(event);
publicAPI.keyUpEvent(data);
};
publicAPI.handleMouseDown = (event) => {
if (event.button > 2) {
// ignore events from extra mouse buttons such as `back` and `forward`
return;
}
interactionRegistration(true);
event.stopPropagation();
event.preventDefault();
const callData = {
...getModifierKeysFor(event),
position: getScreenEventPositionFor(event),
};
switch (event.button) {
case 0:
publicAPI.leftButtonPressEvent(callData);
break;
case 1:
publicAPI.middleButtonPressEvent(callData);
break;
case 2:
publicAPI.rightButtonPressEvent(callData);
break;
default:
vtkErrorMacro(`Unknown mouse button pressed: ${event.button}`);
break;
}
};
//----------------------------------------------------------------------
publicAPI.requestPointerLock = () => {
const canvas = publicAPI.getView().getCanvas();
canvas.requestPointerLock();
};
//----------------------------------------------------------------------
publicAPI.exitPointerLock = () => document.exitPointerLock();
//----------------------------------------------------------------------
publicAPI.isPointerLocked = () => !!document.pointerLockElement;
//----------------------------------------------------------------------
publicAPI.handlePointerLockChange = () => {
if (publicAPI.isPointerLocked()) {
publicAPI.startPointerLockEvent();
} else {
publicAPI.endPointerLockEvent();
}
};
//----------------------------------------------------------------------
function forceRender() {
if (model.view && model.enabled && model.enableRender) {
model.inRender = true;
model.view.traverseAllPasses();
model.inRender = false;
}
// outside the above test so that third-party code can redirect
// the render to the appropriate class
publicAPI.invokeRenderEvent();
}
publicAPI.requestAnimation = (requestor) => {
if (requestor === undefined) {
vtkErrorMacro(`undefined requester, can not start animating`);
return;
}
if (animationRequesters.has(requestor)) {
vtkWarningMacro(`requester is already registered for animating`);
return;
}
animationRequesters.add(requestor);
if (animationRequesters.size === 1) {
model.lastFrameTime = 0.1;
model.lastFrameStart = Date.now();
model.animationRequest = requestAnimationFrame(publicAPI.handleAnimation);
publicAPI.startAnimationEvent();
}
};
publicAPI.isAnimating = () =>
model.vrAnimation || model.animationRequest !== null;
publicAPI.cancelAnimation = (requestor, skipWarning = false) => {
if (!animationRequesters.has(requestor)) {
if (!skipWarning) {
const requestStr =
requestor && requestor.getClassName
? requestor.getClassName()
: requestor;
vtkWarningMacro(`${requestStr} did not request an animation`);
}
return;
}
animationRequesters.delete(requestor);
if (model.animationRequest && animationRequesters.size === 0) {
cancelAnimationFrame(model.animationRequest);
model.animationRequest = null;
publicAPI.endAnimationEvent();
publicAPI.render();
}
};
publicAPI.switchToVRAnimation = () => {
// cancel existing animation if any
if (model.animationRequest) {
cancelAnimationFrame(model.animationRequest);
model.animationRequest = null;
}
model.vrAnimation = true;
};
publicAPI.returnFromVRAnimation = () => {
model.vrAnimation = false;
if (animationRequesters.size !== 0) {
model.FrameTime = -1;
model.animationRequest = requestAnimationFrame(publicAPI.handleAnimation);
}
};
publicAPI.updateGamepads = (displayId) => {
const gamepads = navigator.getGamepads();
// watch for when buttons change state and fire events
for (let i = 0; i < gamepads.length; ++i) {
const gp = gamepads[i];
if (gp && gp.displayId === displayId) {
if (!(gp.index in model.lastGamepadValues)) {
model.lastGamepadValues[gp.index] = { buttons: {} };
}
for (let b = 0; b < gp.buttons.length; ++b) {
if (!(b in model.lastGamepadValues[gp.index].buttons)) {
model.lastGamepadValues[gp.index].buttons[b] = false;
}
if (
model.lastGamepadValues[gp.index].buttons[b] !==
gp.buttons[b].pressed
) {
publicAPI.button3DEvent({
gamepad: gp,
position: gp.pose.position,
orientation: gp.pose.orientation,
pressed: gp.buttons[b].pressed,
device:
gp.hand === 'left'
? Device.LeftController
: Device.RightController,
input:
deviceInputMap[gp.id] && deviceInputMap[gp.id][b]
? deviceInputMap[gp.id][b]
: Input.Trigger,
});
model.lastGamepadValues[gp.index].buttons[b] =
gp.buttons[b].pressed;
}
if (model.lastGamepadValues[gp.index].buttons[b]) {
publicAPI.move3DEvent({
gamepad: gp,
position: gp.pose.position,
orientation: gp.pose.orientation,
device:
gp.hand === 'left'
? Device.LeftController
: Device.RightController,
});
}
}
}
}
};
publicAPI.handleMouseMove = (event) => {
// Do not consume event for move
// event.stopPropagation();
// event.preventDefault();
const callData = {
...getModifierKeysFor(event),
position: getScreenEventPositionFor(event),
};
if (model.moveTimeoutID === 0) {
publicAPI.startMouseMoveEvent(callData);
} else {
publicAPI.mouseMoveEvent(callData);
clearTimeout(model.moveTimeoutID);
}
// start a timer to keep us animating while we get mouse move events
model.moveTimeoutID = setTimeout(() => {
publicAPI.endMouseMoveEvent();
model.moveTimeoutID = 0;
}, 200);
};
publicAPI.handleAnimation = () => {
const currTime = Date.now();
if (model.FrameTime === -1.0) {
model.lastFrameTime = 0.1;
} else {
model.lastFrameTime = (currTime - model.lastFrameStart) / 1000.0;
}
model.lastFrameTime = Math.max(0.01, model.lastFrameTime);
model.lastFrameStart = currTime;
publicAPI.animationEvent();
forceRender();
model.animationRequest = requestAnimationFrame(publicAPI.handleAnimation);
};
publicAPI.handleWheel = (event) => {
event.stopPropagation();
event.preventDefault();
/**
* wheel event values can vary significantly across browsers, platforms
* and devices [1]. `normalizeWheel` uses facebook's solution from their
* fixed-data-table repository [2].
*
* [1] https://developer.mozilla.org/en-US/docs/Web/Events/mousewheel
* [2] https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js
*
* This code will return an object with properties:
*
* spinX -- normalized spin speed (use for zoom) - x plane
* spinY -- " - y plane
* pixelX -- normalized distance (to pixels) - x plane
* pixelY -- " - y plane
*
*/
const callData = {
...normalizeWheel(event),
...getModifierKeysFor(event),
position: getScreenEventPositionFor(event),
};
if (model.wheelTimeoutID === 0) {
publicAPI.startMouseWheelEvent(callData);
} else {
publicAPI.mouseWheelEvent(callData);
clearTimeout(model.wheelTimeoutID);
}
// start a timer to keep us animating while we get wheel events
model.wheelTimeoutID = setTimeout(() => {
publicAPI.endMouseWheelEvent();
model.wheelTimeoutID = 0;
}, 200);
};
publicAPI.handleMouseEnter = (event) => {
const callData = {
...getModifierKeysFor(event),
position: getScreenEventPositionFor(event),
};
publicAPI.mouseEnterEvent(callData);
};
publicAPI.handleMouseLeave = (event) => {
const callData = {
...getModifierKeysFor(event),
position: getScreenEventPositionFor(event),
};
publicAPI.mouseLeaveEvent(callData);
};
publicAPI.handleMouseUp = (event) => {
interactionRegistration(false);
event.stopPropagation();
event.preventDefault();
const callData = {
...getModifierKeysFor(event),
position: getScreenEventPositionFor(event),
};
switch (event.button) {
case 0:
publicAPI.leftButtonReleaseEvent(callData);
break;
case 1:
publicAPI.middleButtonReleaseEvent(callData);
break;
case 2:
publicAPI.rightButtonReleaseEvent(callData);
break;
default:
vtkErrorMacro(`Unknown mouse button released: ${event.button}`);
break;
}
};
publicAPI.handleTouchStart = (event) => {
interactionRegistration(true);
event.stopPropagation();
event.preventDefault();
// If multitouch
if (model.recognizeGestures && event.touches.length > 1) {
const positions = getTouchEventPositionsFor(event.touches);
// did we just transition to multitouch?
if (event.touches.length === 2) {
const touch = event.touches[0];
const callData = {
position: getScreenEventPositionFor(touch),
shiftKey: false,
altKey: false,
controlKey: false,
};
publicAPI.leftButtonReleaseEvent(callData);
}
// handle the gesture
publicAPI.recognizeGesture('TouchStart', positions);
} else {
const touch = event.touches[0];
const callData = {
position: getScreenEventPositionFor(touch),
shiftKey: false,
altKey: false,
controlKey: false,
};
publicAPI.leftButtonPressEvent(callData);
}
};
publicAPI.handleTouchMove = (event) => {
event.stopPropagation();
event.preventDefault();
if (model.recognizeGestures && event.touches.length > 1) {
const positions = getTouchEventPositionsFor(event.touches);
publicAPI.recognizeGesture('TouchMove', positions);
} else {
const touch = event.touches[0];
const callData = {
position: getScreenEventPositionFor(touch),
shiftKey: false,
altKey: false,
controlKey: false,
};
publicAPI.mouseMoveEvent(callData);
}
};
publicAPI.handleTouchEnd = (event) => {
event.stopPropagation();
event.preventDefault();
if (model.recognizeGestures) {
// No more fingers down
if (event.touches.length === 0) {
// If just one finger released, consider as left button
if (event.changedTouches.length === 1) {
const touch = event.changedTouches[0];
const callData = {
position: getScreenEventPositionFor(touch),
shiftKey: false,
altKey: false,
controlKey: false,
};
publicAPI.leftButtonReleaseEvent(callData);
interactionRegistration(false);
} else {
// If more than one finger released, recognize touchend
const positions = getTouchEventPositionsFor(event.changedTouches);
publicAPI.recognizeGesture('TouchEnd', positions);
interactionRegistration(false);
}
} else if (event.touches.length === 1) {
// If one finger left, end touch and start button press
const positions = getTouchEventPositionsFor(event.changedTouches);
publicAPI.recognizeGesture('TouchEnd', positions);
const touch = event.touches[0];
const callData = {
position: getScreenEventPositionFor(touch),
shiftKey: false,
altKey: false,
controlKey: false,
};
publicAPI.leftButtonPressEvent(callData);
} else {
// If more than one finger left, keep touch move
const positions = getTouchEventPositionsFor(event.touches);
publicAPI.recognizeGesture('TouchMove', positions);
}
} else {
const touch = event.changedTouches[0];
const callData = {
position: getScreenEventPositionFor(touch),
shiftKey: false,
altKey: false,
controlKey: false,
};
publicAPI.leftButtonReleaseEvent(callData);
interactionRegistration(false);
}
};
publicAPI.setView = (val) => {
if (model.view === val) {
return;
}
model.view = val;
model.view.getRenderable().setInteractor(publicAPI);
publicAPI.modified();
};
publicAPI.findPokedRenderer = (x = 0, y = 0) => {
if (!model.view) {
return null;
}
const rc = model.view.getRenderable().getRenderersByReference();
rc.sort((a, b) => a.getLayer() - b.getLayer());
let interactiveren = null;
let viewportren = null;
let currentRenderer = null;
let count = rc.length;
while (count--) {
const aren = rc[count];
if (model.view.isInViewport(x, y, aren) && aren.getInteractive()) {
currentRenderer = aren;
break;
}
if (interactiveren === null && aren.getInteractive()) {
// Save this renderer in case we can't find one in the viewport that
// is interactive.
interactiveren = aren;
}
if (viewportren === null && model.view.isInViewport(x, y, aren)) {
// Save this renderer in case we can't find one in the viewport that
// is interactive.
viewportren = aren;
}
}
// We must have a value. If we found an interactive renderer before, that's
// better than a non-interactive renderer.
if (currentRenderer === null) {
currentRenderer = interactiveren;
}
// We must have a value. If we found a renderer that is in the viewport,
// that is better than any old viewport (but not as good as an interactive
// one).
if (currentRenderer === null) {
currentRenderer = viewportren;
}
// We must have a value - take anything.
if (currentRenderer == null) {
currentRenderer = rc[0];
}
return currentRenderer;
};
// only render if we are not animating. If we are animating
// then renders will happen naturally anyhow and we definitely
// do not want extra renders as the make the apparent interaction
// rate slower.
publicAPI.render = () => {
if (model.animationRequest === null && !model.inRender) {
forceRender();
}
};
// create the generic Event methods
handledEvents.forEach((eventName) => {
const lowerFirst = eventName.charAt(0).toLowerCase() + eventName.slice(1);
publicAPI[`${lowerFirst}Event`] = (arg) => {
// Check that interactor enabled
if (!model.enabled) {
return;
}
// Check that a poked renderer exists
const renderer = publicAPI.getCurrentRenderer();
if (!renderer) {
vtkOnceErrorMacro(`
Can not forward events without a current renderer on the interactor.
`);
return;
}
// Pass the eventName and the poked renderer
const callData = {
type: eventName,
pokedRenderer: model.currentRenderer,
// Add the arguments to the call data
...arg,
};
// Call invoke
publicAPI[`invoke${eventName}`](callData);
};
});
// we know we are in multitouch now, so start recognizing
publicAPI.recognizeGesture = (event, positions) => {
// more than two pointers we ignore
if (Object.keys(positions).length > 2) {
return;
}
if (!model.startingEventPositions) {
model.startingEventPositions = {};
}
// store the initial positions
if (event === 'TouchStart') {
Object.keys(positions).forEach((key) => {
model.startingEventPositions[key] = positions[key];
});
// we do not know what the gesture is yet
model.currentGesture = 'Start';
return;
}
// end the gesture if needed
if (event === 'TouchEnd') {
if (model.currentGesture === 'Pinch') {
publicAPI.render();
publicAPI.endPinchEvent();
}
if (model.currentGesture === 'Rotate') {
publicAPI.render();
publicAPI.endRotateEvent();
}
if (model.currentGesture === 'Pan') {
publicAPI.render();
publicAPI.endPanEvent();
}
model.currentGesture = 'Start';
model.startingEventPositions = {};
return;
}
// what are the two pointers we are working with
let count = 0;
const posVals = [];
const startVals = [];
Object.keys(positions).forEach((key) => {
posVals[count] = positions[key];
startVals[count] = model.startingEventPositions[key];
count++;
});
// The meat of the algorithm
// on move events we analyze them to determine what type
// of movement it is and then deal with it.
// calculate the distances
const originalDistance = Math.sqrt(
(startVals[0].x - startVals[1].x) * (startVals[0].x - startVals[1].x) +
(startVals[0].y - startVals[1].y) * (startVals[0].y - startVals[1].y)
);
const newDistance = Math.sqrt(
(posVals[0].x - posVals[1].x) * (posVals[0].x - posVals[1].x) +
(posVals[0].y - posVals[1].y) * (posVals[0].y - posVals[1].y)
);
// calculate rotations
let originalAngle = vtkMath.degreesFromRadians(
Math.atan2(
startVals[1].y - startVals[0].y,
startVals[1].x - startVals[0].x
)
);
let newAngle = vtkMath.degreesFromRadians(
Math.atan2(posVals[1].y - posVals[0].y, posVals[1].x - posVals[0].x)
);
// angles are cyclic so watch for that, 1 and 359 are only 2 apart :)
let angleDeviation = newAngle - originalAngle;
newAngle = newAngle + 180.0 >= 360.0 ? newAngle - 180.0 : newAngle + 180.0;
originalAngle =
originalAngle + 180.0 >= 360.0
? originalAngle - 180.0
: originalAngle + 180.0;
if (Math.abs(newAngle - originalAngle) < Math.abs(angleDeviation)) {
angleDeviation = newAngle - originalAngle;
}
// calculate the translations
const trans = [];
trans[0] =
(posVals[0].x - startVals[0].x + posVals[1].x - startVals[1].x) / 2.0;
trans[1] =
(posVals[0].y - startVals[0].y + posVals[1].y - startVals[1].y) / 2.0;
if (event === 'TouchMove') {
// OK we want to
// - immediately respond to the user
// - allow the user to zoom without panning (saves focal point)
// - allow the user to rotate without panning (saves focal point)
// do we know what gesture we are doing yet? If not
// see if we can figure it out
if (model.currentGesture === 'Start') {
// pinch is a move to/from the center point
// rotate is a move along the circumference
// pan is a move of the center point
// compute the distance along each of these axes in pixels
// the first to break thresh wins
let thresh =
0.01 *
Math.sqrt(
model.container.clientWidth * model.container.clientWidth +
model.container.clientHeight * model.container.clientHeight
);
if (thresh < 15.0) {
thresh = 15.0;
}
const pinchDistance = Math.abs(newDistance - originalDistance);
const rotateDistance =
(newDistance * 3.1415926 * Math.abs(angleDeviation)) / 360.0;
const panDistance = Math.sqrt(
trans[0] * trans[0] + trans[1] * trans[1]
);
if (
pinchDistance > thresh &&
pinchDistance > rotateDistance &&
pinchDistance > panDistance
) {
model.currentGesture = 'Pinch';
const callData = {
scale: 1.0,
touches: positions,
};
publicAPI.startPinchEvent(callData);
} else if (rotateDistance > thresh && rotateDistance > panDistance) {
model.currentGesture = 'Rotate';
const callData = {
rotation: 0.0,
touches: positions,
};
publicAPI.startRotateEvent(callData);
} else if (panDistance > thresh) {
model.currentGesture = 'Pan';
const callData = {
translation: [0, 0],
touches: positions,
};
publicAPI.startPanEvent(callData);
}
} else {
// if we have found a specific type of movement then
// handle it
if (model.currentGesture === 'Rotate') {
const callData = {
rotation: angleDeviation,
touches: positions,
};
publicAPI.rotateEvent(callData);
}
if (model.currentGesture === 'Pinch') {
const callData = {
scale: newDistance / originalDistance,
touches: positions,
};
publicAPI.pinchEvent(callData);
}
if (model.currentGesture === 'Pan') {
const callData = {
translation: trans,
touches: positions,
};
publicAPI.panEvent(callData);
}
}
}
};
publicAPI.handleVisibilityChange = () => {
model.lastFrameStart = Date.now();
};
// Stop animating if the renderWindowInteractor is deleted.
const superDelete = publicAPI.delete;
publicAPI.delete = () => {
while (animationRequesters.size) {
publicAPI.cancelAnimation(animationRequesters.values().next().value);
}
if (typeof document.hidden !== 'undefined') {
document.removeEventListener(
'visibilitychange',
publicAPI.handleVisibilityChange
);
}
superDelete();
};
// Use the Page Visibility API to detect when we switch away from or back to
// this tab, and reset the lastFrameStart. When tabs are not active, browsers
// will stop calling requestAnimationFrame callbacks.
if (typeof document.hidden !== 'undefined') {
document.addEventListener(
'visibilitychange',
publicAPI.handleVisibilityChange,
false
);
}
}
// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------
const DEFAULT_VALUES = {
renderWindow: null,
interactorStyle: null,
picker: null,
pickingManager: null,
initialized: false,
enabled: false,
enableRender: true,
currentRenderer: null,
lightFollowCamera: true,
desiredUpdateRate: 30.0,
stillUpdateRate: 2.0,
container: null,
view: null,
recognizeGestures: true,
currentGesture: 'Start',
animationRequest: null,
lastFrameTime: 0.1,
wheelTimeoutID: 0,
moveTimeoutID: 0,
lastGamepadValues: {},
};
// ----------------------------------------------------------------------------
export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);
// Object methods
macro.obj(publicAPI, model);
macro.event(publicAPI, model, 'RenderEvent');
handledEvents.forEach((eventName) =>
macro.event(publicAPI, model, eventName)
);
// Create get-only macros
macro.get(publicAPI, model, [
'initialized',
'container',
'enabled',
'enableRender',
'interactorStyle',
'lastFrameTime',
'view',
]);
// Create get-set macros
macro.setGet(publicAPI, model, [
'lightFollowCamera',
'enabled',
'recognizeGestures',
'desiredUpdateRate',
'stillUpdateRate',
'picker',
]);
// For more macro methods, see "Sources/macro.js"
// Object specific methods
vtkRenderWindowInteractor(publicAPI, model);
}
// ----------------------------------------------------------------------------
export const newInstance = macro.newInstance(
extend,
'vtkRenderWindowInteractor'
);
// ----------------------------------------------------------------------------
export default { newInstance, extend, handledEvents, ...Constants };