浏览代码

Merge pull request #6180 from BabylonJS/inputmanager

Cleanup
David Catuhe 6 年之前
父节点
当前提交
a80e49fb54
共有 4 个文件被更改,包括 941 次插入742 次删除
  1. 871 0
      src/Inputs/scene.inputManager.ts
  2. 5 1
      src/Meshes/Compression/dracoCompression.ts
  3. 1 1
      src/Sprites/spriteSceneComponent.ts
  4. 64 740
      src/scene.ts

+ 871 - 0
src/Inputs/scene.inputManager.ts

@@ -0,0 +1,871 @@
+import { Observable, Observer } from '../Misc/observable';
+import { PointerInfoPre, PointerInfo, PointerEventTypes } from '../Events/pointerEvents';
+import { Nullable } from '../types';
+import { AbstractActionManager } from '../Actions/abstractActionManager';
+import { PickingInfo } from '../Collisions/pickingInfo';
+import { Vector2, Matrix } from '../Maths/math';
+import { AbstractMesh } from '../Meshes/abstractMesh';
+import { Constants } from '../Engines/constants';
+import { ActionEvent } from '../Actions/actionEvent';
+import { Tools } from '../Misc/tools';
+import { Engine } from '../Engines/engine';
+import { KeyboardEventTypes, KeyboardInfoPre, KeyboardInfo } from '../Events/keyboardEvents';
+
+declare type Scene = import("../scene").Scene;
+
+/** @hidden */
+class _ClickInfo {
+    private _singleClick = false;
+    private _doubleClick = false;
+    private _hasSwiped = false;
+    private _ignore = false;
+
+    public get singleClick(): boolean {
+        return this._singleClick;
+    }
+    public get doubleClick(): boolean {
+        return this._doubleClick;
+    }
+    public get hasSwiped(): boolean {
+        return this._hasSwiped;
+    }
+    public get ignore(): boolean {
+        return this._ignore;
+    }
+
+    public set singleClick(b: boolean) {
+        this._singleClick = b;
+    }
+    public set doubleClick(b: boolean) {
+        this._doubleClick = b;
+    }
+    public set hasSwiped(b: boolean) {
+        this._hasSwiped = b;
+    }
+    public set ignore(b: boolean) {
+        this._ignore = b;
+    }
+}
+
+/**
+ * Class used to manage all inputs for the scene.
+ */
+export class InputManager {
+    /** The distance in pixel that you have to move to prevent some events */
+    public static DragMovementThreshold = 10; // in pixels
+    /** Time in milliseconds to wait to raise long press events if button is still pressed */
+    public static LongPressDelay = 500; // in milliseconds
+    /** Time in milliseconds with two consecutive clicks will be considered as a double click */
+    public static DoubleClickDelay = 300; // in milliseconds
+    /** If you need to check double click without raising a single click at first click, enable this flag */
+    public static ExclusiveDoubleClickMode = false;
+
+    // Pointers
+    private _wheelEventName = "";
+    private _onPointerMove: (evt: PointerEvent) => void;
+    private _onPointerDown: (evt: PointerEvent) => void;
+    private _onPointerUp: (evt: PointerEvent) => void;
+
+    private _initClickEvent: (obs1: Observable<PointerInfoPre>, obs2: Observable<PointerInfo>, evt: PointerEvent, cb: (clickInfo: _ClickInfo, pickResult: Nullable<PickingInfo>) => void) => void;
+    private _initActionManager: (act: Nullable<AbstractActionManager>, clickInfo: _ClickInfo) => Nullable<AbstractActionManager>;
+    private _delayedSimpleClick: (btn: number, clickInfo: _ClickInfo, cb: (clickInfo: _ClickInfo, pickResult: Nullable<PickingInfo>) => void) => void;
+    private _delayedSimpleClickTimeout: number;
+    private _previousDelayedSimpleClickTimeout: number;
+    private _meshPickProceed = false;
+
+    private _previousButtonPressed: number;
+    private _currentPickResult: Nullable<PickingInfo> = null;
+    private _previousPickResult: Nullable<PickingInfo> = null;
+    private _totalPointersPressed = 0;
+    private _doubleClickOccured = false;
+
+    private _pointerOverMesh: Nullable<AbstractMesh>;
+
+    private _pickedDownMesh: Nullable<AbstractMesh>;
+    private _pickedUpMesh: Nullable<AbstractMesh>;
+
+    private _pointerX: number = 0;
+    private _pointerY: number = 0;
+    private _unTranslatedPointerX: number;
+    private _unTranslatedPointerY: number;
+    private _startingPointerPosition = new Vector2(0, 0);
+    private _previousStartingPointerPosition = new Vector2(0, 0);
+    private _startingPointerTime = 0;
+    private _previousStartingPointerTime = 0;
+    private _pointerCaptures: { [pointerId: number]: boolean } = {};
+
+    // Keyboard
+    private _onKeyDown: (evt: KeyboardEvent) => void;
+    private _onKeyUp: (evt: KeyboardEvent) => void;
+    private _onCanvasFocusObserver: Nullable<Observer<Engine>>;
+    private _onCanvasBlurObserver: Nullable<Observer<Engine>>;
+
+    private _scene: Scene;
+
+    /**
+     * Creates a new InputManager
+     * @param scene defines the hosting scene
+     */
+    public constructor(scene: Scene) {
+        this._scene = scene;
+    }
+
+    /**
+    * Gets the mesh that is currently under the pointer
+    */
+    public get meshUnderPointer(): Nullable<AbstractMesh> {
+        return this._pointerOverMesh;
+    }
+
+    /**
+     * Gets the pointer coordinates in 2D without any translation (ie. straight out of the pointer event)
+     */
+    public get unTranslatedPointer(): Vector2 {
+        return new Vector2(this._unTranslatedPointerX, this._unTranslatedPointerY);
+    }
+
+    /**
+     * Gets or sets the current on-screen X position of the pointer
+     */
+    public get pointerX(): number {
+        return this._pointerX;
+    }
+
+    public set pointerX(value: number) {
+        this._pointerX = value;
+    }
+
+    /**
+     * Gets or sets the current on-screen Y position of the pointer
+     */
+    public get pointerY(): number {
+        return this._pointerY;
+    }
+
+    public set pointerY(value: number) {
+        this._pointerY = value;
+    }
+
+    private _updatePointerPosition(evt: PointerEvent): void {
+        var canvasRect = this._scene.getEngine().getRenderingCanvasClientRect();
+
+        if (!canvasRect) {
+            return;
+        }
+
+        this._pointerX = evt.clientX - canvasRect.left;
+        this._pointerY = evt.clientY - canvasRect.top;
+
+        this._unTranslatedPointerX = this._pointerX;
+        this._unTranslatedPointerY = this._pointerY;
+    }
+
+    private _processPointerMove(pickResult: Nullable<PickingInfo>, evt: PointerEvent) {
+        let scene = this._scene;
+        var canvas = scene.getEngine().getRenderingCanvas();
+
+        if (!canvas) {
+            return;
+        }
+
+        canvas.tabIndex = 1;
+
+        // Restore pointer
+        canvas.style.cursor = scene.defaultCursor;
+
+        var isMeshPicked = (pickResult && pickResult.hit && pickResult.pickedMesh) ? true : false;
+        if (isMeshPicked) {
+            scene.setPointerOverMesh(pickResult!.pickedMesh);
+
+            if (this._pointerOverMesh && this._pointerOverMesh.actionManager && this._pointerOverMesh.actionManager.hasPointerTriggers) {
+                if (this._pointerOverMesh.actionManager.hoverCursor) {
+                    canvas.style.cursor = this._pointerOverMesh.actionManager.hoverCursor;
+                } else {
+                    canvas.style.cursor = scene.hoverCursor;
+                }
+            }
+        } else {
+            scene.setPointerOverMesh(null);
+        }
+
+        for (let step of scene._pointerMoveStage) {
+            pickResult = step.action(this._unTranslatedPointerX, this._unTranslatedPointerY, pickResult, isMeshPicked, canvas);
+        }
+
+        if (pickResult) {
+            let type = evt.type === this._wheelEventName ? PointerEventTypes.POINTERWHEEL : PointerEventTypes.POINTERMOVE;
+
+            if (scene.onPointerMove) {
+                scene.onPointerMove(evt, pickResult, type);
+            }
+
+            if (scene.onPointerObservable.hasObservers()) {
+                let pi = new PointerInfo(type, evt, pickResult);
+                this._setRayOnPointerInfo(pi);
+                scene.onPointerObservable.notifyObservers(pi, type);
+            }
+        }
+    }
+
+    // Pointers handling
+    private _setRayOnPointerInfo(pointerInfo: PointerInfo) {
+        let scene = this._scene;
+        if (pointerInfo.pickInfo && !pointerInfo.pickInfo._pickingUnavailable) {
+            if (!pointerInfo.pickInfo.ray) {
+                pointerInfo.pickInfo.ray = scene.createPickingRay(pointerInfo.event.offsetX, pointerInfo.event.offsetY, Matrix.Identity(), scene.activeCamera);
+            }
+        }
+    }
+
+    private _checkPrePointerObservable(pickResult: Nullable<PickingInfo>, evt: PointerEvent, type: number) {
+        let scene = this._scene;
+        let pi = new PointerInfoPre(type, evt, this._unTranslatedPointerX, this._unTranslatedPointerY);
+        if (pickResult) {
+            pi.ray = pickResult.ray;
+        }
+        scene.onPrePointerObservable.notifyObservers(pi, type);
+        if (pi.skipOnPointerObservable) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Use this method to simulate a pointer move on a mesh
+     * The pickResult parameter can be obtained from a scene.pick or scene.pickWithRay
+     * @param pickResult pickingInfo of the object wished to simulate pointer event on
+     * @param pointerEventInit pointer event state to be used when simulating the pointer event (eg. pointer id for multitouch)
+     */
+    public simulatePointerMove(pickResult: PickingInfo, pointerEventInit?: PointerEventInit): void {
+        let evt = new PointerEvent("pointermove", pointerEventInit);
+
+        if (this._checkPrePointerObservable(pickResult, evt, PointerEventTypes.POINTERMOVE)) {
+            return;
+        }
+        this._processPointerMove(pickResult, evt);
+    }
+
+    /**
+     * Use this method to simulate a pointer down on a mesh
+     * The pickResult parameter can be obtained from a scene.pick or scene.pickWithRay
+     * @param pickResult pickingInfo of the object wished to simulate pointer event on
+     * @param pointerEventInit pointer event state to be used when simulating the pointer event (eg. pointer id for multitouch)
+     */
+    public simulatePointerDown(pickResult: PickingInfo, pointerEventInit?: PointerEventInit): void {
+        let evt = new PointerEvent("pointerdown", pointerEventInit);
+
+        if (this._checkPrePointerObservable(pickResult, evt, PointerEventTypes.POINTERDOWN)) {
+            return;
+        }
+
+        this._processPointerDown(pickResult, evt);
+    }
+
+    private _processPointerDown(pickResult: Nullable<PickingInfo>, evt: PointerEvent): void {
+        let scene = this._scene;
+        if (pickResult && pickResult.hit && pickResult.pickedMesh) {
+            this._pickedDownMesh = pickResult.pickedMesh;
+            var actionManager = pickResult.pickedMesh.actionManager;
+            if (actionManager) {
+                if (actionManager.hasPickTriggers) {
+                    actionManager.processTrigger(Constants.ACTION_OnPickDownTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
+                    switch (evt.button) {
+                        case 0:
+                            actionManager.processTrigger(Constants.ACTION_OnLeftPickTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
+                            break;
+                        case 1:
+                            actionManager.processTrigger(Constants.ACTION_OnCenterPickTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
+                            break;
+                        case 2:
+                            actionManager.processTrigger(Constants.ACTION_OnRightPickTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
+                            break;
+                    }
+                }
+
+                if (actionManager.hasSpecificTrigger(Constants.ACTION_OnLongPressTrigger)) {
+                    window.setTimeout(() => {
+                        var pickResult = scene.pick(this._unTranslatedPointerX, this._unTranslatedPointerY,
+                            (mesh: AbstractMesh): boolean => (<boolean>(mesh.isPickable && mesh.isVisible && mesh.isReady() && mesh.actionManager && mesh.actionManager.hasSpecificTrigger(Constants.ACTION_OnLongPressTrigger) && mesh == this._pickedDownMesh)),
+                            false, scene.cameraToUseForPointers);
+
+                        if (pickResult && pickResult.hit && pickResult.pickedMesh && actionManager) {
+                            if (this._totalPointersPressed !== 0 &&
+                                ((Date.now() - this._startingPointerTime) > InputManager.LongPressDelay) &&
+                                !this._isPointerSwiping()) {
+                                this._startingPointerTime = 0;
+                                actionManager.processTrigger(Constants.ACTION_OnLongPressTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
+                            }
+                        }
+                    }, InputManager.LongPressDelay);
+                }
+            }
+        }
+        else {
+            for (let step of scene._pointerDownStage) {
+                pickResult = step.action(this._unTranslatedPointerX, this._unTranslatedPointerY, pickResult, evt);
+            }
+        }
+
+        if (pickResult) {
+            let type = PointerEventTypes.POINTERDOWN;
+
+            if (scene.onPointerDown) {
+                scene.onPointerDown(evt, pickResult, type);
+            }
+
+            if (scene.onPointerObservable.hasObservers()) {
+                let pi = new PointerInfo(type, evt, pickResult);
+                this._setRayOnPointerInfo(pi);
+                scene.onPointerObservable.notifyObservers(pi, type);
+            }
+        }
+    }
+
+    /** @hidden */
+    public _isPointerSwiping(): boolean {
+        return Math.abs(this._startingPointerPosition.x - this._pointerX) > InputManager.DragMovementThreshold ||
+            Math.abs(this._startingPointerPosition.y - this._pointerY) > InputManager.DragMovementThreshold;
+    }
+
+    /**
+     * Use this method to simulate a pointer up on a mesh
+     * The pickResult parameter can be obtained from a scene.pick or scene.pickWithRay
+     * @param pickResult pickingInfo of the object wished to simulate pointer event on
+     * @param pointerEventInit pointer event state to be used when simulating the pointer event (eg. pointer id for multitouch)
+     * @param doubleTap indicates that the pointer up event should be considered as part of a double click (false by default)
+     */
+    public simulatePointerUp(pickResult: PickingInfo, pointerEventInit?: PointerEventInit, doubleTap?: boolean): void {
+        let evt = new PointerEvent("pointerup", pointerEventInit);
+        let clickInfo = new _ClickInfo();
+
+        if (doubleTap) {
+            clickInfo.doubleClick = true;
+        } else {
+            clickInfo.singleClick = true;
+        }
+
+        if (this._checkPrePointerObservable(pickResult, evt, PointerEventTypes.POINTERUP)) {
+            return;
+        }
+
+        this._processPointerUp(pickResult, evt, clickInfo);
+    }
+
+    private _processPointerUp(pickResult: Nullable<PickingInfo>, evt: PointerEvent, clickInfo: _ClickInfo): void {
+        let scene = this._scene;
+        if (pickResult && pickResult && pickResult.pickedMesh) {
+            this._pickedUpMesh = pickResult.pickedMesh;
+            if (this._pickedDownMesh === this._pickedUpMesh) {
+                if (scene.onPointerPick) {
+                    scene.onPointerPick(evt, pickResult);
+                }
+                if (clickInfo.singleClick && !clickInfo.ignore && scene.onPointerObservable.hasObservers()) {
+                    let type = PointerEventTypes.POINTERPICK;
+                    let pi = new PointerInfo(type, evt, pickResult);
+                    this._setRayOnPointerInfo(pi);
+                    scene.onPointerObservable.notifyObservers(pi, type);
+                }
+            }
+            let actionManager = pickResult.pickedMesh._getActionManagerForTrigger();
+            if (actionManager && !clickInfo.ignore) {
+                actionManager.processTrigger(Constants.ACTION_OnPickUpTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
+
+                if (!clickInfo.hasSwiped && clickInfo.singleClick) {
+                    actionManager.processTrigger(Constants.ACTION_OnPickTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
+                }
+
+                let doubleClickActionManager = pickResult.pickedMesh._getActionManagerForTrigger(Constants.ACTION_OnDoublePickTrigger);
+                if (clickInfo.doubleClick && doubleClickActionManager) {
+                    doubleClickActionManager.processTrigger(Constants.ACTION_OnDoublePickTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
+                }
+            }
+        }
+        else {
+            if (!clickInfo.ignore) {
+                for (let step of scene._pointerUpStage) {
+                    pickResult = step.action(this._unTranslatedPointerX, this._unTranslatedPointerY, pickResult, evt);
+                }
+            }
+        }
+
+        if (this._pickedDownMesh && this._pickedDownMesh !== this._pickedUpMesh) {
+            let pickedDownActionManager = this._pickedDownMesh._getActionManagerForTrigger(Constants.ACTION_OnPickOutTrigger);
+            if (pickedDownActionManager) {
+                pickedDownActionManager.processTrigger(Constants.ACTION_OnPickOutTrigger, ActionEvent.CreateNew(this._pickedDownMesh, evt));
+            }
+        }
+
+        let type = 0;
+        if (scene.onPointerObservable.hasObservers()) {
+            if (!clickInfo.ignore && !clickInfo.hasSwiped) {
+                if (clickInfo.singleClick && scene.onPointerObservable.hasSpecificMask(PointerEventTypes.POINTERTAP)) {
+                    type = PointerEventTypes.POINTERTAP;
+                }
+                else if (clickInfo.doubleClick && scene.onPointerObservable.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP)) {
+                    type = PointerEventTypes.POINTERDOUBLETAP;
+                }
+                if (type) {
+                    let pi = new PointerInfo(type, evt, pickResult);
+                    this._setRayOnPointerInfo(pi);
+                    scene.onPointerObservable.notifyObservers(pi, type);
+                }
+            }
+
+            if (!clickInfo.ignore) {
+                type = PointerEventTypes.POINTERUP;
+
+                let pi = new PointerInfo(type, evt, pickResult);
+                this._setRayOnPointerInfo(pi);
+                scene.onPointerObservable.notifyObservers(pi, type);
+            }
+        }
+
+        if (scene.onPointerUp && !clickInfo.ignore) {
+            scene.onPointerUp(evt, pickResult, type);
+        }
+    }
+
+    /**
+     * Gets a boolean indicating if the current pointer event is captured (meaning that the scene has already handled the pointer down)
+     * @param pointerId defines the pointer id to use in a multi-touch scenario (0 by default)
+     * @returns true if the pointer was captured
+     */
+    public isPointerCaptured(pointerId = 0): boolean {
+        return this._pointerCaptures[pointerId];
+    }
+
+    /**
+    * Attach events to the canvas (To handle actionManagers triggers and raise onPointerMove, onPointerDown and onPointerUp
+    * @param attachUp defines if you want to attach events to pointerup
+    * @param attachDown defines if you want to attach events to pointerdown
+    * @param attachMove defines if you want to attach events to pointermove
+    */
+    public attachControl(attachUp = true, attachDown = true, attachMove = true): void {
+        let scene = this._scene;
+        var canvas = scene.getEngine().getRenderingCanvas();
+
+        if (!canvas) {
+            return;
+        }
+
+        let engine = scene.getEngine();
+
+        this._initActionManager = (act: Nullable<AbstractActionManager>, clickInfo: _ClickInfo): Nullable<AbstractActionManager> => {
+            if (!this._meshPickProceed) {
+                let pickResult = scene.pick(this._unTranslatedPointerX, this._unTranslatedPointerY, scene.pointerDownPredicate, false, scene.cameraToUseForPointers);
+                this._currentPickResult = pickResult;
+                if (pickResult) {
+                    act = (pickResult.hit && pickResult.pickedMesh) ? pickResult.pickedMesh._getActionManagerForTrigger() : null;
+                }
+                this._meshPickProceed = true;
+            }
+            return act;
+        };
+
+        this._delayedSimpleClick = (btn: number, clickInfo: _ClickInfo, cb: (clickInfo: _ClickInfo, pickResult: Nullable<PickingInfo>) => void) => {
+            // double click delay is over and that no double click has been raised since, or the 2 consecutive keys pressed are different
+            if ((Date.now() - this._previousStartingPointerTime > InputManager.DoubleClickDelay && !this._doubleClickOccured) ||
+                btn !== this._previousButtonPressed) {
+                this._doubleClickOccured = false;
+                clickInfo.singleClick = true;
+                clickInfo.ignore = false;
+                cb(clickInfo, this._currentPickResult);
+            }
+        };
+
+        this._initClickEvent = (obs1: Observable<PointerInfoPre>, obs2: Observable<PointerInfo>, evt: PointerEvent, cb: (clickInfo: _ClickInfo, pickResult: Nullable<PickingInfo>) => void): void => {
+            let clickInfo = new _ClickInfo();
+            this._currentPickResult = null;
+            let act: Nullable<AbstractActionManager> = null;
+
+            let checkPicking = obs1.hasSpecificMask(PointerEventTypes.POINTERPICK) || obs2.hasSpecificMask(PointerEventTypes.POINTERPICK)
+                || obs1.hasSpecificMask(PointerEventTypes.POINTERTAP) || obs2.hasSpecificMask(PointerEventTypes.POINTERTAP)
+                || obs1.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP) || obs2.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP);
+            if (!checkPicking && AbstractActionManager) {
+                act = this._initActionManager(act, clickInfo);
+                if (act) {
+                    checkPicking = act.hasPickTriggers;
+                }
+            }
+
+            let needToIgnoreNext = false;
+
+            if (checkPicking) {
+                let btn = evt.button;
+                clickInfo.hasSwiped = this._isPointerSwiping();
+
+                if (!clickInfo.hasSwiped) {
+                    let checkSingleClickImmediately = !InputManager.ExclusiveDoubleClickMode;
+
+                    if (!checkSingleClickImmediately) {
+                        checkSingleClickImmediately = !obs1.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP) &&
+                            !obs2.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP);
+
+                        if (checkSingleClickImmediately && !AbstractActionManager.HasSpecificTrigger(Constants.ACTION_OnDoublePickTrigger)) {
+                            act = this._initActionManager(act, clickInfo);
+                            if (act) {
+                                checkSingleClickImmediately = !act.hasSpecificTrigger(Constants.ACTION_OnDoublePickTrigger);
+                            }
+                        }
+                    }
+
+                    if (checkSingleClickImmediately) {
+                        // single click detected if double click delay is over or two different successive keys pressed without exclusive double click or no double click required
+                        if (Date.now() - this._previousStartingPointerTime > InputManager.DoubleClickDelay ||
+                            btn !== this._previousButtonPressed) {
+                            clickInfo.singleClick = true;
+                            cb(clickInfo, this._currentPickResult);
+                            needToIgnoreNext = true;
+                        }
+                    }
+                    // at least one double click is required to be check and exclusive double click is enabled
+                    else {
+                        // wait that no double click has been raised during the double click delay
+                        this._previousDelayedSimpleClickTimeout = this._delayedSimpleClickTimeout;
+                        this._delayedSimpleClickTimeout = window.setTimeout(this._delayedSimpleClick.bind(this, btn, clickInfo, cb), InputManager.DoubleClickDelay);
+                    }
+
+                    let checkDoubleClick = obs1.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP) ||
+                        obs2.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP);
+                    if (!checkDoubleClick && AbstractActionManager.HasSpecificTrigger(Constants.ACTION_OnDoublePickTrigger)) {
+                        act = this._initActionManager(act, clickInfo);
+                        if (act) {
+                            checkDoubleClick = act.hasSpecificTrigger(Constants.ACTION_OnDoublePickTrigger);
+                        }
+                    }
+                    if (checkDoubleClick) {
+                        // two successive keys pressed are equal, double click delay is not over and double click has not just occurred
+                        if (btn === this._previousButtonPressed &&
+                            Date.now() - this._previousStartingPointerTime < InputManager.DoubleClickDelay &&
+                            !this._doubleClickOccured
+                        ) {
+                            // pointer has not moved for 2 clicks, it's a double click
+                            if (!clickInfo.hasSwiped &&
+                                !this._isPointerSwiping()) {
+                                this._previousStartingPointerTime = 0;
+                                this._doubleClickOccured = true;
+                                clickInfo.doubleClick = true;
+                                clickInfo.ignore = false;
+                                if (InputManager.ExclusiveDoubleClickMode && this._previousDelayedSimpleClickTimeout) {
+                                    clearTimeout(this._previousDelayedSimpleClickTimeout);
+                                }
+                                this._previousDelayedSimpleClickTimeout = this._delayedSimpleClickTimeout;
+                                cb(clickInfo, this._currentPickResult);
+                            }
+                            // if the two successive clicks are too far, it's just two simple clicks
+                            else {
+                                this._doubleClickOccured = false;
+                                this._previousStartingPointerTime = this._startingPointerTime;
+                                this._previousStartingPointerPosition.x = this._startingPointerPosition.x;
+                                this._previousStartingPointerPosition.y = this._startingPointerPosition.y;
+                                this._previousButtonPressed = btn;
+                                if (InputManager.ExclusiveDoubleClickMode) {
+                                    if (this._previousDelayedSimpleClickTimeout) {
+                                        clearTimeout(this._previousDelayedSimpleClickTimeout);
+                                    }
+                                    this._previousDelayedSimpleClickTimeout = this._delayedSimpleClickTimeout;
+
+                                    cb(clickInfo, this._previousPickResult);
+                                }
+                                else {
+                                    cb(clickInfo, this._currentPickResult);
+                                }
+                            }
+                            needToIgnoreNext = true;
+                        }
+                        // just the first click of the double has been raised
+                        else {
+                            this._doubleClickOccured = false;
+                            this._previousStartingPointerTime = this._startingPointerTime;
+                            this._previousStartingPointerPosition.x = this._startingPointerPosition.x;
+                            this._previousStartingPointerPosition.y = this._startingPointerPosition.y;
+                            this._previousButtonPressed = btn;
+                        }
+                    }
+                }
+            }
+
+            if (!needToIgnoreNext) {
+                cb(clickInfo, this._currentPickResult);
+            }
+        };
+
+        this._onPointerMove = (evt: PointerEvent) => {
+
+            this._updatePointerPosition(evt);
+
+            // PreObservable support
+            if (this._checkPrePointerObservable(null, evt, evt.type === this._wheelEventName ? PointerEventTypes.POINTERWHEEL : PointerEventTypes.POINTERMOVE)) {
+                return;
+            }
+
+            if (!scene.cameraToUseForPointers && !scene.activeCamera) {
+                return;
+            }
+
+            if (!scene.pointerMovePredicate) {
+                scene.pointerMovePredicate = (mesh: AbstractMesh): boolean => (mesh.isPickable && mesh.isVisible && mesh.isReady() && mesh.isEnabled() && (mesh.enablePointerMoveEvents || scene.constantlyUpdateMeshUnderPointer || (mesh.actionManager !== null && mesh.actionManager !== undefined)) && (!scene.cameraToUseForPointers || (scene.cameraToUseForPointers.layerMask & mesh.layerMask) !== 0));
+            }
+
+            // Meshes
+            var pickResult = scene.pick(this._unTranslatedPointerX, this._unTranslatedPointerY, scene.pointerMovePredicate, false, scene.cameraToUseForPointers);
+
+            this._processPointerMove(pickResult, evt);
+        };
+
+        this._onPointerDown = (evt: PointerEvent) => {
+            this._totalPointersPressed++;
+            this._pickedDownMesh = null;
+            this._meshPickProceed = false;
+
+            this._updatePointerPosition(evt);
+
+            if (scene.preventDefaultOnPointerDown && canvas) {
+                evt.preventDefault();
+                canvas.focus();
+            }
+
+            this._startingPointerPosition.x = this._pointerX;
+            this._startingPointerPosition.y = this._pointerY;
+            this._startingPointerTime = Date.now();
+
+            // PreObservable support
+            if (this._checkPrePointerObservable(null, evt, PointerEventTypes.POINTERDOWN)) {
+                return;
+            }
+
+            if (!scene.cameraToUseForPointers && !scene.activeCamera) {
+                return;
+            }
+
+            this._pointerCaptures[evt.pointerId] = true;
+
+            if (!scene.pointerDownPredicate) {
+                scene.pointerDownPredicate = (mesh: AbstractMesh): boolean => {
+                    return mesh.isPickable && mesh.isVisible && mesh.isReady() && mesh.isEnabled() && (!scene.cameraToUseForPointers || (scene.cameraToUseForPointers.layerMask & mesh.layerMask) !== 0);
+                };
+            }
+
+            // Meshes
+            this._pickedDownMesh = null;
+            var pickResult = scene.pick(this._unTranslatedPointerX, this._unTranslatedPointerY, scene.pointerDownPredicate, false, scene.cameraToUseForPointers);
+
+            this._processPointerDown(pickResult, evt);
+        };
+
+        this._onPointerUp = (evt: PointerEvent) => {
+            if (this._totalPointersPressed === 0) {  // We are attaching the pointer up to windows because of a bug in FF
+                return;                             // So we need to test it the pointer down was pressed before.
+            }
+
+            this._totalPointersPressed--;
+            this._pickedUpMesh = null;
+            this._meshPickProceed = false;
+
+            this._updatePointerPosition(evt);
+
+            if (scene.preventDefaultOnPointerUp && canvas) {
+                evt.preventDefault();
+                canvas.focus();
+            }
+
+            this._initClickEvent(scene.onPrePointerObservable, scene.onPointerObservable, evt, (clickInfo: _ClickInfo, pickResult: Nullable<PickingInfo>) => {
+                // PreObservable support
+                if (scene.onPrePointerObservable.hasObservers()) {
+                    if (!clickInfo.ignore) {
+                        if (!clickInfo.hasSwiped) {
+                            if (clickInfo.singleClick && scene.onPrePointerObservable.hasSpecificMask(PointerEventTypes.POINTERTAP)) {
+                                if (this._checkPrePointerObservable(null, evt, PointerEventTypes.POINTERTAP)) {
+                                    return;
+                                }
+                            }
+                            if (clickInfo.doubleClick && scene.onPrePointerObservable.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP)) {
+                                if (this._checkPrePointerObservable(null, evt, PointerEventTypes.POINTERDOUBLETAP)) {
+                                    return;
+                                }
+                            }
+                        }
+                        if (this._checkPrePointerObservable(null, evt, PointerEventTypes.POINTERUP)) {
+                            return;
+                        }
+                    }
+                }
+
+                if (!this._pointerCaptures[evt.pointerId]) {
+                    return;
+                }
+
+                this._pointerCaptures[evt.pointerId] = false;
+                if (!scene.cameraToUseForPointers && !scene.activeCamera) {
+                    return;
+                }
+
+                if (!scene.pointerUpPredicate) {
+                    scene.pointerUpPredicate = (mesh: AbstractMesh): boolean => {
+                        return mesh.isPickable && mesh.isVisible && mesh.isReady() && mesh.isEnabled() && (!scene.cameraToUseForPointers || (scene.cameraToUseForPointers.layerMask & mesh.layerMask) !== 0);
+                    };
+                }
+
+                // Meshes
+                if (!this._meshPickProceed && (AbstractActionManager && AbstractActionManager.HasTriggers || scene.onPointerObservable.hasObservers())) {
+                    this._initActionManager(null, clickInfo);
+                }
+                if (!pickResult) {
+                    pickResult = this._currentPickResult;
+                }
+
+                this._processPointerUp(pickResult, evt, clickInfo);
+
+                this._previousPickResult = this._currentPickResult;
+            });
+        };
+
+        this._onKeyDown = (evt: KeyboardEvent) => {
+            let type = KeyboardEventTypes.KEYDOWN;
+            if (scene.onPreKeyboardObservable.hasObservers()) {
+                let pi = new KeyboardInfoPre(type, evt);
+                scene.onPreKeyboardObservable.notifyObservers(pi, type);
+                if (pi.skipOnPointerObservable) {
+                    return;
+                }
+            }
+
+            if (scene.onKeyboardObservable.hasObservers()) {
+                let pi = new KeyboardInfo(type, evt);
+                scene.onKeyboardObservable.notifyObservers(pi, type);
+            }
+
+            if (scene.actionManager) {
+                scene.actionManager.processTrigger(Constants.ACTION_OnKeyDownTrigger, ActionEvent.CreateNewFromScene(scene, evt));
+            }
+        };
+
+        this._onKeyUp = (evt: KeyboardEvent) => {
+            let type = KeyboardEventTypes.KEYUP;
+            if (scene.onPreKeyboardObservable.hasObservers()) {
+                let pi = new KeyboardInfoPre(type, evt);
+                scene.onPreKeyboardObservable.notifyObservers(pi, type);
+                if (pi.skipOnPointerObservable) {
+                    return;
+                }
+            }
+
+            if (scene.onKeyboardObservable.hasObservers()) {
+                let pi = new KeyboardInfo(type, evt);
+                scene.onKeyboardObservable.notifyObservers(pi, type);
+            }
+
+            if (scene.actionManager) {
+                scene.actionManager.processTrigger(Constants.ACTION_OnKeyUpTrigger, ActionEvent.CreateNewFromScene(scene, evt));
+            }
+        };
+
+        // Keyboard events
+        this._onCanvasFocusObserver = engine.onCanvasFocusObservable.add(() => {
+            if (!canvas) {
+                return;
+            }
+            canvas.addEventListener("keydown", this._onKeyDown, false);
+            canvas.addEventListener("keyup", this._onKeyUp, false);
+        });
+
+        this._onCanvasBlurObserver = engine.onCanvasBlurObservable.add(() => {
+            if (!canvas) {
+                return;
+            }
+            canvas.removeEventListener("keydown", this._onKeyDown);
+            canvas.removeEventListener("keyup", this._onKeyUp);
+        });
+
+        // Pointer events
+        var eventPrefix = Tools.GetPointerPrefix();
+
+        if (attachMove) {
+            canvas.addEventListener(eventPrefix + "move", <any>this._onPointerMove, false);
+
+            // Wheel
+            this._wheelEventName = "onwheel" in document.createElement("div") ? "wheel" :       // Modern browsers support "wheel"
+                (<any>document).onmousewheel !== undefined ? "mousewheel" :                     // Webkit and IE support at least "mousewheel"
+                    "DOMMouseScroll";                                                           // let's assume that remaining browsers are older Firefox
+
+            canvas.addEventListener(this._wheelEventName, <any>this._onPointerMove, false);
+        }
+
+        if (attachDown) {
+            canvas.addEventListener(eventPrefix + "down", <any>this._onPointerDown, false);
+        }
+
+        if (attachUp) {
+            window.addEventListener(eventPrefix + "up", <any>this._onPointerUp, false);
+        }
+    }
+
+    /**
+     * Detaches all event handlers
+     */
+    public detachControl() {
+        const eventPrefix = Tools.GetPointerPrefix();
+        const canvas = this._scene.getEngine().getRenderingCanvas();
+        const engine = this._scene.getEngine();
+
+        if (!canvas) {
+            return;
+        }
+
+        // Pointer
+        canvas.removeEventListener(eventPrefix + "move", <any>this._onPointerMove);
+        canvas.removeEventListener(eventPrefix + "down", <any>this._onPointerDown);
+        window.removeEventListener(eventPrefix + "up", <any>this._onPointerUp);
+
+        // Blur / Focus
+        if (this._onCanvasBlurObserver) {
+            engine.onCanvasBlurObservable.remove(this._onCanvasBlurObserver);
+        }
+
+        if (this._onCanvasFocusObserver) {
+            engine.onCanvasFocusObservable.remove(this._onCanvasFocusObserver);
+        }
+
+        // Keyboard
+        canvas.removeEventListener("keydown", this._onKeyDown);
+        canvas.removeEventListener("keyup", this._onKeyUp);
+
+        // Cursor
+        canvas.style.cursor = this._scene.defaultCursor;
+    }
+
+    /**
+     * Force the value of meshUnderPointer
+     * @param mesh defines the mesh to use
+     */
+    public setPointerOverMesh(mesh: Nullable<AbstractMesh>): void {
+        if (this._pointerOverMesh === mesh) {
+            return;
+        }
+
+        let actionManager: Nullable<AbstractActionManager>;
+        if (this._pointerOverMesh) {
+            actionManager = this._pointerOverMesh._getActionManagerForTrigger(Constants.ACTION_OnPointerOutTrigger);
+            if (actionManager) {
+                actionManager.processTrigger(Constants.ACTION_OnPointerOutTrigger, ActionEvent.CreateNew(this._pointerOverMesh));
+            }
+        }
+
+        this._pointerOverMesh = mesh;
+        if (this._pointerOverMesh) {
+            actionManager = this._pointerOverMesh._getActionManagerForTrigger(Constants.ACTION_OnPointerOverTrigger);
+            if (actionManager) {
+                actionManager.processTrigger(Constants.ACTION_OnPointerOverTrigger, ActionEvent.CreateNew(this._pointerOverMesh));
+            }
+        }
+    }
+
+    /**
+     * Gets the mesh under the pointer
+     * @returns a Mesh or null if no mesh is under the pointer
+     */
+    public getPointerOverMesh(): Nullable<AbstractMesh> {
+        return this._pointerOverMesh;
+    }
+}

+ 5 - 1
src/Meshes/Compression/dracoCompression.ts

@@ -117,7 +117,11 @@ export class DracoCompression implements IDisposable {
     public static DefaultNumWorkers = DracoCompression.GetDefaultNumWorkers();
 
     private static GetDefaultNumWorkers(): number {
-        const hardwareConcurrency = navigator && navigator.hardwareConcurrency;
+        if (typeof navigator === "undefined") {
+            return 1;
+        }
+
+        const hardwareConcurrency =  navigator.hardwareConcurrency;
         if (!hardwareConcurrency) {
             return 1;
         }

+ 1 - 1
src/Sprites/spriteSceneComponent.ts

@@ -295,7 +295,7 @@ export class SpriteSceneComponent implements ISceneComponent {
                     if (spritePickResult.pickedSprite.actionManager) {
                         spritePickResult.pickedSprite.actionManager.processTrigger(Constants.ACTION_OnPickUpTrigger, ActionEvent.CreateNewFromSprite(spritePickResult.pickedSprite, scene, evt));
                         if (spritePickResult.pickedSprite.actionManager) {
-                            if (!this.scene._isPointerSwiping()) {
+                            if (!this.scene._inputManager._isPointerSwiping()) {
                                 spritePickResult.pickedSprite.actionManager.processTrigger(Constants.ACTION_OnPickTrigger, ActionEvent.CreateNewFromSprite(spritePickResult.pickedSprite, scene, evt));
                             }
                         }

+ 64 - 740
src/scene.ts

@@ -29,7 +29,7 @@ import { Light } from "./Lights/light";
 import { PickingInfo } from "./Collisions/pickingInfo";
 import { ICollisionCoordinator } from "./Collisions/collisionCoordinator";
 import { PointerEventTypes, PointerInfoPre, PointerInfo } from "./Events/pointerEvents";
-import { KeyboardInfoPre, KeyboardInfo, KeyboardEventTypes } from "./Events/keyboardEvents";
+import { KeyboardInfoPre, KeyboardInfo } from "./Events/keyboardEvents";
 import { ActionEvent } from "./Actions/actionEvent";
 import { PostProcess } from "./PostProcesses/postProcess";
 import { PostProcessManager } from "./PostProcesses/postProcessManager";
@@ -46,6 +46,7 @@ import { EngineStore } from "./Engines/engineStore";
 import { AbstractActionManager } from './Actions/abstractActionManager';
 import { _DevTools } from './Misc/devTools';
 import { WebRequest } from './Misc/webRequest';
+import { InputManager } from './Inputs/scene.inputManager';
 
 declare type Ray = import("./Culling/ray").Ray;
 declare type TrianglePickingPredicate = import("./Culling/ray").TrianglePickingPredicate;
@@ -65,40 +66,6 @@ export interface IDisposable {
     dispose(): void;
 }
 
-/** @hidden */
-class ClickInfo {
-    private _singleClick = false;
-    private _doubleClick = false;
-    private _hasSwiped = false;
-    private _ignore = false;
-
-    public get singleClick(): boolean {
-        return this._singleClick;
-    }
-    public get doubleClick(): boolean {
-        return this._doubleClick;
-    }
-    public get hasSwiped(): boolean {
-        return this._hasSwiped;
-    }
-    public get ignore(): boolean {
-        return this._ignore;
-    }
-
-    public set singleClick(b: boolean) {
-        this._singleClick = b;
-    }
-    public set doubleClick(b: boolean) {
-        this._doubleClick = b;
-    }
-    public set hasSwiped(b: boolean) {
-        this._hasSwiped = b;
-    }
-    public set ignore(b: boolean) {
-        this._ignore = b;
-    }
-}
-
 /** Interface defining initialization parameters for Scene class */
 export interface SceneOptions {
     /**
@@ -169,6 +136,12 @@ export class Scene extends AbstractScene implements IAnimatable {
     // Members
 
     /** @hidden */
+    public _inputManager = new InputManager(this);
+
+    /** Define this parameter if you are using multiple cameras and you want to specify which one should be used for pointer position */
+    public cameraToUseForPointers: Nullable<Camera> = null;
+
+    /** @hidden */
     public readonly _isScene = true;
 
     /**
@@ -634,9 +607,6 @@ export class Scene extends AbstractScene implements IAnimatable {
      * Gets or sets a predicate used to select candidate meshes for a pointer move event
      */
     public pointerMovePredicate: (Mesh: AbstractMesh) => boolean;
-    private _onPointerMove: (evt: PointerEvent) => void;
-    private _onPointerDown: (evt: PointerEvent) => void;
-    private _onPointerUp: (evt: PointerEvent) => void;
 
     /** Callback called when a pointer move is detected */
     public onPointerMove: (evt: PointerEvent, pickInfo: PickingInfo, type: PointerEventTypes) => void;
@@ -662,47 +632,50 @@ export class Scene extends AbstractScene implements IAnimatable {
      * Gets the pointer coordinates without any translation (ie. straight out of the pointer event)
      */
     public get unTranslatedPointer(): Vector2 {
-        return new Vector2(this._unTranslatedPointerX, this._unTranslatedPointerY);
+        return this._inputManager.unTranslatedPointer;
     }
 
-    /** The distance in pixel that you have to move to prevent some events */
-    public static DragMovementThreshold = 10; // in pixels
-    /** Time in milliseconds to wait to raise long press events if button is still pressed */
-    public static LongPressDelay = 500; // in milliseconds
-    /** Time in milliseconds with two consecutive clicks will be considered as a double click */
-    public static DoubleClickDelay = 300; // in milliseconds
-    /** If you need to check double click without raising a single click at first click, enable this flag */
-    public static ExclusiveDoubleClickMode = false;
+    /**
+     * Gets or sets the distance in pixel that you have to move to prevent some events. Default is 10 pixels
+     */
+    public static get DragMovementThreshold() {
+        return InputManager.DragMovementThreshold;
+    }
 
-    private _initClickEvent: (obs1: Observable<PointerInfoPre>, obs2: Observable<PointerInfo>, evt: PointerEvent, cb: (clickInfo: ClickInfo, pickResult: Nullable<PickingInfo>) => void) => void;
-    private _initActionManager: (act: Nullable<AbstractActionManager>, clickInfo: ClickInfo) => Nullable<AbstractActionManager>;
-    private _delayedSimpleClick: (btn: number, clickInfo: ClickInfo, cb: (clickInfo: ClickInfo, pickResult: Nullable<PickingInfo>) => void) => void;
-    private _delayedSimpleClickTimeout: number;
-    private _previousDelayedSimpleClickTimeout: number;
-    private _meshPickProceed = false;
+    public static set DragMovementThreshold(value: number) {
+        InputManager.DragMovementThreshold = value;
+    }
 
-    private _previousButtonPressed: number;
-    private _currentPickResult: Nullable<PickingInfo> = null;
-    private _previousPickResult: Nullable<PickingInfo> = null;
-    private _totalPointersPressed = 0;
-    private _doubleClickOccured = false;
+    /**
+     * Time in milliseconds to wait to raise long press events if button is still pressed. Default is 500 ms
+     */
+    public static get LongPressDelay() {
+        return InputManager.LongPressDelay;
+    }
 
-    /** Define this parameter if you are using multiple cameras and you want to specify which one should be used for pointer position */
-    public cameraToUseForPointers: Nullable<Camera> = null;
-    private _pointerX: number = 0;
-    private _pointerY: number = 0;
-    private _unTranslatedPointerX: number;
-    private _unTranslatedPointerY: number;
-    private _startingPointerPosition = new Vector2(0, 0);
-    private _previousStartingPointerPosition = new Vector2(0, 0);
-    private _startingPointerTime = 0;
-    private _previousStartingPointerTime = 0;
-    private _pointerCaptures: { [pointerId: number]: boolean } = {};
+    public static set LongPressDelay(value: number) {
+        InputManager.LongPressDelay = value;
+    }
 
-    // Deterministic lockstep
-    private _timeAccumulator: number = 0;
-    private _currentStepId: number = 0;
-    private _currentInternalStep: number = 0;
+    /**
+     * Time in milliseconds to wait to raise long press events if button is still pressed. Default is 300 ms
+     */
+    public static get DoubleClickDelay() {
+        return InputManager.DoubleClickDelay;
+    }
+
+    public static set DoubleClickDelay(value: number) {
+        InputManager.DoubleClickDelay = value;
+    }
+
+    /** If you need to check double click without raising a single click at first click, enable this flag */
+    public static get ExclusiveDoubleClickMode() {
+        return InputManager.ExclusiveDoubleClickMode;
+    }
+
+    public static set ExclusiveDoubleClickMode(value: boolean) {
+        InputManager.ExclusiveDoubleClickMode = value;
+    }
 
     // Mirror
     /** @hidden */
@@ -720,10 +693,6 @@ export class Scene extends AbstractScene implements IAnimatable {
      * Observable event triggered each time an keyboard event is received from the hosting window
      */
     public onKeyboardObservable = new Observable<KeyboardInfo>();
-    private _onKeyDown: (evt: KeyboardEvent) => void;
-    private _onKeyUp: (evt: KeyboardEvent) => void;
-    private _onCanvasFocusObserver: Nullable<Observer<Engine>>;
-    private _onCanvasBlurObserver: Nullable<Observer<Engine>>;
 
     // Coordinates system
 
@@ -742,6 +711,11 @@ export class Scene extends AbstractScene implements IAnimatable {
         return this._useRightHandedSystem;
     }
 
+    // Deterministic lockstep
+    private _timeAccumulator: number = 0;
+    private _currentStepId: number = 0;
+    private _currentInternalStep: number = 0;
+
     /**
      * Sets the step Id used by deterministic lock step
      * @see http://doc.babylonjs.com/babylon101/animations#deterministic-lockstep
@@ -1119,7 +1093,6 @@ export class Scene extends AbstractScene implements IAnimatable {
     /** @hidden */
     public _viewMatrix: Matrix;
     private _projectionMatrix: Matrix;
-    private _wheelEventName = "";
     /** @hidden */
     public _forcedViewPosition: Nullable<Vector3>;
 
@@ -1143,10 +1116,6 @@ export class Scene extends AbstractScene implements IAnimatable {
     /** @hidden */
     public readonly useClonedMeshhMap: boolean;
 
-    private _pointerOverMesh: Nullable<AbstractMesh>;
-
-    private _pickedDownMesh: Nullable<AbstractMesh>;
-    private _pickedUpMesh: Nullable<AbstractMesh>;
     private _externalData: StringDictionary<Object>;
     private _uid: Nullable<string>;
 
@@ -1418,29 +1387,29 @@ export class Scene extends AbstractScene implements IAnimatable {
      * Gets the mesh that is currently under the pointer
      */
     public get meshUnderPointer(): Nullable<AbstractMesh> {
-        return this._pointerOverMesh;
+        return this._inputManager.meshUnderPointer;
     }
 
     /**
      * Gets or sets the current on-screen X position of the pointer
      */
     public get pointerX(): number {
-        return this._pointerX;
+        return this._inputManager.pointerX;
     }
 
     public set pointerX(value: number) {
-        this._pointerX = value;
+        this._inputManager.pointerX = value;
     }
 
     /**
      * Gets or sets the current on-screen Y position of the pointer
      */
     public get pointerY(): number {
-        return this._pointerY;
+        return this._inputManager.pointerY;
     }
 
     public set pointerY(value: number) {
-        this._pointerY = value;
+        this._inputManager.pointerY = value;
     }
 
     /**
@@ -1587,35 +1556,12 @@ export class Scene extends AbstractScene implements IAnimatable {
         this._renderId++;
     }
 
-    private _updatePointerPosition(evt: PointerEvent): void {
-        var canvasRect = this._engine.getRenderingCanvasClientRect();
-
-        if (!canvasRect) {
-            return;
-        }
-
-        this._pointerX = evt.clientX - canvasRect.left;
-        this._pointerY = evt.clientY - canvasRect.top;
-
-        this._unTranslatedPointerX = this._pointerX;
-        this._unTranslatedPointerY = this._pointerY;
-    }
-
     private _createUbo(): void {
         this._sceneUbo = new UniformBuffer(this._engine, undefined, true);
         this._sceneUbo.addUniform("viewProjection", 16);
         this._sceneUbo.addUniform("view", 16);
     }
 
-    // Pointers handling
-    private _setRayOnPointerInfo(pointerInfo: PointerInfo) {
-        if (pointerInfo.pickInfo && !pointerInfo.pickInfo._pickingUnavailable) {
-            if (!pointerInfo.pickInfo.ray) {
-                pointerInfo.pickInfo.ray = this.createPickingRay(pointerInfo.event.offsetX, pointerInfo.event.offsetY, Matrix.Identity(), this.activeCamera);
-            }
-        }
-    }
-
     /**
      * Use this method to simulate a pointer move on a mesh
      * The pickResult parameter can be obtained from a scene.pick or scene.pickWithRay
@@ -1624,74 +1570,10 @@ export class Scene extends AbstractScene implements IAnimatable {
      * @returns the current scene
      */
     public simulatePointerMove(pickResult: PickingInfo, pointerEventInit?: PointerEventInit): Scene {
-        let evt = new PointerEvent("pointermove", pointerEventInit);
-
-        if (this._checkPrePointerObservable(pickResult, evt, PointerEventTypes.POINTERMOVE)) {
-            return this;
-        }
-        return this._processPointerMove(pickResult, evt);
-    }
-
-    private _processPointerMove(pickResult: Nullable<PickingInfo>, evt: PointerEvent): Scene {
-
-        var canvas = this._engine.getRenderingCanvas();
-
-        if (!canvas) {
-            return this;
-        }
-
-        // Restore pointer
-        canvas.style.cursor = this.defaultCursor;
-
-        var isMeshPicked = (pickResult && pickResult.hit && pickResult.pickedMesh) ? true : false;
-        if (isMeshPicked) {
-            this.setPointerOverMesh(pickResult!.pickedMesh);
-
-            if (this._pointerOverMesh && this._pointerOverMesh.actionManager && this._pointerOverMesh.actionManager.hasPointerTriggers) {
-                if (this._pointerOverMesh.actionManager.hoverCursor) {
-                    canvas.style.cursor = this._pointerOverMesh.actionManager.hoverCursor;
-                } else {
-                    canvas.style.cursor = this.hoverCursor;
-                }
-            }
-        } else {
-            this.setPointerOverMesh(null);
-        }
-
-        for (let step of this._pointerMoveStage) {
-            pickResult = step.action(this._unTranslatedPointerX, this._unTranslatedPointerY, pickResult, isMeshPicked, canvas);
-        }
-
-        if (pickResult) {
-            let type = evt.type === this._wheelEventName ? PointerEventTypes.POINTERWHEEL : PointerEventTypes.POINTERMOVE;
-
-            if (this.onPointerMove) {
-                this.onPointerMove(evt, pickResult, type);
-            }
-
-            if (this.onPointerObservable.hasObservers()) {
-                let pi = new PointerInfo(type, evt, pickResult);
-                this._setRayOnPointerInfo(pi);
-                this.onPointerObservable.notifyObservers(pi, type);
-            }
-        }
-
+        this._inputManager.simulatePointerMove(pickResult, pointerEventInit);
         return this;
     }
 
-    private _checkPrePointerObservable(pickResult: Nullable<PickingInfo>, evt: PointerEvent, type: number) {
-        let pi = new PointerInfoPre(type, evt, this._unTranslatedPointerX, this._unTranslatedPointerY);
-        if (pickResult) {
-            pi.ray = pickResult.ray;
-        }
-        this.onPrePointerObservable.notifyObservers(pi, type);
-        if (pi.skipOnPointerObservable) {
-            return true;
-        } else {
-            return false;
-        }
-    }
-
     /**
      * Use this method to simulate a pointer down on a mesh
      * The pickResult parameter can be obtained from a scene.pick or scene.pickWithRay
@@ -1700,73 +1582,7 @@ export class Scene extends AbstractScene implements IAnimatable {
      * @returns the current scene
      */
     public simulatePointerDown(pickResult: PickingInfo, pointerEventInit?: PointerEventInit): Scene {
-        let evt = new PointerEvent("pointerdown", pointerEventInit);
-
-        if (this._checkPrePointerObservable(pickResult, evt, PointerEventTypes.POINTERDOWN)) {
-            return this;
-        }
-
-        return this._processPointerDown(pickResult, evt);
-    }
-
-    private _processPointerDown(pickResult: Nullable<PickingInfo>, evt: PointerEvent): Scene {
-        if (pickResult && pickResult.hit && pickResult.pickedMesh) {
-            this._pickedDownMesh = pickResult.pickedMesh;
-            var actionManager = pickResult.pickedMesh.actionManager;
-            if (actionManager) {
-                if (actionManager.hasPickTriggers) {
-                    actionManager.processTrigger(Constants.ACTION_OnPickDownTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
-                    switch (evt.button) {
-                        case 0:
-                            actionManager.processTrigger(Constants.ACTION_OnLeftPickTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
-                            break;
-                        case 1:
-                            actionManager.processTrigger(Constants.ACTION_OnCenterPickTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
-                            break;
-                        case 2:
-                            actionManager.processTrigger(Constants.ACTION_OnRightPickTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
-                            break;
-                    }
-                }
-
-                if (actionManager.hasSpecificTrigger(Constants.ACTION_OnLongPressTrigger)) {
-                    window.setTimeout(() => {
-                        var pickResult = this.pick(this._unTranslatedPointerX, this._unTranslatedPointerY,
-                            (mesh: AbstractMesh): boolean => (<boolean>(mesh.isPickable && mesh.isVisible && mesh.isReady() && mesh.actionManager && mesh.actionManager.hasSpecificTrigger(Constants.ACTION_OnLongPressTrigger) && mesh == this._pickedDownMesh)),
-                            false, this.cameraToUseForPointers);
-
-                        if (pickResult && pickResult.hit && pickResult.pickedMesh && actionManager) {
-                            if (this._totalPointersPressed !== 0 &&
-                                ((Date.now() - this._startingPointerTime) > Scene.LongPressDelay) &&
-                                !this._isPointerSwiping()) {
-                                this._startingPointerTime = 0;
-                                actionManager.processTrigger(Constants.ACTION_OnLongPressTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
-                            }
-                        }
-                    }, Scene.LongPressDelay);
-                }
-            }
-        }
-        else {
-            for (let step of this._pointerDownStage) {
-                pickResult = step.action(this._unTranslatedPointerX, this._unTranslatedPointerY, pickResult, evt);
-            }
-        }
-
-        if (pickResult) {
-            let type = PointerEventTypes.POINTERDOWN;
-
-            if (this.onPointerDown) {
-                this.onPointerDown(evt, pickResult, type);
-            }
-
-            if (this.onPointerObservable.hasObservers()) {
-                let pi = new PointerInfo(type, evt, pickResult);
-                this._setRayOnPointerInfo(pi);
-                this.onPointerObservable.notifyObservers(pi, type);
-            }
-        }
-
+        this._inputManager.simulatePointerDown(pickResult, pointerEventInit);
         return this;
     }
 
@@ -1779,94 +1595,7 @@ export class Scene extends AbstractScene implements IAnimatable {
      * @returns the current scene
      */
     public simulatePointerUp(pickResult: PickingInfo, pointerEventInit?: PointerEventInit, doubleTap?: boolean): Scene {
-        let evt = new PointerEvent("pointerup", pointerEventInit);
-        let clickInfo = new ClickInfo();
-
-        if (doubleTap) {
-            clickInfo.doubleClick = true;
-        } else {
-            clickInfo.singleClick = true;
-        }
-
-        if (this._checkPrePointerObservable(pickResult, evt, PointerEventTypes.POINTERUP)) {
-            return this;
-        }
-
-        return this._processPointerUp(pickResult, evt, clickInfo);
-    }
-
-    private _processPointerUp(pickResult: Nullable<PickingInfo>, evt: PointerEvent, clickInfo: ClickInfo): Scene {
-        if (pickResult && pickResult && pickResult.pickedMesh) {
-            this._pickedUpMesh = pickResult.pickedMesh;
-            if (this._pickedDownMesh === this._pickedUpMesh) {
-                if (this.onPointerPick) {
-                    this.onPointerPick(evt, pickResult);
-                }
-                if (clickInfo.singleClick && !clickInfo.ignore && this.onPointerObservable.hasObservers()) {
-                    let type = PointerEventTypes.POINTERPICK;
-                    let pi = new PointerInfo(type, evt, pickResult);
-                    this._setRayOnPointerInfo(pi);
-                    this.onPointerObservable.notifyObservers(pi, type);
-                }
-            }
-            let actionManager = pickResult.pickedMesh._getActionManagerForTrigger();
-            if (actionManager && !clickInfo.ignore) {
-                actionManager.processTrigger(Constants.ACTION_OnPickUpTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
-
-                if (!clickInfo.hasSwiped && clickInfo.singleClick) {
-                    actionManager.processTrigger(Constants.ACTION_OnPickTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
-                }
-
-                let doubleClickActionManager = pickResult.pickedMesh._getActionManagerForTrigger(Constants.ACTION_OnDoublePickTrigger);
-                if (clickInfo.doubleClick && doubleClickActionManager) {
-                    doubleClickActionManager.processTrigger(Constants.ACTION_OnDoublePickTrigger, ActionEvent.CreateNew(pickResult.pickedMesh, evt));
-                }
-            }
-        }
-        else {
-            if (!clickInfo.ignore) {
-                for (let step of this._pointerUpStage) {
-                    pickResult = step.action(this._unTranslatedPointerX, this._unTranslatedPointerY, pickResult, evt);
-                }
-            }
-        }
-
-        if (this._pickedDownMesh && this._pickedDownMesh !== this._pickedUpMesh) {
-            let pickedDownActionManager = this._pickedDownMesh._getActionManagerForTrigger(Constants.ACTION_OnPickOutTrigger);
-            if (pickedDownActionManager) {
-                pickedDownActionManager.processTrigger(Constants.ACTION_OnPickOutTrigger, ActionEvent.CreateNew(this._pickedDownMesh, evt));
-            }
-        }
-
-        let type = 0;
-        if (this.onPointerObservable.hasObservers()) {
-            if (!clickInfo.ignore && !clickInfo.hasSwiped) {
-                if (clickInfo.singleClick && this.onPointerObservable.hasSpecificMask(PointerEventTypes.POINTERTAP)) {
-                    type = PointerEventTypes.POINTERTAP;
-                }
-                else if (clickInfo.doubleClick && this.onPointerObservable.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP)) {
-                    type = PointerEventTypes.POINTERDOUBLETAP;
-                }
-                if (type) {
-                    let pi = new PointerInfo(type, evt, pickResult);
-                    this._setRayOnPointerInfo(pi);
-                    this.onPointerObservable.notifyObservers(pi, type);
-                }
-            }
-
-            if (!clickInfo.ignore) {
-                type = PointerEventTypes.POINTERUP;
-
-                let pi = new PointerInfo(type, evt, pickResult);
-                this._setRayOnPointerInfo(pi);
-                this.onPointerObservable.notifyObservers(pi, type);
-            }
-        }
-
-        if (this.onPointerUp && !clickInfo.ignore) {
-            this.onPointerUp(evt, pickResult, type);
-        }
-
+        this._inputManager.simulatePointerUp(pickResult, pointerEventInit, doubleTap);
         return this;
     }
 
@@ -1876,13 +1605,7 @@ export class Scene extends AbstractScene implements IAnimatable {
      * @returns true if the pointer was captured
      */
     public isPointerCaptured(pointerId = 0): boolean {
-        return this._pointerCaptures[pointerId];
-    }
-
-    /** @hidden */
-    public _isPointerSwiping(): boolean {
-        return Math.abs(this._startingPointerPosition.x - this._pointerX) > Scene.DragMovementThreshold ||
-            Math.abs(this._startingPointerPosition.y - this._pointerY) > Scene.DragMovementThreshold;
+        return this._inputManager.isPointerCaptured(pointerId);
     }
 
     /**
@@ -1892,393 +1615,12 @@ export class Scene extends AbstractScene implements IAnimatable {
     * @param attachMove defines if you want to attach events to pointermove
     */
     public attachControl(attachUp = true, attachDown = true, attachMove = true): void {
-        this._initActionManager = (act: Nullable<AbstractActionManager>, clickInfo: ClickInfo): Nullable<AbstractActionManager> => {
-            if (!this._meshPickProceed) {
-                let pickResult = this.pick(this._unTranslatedPointerX, this._unTranslatedPointerY, this.pointerDownPredicate, false, this.cameraToUseForPointers);
-                this._currentPickResult = pickResult;
-                if (pickResult) {
-                    act = (pickResult.hit && pickResult.pickedMesh) ? pickResult.pickedMesh._getActionManagerForTrigger() : null;
-                }
-                this._meshPickProceed = true;
-            }
-            return act;
-        };
-
-        this._delayedSimpleClick = (btn: number, clickInfo: ClickInfo, cb: (clickInfo: ClickInfo, pickResult: Nullable<PickingInfo>) => void) => {
-            // double click delay is over and that no double click has been raised since, or the 2 consecutive keys pressed are different
-            if ((Date.now() - this._previousStartingPointerTime > Scene.DoubleClickDelay && !this._doubleClickOccured) ||
-                btn !== this._previousButtonPressed) {
-                this._doubleClickOccured = false;
-                clickInfo.singleClick = true;
-                clickInfo.ignore = false;
-                cb(clickInfo, this._currentPickResult);
-            }
-        };
-
-        this._initClickEvent = (obs1: Observable<PointerInfoPre>, obs2: Observable<PointerInfo>, evt: PointerEvent, cb: (clickInfo: ClickInfo, pickResult: Nullable<PickingInfo>) => void): void => {
-            let clickInfo = new ClickInfo();
-            this._currentPickResult = null;
-            let act: Nullable<AbstractActionManager> = null;
-
-            let checkPicking = obs1.hasSpecificMask(PointerEventTypes.POINTERPICK) || obs2.hasSpecificMask(PointerEventTypes.POINTERPICK)
-                || obs1.hasSpecificMask(PointerEventTypes.POINTERTAP) || obs2.hasSpecificMask(PointerEventTypes.POINTERTAP)
-                || obs1.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP) || obs2.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP);
-            if (!checkPicking && AbstractActionManager) {
-                act = this._initActionManager(act, clickInfo);
-                if (act) {
-                    checkPicking = act.hasPickTriggers;
-                }
-            }
-
-            let needToIgnoreNext = false;
-
-            if (checkPicking) {
-                let btn = evt.button;
-                clickInfo.hasSwiped = this._isPointerSwiping();
-
-                if (!clickInfo.hasSwiped) {
-                    let checkSingleClickImmediately = !Scene.ExclusiveDoubleClickMode;
-
-                    if (!checkSingleClickImmediately) {
-                        checkSingleClickImmediately = !obs1.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP) &&
-                            !obs2.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP);
-
-                        if (checkSingleClickImmediately && !AbstractActionManager.HasSpecificTrigger(Constants.ACTION_OnDoublePickTrigger)) {
-                            act = this._initActionManager(act, clickInfo);
-                            if (act) {
-                                checkSingleClickImmediately = !act.hasSpecificTrigger(Constants.ACTION_OnDoublePickTrigger);
-                            }
-                        }
-                    }
-
-                    if (checkSingleClickImmediately) {
-                        // single click detected if double click delay is over or two different successive keys pressed without exclusive double click or no double click required
-                        if (Date.now() - this._previousStartingPointerTime > Scene.DoubleClickDelay ||
-                            btn !== this._previousButtonPressed) {
-                            clickInfo.singleClick = true;
-                            cb(clickInfo, this._currentPickResult);
-                            needToIgnoreNext = true;
-                        }
-                    }
-                    // at least one double click is required to be check and exclusive double click is enabled
-                    else {
-                        // wait that no double click has been raised during the double click delay
-                        this._previousDelayedSimpleClickTimeout = this._delayedSimpleClickTimeout;
-                        this._delayedSimpleClickTimeout = window.setTimeout(this._delayedSimpleClick.bind(this, btn, clickInfo, cb), Scene.DoubleClickDelay);
-                    }
-
-                    let checkDoubleClick = obs1.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP) ||
-                        obs2.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP);
-                    if (!checkDoubleClick && AbstractActionManager.HasSpecificTrigger(Constants.ACTION_OnDoublePickTrigger)) {
-                        act = this._initActionManager(act, clickInfo);
-                        if (act) {
-                            checkDoubleClick = act.hasSpecificTrigger(Constants.ACTION_OnDoublePickTrigger);
-                        }
-                    }
-                    if (checkDoubleClick) {
-                        // two successive keys pressed are equal, double click delay is not over and double click has not just occurred
-                        if (btn === this._previousButtonPressed &&
-                            Date.now() - this._previousStartingPointerTime < Scene.DoubleClickDelay &&
-                            !this._doubleClickOccured
-                        ) {
-                            // pointer has not moved for 2 clicks, it's a double click
-                            if (!clickInfo.hasSwiped &&
-                                !this._isPointerSwiping()) {
-                                this._previousStartingPointerTime = 0;
-                                this._doubleClickOccured = true;
-                                clickInfo.doubleClick = true;
-                                clickInfo.ignore = false;
-                                if (Scene.ExclusiveDoubleClickMode && this._previousDelayedSimpleClickTimeout) {
-                                    clearTimeout(this._previousDelayedSimpleClickTimeout);
-                                }
-                                this._previousDelayedSimpleClickTimeout = this._delayedSimpleClickTimeout;
-                                cb(clickInfo, this._currentPickResult);
-                            }
-                            // if the two successive clicks are too far, it's just two simple clicks
-                            else {
-                                this._doubleClickOccured = false;
-                                this._previousStartingPointerTime = this._startingPointerTime;
-                                this._previousStartingPointerPosition.x = this._startingPointerPosition.x;
-                                this._previousStartingPointerPosition.y = this._startingPointerPosition.y;
-                                this._previousButtonPressed = btn;
-                                if (Scene.ExclusiveDoubleClickMode) {
-                                    if (this._previousDelayedSimpleClickTimeout) {
-                                        clearTimeout(this._previousDelayedSimpleClickTimeout);
-                                    }
-                                    this._previousDelayedSimpleClickTimeout = this._delayedSimpleClickTimeout;
-
-                                    cb(clickInfo, this._previousPickResult);
-                                }
-                                else {
-                                    cb(clickInfo, this._currentPickResult);
-                                }
-                            }
-                            needToIgnoreNext = true;
-                        }
-                        // just the first click of the double has been raised
-                        else {
-                            this._doubleClickOccured = false;
-                            this._previousStartingPointerTime = this._startingPointerTime;
-                            this._previousStartingPointerPosition.x = this._startingPointerPosition.x;
-                            this._previousStartingPointerPosition.y = this._startingPointerPosition.y;
-                            this._previousButtonPressed = btn;
-                        }
-                    }
-                }
-            }
-
-            if (!needToIgnoreNext) {
-                cb(clickInfo, this._currentPickResult);
-            }
-        };
-
-        this._onPointerMove = (evt: PointerEvent) => {
-
-            this._updatePointerPosition(evt);
-
-            // PreObservable support
-            if (this._checkPrePointerObservable(null, evt, evt.type === this._wheelEventName ? PointerEventTypes.POINTERWHEEL : PointerEventTypes.POINTERMOVE)) {
-                return;
-            }
-
-            if (!this.cameraToUseForPointers && !this.activeCamera) {
-                return;
-            }
-
-            if (!this.pointerMovePredicate) {
-                this.pointerMovePredicate = (mesh: AbstractMesh): boolean => (mesh.isPickable && mesh.isVisible && mesh.isReady() && mesh.isEnabled() && (mesh.enablePointerMoveEvents || this.constantlyUpdateMeshUnderPointer || (mesh.actionManager !== null && mesh.actionManager !== undefined)) && (!this.cameraToUseForPointers || (this.cameraToUseForPointers.layerMask & mesh.layerMask) !== 0));
-            }
-
-            // Meshes
-            var pickResult = this.pick(this._unTranslatedPointerX, this._unTranslatedPointerY, this.pointerMovePredicate, false, this.cameraToUseForPointers);
-
-            this._processPointerMove(pickResult, evt);
-        };
-
-        this._onPointerDown = (evt: PointerEvent) => {
-            this._totalPointersPressed++;
-            this._pickedDownMesh = null;
-            this._meshPickProceed = false;
-
-            this._updatePointerPosition(evt);
-
-            if (this.preventDefaultOnPointerDown && canvas) {
-                evt.preventDefault();
-                canvas.focus();
-            }
-
-            this._startingPointerPosition.x = this._pointerX;
-            this._startingPointerPosition.y = this._pointerY;
-            this._startingPointerTime = Date.now();
-
-            // PreObservable support
-            if (this._checkPrePointerObservable(null, evt, PointerEventTypes.POINTERDOWN)) {
-                return;
-            }
-
-            if (!this.cameraToUseForPointers && !this.activeCamera) {
-                return;
-            }
-
-            this._pointerCaptures[evt.pointerId] = true;
-
-            if (!this.pointerDownPredicate) {
-                this.pointerDownPredicate = (mesh: AbstractMesh): boolean => {
-                    return mesh.isPickable && mesh.isVisible && mesh.isReady() && mesh.isEnabled() && (!this.cameraToUseForPointers || (this.cameraToUseForPointers.layerMask & mesh.layerMask) !== 0);
-                };
-            }
-
-            // Meshes
-            this._pickedDownMesh = null;
-            var pickResult = this.pick(this._unTranslatedPointerX, this._unTranslatedPointerY, this.pointerDownPredicate, false, this.cameraToUseForPointers);
-
-            this._processPointerDown(pickResult, evt);
-        };
-
-        this._onPointerUp = (evt: PointerEvent) => {
-            if (this._totalPointersPressed === 0) {  // We are attaching the pointer up to windows because of a bug in FF
-                return;                             // So we need to test it the pointer down was pressed before.
-            }
-
-            this._totalPointersPressed--;
-            this._pickedUpMesh = null;
-            this._meshPickProceed = false;
-
-            this._updatePointerPosition(evt);
-
-            if (this.preventDefaultOnPointerUp && canvas) {
-                evt.preventDefault();
-                canvas.focus();
-            }
-
-            this._initClickEvent(this.onPrePointerObservable, this.onPointerObservable, evt, (clickInfo: ClickInfo, pickResult: Nullable<PickingInfo>) => {
-                // PreObservable support
-                if (this.onPrePointerObservable.hasObservers()) {
-                    if (!clickInfo.ignore) {
-                        if (!clickInfo.hasSwiped) {
-                            if (clickInfo.singleClick && this.onPrePointerObservable.hasSpecificMask(PointerEventTypes.POINTERTAP)) {
-                                if (this._checkPrePointerObservable(null, evt, PointerEventTypes.POINTERTAP)) {
-                                    return;
-                                }
-                            }
-                            if (clickInfo.doubleClick && this.onPrePointerObservable.hasSpecificMask(PointerEventTypes.POINTERDOUBLETAP)) {
-                                if (this._checkPrePointerObservable(null, evt, PointerEventTypes.POINTERDOUBLETAP)) {
-                                    return;
-                                }
-                            }
-                        }
-                        if (this._checkPrePointerObservable(null, evt, PointerEventTypes.POINTERUP)) {
-                            return;
-                        }
-                    }
-                }
-
-                if (!this._pointerCaptures[evt.pointerId]) {
-                    return;
-                }
-
-                this._pointerCaptures[evt.pointerId] = false;
-                if (!this.cameraToUseForPointers && !this.activeCamera) {
-                    return;
-                }
-
-                if (!this.pointerUpPredicate) {
-                    this.pointerUpPredicate = (mesh: AbstractMesh): boolean => {
-                        return mesh.isPickable && mesh.isVisible && mesh.isReady() && mesh.isEnabled() && (!this.cameraToUseForPointers || (this.cameraToUseForPointers.layerMask & mesh.layerMask) !== 0);
-                    };
-                }
-
-                // Meshes
-                if (!this._meshPickProceed && (AbstractActionManager && AbstractActionManager.HasTriggers || this.onPointerObservable.hasObservers())) {
-                    this._initActionManager(null, clickInfo);
-                }
-                if (!pickResult) {
-                    pickResult = this._currentPickResult;
-                }
-
-                this._processPointerUp(pickResult, evt, clickInfo);
-
-                this._previousPickResult = this._currentPickResult;
-            });
-        };
-
-        this._onKeyDown = (evt: KeyboardEvent) => {
-            let type = KeyboardEventTypes.KEYDOWN;
-            if (this.onPreKeyboardObservable.hasObservers()) {
-                let pi = new KeyboardInfoPre(type, evt);
-                this.onPreKeyboardObservable.notifyObservers(pi, type);
-                if (pi.skipOnPointerObservable) {
-                    return;
-                }
-            }
-
-            if (this.onKeyboardObservable.hasObservers()) {
-                let pi = new KeyboardInfo(type, evt);
-                this.onKeyboardObservable.notifyObservers(pi, type);
-            }
-
-            if (this.actionManager) {
-                this.actionManager.processTrigger(Constants.ACTION_OnKeyDownTrigger, ActionEvent.CreateNewFromScene(this, evt));
-            }
-        };
-
-        this._onKeyUp = (evt: KeyboardEvent) => {
-            let type = KeyboardEventTypes.KEYUP;
-            if (this.onPreKeyboardObservable.hasObservers()) {
-                let pi = new KeyboardInfoPre(type, evt);
-                this.onPreKeyboardObservable.notifyObservers(pi, type);
-                if (pi.skipOnPointerObservable) {
-                    return;
-                }
-            }
-
-            if (this.onKeyboardObservable.hasObservers()) {
-                let pi = new KeyboardInfo(type, evt);
-                this.onKeyboardObservable.notifyObservers(pi, type);
-            }
-
-            if (this.actionManager) {
-                this.actionManager.processTrigger(Constants.ACTION_OnKeyUpTrigger, ActionEvent.CreateNewFromScene(this, evt));
-            }
-        };
-
-        let engine = this.getEngine();
-        this._onCanvasFocusObserver = engine.onCanvasFocusObservable.add(() => {
-            if (!canvas) {
-                return;
-            }
-            canvas.addEventListener("keydown", this._onKeyDown, false);
-            canvas.addEventListener("keyup", this._onKeyUp, false);
-        });
-
-        this._onCanvasBlurObserver = engine.onCanvasBlurObservable.add(() => {
-            if (!canvas) {
-                return;
-            }
-            canvas.removeEventListener("keydown", this._onKeyDown);
-            canvas.removeEventListener("keyup", this._onKeyUp);
-        });
-
-        var eventPrefix = Tools.GetPointerPrefix();
-        var canvas = this._engine.getRenderingCanvas();
-
-        if (!canvas) {
-            return;
-        }
-
-        if (attachMove) {
-            canvas.addEventListener(eventPrefix + "move", <any>this._onPointerMove, false);
-
-            // Wheel
-            this._wheelEventName = "onwheel" in document.createElement("div") ? "wheel" :       // Modern browsers support "wheel"
-                (<any>document).onmousewheel !== undefined ? "mousewheel" :                     // Webkit and IE support at least "mousewheel"
-                    "DOMMouseScroll";                                                           // let's assume that remaining browsers are older Firefox
-
-            canvas.addEventListener(this._wheelEventName, <any>this._onPointerMove, false);
-        }
-
-        if (attachDown) {
-            canvas.addEventListener(eventPrefix + "down", <any>this._onPointerDown, false);
-        }
-
-        if (attachUp) {
-            window.addEventListener(eventPrefix + "up", <any>this._onPointerUp, false);
-        }
-
-        canvas.tabIndex = 1;
+        this._inputManager.attachControl(attachUp, attachDown, attachMove);
     }
 
     /** Detaches all event handlers*/
     public detachControl() {
-        let engine = this.getEngine();
-        var eventPrefix = Tools.GetPointerPrefix();
-        var canvas = engine.getRenderingCanvas();
-
-        if (!canvas) {
-            return;
-        }
-
-        canvas.removeEventListener(eventPrefix + "move", <any>this._onPointerMove);
-        canvas.removeEventListener(eventPrefix + "down", <any>this._onPointerDown);
-        window.removeEventListener(eventPrefix + "up", <any>this._onPointerUp);
-
-        if (this._onCanvasBlurObserver) {
-            engine.onCanvasBlurObservable.remove(this._onCanvasBlurObserver);
-        }
-
-        if (this._onCanvasFocusObserver) {
-            engine.onCanvasFocusObservable.remove(this._onCanvasFocusObserver);
-        }
-
-        // Wheel
-        canvas.removeEventListener(this._wheelEventName, <any>this._onPointerMove);
-
-        // Keyboard
-        canvas.removeEventListener("keydown", this._onKeyDown);
-        canvas.removeEventListener("keyup", this._onKeyUp);
-
-        // Cursor
-        canvas.style.cursor = this.defaultCursor;
+        this._inputManager.detachControl();
     }
 
     /**
@@ -4875,25 +4217,7 @@ export class Scene extends AbstractScene implements IAnimatable {
      * @param mesh defines the mesh to use
      */
     public setPointerOverMesh(mesh: Nullable<AbstractMesh>): void {
-        if (this._pointerOverMesh === mesh) {
-            return;
-        }
-
-        let actionManager: Nullable<AbstractActionManager>;
-        if (this._pointerOverMesh) {
-            actionManager = this._pointerOverMesh._getActionManagerForTrigger(Constants.ACTION_OnPointerOutTrigger);
-            if (actionManager) {
-                actionManager.processTrigger(Constants.ACTION_OnPointerOutTrigger, ActionEvent.CreateNew(this._pointerOverMesh));
-            }
-        }
-
-        this._pointerOverMesh = mesh;
-        if (this._pointerOverMesh) {
-            actionManager = this._pointerOverMesh._getActionManagerForTrigger(Constants.ACTION_OnPointerOverTrigger);
-            if (actionManager) {
-                actionManager.processTrigger(Constants.ACTION_OnPointerOverTrigger, ActionEvent.CreateNew(this._pointerOverMesh));
-            }
-        }
+        this._inputManager.setPointerOverMesh(mesh);
     }
 
     /**
@@ -4901,7 +4225,7 @@ export class Scene extends AbstractScene implements IAnimatable {
      * @returns a Mesh or null if no mesh is under the pointer
      */
     public getPointerOverMesh(): Nullable<AbstractMesh> {
-        return this._pointerOverMesh;
+        return this._inputManager.getPointerOverMesh();
     }
 
     // Misc.