Jelajahi Sumber

Merge pull request #5798 from mrdunk/FollowCamera_PointersInput

FollowCameraPointersInput and a Base class for any CameraPointersInput.
David Catuhe 6 tahun lalu
induk
melakukan
4ecbab593c

+ 2 - 0
dist/preview release/what's new.md

@@ -97,9 +97,11 @@
 - Added MouseWheel bindings for FollowCamera. ([mrdunk](https://github.com))
 - Tweak MouseWheel bindings for FollowCamera orientations. ([mrdunk](https://github.com))
 - Added maximum and minimum limits for FollowCamera parameters. ([mrdunk](https://github.com))
+- Convert ArcRotateCamera to use new BaseCameraPointersInput. ([mrdunk](https://github.com))
 - Added per solid particle culling possibility : `solidParticle.isInFrustum()`  ([jerome](https://github.com/jbousquie))
 - Added transparency support to `GlowLayer` ([Sebavan](https://github.com/Sebavan))
 - Added option `forceDisposeChildren` to multiMaterial.dispose ([danjpar](https://github.com/danjpar))
+- Added Pointer bindings for FollowCamera. ([mrdunk](https://github.com))
 - Inspector light gizmo ([TrevorDev](https://github.com/TrevorDev))
 - Added option `multiMultiMaterials` to mesh.mergeMeshes ([danjpar](https://github.com/danjpar))
 - Expose fallback camera distortion metrics option in vrExperienceHelper ([TrevorDev](https://github.com/TrevorDev))

+ 340 - 0
src/Cameras/Inputs/BaseCameraPointersInput.ts

@@ -0,0 +1,340 @@
+import { Nullable } from "../../types";
+import { serialize } from "../../Misc/decorators";
+import { EventState, Observer } from "../../Misc/observable";
+import { Tools } from "../../Misc/tools";
+import { Camera } from "../../Cameras/camera";
+import { ICameraInput } from "../../Cameras/cameraInputsManager";
+import { PointerInfo, PointerEventTypes, PointerTouch } from "../../Events/pointerEvents";
+
+/**
+ * Base class for Camera Pointer Inputs.
+ * See FollowCameraPointersInput in src/Cameras/Inputs/followCameraPointersInput.ts
+ * for example usage.
+ */
+export abstract class BaseCameraPointersInput implements ICameraInput<Camera> {
+    /**
+     * Defines the camera the input is attached to.
+     */
+    public abstract camera: Camera;
+
+    /**
+     * Whether keyboard modifier keys are pressed at time of last mouse event.
+     */
+    protected _altKey: boolean;
+    protected _ctrlKey: boolean;
+    protected _metaKey: boolean;
+    protected _shiftKey: boolean;
+
+    /**
+     * Which mouse buttons were pressed at time of last mouse event.
+     * https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
+     */
+    protected _buttonsPressed: number;
+
+    /**
+     * Defines the buttons associated with the input to handle camera move.
+     */
+    @serialize()
+    public buttons = [0, 1, 2];
+
+    /**
+     * Attach the input controls to a specific dom element to get the input from.
+     * @param element Defines the element the controls should be listened from
+     * @param noPreventDefault Defines whether event caught by the controls should call preventdefault() (https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault)
+     */
+    public attachControl(element: HTMLElement, noPreventDefault?: boolean): void {
+        var engine = this.camera.getEngine();
+        var pointA: Nullable<PointerTouch> = null;
+        var pointB: Nullable<PointerTouch> = null;
+        var previousPinchSquaredDistance = 0;
+        var previousMultiTouchPanPosition: Nullable<PointerTouch> = null;
+
+        this._altKey = false;
+        this._ctrlKey = false;
+        this._metaKey = false;
+        this._shiftKey = false;
+        this._buttonsPressed = 0;
+
+        this._pointerInput = (p, s) => {
+            var evt = <PointerEvent>p.event;
+            let isTouch = evt.pointerType === "touch";
+
+            if (engine.isInVRExclusivePointerMode) {
+                return;
+            }
+
+            if (p.type !== PointerEventTypes.POINTERMOVE &&
+                this.buttons.indexOf(evt.button) === -1) {
+                return;
+            }
+
+            let srcElement = <HTMLElement>(evt.srcElement || evt.target);
+
+            this._altKey = evt.altKey;
+            this._ctrlKey = evt.ctrlKey;
+            this._metaKey = evt.metaKey;
+            this._shiftKey = evt.shiftKey;
+            this._buttonsPressed = evt.buttons;
+
+            if (engine.isPointerLock) {
+                var offsetX = evt.movementX ||
+                              evt.mozMovementX ||
+                              evt.webkitMovementX ||
+                              evt.msMovementX ||
+                              0;
+                var offsetY = evt.movementY ||
+                              evt.mozMovementY ||
+                              evt.webkitMovementY ||
+                              evt.msMovementY ||
+                              0;
+
+                this.onTouch(null, offsetX, offsetY);
+                pointA = null;
+                pointB = null;
+            } else if (p.type === PointerEventTypes.POINTERDOWN && srcElement) {
+                try {
+                    srcElement.setPointerCapture(evt.pointerId);
+                } catch (e) {
+                    //Nothing to do with the error. Execution will continue.
+                }
+
+                if (pointA === null) {
+                    pointA = {x: evt.clientX,
+                              y: evt.clientY,
+                              pointerId: evt.pointerId,
+                              type: evt.pointerType };
+                } else if (pointB === null) {
+                    pointB = {x: evt.clientX,
+                              y: evt.clientY,
+                              pointerId: evt.pointerId,
+                              type: evt.pointerType };
+                }
+
+                this.onButtonDown(evt, pointB ? 2 : 1);
+
+                if (!noPreventDefault) {
+                    evt.preventDefault();
+                    element.focus();
+                }
+            } else if (p.type === PointerEventTypes.POINTERDOUBLETAP) {
+                this.onDoubleTap(evt.pointerType);
+            } else if (p.type === PointerEventTypes.POINTERUP && srcElement) {
+                try {
+                    srcElement.releasePointerCapture(evt.pointerId);
+                } catch (e) {
+                    //Nothing to do with the error.
+                }
+
+                if (!isTouch) {
+                    pointB = null; // Mouse and pen are mono pointer
+                }
+
+                //would be better to use pointers.remove(evt.pointerId) for multitouch gestures,
+                //but emptying completely pointers collection is required to fix a bug on iPhone :
+                //when changing orientation while pinching camera,
+                //one pointer stay pressed forever if we don't release all pointers
+                //will be ok to put back pointers.remove(evt.pointerId); when iPhone bug corrected
+                if (engine._badOS) {
+                    pointA = pointB = null;
+                } else {
+                    //only remove the impacted pointer in case of multitouch allowing on most
+                    //platforms switching from rotate to zoom and pan seamlessly.
+                    if (pointB && pointA && pointA.pointerId == evt.pointerId) {
+                        pointA = pointB;
+                        pointB = null;
+                    } else if (pointA && pointB && pointB.pointerId == evt.pointerId) {
+                        pointB = null;
+                    } else {
+                        pointA = pointB = null;
+                    }
+                }
+
+                if (previousPinchSquaredDistance !== 0 || previousMultiTouchPanPosition) {
+                    // Previous pinch data is populated but a button has been lifted
+                    // so pinch has ended.
+                    this.onMultiTouch(
+                      pointA,
+                      pointB,
+                      previousPinchSquaredDistance,
+                      0,  // pinchSquaredDistance
+                      previousMultiTouchPanPosition,
+                      null  // multiTouchPanPosition
+                    );
+                  previousPinchSquaredDistance = 0;
+                  previousMultiTouchPanPosition = null;
+                }
+
+                this.onButtonUp(evt);
+
+                if (!noPreventDefault) {
+                    evt.preventDefault();
+                }
+            } else if (p.type === PointerEventTypes.POINTERMOVE) {
+                if (!noPreventDefault) {
+                    evt.preventDefault();
+                }
+
+                // One button down
+                if (pointA && pointB === null) {
+                    var offsetX = evt.clientX - pointA.x;
+                    var offsetY = evt.clientY - pointA.y;
+                    this.onTouch(pointA, offsetX, offsetY);
+
+                    pointA.x = evt.clientX;
+                    pointA.y = evt.clientY;
+                }
+                // Two buttons down: pinch
+                else if (pointA && pointB) {
+                    var ed = (pointA.pointerId === evt.pointerId) ? pointA : pointB;
+                    ed.x = evt.clientX;
+                    ed.y = evt.clientY;
+                    var distX = pointA.x - pointB.x;
+                    var distY = pointA.y - pointB.y;
+                    var pinchSquaredDistance = (distX * distX) + (distY * distY);
+                    var multiTouchPanPosition = {x: (pointA.x + pointB.x) / 2,
+                                                 y: (pointA.y + pointB.y) / 2,
+                                                 pointerId: evt.pointerId,
+                                                 type: p.type};
+
+                    this.onMultiTouch(
+                      pointA,
+                      pointB,
+                      previousPinchSquaredDistance,
+                      pinchSquaredDistance,
+                      previousMultiTouchPanPosition,
+                      multiTouchPanPosition);
+
+                    previousMultiTouchPanPosition = multiTouchPanPosition;
+                    previousPinchSquaredDistance = pinchSquaredDistance;
+                }
+            }
+        };
+
+        this._observer = this.camera.getScene().onPointerObservable.add(
+            this._pointerInput,
+            PointerEventTypes.POINTERDOWN | PointerEventTypes.POINTERUP |
+            PointerEventTypes.POINTERMOVE);
+
+        this._onLostFocus = () => {
+            pointA = pointB = null;
+            previousPinchSquaredDistance = 0;
+            previousMultiTouchPanPosition = null;
+            this.onLostFocus();
+        };
+
+        element.addEventListener("contextmenu",
+            <EventListener>this.onContextMenu.bind(this), false);
+
+        Tools.RegisterTopRootEvents([
+            { name: "blur", handler: this._onLostFocus }
+        ]);
+    }
+
+    /**
+     * Detach the current controls from the specified dom element.
+     * @param element Defines the element to stop listening the inputs from
+     */
+    public detachControl(element: Nullable<HTMLElement>): void {
+        if (this._onLostFocus) {
+            Tools.UnregisterTopRootEvents([
+                { name: "blur", handler: this._onLostFocus }
+            ]);
+        }
+
+        if (element && this._observer) {
+            this.camera.getScene().onPointerObservable.remove(this._observer);
+            this._observer = null;
+
+            if (this.onContextMenu) {
+                element.removeEventListener("contextmenu", <EventListener>this.onContextMenu);
+            }
+
+            this._onLostFocus = null;
+        }
+
+        this._altKey = false;
+        this._ctrlKey = false;
+        this._metaKey = false;
+        this._shiftKey = false;
+        this._buttonsPressed = 0;
+    }
+
+    /**
+     * Gets the class name of the current input.
+     * @returns the class name
+     */
+    public getClassName(): string {
+        return "BaseCameraPointersInput";
+    }
+
+    /**
+     * Get the friendly name associated with the input class.
+     * @returns the input friendly name
+     */
+    public getSimpleName(): string {
+        return "pointers";
+    }
+
+    /**
+     * Called on pointer POINTERDOUBLETAP event.
+     * Override this method to provide functionality on POINTERDOUBLETAP event.
+     */
+    protected onDoubleTap(type: string) {
+    }
+
+    /**
+     * Called on pointer POINTERMOVE event if only a single touch is active.
+     * Override this method to provide functionality.
+     */
+    protected onTouch(point: Nullable<PointerTouch>,
+                      offsetX: number,
+                      offsetY: number): void {
+    }
+
+    /**
+     * Called on pointer POINTERMOVE event if multiple touches are active.
+     * Override this method to provide functionality.
+     */
+    protected onMultiTouch(pointA: Nullable<PointerTouch>,
+                           pointB: Nullable<PointerTouch>,
+                           previousPinchSquaredDistance: number,
+                           pinchSquaredDistance: number,
+                           previousMultiTouchPanPosition: Nullable<PointerTouch>,
+                           multiTouchPanPosition: Nullable<PointerTouch>): void {
+    }
+
+    /**
+     * Called on JS contextmenu event.
+     * Override this method to provide functionality.
+     */
+    protected onContextMenu(evt: PointerEvent): void {
+        evt.preventDefault();
+    }
+
+    /**
+     * Called each time a new POINTERDOWN event occurs. Ie, for each button
+     * press.
+     * Override this method to provide functionality.
+     */
+    protected onButtonDown(evt: PointerEvent, buttonCount: number): void {
+    }
+
+    /**
+     * Called each time a new POINTERUP event occurs. Ie, for each button
+     * release.
+     * Override this method to provide functionality.
+     */
+    protected onButtonUp(evt: PointerEvent): void {
+    }
+
+    /**
+     * Called when window becomes inactive.
+     * Override this method to provide functionality.
+     */
+    protected onLostFocus(): void {
+    }
+
+    private _pointerInput: (p: PointerInfo, s: EventState) => void;
+    private _observer: Nullable<Observer<PointerInfo>>;
+    private _onLostFocus: Nullable<(e: FocusEvent) => any>;
+}

+ 128 - 330
src/Cameras/Inputs/arcRotateCameraPointersInput.ts

@@ -1,35 +1,44 @@
 import { Nullable } from "../../types";
 import { serialize } from "../../Misc/decorators";
-import { EventState, Observer } from "../../Misc/observable";
-import { Tools } from "../../Misc/tools";
 import { ArcRotateCamera } from "../../Cameras/arcRotateCamera";
-import { ICameraInput, CameraInputTypes } from "../../Cameras/cameraInputsManager";
-import { PointerInfo, PointerEventTypes } from "../../Events/pointerEvents";
+import { CameraInputTypes } from "../../Cameras/cameraInputsManager";
+import { BaseCameraPointersInput } from "../../Cameras/Inputs/BaseCameraPointersInput";
+import { PointerTouch } from "../../Events/pointerEvents";
 
 /**
  * Manage the pointers inputs to control an arc rotate camera.
  * @see http://doc.babylonjs.com/how_to/customizing_camera_inputs
  */
-export class ArcRotateCameraPointersInput implements ICameraInput<ArcRotateCamera> {
+export class ArcRotateCameraPointersInput extends BaseCameraPointersInput {
     /**
      * Defines the camera the input is attached to.
      */
     public camera: ArcRotateCamera;
 
     /**
+     * Gets the class name of the current input.
+     * @returns the class name
+     */
+    public getClassName(): string {
+        return "ArcRotateCameraPointersInput";
+    }
+
+    /**
      * Defines the buttons associated with the input to handle camera move.
      */
     @serialize()
     public buttons = [0, 1, 2];
 
     /**
-     * Defines the pointer angular sensibility  along the X axis or how fast is the camera rotating.
+     * Defines the pointer angular sensibility  along the X axis or how fast is
+     * the camera rotating.
      */
     @serialize()
     public angularSensibilityX = 1000.0;
 
     /**
-     * Defines the pointer angular sensibility along the Y axis or how fast is the camera rotating.
+     * Defines the pointer angular sensibility along the Y axis or how fast is
+     * the camera rotating.
      */
     @serialize()
     public angularSensibilityY = 1000.0;
@@ -41,8 +50,10 @@ export class ArcRotateCameraPointersInput implements ICameraInput<ArcRotateCamer
     public pinchPrecision = 12.0;
 
     /**
-     * pinchDeltaPercentage will be used instead of pinchPrecision if different from 0.
-     * It defines the percentage of current camera.radius to use as delta when pinch zoom is used.
+     * pinchDeltaPercentage will be used instead of pinchPrecision if different
+     * from 0.
+     * It defines the percentage of current camera.radius to use as delta when
+     * pinch zoom is used.
      */
     @serialize()
     public pinchDeltaPercentage = 0;
@@ -60,7 +71,8 @@ export class ArcRotateCameraPointersInput implements ICameraInput<ArcRotateCamer
     public multiTouchPanning: boolean = true;
 
     /**
-     * Defines whether panning is enabled for both pan (2 fingers swipe) and zoom (pinch) through multitouch.
+     * Defines whether panning is enabled for both pan (2 fingers swipe) and
+     * zoom (pinch) through multitouch.
      */
     @serialize()
     public multiTouchPanAndZoom: boolean = true;
@@ -71,351 +83,137 @@ export class ArcRotateCameraPointersInput implements ICameraInput<ArcRotateCamer
     public pinchInwards = true;
 
     private _isPanClick: boolean = false;
-    private _pointerInput: (p: PointerInfo, s: EventState) => void;
-    private _observer: Nullable<Observer<PointerInfo>>;
-    private _onMouseMove: Nullable<(e: MouseEvent) => any>;
-    private _onGestureStart: Nullable<(e: PointerEvent) => void>;
-    private _onGesture: Nullable<(e: MSGestureEvent) => void>;
-    private _MSGestureHandler: Nullable<MSGesture>;
-    private _onLostFocus: Nullable<(e: FocusEvent) => any>;
-    private _onContextMenu: Nullable<(e: Event) => void>;
+    private _twoFingerActivityCount: number = 0;
+    private _isPinching: boolean = false;
 
     /**
-     * Attach the input controls to a specific dom element to get the input from.
-     * @param element Defines the element the controls should be listened from
-     * @param noPreventDefault Defines whether event caught by the controls should call preventdefault() (https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault)
+     * Called on pointer POINTERMOVE event if only a single touch is active.
      */
-    public attachControl(element: HTMLElement, noPreventDefault?: boolean): void {
-        var engine = this.camera.getEngine();
-        var cacheSoloPointer: Nullable<{ x: number, y: number, pointerId: number, type: any }>; // cache pointer object for better perf on camera rotation
-        var pointA: Nullable<{ x: number, y: number, pointerId: number, type: any }> = null;
-        var pointB: Nullable<{ x: number, y: number, pointerId: number, type: any }> = null;
-        var previousPinchSquaredDistance = 0;
-        var initialDistance = 0;
-        var twoFingerActivityCount = 0;
-        var previousMultiTouchPanPosition: { x: number, y: number, isPaning: boolean, isPinching: boolean } = { x: 0, y: 0, isPaning: false, isPinching: false };
-
-        this._pointerInput = (p, s) => {
-            var evt = <PointerEvent>p.event;
-            let isTouch = (<any>p.event).pointerType === "touch";
-
-            if (engine.isInVRExclusivePointerMode) {
-                return;
-            }
-
-            if (p.type !== PointerEventTypes.POINTERMOVE && this.buttons.indexOf(evt.button) === -1) {
-                return;
-            }
-
-            let srcElement = <HTMLElement>(evt.srcElement || evt.target);
-
-            if (p.type === PointerEventTypes.POINTERDOWN && srcElement) {
-                try {
-                    srcElement.setPointerCapture(evt.pointerId);
-                } catch (e) {
-                    //Nothing to do with the error. Execution will continue.
-                }
-
-                // Manage panning with pan button click
-                this._isPanClick = evt.button === this.camera._panningMouseButton;
-
-                // manage pointers
-                cacheSoloPointer = { x: evt.clientX, y: evt.clientY, pointerId: evt.pointerId, type: evt.pointerType };
-                if (pointA === null) {
-                    pointA = cacheSoloPointer;
-                }
-                else if (pointB === null) {
-                    pointB = cacheSoloPointer;
-                }
-                if (!noPreventDefault) {
-                    evt.preventDefault();
-                    element.focus();
-                }
-            }
-            else if (p.type === PointerEventTypes.POINTERDOUBLETAP) {
-                if (this.camera.useInputToRestoreState) {
-                    this.camera.restoreState();
-                }
-            }
-            else if (p.type === PointerEventTypes.POINTERUP && srcElement) {
-                try {
-                    srcElement.releasePointerCapture(evt.pointerId);
-                } catch (e) {
-                    //Nothing to do with the error.
-                }
-
-                cacheSoloPointer = null;
-                previousPinchSquaredDistance = 0;
-                previousMultiTouchPanPosition.isPaning = false;
-                previousMultiTouchPanPosition.isPinching = false;
-                twoFingerActivityCount = 0;
-                initialDistance = 0;
-
-                if (!isTouch) {
-                    pointB = null; // Mouse and pen are mono pointer
-                }
-
-                //would be better to use pointers.remove(evt.pointerId) for multitouch gestures,
-                //but emptying completly pointers collection is required to fix a bug on iPhone :
-                //when changing orientation while pinching camera, one pointer stay pressed forever if we don't release all pointers
-                //will be ok to put back pointers.remove(evt.pointerId); when iPhone bug corrected
-                if (engine._badOS) {
-                    pointA = pointB = null;
-                }
-                else {
-                    //only remove the impacted pointer in case of multitouch allowing on most
-                    //platforms switching from rotate to zoom and pan seamlessly.
-                    if (pointB && pointA && pointA.pointerId == evt.pointerId) {
-                        pointA = pointB;
-                        pointB = null;
-                        cacheSoloPointer = { x: pointA.x, y: pointA.y, pointerId: pointA.pointerId, type: evt.pointerType };
-                    }
-                    else if (pointA && pointB && pointB.pointerId == evt.pointerId) {
-                        pointB = null;
-                        cacheSoloPointer = { x: pointA.x, y: pointA.y, pointerId: pointA.pointerId, type: evt.pointerType };
-                    }
-                    else {
-                        pointA = pointB = null;
-                    }
-                }
-
-                if (!noPreventDefault) {
-                    evt.preventDefault();
-                }
-            } else if (p.type === PointerEventTypes.POINTERMOVE) {
-                if (!noPreventDefault) {
-                    evt.preventDefault();
-                }
-
-                // One button down
-                if (pointA && pointB === null && cacheSoloPointer) {
-                    if (this.panningSensibility !== 0 &&
-                        ((evt.ctrlKey && this.camera._useCtrlForPanning) || this._isPanClick)) {
-                        this.camera.inertialPanningX += -(evt.clientX - cacheSoloPointer.x) / this.panningSensibility;
-                        this.camera.inertialPanningY += (evt.clientY - cacheSoloPointer.y) / this.panningSensibility;
-                    } else {
-                        var offsetX = evt.clientX - cacheSoloPointer.x;
-                        var offsetY = evt.clientY - cacheSoloPointer.y;
-                        this.camera.inertialAlphaOffset -= offsetX / this.angularSensibilityX;
-                        this.camera.inertialBetaOffset -= offsetY / this.angularSensibilityY;
-                    }
-
-                    cacheSoloPointer.x = evt.clientX;
-                    cacheSoloPointer.y = evt.clientY;
-                }
-
-                // Two buttons down: pinch/pan
-                else if (pointA && pointB) {
-                    //if (noPreventDefault) { evt.preventDefault(); } //if pinch gesture, could be useful to force preventDefault to avoid html page scroll/zoom in some mobile browsers
-                    var ed = (pointA.pointerId === evt.pointerId) ? pointA : pointB;
-                    ed.x = evt.clientX;
-                    ed.y = evt.clientY;
-                    var direction = this.pinchInwards ? 1 : -1;
-                    var distX = pointA.x - pointB.x;
-                    var distY = pointA.y - pointB.y;
-                    var pinchSquaredDistance = (distX * distX) + (distY * distY);
-                    var pinchDistance = Math.sqrt(pinchSquaredDistance);
-
-                    if (previousPinchSquaredDistance === 0) {
-                        initialDistance = pinchDistance;
-                        previousPinchSquaredDistance = pinchSquaredDistance;
-                        previousMultiTouchPanPosition.x = (pointA.x + pointB.x) / 2;
-                        previousMultiTouchPanPosition.y = (pointA.y + pointB.y) / 2;
-                        return;
-                    }
-
-                    if (this.multiTouchPanAndZoom) {
-                        if (this.pinchDeltaPercentage) {
-                            this.camera.inertialRadiusOffset += ((pinchSquaredDistance - previousPinchSquaredDistance) * 0.001) * this.camera.radius * this.pinchDeltaPercentage;
-                        } else {
-                            this.camera.inertialRadiusOffset += (pinchSquaredDistance - previousPinchSquaredDistance) /
-                                (this.pinchPrecision *
-                                    ((this.angularSensibilityX + this.angularSensibilityY) / 2) *
-                                    direction);
-                        }
-
-                        if (this.panningSensibility !== 0) {
-                            var pointersCenterX = (pointA.x + pointB.x) / 2;
-                            var pointersCenterY = (pointA.y + pointB.y) / 2;
-                            var pointersCenterDistX = pointersCenterX - previousMultiTouchPanPosition.x;
-                            var pointersCenterDistY = pointersCenterY - previousMultiTouchPanPosition.y;
-
-                            previousMultiTouchPanPosition.x = pointersCenterX;
-                            previousMultiTouchPanPosition.y = pointersCenterY;
-
-                            this.camera.inertialPanningX += -(pointersCenterDistX) / (this.panningSensibility);
-                            this.camera.inertialPanningY += (pointersCenterDistY) / (this.panningSensibility);
-                        }
-                    }
-                    else {
-                        twoFingerActivityCount++;
-
-                        if (previousMultiTouchPanPosition.isPinching || (twoFingerActivityCount < 20 && Math.abs(pinchDistance - initialDistance) > this.camera.pinchToPanMaxDistance)) {
-                            if (this.pinchDeltaPercentage) {
-                                this.camera.inertialRadiusOffset += ((pinchSquaredDistance - previousPinchSquaredDistance) * 0.001) * this.camera.radius * this.pinchDeltaPercentage;
-                            } else {
-                                this.camera.inertialRadiusOffset += (pinchSquaredDistance - previousPinchSquaredDistance) /
-                                    (this.pinchPrecision *
-                                        ((this.angularSensibilityX + this.angularSensibilityY) / 2) *
-                                        direction);
-                            }
-                            previousMultiTouchPanPosition.isPaning = false;
-                            previousMultiTouchPanPosition.isPinching = true;
-                        }
-                        else {
-                            if (cacheSoloPointer && cacheSoloPointer.pointerId === ed.pointerId && this.panningSensibility !== 0 && this.multiTouchPanning) {
-                                if (!previousMultiTouchPanPosition.isPaning) {
-                                    previousMultiTouchPanPosition.isPaning = true;
-                                    previousMultiTouchPanPosition.isPinching = false;
-                                    previousMultiTouchPanPosition.x = ed.x;
-                                    previousMultiTouchPanPosition.y = ed.y;
-                                    return;
-                                }
-
-                                this.camera.inertialPanningX += -(ed.x - previousMultiTouchPanPosition.x) / (this.panningSensibility);
-                                this.camera.inertialPanningY += (ed.y - previousMultiTouchPanPosition.y) / (this.panningSensibility);
-                            }
-                        }
-
-                        if (cacheSoloPointer && cacheSoloPointer.pointerId === evt.pointerId) {
-                            previousMultiTouchPanPosition.x = ed.x;
-                            previousMultiTouchPanPosition.y = ed.y;
-                        }
-                    }
-
-                    previousPinchSquaredDistance = pinchSquaredDistance;
-                }
-            }
-        };
-
-        this._observer = this.camera.getScene().onPointerObservable.add(this._pointerInput, PointerEventTypes.POINTERDOWN | PointerEventTypes.POINTERUP | PointerEventTypes.POINTERMOVE | PointerEventTypes.POINTERDOUBLETAP);
-
-        this._onContextMenu = (evt) => {
-            evt.preventDefault();
-        };
-
-        if (!this.camera._useCtrlForPanning) {
-            element.addEventListener("contextmenu", this._onContextMenu, false);
-        }
-
-        this._onLostFocus = () => {
-            //this._keys = [];
-            pointA = pointB = null;
-            previousPinchSquaredDistance = 0;
-            previousMultiTouchPanPosition.isPaning = false;
-            previousMultiTouchPanPosition.isPinching = false;
-            twoFingerActivityCount = 0;
-            cacheSoloPointer = null;
-            initialDistance = 0;
-        };
-
-        this._onMouseMove = (evt) => {
-            if (!engine.isPointerLock) {
-                return;
-            }
-
-            var offsetX = evt.movementX || evt.mozMovementX || evt.webkitMovementX || evt.msMovementX || 0;
-            var offsetY = evt.movementY || evt.mozMovementY || evt.webkitMovementY || evt.msMovementY || 0;
-
+    protected onTouch(point: Nullable<PointerTouch>,
+                      offsetX: number,
+                      offsetY: number): void {
+        if (this.panningSensibility !== 0 &&
+          ((this._ctrlKey && this.camera._useCtrlForPanning) || this._isPanClick)) {
+            this.camera.inertialPanningX += -offsetX / this.panningSensibility;
+            this.camera.inertialPanningY += offsetY / this.panningSensibility;
+        } else {
             this.camera.inertialAlphaOffset -= offsetX / this.angularSensibilityX;
             this.camera.inertialBetaOffset -= offsetY / this.angularSensibilityY;
-
-            if (!noPreventDefault) {
-                evt.preventDefault();
-            }
-        };
-
-        this._onGestureStart = (e) => {
-            if (window.MSGesture === undefined) {
-                return;
-            }
-
-            if (!this._MSGestureHandler) {
-                this._MSGestureHandler = new MSGesture();
-                this._MSGestureHandler.target = element;
-            }
-
-            this._MSGestureHandler.addPointer(e.pointerId);
-        };
-
-        this._onGesture = (e) => {
-            this.camera.radius *= e.scale;
-
-            if (e.preventDefault) {
-                if (!noPreventDefault) {
-                    e.stopPropagation();
-                    e.preventDefault();
-                }
-            }
-        };
-
-        element.addEventListener("mousemove", this._onMouseMove, false);
-        element.addEventListener("MSPointerDown", <EventListener>this._onGestureStart, false);
-        element.addEventListener("MSGestureChange", <EventListener>this._onGesture, false);
-
-        Tools.RegisterTopRootEvents([
-            { name: "blur", handler: this._onLostFocus }
-        ]);
+        }
     }
 
     /**
-     * Detach the current controls from the specified dom element.
-     * @param element Defines the element to stop listening the inputs from
+     * Called on pointer POINTERDOUBLETAP event.
      */
-    public detachControl(element: Nullable<HTMLElement>): void {
-        if (this._onLostFocus) {
-            Tools.UnregisterTopRootEvents([
-                { name: "blur", handler: this._onLostFocus }
-            ]);
+    protected onDoubleTap(type: string) {
+        if (this.camera.useInputToRestoreState) {
+            this.camera.restoreState();
         }
+    }
 
-        if (element && this._observer) {
-            this.camera.getScene().onPointerObservable.remove(this._observer);
-            this._observer = null;
-
-            if (this._onContextMenu) {
-                element.removeEventListener("contextmenu", this._onContextMenu);
-            }
+    /**
+     * Called on pointer POINTERMOVE event if multiple touches are active.
+     */
+    protected onMultiTouch(pointA: Nullable<PointerTouch>,
+                           pointB: Nullable<PointerTouch>,
+                           previousPinchSquaredDistance: number,
+                           pinchSquaredDistance: number,
+                           previousMultiTouchPanPosition: Nullable<PointerTouch>,
+                           multiTouchPanPosition: Nullable<PointerTouch>): void
+    {
+        if (previousPinchSquaredDistance === 0 && previousMultiTouchPanPosition === null) {
+            // First time this method is called for new pinch.
+            // Next time this is called there will be a
+            // previousPinchSquaredDistance and pinchSquaredDistance to compare.
+            return;
+        }
+        if (pinchSquaredDistance === 0 && multiTouchPanPosition === null) {
+            // Last time this method is called at the end of a pinch.
+            return;
+        }
 
-            if (this._onMouseMove) {
-                element.removeEventListener("mousemove", this._onMouseMove);
+        var direction = this.pinchInwards ? 1 : -1;
+
+        if (this.multiTouchPanAndZoom) {
+            if (this.pinchDeltaPercentage) {
+                this.camera.inertialRadiusOffset +=
+                    (pinchSquaredDistance - previousPinchSquaredDistance) * 0.001 *
+                    this.camera.radius * this.pinchDeltaPercentage;
+            } else {
+                this.camera.inertialRadiusOffset +=
+                    (pinchSquaredDistance - previousPinchSquaredDistance) /
+                    (this.pinchPrecision * direction *
+                    (this.angularSensibilityX + this.angularSensibilityY) / 2);
             }
 
-            if (this._onGestureStart) {
-                element.removeEventListener("MSPointerDown", <EventListener>this._onGestureStart);
+            if (this.panningSensibility !== 0 &&
+              previousMultiTouchPanPosition && multiTouchPanPosition) {
+                var moveDeltaX = multiTouchPanPosition.x - previousMultiTouchPanPosition.x;
+                var moveDeltaY = multiTouchPanPosition.y - previousMultiTouchPanPosition.y;
+                this.camera.inertialPanningX += -moveDeltaX / this.panningSensibility;
+                this.camera.inertialPanningY += moveDeltaY / this.panningSensibility;
             }
+        } else {
+            this._twoFingerActivityCount++;
+            var previousPinchDistance = Math.sqrt(previousPinchSquaredDistance);
+            var pinchDistance = Math.sqrt(pinchSquaredDistance);
+            if (this._isPinching ||
+              (this._twoFingerActivityCount < 20 &&
+               Math.abs(pinchDistance - previousPinchDistance) >
+               this.camera.pinchToPanMaxDistance)) {
+                // Since pinch has not been active long, assume we intend to zoom.
+                if (this.pinchDeltaPercentage) {
+                    this.camera.inertialRadiusOffset +=
+                      (pinchSquaredDistance - previousPinchSquaredDistance) * 0.001 *
+                      this.camera.radius * this.pinchDeltaPercentage;
+                } else {
+                    this.camera.inertialRadiusOffset +=
+                        (pinchSquaredDistance - previousPinchSquaredDistance) /
+                        (this.pinchPrecision * direction *
+                        (this.angularSensibilityX + this.angularSensibilityY) / 2);
+                }
 
-            if (this._onGesture) {
-                element.removeEventListener("MSGestureChange", <EventListener>this._onGesture);
+                // Since we are pinching, remain pinching on next iteration.
+                this._isPinching = true;
+            } else {
+                // Pause between pinch starting and moving implies not a zoom event.
+                // Pan instead.
+                if (this.panningSensibility !== 0 && this.multiTouchPanning &&
+                  multiTouchPanPosition && previousMultiTouchPanPosition) {
+                    var moveDeltaX = multiTouchPanPosition.x - previousMultiTouchPanPosition.x;
+                    var moveDeltaY = multiTouchPanPosition.y - previousMultiTouchPanPosition.y;
+                    this.camera.inertialPanningX += -moveDeltaX / this.panningSensibility;
+                    this.camera.inertialPanningY += moveDeltaY / this.panningSensibility;
+                }
             }
-
-            this._isPanClick = false;
-            this.pinchInwards = true;
-
-            this._onMouseMove = null;
-            this._onGestureStart = null;
-            this._onGesture = null;
-            this._MSGestureHandler = null;
-            this._onLostFocus = null;
-            this._onContextMenu = null;
         }
     }
 
     /**
-     * Gets the class name of the current intput.
-     * @returns the class name
+     * Called each time a new POINTERDOWN event occurs. Ie, for each button
+     * press.
      */
-    public getClassName(): string {
-        return "ArcRotateCameraPointersInput";
+    protected onButtonDown(evt: PointerEvent, buttonCount: number): void {
+        this._isPanClick = evt.button === this.camera._panningMouseButton;
     }
 
     /**
-     * Get the friendly name associated with the input class.
-     * @returns the input friendly name
+     * Called each time a new POINTERUP event occurs. Ie, for each button
+     * release.
      */
-    public getSimpleName(): string {
-        return "pointers";
+    protected onButtonUp(evt: PointerEvent): void {
+        this._twoFingerActivityCount = 0;
+        this._isPinching = false;
     }
-}
 
-(<any>CameraInputTypes)["ArcRotateCameraPointersInput"] = ArcRotateCameraPointersInput;
+    /**
+     * Called when window becomes inactive.
+     */
+    protected onLostFocus(): void {
+        this._isPanClick = false;
+        this._twoFingerActivityCount = 0;
+        this._isPinching = false;
+    }
+}
+(<any>CameraInputTypes)["ArcRotateCameraPointersInput"] =
+  ArcRotateCameraPointersInput;

+ 3 - 3
src/Cameras/Inputs/followCameraMouseWheelInput.ts

@@ -16,19 +16,19 @@ export class FollowCameraMouseWheelInput implements ICameraInput<FollowCamera> {
     public camera: FollowCamera;
 
     /**
-     * Moue wheel controls zoom. (Moue wheel modifies camera.radius value.)
+     * Moue wheel controls zoom. (Mouse wheel modifies camera.radius value.)
      */
     @serialize()
     public axisControlRadius: boolean = true;
 
     /**
-     * Moue wheel controls height. (Moue wheel modifies camera.heightOffset value.)
+     * Moue wheel controls height. (Mouse wheel modifies camera.heightOffset value.)
      */
     @serialize()
     public axisControlHeight: boolean = false;
 
     /**
-     * Moue wheel controls angle. (Moue wheel modifies camera.rotationOffset value.)
+     * Moue wheel controls angle. (Mouse wheel modifies camera.rotationOffset value.)
      */
     @serialize()
     public axisControlRotation: boolean = false;

+ 222 - 0
src/Cameras/Inputs/followCameraPointersInput.ts

@@ -0,0 +1,222 @@
+import { Nullable } from "../../types";
+import { serialize } from "../../Misc/decorators";
+import { FollowCamera } from "../../Cameras/followCamera";
+import { CameraInputTypes } from "../../Cameras/cameraInputsManager";
+import { BaseCameraPointersInput } from "../../Cameras/Inputs/BaseCameraPointersInput";
+import { PointerTouch } from "../../Events/pointerEvents";
+
+/**
+ * Manage the pointers inputs to control an follow camera.
+ * @see http://doc.babylonjs.com/how_to/customizing_camera_inputs
+ */
+export class FollowCameraPointersInput extends BaseCameraPointersInput {
+    /**
+     * Defines the camera the input is attached to.
+     */
+    public camera: FollowCamera;
+
+    /**
+     * Gets the class name of the current input.
+     * @returns the class name
+     */
+    public getClassName(): string {
+        return "FollowCameraPointersInput";
+    }
+
+    /**
+     * Defines the pointer angular sensibility along the X axis or how fast is
+     * the camera rotating.
+     * A negative number will reverse the axis direction.
+     */
+    @serialize()
+    public angularSensibilityX = 1;
+
+    /**
+     * Defines the pointer angular sensibility along the Y axis or how fast is
+     * the camera rotating.
+     * A negative number will reverse the axis direction.
+     */
+    @serialize()
+    public angularSensibilityY = 1;
+
+    /**
+     * Defines the pointer pinch precision or how fast is the camera zooming.
+     * A negative number will reverse the axis direction.
+     */
+    @serialize()
+    public pinchPrecision = 10000.0;
+
+    /**
+     * pinchDeltaPercentage will be used instead of pinchPrecision if different
+     * from 0.
+     * It defines the percentage of current camera.radius to use as delta when
+     * pinch zoom is used.
+     */
+    @serialize()
+    public pinchDeltaPercentage = 0;
+
+    /**
+     * Pointer X axis controls zoom. (X axis modifies camera.radius value.)
+     */
+    @serialize()
+    public axisXControlRadius: boolean = false;
+
+    /**
+     * Pointer X axis controls height. (X axis modifies camera.heightOffset value.)
+     */
+    @serialize()
+    public axisXControlHeight: boolean = false;
+
+    /**
+     * Pointer X axis controls angle. (X axis modifies camera.rotationOffset value.)
+     */
+    @serialize()
+    public axisXControlRotation: boolean = true;
+
+    /**
+     * Pointer Y axis controls zoom. (Y axis modifies camera.radius value.)
+     */
+    @serialize()
+    public axisYControlRadius: boolean = false;
+
+    /**
+     * Pointer Y axis controls height. (Y axis modifies camera.heightOffset value.)
+     */
+    @serialize()
+    public axisYControlHeight: boolean = true;
+
+    /**
+     * Pointer Y axis controls angle. (Y axis modifies camera.rotationOffset value.)
+     */
+    @serialize()
+    public axisYControlRotation: boolean = false;
+
+    /**
+     * Pinch controls zoom. (Pinch modifies camera.radius value.)
+     */
+    @serialize()
+    public axisPinchControlRadius: boolean = true;
+
+    /**
+     * Pinch controls height. (Pinch modifies camera.heightOffset value.)
+     */
+    @serialize()
+    public axisPinchControlHeight: boolean = false;
+
+    /**
+     * Pinch controls angle. (Pinch modifies camera.rotationOffset value.)
+     */
+    @serialize()
+    public axisPinchControlRotation: boolean = false;
+
+    /**
+     * Log error messages if basic misconfiguration has occurred.
+     */
+    public warningEnable: boolean = true;
+
+    protected onTouch(pointA: Nullable<PointerTouch>,
+                      offsetX: number,
+                      offsetY: number): void
+    {
+        this._warning();
+
+        if (this.axisXControlRotation) {
+            this.camera.rotationOffset += offsetX / this.angularSensibilityX;
+        } else if (this.axisYControlRotation) {
+            this.camera.rotationOffset += offsetY / this.angularSensibilityX;
+        }
+
+        if (this.axisXControlHeight) {
+            this.camera.heightOffset += offsetX / this.angularSensibilityY;
+        } else if (this.axisYControlHeight) {
+            this.camera.heightOffset += offsetY / this.angularSensibilityY;
+        }
+
+        if (this.axisXControlRadius) {
+            this.camera.radius -= offsetX / this.angularSensibilityY;
+        } else if (this.axisYControlRadius) {
+            this.camera.radius -= offsetY / this.angularSensibilityY;
+        }
+    }
+
+    protected doMultiTouch(pointA: Nullable<PointerTouch>,
+                           pointB: Nullable<PointerTouch>,
+                           previousPinchSquaredDistance: number,
+                           pinchSquaredDistance: number,
+                           previousMultiTouchPanPosition: Nullable<PointerTouch>,
+                           multiTouchPanPosition: Nullable<PointerTouch>): void
+    {
+        if (previousPinchSquaredDistance === 0 && previousMultiTouchPanPosition === null) {
+            // First time this method is called for new pinch.
+            // Next time this is called there will be a
+            // previousPinchSquaredDistance and pinchSquaredDistance to compare.
+            return;
+        }
+        if (pinchSquaredDistance === 0 && multiTouchPanPosition === null) {
+            // Last time this method is called at the end of a pinch.
+            return;
+        }
+        var pinchDelta =
+            (pinchSquaredDistance - previousPinchSquaredDistance) /
+            (this.pinchPrecision * (this.angularSensibilityX + this.angularSensibilityY) / 2);
+
+        if (this.pinchDeltaPercentage) {
+            pinchDelta *= 0.01 * this.pinchDeltaPercentage;
+            if (this.axisPinchControlRotation) {
+                this.camera.rotationOffset += pinchDelta * this.camera.rotationOffset;
+            }
+            if (this.axisPinchControlHeight) {
+                this.camera.heightOffset += pinchDelta * this.camera.heightOffset;
+            }
+            if (this.axisPinchControlRadius) {
+                this.camera.radius -= pinchDelta * this.camera.radius;
+            }
+        } else {
+            if (this.axisPinchControlRotation) {
+                this.camera.rotationOffset += pinchDelta;
+            }
+
+            if (this.axisPinchControlHeight) {
+                this.camera.heightOffset += pinchDelta;
+            }
+
+            if (this.axisPinchControlRadius) {
+                this.camera.radius -= pinchDelta;
+            }
+        }
+    }
+
+    /* Check for obvious misconfiguration. */
+    private _warningCounter: number = 0;
+    private _warning(): void {
+        if (!this.warningEnable || this._warningCounter++ % 100 !== 0) {
+            return;
+        }
+        let warn = "It probably only makes sense to control ONE camera " +
+                   "property with each pointer axis. Set 'warningEnable = false' " +
+                   "if you are sure. Currently enabled: ";
+
+        console.assert((<number>(<unknown>this.axisXControlRotation) +
+                        <number>(<unknown>this.axisXControlHeight) +
+                        <number>(<unknown>this.axisXControlRadius)) <= 1,
+                       warn +
+                       "axisXControlRotation: " + this.axisXControlRotation +
+                       ", axisXControlHeight: " + this.axisXControlHeight +
+                       ", axisXControlRadius: " + this.axisXControlRadius);
+        console.assert((<number>(<unknown>this.axisYControlRotation) +
+                        <number>(<unknown>this.axisYControlHeight) +
+                        <number>(<unknown>this.axisYControlRadius)) <= 1,
+                       warn +
+                       "axisYControlRotation: " + this.axisYControlRotation +
+                       ", axisYControlHeight: " + this.axisYControlHeight +
+                       ", axisYControlRadius: " + this.axisYControlRadius);
+        console.assert((<number>(<unknown>this.axisPinchControlRotation) +
+                        <number>(<unknown>this.axisPinchControlHeight) +
+                        <number>(<unknown>this.axisPinchControlRadius)) <= 1,
+                       warn +
+                       "axisPinchControlRotation: " + this.axisPinchControlRotation +
+                       ", axisPinchControlHeight: " + this.axisPinchControlHeight +
+                       ", axisPinchControlRadius: " + this.axisPinchControlRadius);
+    }
+}
+(<any>CameraInputTypes)["FollowCameraPointersInput"] = FollowCameraPointersInput;

+ 4 - 1
src/Cameras/Inputs/index.ts

@@ -5,9 +5,12 @@ export * from "./arcRotateCameraPointersInput";
 export * from "./arcRotateCameraVRDeviceOrientationInput";
 export * from "./flyCameraKeyboardInput";
 export * from "./flyCameraMouseInput";
+export * from "./followCameraKeyboardMoveInput";
+export * from "./followCameraMouseWheelInput";
+export * from "./followCameraPointersInput";
 export * from "./freeCameraDeviceOrientationInput";
 export * from "./freeCameraGamepadInput";
 export * from "./freeCameraKeyboardMoveInput";
 export * from "./freeCameraMouseInput";
 export * from "./freeCameraTouchInput";
-export * from "./freeCameraVirtualJoystickInput";
+export * from "./freeCameraVirtualJoystickInput";

+ 1 - 1
src/Cameras/followCamera.ts

@@ -120,7 +120,7 @@ export class FollowCamera extends TargetCamera {
 
         this.lockedTarget = lockedTarget;
         this.inputs = new FollowCameraInputsManager(this);
-        this.inputs.addKeyboard().addMouseWheel();
+        this.inputs.addKeyboard().addMouseWheel().addPointers();
         // Uncomment the following line when the relevant handlers have been implemented.
         // this.inputs.addKeyboard().addMouseWheel().addPointers().addVRDeviceOrientation();
     }

+ 2 - 1
src/Cameras/followCameraInputsManager.ts

@@ -2,6 +2,7 @@ import { CameraInputsManager } from "./cameraInputsManager";
 import { FollowCamera } from "./followCamera";
 import { FollowCameraKeyboardMoveInput } from './Inputs/followCameraKeyboardMoveInput';
 import { FollowCameraMouseWheelInput } from './Inputs/followCameraMouseWheelInput';
+import { FollowCameraPointersInput } from './Inputs/followCameraPointersInput';
 
 /**
  * Default Inputs manager for the FollowCamera.
@@ -40,7 +41,7 @@ export class FollowCameraInputsManager extends CameraInputsManager<FollowCamera>
      * @returns the current input manager
      */
     public addPointers(): FollowCameraInputsManager {
-        console.warn("Pointer support not yet implemented for FollowCamera.");
+        this.add(new FollowCameraPointersInput());
         return this;
     }
 

+ 22 - 0
src/Events/pointerEvents.ts

@@ -115,3 +115,25 @@ export class PointerInfo extends PointerInfoBase {
         super(type, event);
     }
 }
+
+/**
+ * Data relating to a touch event on the screen.
+ */
+export interface PointerTouch {
+    /**
+     * X coordinate of touch.
+     */
+    x: number;
+    /**
+     * Y coordinate of touch.
+     */
+    y: number;
+    /**
+     * Id of touch. Unique for each finger.
+     */
+    pointerId: number;
+    /**
+     * Event type passed from DOM.
+     */
+    type: any;
+}