@kitware/vtk.js
Version:
Visualization Toolkit for the Web
1,096 lines (1,035 loc) • 39.3 kB
JavaScript
import { m as macro } from '../../macros2.js';
import { B as degreesFromRadians } from '../../Common/Core/Math/index.js';
import Constants from './RenderWindowInteractor/Constants.js';
const {
Device,
Input
} = Constants;
const {
vtkWarningMacro,
vtkErrorMacro,
normalizeWheel,
vtkOnceErrorMacro
} = macro;
// ----------------------------------------------------------------------------
// Global methods
// ----------------------------------------------------------------------------
const EMPTY_MOUSE_EVENT = {
ctrlKey: false,
altKey: false,
shiftKey: false
};
const deviceInputMap = {
'xr-standard': [Input.Trigger, Input.Grip, Input.TrackPad, Input.Thumbstick, Input.A, Input.B]
};
const handledEvents = ['StartAnimation', 'Animation', 'EndAnimation', 'PointerEnter', 'PointerLeave', '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', 'StartInteraction', 'Interaction', 'EndInteraction', 'AnimationFrameRateUpdate'];
function preventDefault(event) {
if (event.cancelable) {
event.preventDefault();
}
}
function pointerCacheToPositions(cache) {
const positions = Object.create(null);
cache.forEach(_ref => {
let {
pointerId,
position
} = _ref;
positions[pointerId] = position;
});
return positions;
}
// ----------------------------------------------------------------------------
// vtkRenderWindowInteractor methods
// ----------------------------------------------------------------------------
function vtkRenderWindowInteractor(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkRenderWindowInteractor');
// Capture "parentClass" api for internal use
const superClass = {
...publicAPI
};
// Initialize list of requesters
const animationRequesters = new Set();
// map from pointerId to { pointerId: number, position: [x, y] }
const pointerCache = new Map();
// Factor to apply on wheel spin.
let wheelCoefficient = 1;
// 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) {
if (!model._forcedRenderer) {
model.currentRenderer = publicAPI.findPokedRenderer(x, y);
}
}
publicAPI.getCurrentRenderer = () => {
if (model.currentRenderer) {
return model.currentRenderer;
}
updateCurrentRenderer(0, 0);
return model.currentRenderer;
};
function _getScreenEventPositionFor(source) {
const canvas = model._view.getCanvas();
const bounds = canvas.getBoundingClientRect();
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,
movementX: scaleX * source.movementX,
movementY: scaleY * source.movementY
};
// if multitouch, do not update the current renderer
if (pointerCache.size <= 1 || !model.currentRenderer) {
updateCurrentRenderer(position.x, position.y);
}
return position;
}
// Allow user to override it
const getScreenEventPositionFor = model._getScreenEventPositionFor || _getScreenEventPositionFor;
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 getDeviceTypeFor(event) {
return event.pointerType || '';
}
const _bindEvents = () => {
if (model.container === null) {
return;
}
const {
container
} = model;
container.addEventListener('contextmenu', preventDefault);
container.addEventListener('wheel', publicAPI.handleWheel);
container.addEventListener('DOMMouseScroll', publicAPI.handleWheel);
container.addEventListener('pointerenter', publicAPI.handlePointerEnter);
container.addEventListener('pointerleave', publicAPI.handlePointerLeave);
container.addEventListener('pointermove', publicAPI.handlePointerMove, {
passive: false
});
container.addEventListener('pointerdown', publicAPI.handlePointerDown, {
passive: false
});
container.addEventListener('pointerup', publicAPI.handlePointerUp);
container.addEventListener('pointercancel', publicAPI.handlePointerCancel);
container.addEventListener('keypress', publicAPI.handleKeyPress);
container.addEventListener('keydown', publicAPI.handleKeyDown);
// Observe keyup on document in case the focus changes
// between keydown and keyup.
document.addEventListener('keyup', publicAPI.handleKeyUp);
document.addEventListener('pointerlockchange', publicAPI.handlePointerLockChange);
container.tabIndex = 0; // to receive key events
// using touchAction is more performant than preventDefault
// in a touchstart handler.
container.style.touchAction = 'none';
container.style.userSelect = 'none';
// disables tap highlight for when cursor is pointer
container.style.webkitTapHighlightColor = 'rgba(0,0,0,0)';
};
// For backward compatibility.
// Necessary for using unbind/bindEvent without calling setContainer.
publicAPI.bindEvents = container => {
if (container === null) {
return;
}
const res = superClass.setContainer(container);
if (res) {
_bindEvents();
}
};
const _unbindEvents = () => {
// Clear any previous timeouts and state variables that control mouse / touchpad behavior.
clearTimeout(model.moveTimeoutID);
clearTimeout(model.wheelTimeoutID);
model.moveTimeoutID = 0;
model.wheelTimeoutID = 0;
wheelCoefficient = 1.0;
const {
container
} = model;
if (container) {
container.removeEventListener('contextmenu', preventDefault);
container.removeEventListener('wheel', publicAPI.handleWheel);
container.removeEventListener('DOMMouseScroll', publicAPI.handleWheel);
container.removeEventListener('pointerenter', publicAPI.handlePointerEnter);
container.removeEventListener('pointerleave', publicAPI.handlePointerLeave);
container.removeEventListener('pointermove', publicAPI.handlePointerMove, {
passive: false
});
container.removeEventListener('pointerdown', publicAPI.handlePointerDown, {
passive: false
});
container.removeEventListener('pointerup', publicAPI.handlePointerUp);
container.removeEventListener('pointercancel', publicAPI.handlePointerCancel);
container.removeEventListener('keypress', publicAPI.handleKeyPress);
container.removeEventListener('keydown', publicAPI.handleKeyDown);
}
document.removeEventListener('keyup', publicAPI.handleKeyUp);
document.removeEventListener('pointerlockchange', publicAPI.handlePointerLockChange);
pointerCache.clear();
};
publicAPI.unbindEvents = () => {
_unbindEvents();
superClass.setContainer(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.handlePointerEnter = event => {
const callData = {
...getModifierKeysFor(event),
position: getScreenEventPositionFor(event),
deviceType: getDeviceTypeFor(event)
};
publicAPI.pointerEnterEvent(callData);
if (callData.deviceType === 'mouse') {
publicAPI.mouseEnterEvent(callData);
}
};
publicAPI.handlePointerLeave = event => {
const callData = {
...getModifierKeysFor(event),
position: getScreenEventPositionFor(event),
deviceType: getDeviceTypeFor(event)
};
publicAPI.pointerLeaveEvent(callData);
if (callData.deviceType === 'mouse') {
publicAPI.mouseLeaveEvent(callData);
}
};
publicAPI.handlePointerDown = event => {
if (event.button > 2 || publicAPI.isPointerLocked()) {
// ignore events from extra mouse buttons such as `back` and `forward`
return;
}
if (model.preventDefaultOnPointerDown) {
preventDefault(event);
}
if (event.target.hasPointerCapture(event.pointerId)) {
event.target.releasePointerCapture(event.pointerId);
}
model.container.setPointerCapture(event.pointerId);
if (pointerCache.has(event.pointerId)) {
vtkWarningMacro('[RenderWindowInteractor] duplicate pointerId detected');
}
pointerCache.set(event.pointerId, {
pointerId: event.pointerId,
position: getScreenEventPositionFor(event)
});
switch (event.pointerType) {
case 'pen':
case 'touch':
publicAPI.handleTouchStart(event);
break;
case 'mouse':
default:
publicAPI.handleMouseDown(event);
break;
}
};
publicAPI.handlePointerUp = event => {
if (pointerCache.has(event.pointerId)) {
if (model.preventDefaultOnPointerUp) {
preventDefault(event);
}
pointerCache.delete(event.pointerId);
model.container.releasePointerCapture(event.pointerId);
switch (event.pointerType) {
case 'pen':
case 'touch':
publicAPI.handleTouchEnd(event);
break;
case 'mouse':
default:
publicAPI.handleMouseUp(event);
break;
}
}
};
publicAPI.handlePointerCancel = event => {
if (pointerCache.has(event.pointerId)) {
pointerCache.delete(event.pointerId);
switch (event.pointerType) {
case 'pen':
case 'touch':
publicAPI.handleTouchEnd(event);
break;
case 'mouse':
default:
publicAPI.handleMouseUp(event);
break;
}
}
};
publicAPI.handlePointerMove = event => {
if (pointerCache.has(event.pointerId)) {
const pointer = pointerCache.get(event.pointerId);
pointer.position = getScreenEventPositionFor(event);
}
switch (event.pointerType) {
case 'pen':
case 'touch':
publicAPI.handleTouchMove(event);
break;
case 'mouse':
default:
publicAPI.handleMouseMove(event);
break;
}
};
publicAPI.handleMouseDown = event => {
const callData = {
...getModifierKeysFor(event),
position: getScreenEventPositionFor(event),
deviceType: getDeviceTypeFor(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 = () => {
if (model.container) {
model.container.requestPointerLock();
}
};
//----------------------------------------------------------------------
publicAPI.exitPointerLock = () => document.exitPointerLock?.();
//----------------------------------------------------------------------
publicAPI.isPointerLocked = () => !!model.container && document.pointerLockElement === model.container;
//----------------------------------------------------------------------
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 (!model.animationRequest && animationRequesters.size === 1 && !model.xrAnimation) {
model._animationStartTime = Date.now();
model._animationFrameCount = 0;
model.animationRequest = requestAnimationFrame(publicAPI.handleAnimation);
publicAPI.startAnimationEvent();
}
};
// continue animating for at least the specified duration of
// milliseconds.
publicAPI.extendAnimation = duration => {
const newEnd = Date.now() + duration;
model._animationExtendedEnd = Math.max(model._animationExtendedEnd, newEnd);
if (!model.animationRequest && animationRequesters.size === 0 && !model.xrAnimation) {
model._animationStartTime = Date.now();
model._animationFrameCount = 0;
model.animationRequest = requestAnimationFrame(publicAPI.handleAnimation);
publicAPI.startAnimationEvent();
}
};
publicAPI.isAnimating = () => model.xrAnimation || model.animationRequest !== null;
publicAPI.cancelAnimation = function (requestor) {
let skipWarning = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 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 && Date.now() > model._animationExtendedEnd) {
cancelAnimationFrame(model.animationRequest);
model.animationRequest = null;
publicAPI.endAnimationEvent();
publicAPI.render();
}
};
publicAPI.switchToXRAnimation = () => {
// cancel existing animation if any
if (model.animationRequest) {
cancelAnimationFrame(model.animationRequest);
model.animationRequest = null;
}
model.xrAnimation = true;
};
publicAPI.returnFromXRAnimation = () => {
model.xrAnimation = false;
if (animationRequesters.size !== 0) {
model.recentAnimationFrameRate = 10.0;
model.animationRequest = requestAnimationFrame(publicAPI.handleAnimation);
}
};
publicAPI.updateXRGamepads = (xrSession, xrFrame, xrRefSpace) => {
// watch for when buttons change state and fire events
xrSession.inputSources.forEach(inputSource => {
const gripPose = inputSource.gripSpace == null ? null : xrFrame.getPose(inputSource.gripSpace, xrRefSpace);
const targetRayPose = inputSource.gripSpace == null ? null : xrFrame.getPose(inputSource.targetRaySpace, xrRefSpace);
const gamepad = inputSource.gamepad;
const hand = inputSource.handedness;
if (!gamepad) {
return;
}
if (!(gamepad.index in model.lastGamepadValues)) {
model.lastGamepadValues[gamepad.index] = {
left: {
buttons: {}
},
right: {
buttons: {}
},
none: {
buttons: {}
}
};
}
for (let buttonIdx = 0; buttonIdx < gamepad.buttons.length; ++buttonIdx) {
if (!(buttonIdx in model.lastGamepadValues[gamepad.index][hand].buttons)) {
model.lastGamepadValues[gamepad.index][hand].buttons[buttonIdx] = false;
}
if (model.lastGamepadValues[gamepad.index][hand].buttons[buttonIdx] !== gamepad.buttons[buttonIdx].pressed && gripPose != null) {
publicAPI.button3DEvent({
gamepad,
position: gripPose.transform.position,
orientation: gripPose.transform.orientation,
targetPosition: targetRayPose.transform.position,
targetOrientation: targetRayPose.transform.orientation,
pressed: gamepad.buttons[buttonIdx].pressed,
device: inputSource.handedness === 'left' ? Device.LeftController : Device.RightController,
input: deviceInputMap[gamepad.mapping] && deviceInputMap[gamepad.mapping][buttonIdx] ? deviceInputMap[gamepad.mapping][buttonIdx] : Input.Trigger
});
model.lastGamepadValues[gamepad.index][hand].buttons[buttonIdx] = gamepad.buttons[buttonIdx].pressed;
}
if (model.lastGamepadValues[gamepad.index][hand].buttons[buttonIdx] && gripPose != null) {
publicAPI.move3DEvent({
gamepad,
position: gripPose.transform.position,
orientation: gripPose.transform.orientation,
targetPosition: targetRayPose.transform.position,
targetOrientation: targetRayPose.transform.orientation,
device: inputSource.handedness === 'left' ? Device.LeftController : Device.RightController
});
}
}
});
};
publicAPI.handleMouseMove = event => {
const callData = {
...getModifierKeysFor(event),
position: getScreenEventPositionFor(event),
deviceType: getDeviceTypeFor(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();
model._animationFrameCount++;
if (currTime - model._animationStartTime > 1000.0 && model._animationFrameCount > 1) {
model.recentAnimationFrameRate = 1000.0 * (model._animationFrameCount - 1) / (currTime - model._animationStartTime);
model.lastFrameTime = 1.0 / model.recentAnimationFrameRate;
publicAPI.animationFrameRateUpdateEvent();
model._animationStartTime = currTime;
model._animationFrameCount = 1;
}
publicAPI.animationEvent();
forceRender();
if (animationRequesters.size > 0 || Date.now() < model._animationExtendedEnd) {
model.animationRequest = requestAnimationFrame(publicAPI.handleAnimation);
} else {
cancelAnimationFrame(model.animationRequest);
model.animationRequest = null;
publicAPI.endAnimationEvent();
publicAPI.render();
}
};
publicAPI.handleWheel = event => {
preventDefault(event);
/**
* 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),
deviceType: getDeviceTypeFor(event)
};
// Wheel events are thought to scroll pages (i.e. multiple lines at once).
// See normalizeWheel() documentation for more context.
// While trackpad wheel events are many small (<1) wheel spins,
// mouse wheel events have absolute spin values higher than 1.
// Here the first spin value is "recorded", and used to normalize
// all the following mouse wheel events.
if (model.wheelTimeoutID === 0) {
// we attempt to distinguish between trackpads and mice
// .3 will be larger than the first trackpad event,
// but small enough to detect some common edge case mice
if (Math.abs(callData.spinY) >= 0.3) {
// Event is coming from mouse wheel
wheelCoefficient = Math.abs(callData.spinY);
} else {
// Event is coming from trackpad
wheelCoefficient = 1;
}
}
callData.spinY /= wheelCoefficient;
if (model.wheelTimeoutID === 0) {
publicAPI.startMouseWheelEvent(callData);
publicAPI.mouseWheelEvent(callData);
} else {
publicAPI.mouseWheelEvent(callData);
clearTimeout(model.wheelTimeoutID);
}
if (model.mouseScrollDebounceByPass) {
publicAPI.extendAnimation(600);
publicAPI.endMouseWheelEvent();
model.wheelTimeoutID = 0;
} else {
// start a timer to keep us animating while we get wheel events
model.wheelTimeoutID = setTimeout(() => {
publicAPI.extendAnimation(600);
publicAPI.endMouseWheelEvent();
model.wheelTimeoutID = 0;
}, 200);
}
};
publicAPI.handleMouseUp = event => {
const callData = {
...getModifierKeysFor(event),
position: getScreenEventPositionFor(event),
deviceType: getDeviceTypeFor(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 => {
const pointers = [...pointerCache.values()];
// If multitouch
if (model.recognizeGestures && pointers.length > 1) {
const positions = pointerCacheToPositions(pointerCache);
// did we just transition to multitouch?
if (pointers.length === 2) {
const callData = {
...getModifierKeysFor(EMPTY_MOUSE_EVENT),
position: pointers[0].position,
deviceType: getDeviceTypeFor(event)
};
publicAPI.leftButtonReleaseEvent(callData);
}
// handle the gesture
publicAPI.recognizeGesture('TouchStart', positions);
} else if (pointers.length === 1) {
const callData = {
...getModifierKeysFor(EMPTY_MOUSE_EVENT),
position: getScreenEventPositionFor(event),
deviceType: getDeviceTypeFor(event)
};
publicAPI.leftButtonPressEvent(callData);
}
};
publicAPI.handleTouchMove = event => {
const pointers = [...pointerCache.values()];
if (model.recognizeGestures && pointers.length > 1) {
const positions = pointerCacheToPositions(pointerCache);
publicAPI.recognizeGesture('TouchMove', positions);
} else if (pointers.length === 1) {
const callData = {
...getModifierKeysFor(EMPTY_MOUSE_EVENT),
position: pointers[0].position,
deviceType: getDeviceTypeFor(event)
};
publicAPI.mouseMoveEvent(callData);
}
};
publicAPI.handleTouchEnd = event => {
const pointers = [...pointerCache.values()];
if (model.recognizeGestures) {
// No more fingers down
if (pointers.length === 0) {
const callData = {
...getModifierKeysFor(EMPTY_MOUSE_EVENT),
position: getScreenEventPositionFor(event),
deviceType: getDeviceTypeFor(event)
};
publicAPI.leftButtonReleaseEvent(callData);
} else if (pointers.length === 1) {
// If one finger left, end touch and start button press
const positions = pointerCacheToPositions(pointerCache);
publicAPI.recognizeGesture('TouchEnd', positions);
const callData = {
...getModifierKeysFor(EMPTY_MOUSE_EVENT),
position: pointers[0].position,
deviceType: getDeviceTypeFor(event)
};
publicAPI.leftButtonPressEvent(callData);
} else {
// If more than one finger left, keep touch move
const positions = pointerCacheToPositions(pointerCache);
publicAPI.recognizeGesture('TouchMove', positions);
}
} else if (pointers.length === 1) {
const callData = {
...getModifierKeysFor(EMPTY_MOUSE_EVENT),
position: pointers[0].position,
deviceType: getDeviceTypeFor(event)
};
publicAPI.leftButtonReleaseEvent(callData);
}
};
publicAPI.setView = val => {
if (model._view === val) {
return;
}
model._view = val;
model._view.getRenderable().setInteractor(publicAPI);
publicAPI.modified();
};
publicAPI.getFirstRenderer = () => model._view?.getRenderable()?.getRenderersByReference()?.[0];
publicAPI.findPokedRenderer = function () {
let x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
let y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
if (!model._view) {
return null;
}
// The original order of renderers needs to remain as
// the first one is the one we want to manipulate the camera on.
const rc = model._view?.getRenderable()?.getRenderers();
if (!rc || rc.length === 0) {
return null;
}
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 (!publicAPI.isAnimating() && !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,
firstRenderer: publicAPI.getFirstRenderer(),
// 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 = degreesFromRadians(Math.atan2(startVals[1].y - startVals[0].y, startVals[1].x - startVals[0].x));
let newAngle = 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._animationStartTime = Date.now();
model._animationFrameCount = 0;
};
publicAPI.setCurrentRenderer = r => {
model._forcedRenderer = !!r;
model.currentRenderer = r;
};
publicAPI.setContainer = container => {
_unbindEvents();
const res = superClass.setContainer(container ?? null);
if (res) {
_bindEvents();
}
return res;
};
// Stop animating if the renderWindowInteractor is deleted.
publicAPI.delete = () => {
while (animationRequesters.size) {
publicAPI.cancelAnimation(animationRequesters.values().next().value);
}
if (typeof document.hidden !== 'undefined') {
document.removeEventListener('visibilitychange', publicAPI.handleVisibilityChange);
}
if (model.container) {
publicAPI.setContainer(null);
}
superClass.delete();
};
// Use the Page Visibility API to detect when we switch away from or back to
// this tab, and reset the animationFrameStart. 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,
recentAnimationFrameRate: 10.0,
wheelTimeoutID: 0,
moveTimeoutID: 0,
lastGamepadValues: {},
preventDefaultOnPointerDown: false,
preventDefaultOnPointerUp: false,
mouseScrollDebounceByPass: false
};
// ----------------------------------------------------------------------------
function extend(publicAPI, model) {
let initialValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
Object.assign(model, DEFAULT_VALUES, initialValues);
// Object methods
macro.obj(publicAPI, model);
// run animation at least until this time
model._animationExtendedEnd = 0;
macro.event(publicAPI, model, 'RenderEvent');
handledEvents.forEach(eventName => macro.event(publicAPI, model, eventName));
// Create get-only macros
macro.get(publicAPI, model, ['initialized', 'interactorStyle', 'lastFrameTime', 'recentAnimationFrameRate', '_view']);
// Create get-set macros
macro.setGet(publicAPI, model, ['container', 'lightFollowCamera', 'enabled', 'enableRender', 'recognizeGestures', 'desiredUpdateRate', 'stillUpdateRate', 'picker', 'preventDefaultOnPointerDown', 'preventDefaultOnPointerUp', 'mouseScrollDebounceByPass']);
macro.moveToProtected(publicAPI, model, ['view']);
// For more macro methods, see "Sources/macros.js"
// Object specific methods
vtkRenderWindowInteractor(publicAPI, model);
}
// ----------------------------------------------------------------------------
const newInstance = macro.newInstance(extend, 'vtkRenderWindowInteractor');
// ----------------------------------------------------------------------------
var vtkRenderWindowInteractor$1 = {
newInstance,
extend,
handledEvents,
...Constants
};
export { vtkRenderWindowInteractor$1 as default, extend, newInstance };