浏览代码

Pointer inputs for FollowCamera.

duncan law 6 年之前
父节点
当前提交
342e32b833

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

@@ -0,0 +1,391 @@
+import { Nullable } from "../../types";
+import { serialize } from "../../Misc/decorators";
+import { EventState, Observer } from "../../Misc/observable";
+import { Tools } from "../../Misc/tools";
+import { TargetCamera } from "../../Cameras/targetCamera";
+import { FollowCamera } from "../../Cameras/followCamera";
+import { ICameraInput, CameraInputTypes } from "../../Cameras/cameraInputsManager";
+import { PointerInfo, PointerEventTypes, PointerTouch } from "../../Events/pointerEvents";
+
+export class CameraPointersInputBase implements ICameraInput<TargetCamera> {
+    /**
+     * Defines the camera the input is attached to.
+     */
+    public camera: TargetCamera;
+
+    /**
+     * Defines the buttons associated with the input to handle camera move.
+     */
+    @serialize()
+    public buttons = [0, 1, 2];
+    
+    /** @hidden
+     * Log debug messages from un-implemented methods.
+     */
+    public debug: boolean = false;
+
+    private _pointerInput: (p: PointerInfo, s: EventState) => void;
+    private _observer: Nullable<Observer<PointerInfo>>;
+    private _MSGestureHandler: Nullable<MSGesture>;
+    private _onLostFocus: Nullable<(e: FocusEvent) => any>;
+    private _element: HTMLElement;
+    private _noPreventDefault: boolean;
+
+    /**
+     * 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 {
+      this._element = element;
+      this._noPreventDefault = noPreventDefault || false;
+        var engine = this.camera.getEngine();
+        var cacheSoloPointer: Nullable<PointerTouch>; // cache pointer object for better perf on camera rotation
+        var pointA: Nullable<PointerTouch> = null;
+        var pointB: Nullable<PointerTouch> = null;
+        var previousPinchSquaredDistance = 0;
+        var previousMultiTouchPanPosition: Nullable<PointerTouch> = null;
+
+        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 (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.doTouch(null, offsetX, offsetY);
+            } else if (p.type === PointerEventTypes.POINTERDOWN && srcElement) {
+                try {
+                    srcElement.setPointerCapture(evt.pointerId);
+                } catch (e) {
+                    //Nothing to do with the error. Execution will continue.
+                }
+
+                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) {
+                this.doDoubleTouch(evt.pointerType);
+            } else if (p.type === PointerEventTypes.POINTERUP && srcElement) {
+                try {
+                    srcElement.releasePointerCapture(evt.pointerId);
+                } catch (e) {
+                    //Nothing to do with the error.
+                }
+
+                cacheSoloPointer = null;
+
+                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;
+                        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) {
+                    var offsetX = evt.clientX - cacheSoloPointer.x;
+                    var offsetY = evt.clientY - cacheSoloPointer.y;
+                    this.doTouch(pointA, offsetX, offsetY);
+
+                    cacheSoloPointer.x = evt.clientX;
+                    cacheSoloPointer.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};
+
+                    if (previousPinchSquaredDistance === 0) {
+                        previousPinchSquaredDistance = pinchSquaredDistance;
+                        previousMultiTouchPanPosition = {x: (pointA.x + pointB.x) / 2,
+                                                         y: (pointA.y + pointB.y) / 2,
+                                                         pointerId: evt.pointerId,
+                                                         type: p.type};
+                        return;
+                    }
+
+                    this.doMultiTouch(
+                      pointA,
+                      pointB,
+                      previousPinchSquaredDistance,
+                      pinchSquaredDistance,
+                      previousMultiTouchPanPosition,
+                      multiTouchPanPosition);
+                  
+                    previousMultiTouchPanPosition = {x: (pointA.x + pointB.x) / 2,
+                                                     y: (pointA.y + pointB.y) / 2,
+                                                     pointerId: evt.pointerId,
+                                                     type: p.type};
+                    previousPinchSquaredDistance = pinchSquaredDistance;
+                }
+            }
+        };
+
+        this._observer = this.camera.getScene().onPointerObservable.add(
+            this._pointerInput,
+            PointerEventTypes.POINTERDOWN | PointerEventTypes.POINTERUP |
+            PointerEventTypes.POINTERMOVE);
+
+        this._onLostFocus = () => {
+            this.log("FollowCameraPointersInput._onLostFocus");
+            pointA = pointB = null;
+            cacheSoloPointer = null;
+        };
+
+        element.addEventListener("contextmenu", this.onContextMenu.bind(this), false);
+        element.addEventListener("MSPointerDown", 
+            <EventListener>this.onGestureStart.bind(this), false);
+        element.addEventListener("MSGestureChange",
+            <EventListener>this.onGesture.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", this.onContextMenu);
+            }
+
+            if (this.onGestureStart) {
+                element.removeEventListener("MSPointerDown", <EventListener>this.onGestureStart);
+            }
+
+            if (this.onGesture) {
+                element.removeEventListener("MSGestureChange", <EventListener>this.onGesture);
+            }
+
+            this._MSGestureHandler = null;
+            this._onLostFocus = null;
+        }
+    }
+
+    /**
+     * Gets the class name of the current input.
+     * @returns the class name
+     */
+    public getClassName(): string {
+        return "ArcRotateCameraPointersInput";
+    }
+
+    /**
+     * Get the friendly name associated with the input class.
+     * @returns the input friendly name
+     */
+    public getSimpleName(): string {
+        return "pointers";
+    }
+
+    protected doDoubleTouch(type: string) {
+        this.log("FollowCameraPointersBase.doDoubleTouch(", type, ")");
+    }
+
+    protected doTouch(pointA: Nullable<PointerTouch>,
+                      offsetX: number,
+                      offsetY: number): void
+    {
+        this.log("FollowCameraPointersBase.doTouch(",
+                 pointA,
+                 offsetX,
+                 offsetY,
+                 ")");
+    }
+
+    protected doMultiTouch(pointA: Nullable<PointerTouch>,
+                           pointB: Nullable<PointerTouch>,
+                           previousPinchSquaredDistance: number,
+                           pinchSquaredDistance: number,
+                           previousMultiTouchPanPosition: Nullable<PointerTouch>,
+                           multiTouchPanPosition: Nullable<PointerTouch>): void {
+      this.log("FollowCameraPointersBase.doMultiTouch(",
+               pointA,
+               pointB,
+               previousPinchSquaredDistance,
+               pinchSquaredDistance,
+               previousMultiTouchPanPosition,
+               multiTouchPanPosition,
+               ")");
+    }
+
+    protected onContextMenu(evt: PointerEvent): void {
+        this.log("FollowCameraPointersInput.onContextMenu");
+        evt.preventDefault();
+    };
+
+    protected onGestureStart(e: PointerEvent): void {
+        this.log("FollowCameraPointersInput.onGestureStart");
+        if (window.MSGesture === undefined) {
+            return;
+        }
+
+        if (!this._MSGestureHandler) {
+            this._MSGestureHandler = new MSGesture();
+            this._MSGestureHandler.target = this._element;
+        }
+
+        this._MSGestureHandler.addPointer(e.pointerId);
+    };
+
+    protected onGesture(e: PointerEvent): void {
+        this.log("FollowCameraPointersInput.onGesture");
+
+        if (e.preventDefault) {
+            if (!this._noPreventDefault) {
+                e.stopPropagation();
+                e.preventDefault();
+            }
+        }
+    };
+
+    protected log(...args: any[]) {
+        if(this.debug){
+            console.log.apply(console, arguments);
+        }
+    }
+}
+
+/**
+ * Manage the pointers inputs to control an follow camera.
+ * @see http://doc.babylonjs.com/how_to/customizing_camera_inputs
+ */
+export class FollowCameraPointersInput extends CameraPointersInputBase {
+    /**
+     * Defines the camera the input is attached to.
+     */
+    public camera: FollowCamera;
+
+    /**
+     * Defines the pointer angular sensibility  along the X axis or how fast is the camera rotating.
+     */
+    @serialize()
+    public angularSensibilityX = 1;
+
+    /**
+     * Defines the pointer angular sensibility along the Y axis or how fast is the camera rotating.
+     */
+    @serialize()
+    public angularSensibilityY = 1;
+
+    /**
+     * Defines the pointer pinch precision or how fast is the camera zooming.
+     */
+    @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;
+    
+    protected doTouch(pointA: Nullable<PointerTouch>,
+                      offsetX: number,
+                      offsetY: number): void
+    {
+        //super.doTouch(pointA, offsetX, offsetY);
+        this.camera.rotationOffset -= offsetX / this.angularSensibilityX;
+        this.camera.heightOffset -= offsetY / this.angularSensibilityY;
+    }
+
+    protected doMultiTouch(pointA: Nullable<PointerTouch>,
+                           pointB: Nullable<PointerTouch>,
+                           previousPinchSquaredDistance: number,
+                           pinchSquaredDistance: number,
+                           previousMultiTouchPanPosition: Nullable<PointerTouch>,
+                           multiTouchPanPosition: Nullable<PointerTouch>): void
+    {
+        this.camera.radius +=
+            (pinchSquaredDistance - previousPinchSquaredDistance) /
+            (this.pinchPrecision * (this.angularSensibilityX + this.angularSensibilityY) / 2);
+    }
+}
+(<any>CameraInputTypes)["FollowCameraPointersInput"] = FollowCameraPointersInput;

+ 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;
     }
 

+ 10 - 0
src/Events/pointerEvents.ts

@@ -113,3 +113,13 @@ export class PointerInfo extends PointerInfoBase {
         super(type, event);
     }
 }
+
+/**
+ * Data relating to a touch event on the screen.
+ */
+export interface PointerTouch {
+  x: number;
+  y: number;
+  pointerId: number;
+  type: any;
+}