UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

1,096 lines (1,035 loc) 39.3 kB
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 };