UNPKG

@xeokit/xeokit-sdk

Version:

3D BIM IFC Viewer SDK for AEC engineering applications. Open Source JavaScript Toolkit based on pure WebGL for top performance, real-world coordinates and full double precision

1,477 lines (1,374 loc) 67.5 kB
import {Component} from '../Component.js'; import {CameraFlightAnimation} from './../camera/CameraFlightAnimation.js'; import {PanController} from "./lib/controllers/PanController.js"; import {PivotController} from "./lib/controllers/PivotController.js"; import {PickController} from "./lib/controllers/PickController.js"; import {MousePanRotateDollyHandler} from "./lib/handlers/MousePanRotateDollyHandler.js"; import {KeyboardAxisViewHandler} from "./lib/handlers/KeyboardAxisViewHandler.js"; import {MousePickHandler} from "./lib/handlers/MousePickHandler.js"; import {KeyboardPanRotateDollyHandler} from "./lib/handlers/KeyboardPanRotateDollyHandler.js"; import {CameraUpdater} from "./lib/CameraUpdater.js"; import {MouseMiscHandler} from "./lib/handlers/MouseMiscHandler.js"; import {TouchPanRotateAndDollyHandler} from "./lib/handlers/TouchPanRotateAndDollyHandler.js"; import {utils} from "../utils.js"; import {math} from "../math/math.js"; import {TouchPickHandler} from "./lib/handlers/TouchPickHandler.js"; const DEFAULT_SNAP_PICK_RADIUS = 30; const DEFAULT_SNAP_VERTEX = true; const DEFAULT_SNAP_EDGE = true; /** * @desc Controls the {@link Camera} with user input, and fires events when the user interacts with pickable {@link Entity}s. * * # Contents * * * [Overview](#overview) * * [Examples](#examples) * * [Orbit Mode](#orbit-mode) * + [Following the Pointer in Orbit Mode](#--following-the-pointer-in-orbit-mode--) * + [Showing the Pivot Position](#--showing-the-pivot-position--) * + [Axis-Aligned Views in Orbit Mode](#--axis-aligned-views-in-orbit-mode--) * + [View-Fitting Entitys in Orbit Mode](#--view-fitting-entitys-in-orbit-mode--) * * [First-Person Mode](#first-person-mode) * + [Following the Pointer in First-Person Mode](#--following-the-pointer-in-first-person-mode--) * + [Constraining Vertical Position in First-Person Mode](#--constraining-vertical-position-in-first-person-mode--) * + [Axis-Aligned Views in First-Person Mode](#--axis-aligned-views-in-first-person-mode--) * + [View-Fitting Entitys in First-Person Mode](#--view-fitting-entitys-in-first-person-mode--) * * [Plan-View Mode](#plan-view-mode) * + [Following the Pointer in Plan-View Mode](#--following-the-pointer-in-plan-view-mode--) * + [Axis-Aligned Views in Plan-View Mode](#--axis-aligned-views-in-plan-view-mode--) * * [CameraControl Events](#cameracontrol-events) * + ["hover"](#---hover---) * + ["hoverOff"](#---hoveroff---) * + ["hoverEnter"](#---hoverenter---) * + ["hoverOut"](#---hoverout---) * + ["picked"](#---picked---) * + ["pickedSurface"](#---pickedsurface---) * + ["pickedNothing"](#---pickednothing---) * + ["doublePicked"](#---doublepicked---) * + ["doublePickedSurface"](#---doublepickedsurface---) * + ["doublePickedNothing"](#---doublepickednothing---) * + ["rightClick"](#---rightclick---) * * [Custom Keyboard Mappings](#custom-keyboard-mappings) * * <br><br> * * # Overview * * * Each {@link Viewer} has a ````CameraControl````, located at {@link Viewer#cameraControl}. * * {@link CameraControl#navMode} selects the navigation mode: * * ````"orbit"```` rotates the {@link Camera} position about the target. * * ````"firstPerson"```` rotates the World about the Camera position. * * ````"planView"```` never rotates, but still allows to pan and dolly, typically for an axis-aligned view. * * {@link CameraControl#followPointer} makes the Camera follow the mouse or touch pointer. * * {@link CameraControl#constrainVertical} locks the Camera to its current height when in first-person mode. * * ````CameraControl```` fires pick events when we hover, click or tap on an {@link Entity}. * <br><br> * * # Examples * * * [Orbit Navigation - Duplex Model](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_orbit_Duplex) * * [Orbit Navigation - Holter Tower Model](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_orbit_HolterTower) * * [First-Person Navigation - Duplex Model](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_firstPerson_Duplex) * * [First-Person Navigation - Holter Tower Model](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_firstPerson_HolterTower) * * [Plan-view Navigation - Schependomlaan Model](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_planView_Schependomlaan) * * [Custom Keyboard Mapping](https://xeokit.github.io/xeokit-sdk/examples/index.html#CameraControl_keyMap) * <br><br> * * # Orbit Mode * * In orbit mode, ````CameraControl```` orbits the {@link Camera} about the target. * * To enable orbit mode: * * ````javascript * const cameraControl = myViewer.cameraControl; * cameraControl.navMode = "orbit"; * ```` * * Then orbit by: * * * left-dragging the mouse, * * tap-dragging the touch pad, and * * pressing arrow keys, or ````Q```` and ````E```` on a QWERTY keyboard, or ````A```` and ````E```` on an AZERTY keyboard. * <br><br> * * Dolly forwards and backwards by: * * * spinning the mouse wheel, * * pinching on the touch pad, and * * pressing the ````+```` and ````-```` keys, or ````W```` and ````S```` on a QWERTY keyboard, or ````Z```` and ````S```` for AZERTY. * <br><br> * * Pan horizontally and vertically by: * * * right-dragging the mouse, * * left-dragging the mouse with the SHIFT key down, * * tap-dragging the touch pad with SHIFT down, * * pressing the ````A````, ````D````, ````Z```` and ````X```` keys on a QWERTY keyboard, and * * pressing the ````Q````, ````D````, ````W```` and ````X```` keys on an AZERTY keyboard, * <br><br> * * ## Following the Pointer in Orbit Mode * * When {@link CameraControl#followPointer} is ````true````in orbiting mode, the mouse or touch pointer will dynamically * indicate the target that the {@link Camera} will orbit, as well as dolly to and from. * * Lets ensure that we're in orbit mode, then enable the {@link Camera} to follow the pointer: * * ````javascript * cameraControl.navMode = "orbit"; * cameraControl.followPointer = true; * ```` * * ## Smart Pivoting * * TODO * * ## Showing the Pivot Position * * We can configure {@link CameraControl#pivotElement} with an HTML element to indicate the current * pivot position. The indicator will appear momentarily each time we move the {@link Camera} while in orbit mode with * {@link CameraControl#followPointer} set ````true````. * * First we'll define some CSS to style our pivot indicator as a black dot with a white border: * * ````css * .camera-pivot-marker { * color: #ffffff; * position: absolute; * width: 25px; * height: 25px; * border-radius: 15px; * border: 2px solid #ebebeb; * background: black; * visibility: hidden; * box-shadow: 5px 5px 15px 1px #000000; * z-index: 10000; * pointer-events: none; * } * ```` * * Then we'll attach our pivot indicator's HTML element to the ````CameraControl````: * * ````javascript * const pivotElement = document.createRange().createContextualFragment("<div class='camera-pivot-marker'></div>").firstChild; * * document.body.appendChild(pivotElement); * * cameraControl.pivotElement = pivotElement; * ```` * * ## Axis-Aligned Views in Orbit Mode * * In orbit mode, we can use keys 1-6 to position the {@link Camera} to look at the center of the {@link Scene} from along each of the * six World-space axis. Pressing one of these keys will fly the {@link Camera} to the corresponding axis-aligned view. * * ## View-Fitting Entitys in Orbit Mode * * When {@link CameraControl#doublePickFlyTo} is ````true````, we can left-double-click or * double-tap (ie. "double-pick") an {@link Entity} to fit it to view. This will cause the {@link Camera} * to fly to that Entity. Our target then becomes the center of that Entity. If we are currently pivoting, * then our pivot position is then also set to the Entity center. * * Disable that behaviour by setting {@link CameraControl#doublePickFlyTo} ````false````. * * # First-Person Mode * * In first-person mode, ````CameraControl```` rotates the World about the {@link Camera} position. * * To enable first-person mode: * * ````javascript * cameraControl.navMode = "firstPerson"; * ```` * * Then rotate by: * * * left-dragging the mouse, * * tap-dragging the touch pad, * * pressing arrow keys, or ````Q```` and ````E```` on a QWERTY keyboard, or ````A```` and ````E```` on an AZERTY keyboard. * <br><br> * * Dolly forwards and backwards by: * * * spinning the mouse wheel, * * pinching on the touch pad, and * * pressing the ````+```` and ````-```` keys, or ````W```` and ````S```` on a QWERTY keyboard, or ````Z```` and ````S```` for AZERTY. * <br><br> * * Pan left, right, up and down by: * * * left-dragging or right-dragging the mouse, and * * tap-dragging the touch pad with SHIFT down. * * Pan forwards, backwards, left, right, up and down by pressing the ````WSADZX```` keys on a QWERTY keyboard, * or ````WSQDWX```` keys on an AZERTY keyboard. * <br><br> * * ## Following the Pointer in First-Person Mode * * When {@link CameraControl#followPointer} is ````true```` in first-person mode, the mouse or touch pointer will dynamically * indicate the target to which the {@link Camera} will dolly to and from. In first-person mode, however, the World will always rotate * about the {@link Camera} position. * * Lets ensure that we're in first-person mode, then enable the {@link Camera} to follow the pointer: * * ````javascript * cameraControl.navMode = "firstPerson"; * cameraControl.followPointer = true; * ```` * * When the pointer is over empty space, the target will remain the last object that the pointer was over. * * ## Constraining Vertical Position in First-Person Mode * * In first-person mode, we can lock the {@link Camera} to its current position on the vertical World axis, which is useful for walk-through navigation: * * ````javascript * cameraControl.constrainVertical = true; * ```` * * ## Axis-Aligned Views in First-Person Mode * * In first-person mode we can use keys 1-6 to position the {@link Camera} to look at the center of * the {@link Scene} from along each of the six World-space axis. Pressing one of these keys will fly the {@link Camera} to the * corresponding axis-aligned view. * * ## View-Fitting Entitys in First-Person Mode * * As in orbit mode, when in first-person mode and {@link CameraControl#doublePickFlyTo} is ````true````, we can double-click * or double-tap an {@link Entity} (ie. "double-picking") to fit it in view. This will cause the {@link Camera} to fly to * that Entity. Our target then becomes the center of that Entity. * * Disable that behaviour by setting {@link CameraControl#doublePickFlyTo} ````false````. * * # Plan-View Mode * * In plan-view mode, ````CameraControl```` pans and rotates the {@link Camera}, without rotating it. * * To enable plan-view mode: * * ````javascript * cameraControl.navMode = "planView"; * ```` * * Dolly forwards and backwards by: * * * spinning the mouse wheel, * * pinching on the touch pad, and * * pressing the ````+```` and ````-```` keys. * * <br> * Pan left, right, up and down by: * * * left-dragging or right-dragging the mouse, and * * tap-dragging the touch pad with SHIFT down. * * Pan forwards, backwards, left, right, up and down by pressing the ````WSADZX```` keys on a QWERTY keyboard, * or ````WSQDWX```` keys on an AZERTY keyboard. * <br><br> * * ## Following the Pointer in Plan-View Mode * * When {@link CameraControl#followPointer} is ````true```` in plan-view mode, the mouse or touch pointer will dynamically * indicate the target to which the {@link Camera} will dolly to and from. In plan-view mode, however, the {@link Camera} cannot rotate. * * Lets ensure that we're in plan-view mode, then enable the {@link Camera} to follow the pointer: * * ````javascript * cameraControl.navMode = "planView"; * cameraControl.followPointer = true; // Default * ```` * * When the pointer is over empty space, the target will remain the last object that the pointer was over. * * ## Axis-Aligned Views in Plan-View Mode * * As in orbit and first-person modes, in plan-view mode we can use keys 1-6 to position the {@link Camera} to look at the center of * the {@link Scene} from along each of the six World-space axis. Pressing one of these keys will fly the {@link Camera} to the * corresponding axis-aligned view. * * # CameraControl Events * * ````CameraControl```` fires events as we interact with {@link Entity}s using mouse or touch input. * * The following examples demonstrate how to subscribe to those events. * * The first example shows how to save a handle to a subscription, which we can later use to unsubscribe. * * ## "hover" * * Event fired when the pointer moves while hovering over an Entity. * * ````javascript * const onHover = cameraControl.on("hover", (e) => { * const entity = e.entity; // Entity * const canvasPos = e.canvasPos; // 2D canvas position * }); * ```` * * To unsubscribe from the event: * * ````javascript * cameraControl.off(onHover); * ```` * * ## "hoverOff" * * Event fired when the pointer moves while hovering over empty space. * * ````javascript * cameraControl.on("hoverOff", (e) => { * const canvasPos = e.canvasPos; * }); * ```` * * ## "hoverEnter" * * Event fired when the pointer moves onto an Entity. * * ````javascript * cameraControl.on("hoverEnter", (e) => { * const entity = e.entity; * const canvasPos = e.canvasPos; * }); * ```` * * ## "hoverOut" * * Event fired when the pointer moves off an Entity. * * ````javascript * cameraControl.on("hoverOut", (e) => { * const entity = e.entity; * const canvasPos = e.canvasPos; * }); * ```` * * ## "picked" * * Event fired when we left-click or tap on an Entity. * * ````javascript * cameraControl.on("picked", (e) => { * const entity = e.entity; * const canvasPos = e.canvasPos; * }); * ```` * * ## "pickedSurface" * * Event fired when we left-click or tap on the surface of an Entity. * * ````javascript * cameraControl.on("picked", (e) => { * const entity = e.entity; * const canvasPos = e.canvasPos; * const worldPos = e.worldPos; // 3D World-space position * const viewPos = e.viewPos; // 3D View-space position * const worldNormal = e.worldNormal; // 3D World-space normal vector * }); * ```` * * ## "pickedNothing" * * Event fired when we left-click or tap on empty space. * * ````javascript * cameraControl.on("pickedNothing", (e) => { * const canvasPos = e.canvasPos; * }); * ```` * * ## "doublePicked" * * Event fired wwhen we left-double-click or double-tap on an Entity. * * ````javascript * cameraControl.on("doublePicked", (e) => { * const entity = e.entity; * const canvasPos = e.canvasPos; * }); * ```` * * ## "doublePickedSurface" * * Event fired when we left-double-click or double-tap on the surface of an Entity. * * ````javascript * cameraControl.on("doublePickedSurface", (e) => { * const entity = e.entity; * const canvasPos = e.canvasPos; * const worldPos = e.worldPos; * const viewPos = e.viewPos; * const worldNormal = e.worldNormal; * }); * ```` * * ## "doublePickedNothing" * * Event fired when we left-double-click or double-tap on empty space. * * ````javascript * cameraControl.on("doublePickedNothing", (e) => { * const canvasPos = e.canvasPos; * }); * ```` * * ## "rightClick" * * Event fired when we right-click on the canvas. * * ````javascript * cameraControl.on("rightClick", (e) => { * const event = e.event; // Mouse event * const canvasPos = e.canvasPos; * }); * ```` * * ## Custom Keyboard Mappings * * We can customize````CameraControl```` key bindings as shown below. * * In this example, we'll just set the default bindings for a QWERTY keyboard. * * ````javascript * const input = myViewer.scene.input; * * cameraControl.navMode = "orbit"; * cameraControl.followPointer = true; * * const keyMap = {}; * * keyMap[cameraControl.PAN_LEFT] = [input.KEY_A]; * keyMap[cameraControl.PAN_RIGHT] = [input.KEY_D]; * keyMap[cameraControl.PAN_UP] = [input.KEY_Z]; * keyMap[cameraControl.PAN_DOWN] = [input.KEY_X]; * keyMap[cameraControl.DOLLY_FORWARDS] = [input.KEY_W, input.KEY_ADD]; * keyMap[cameraControl.DOLLY_BACKWARDS] = [input.KEY_S, input.KEY_SUBTRACT]; * keyMap[cameraControl.ROTATE_X_POS] = [input.KEY_DOWN_ARROW]; * keyMap[cameraControl.ROTATE_X_NEG] = [input.KEY_UP_ARROW]; * keyMap[cameraControl.ROTATE_Y_POS] = [input.KEY_LEFT_ARROW]; * keyMap[cameraControl.ROTATE_Y_NEG] = [input.KEY_RIGHT_ARROW]; * keyMap[cameraControl.AXIS_VIEW_RIGHT] = [input.KEY_NUM_1]; * keyMap[cameraControl.AXIS_VIEW_BACK] = [input.KEY_NUM_2]; * keyMap[cameraControl.AXIS_VIEW_LEFT] = [input.KEY_NUM_3]; * keyMap[cameraControl.AXIS_VIEW_FRONT] = [input.KEY_NUM_4]; * keyMap[cameraControl.AXIS_VIEW_TOP] = [input.KEY_NUM_5]; * keyMap[cameraControl.AXIS_VIEW_BOTTOM] = [input.KEY_NUM_6]; * keyMap[cameraControl.MOUSE_PAN] = [[input.KEY_SHIFT, input.MOUSE_LEFT_BUTTON]]; * keyMap[cameraControl.MOUSE_ROTATE] = [ * [input.KEY_SHIFT, input.MOUSE_MIDDLE_BUTTON], * [input.KEY_SHIFT, input.MOUSE_RIGHT_BUTTON] * ] * keyMap[cameraControl.MOUSE_DOLLY] = [[input.KEY_CTRL, input.MOUSE_RIGHT_BUTTON]]; * * cameraControl.keyMap = keyMap; * ```` * * We can also just configure default bindings for a specified keyboard layout, like this: * * ````javascript * cameraControl.keyMap = "qwerty"; * ```` * * Then, ````CameraControl```` will internally set {@link CameraControl#keyMap} to the default key map for the QWERTY * layout (which is the same set of mappings we set in the previous example). In other words, if we subsequently * read {@link CameraControl#keyMap}, it will now be a key map, instead of the "qwerty" string value we set it to. * * Supported layouts are, so far: * * * ````"qwerty"```` * * ````"azerty"```` * * ## Basic Keyboard Mapping * * ````"OR" Relation```` * Set multiple keys to trigger an action if any one is pressed: * * ````javascript * keyMap[cameraControl.DOLLY_BACKWARDS] = [input.KEY_S, input.KEY_SUBTRACT]; * ```` * * If either ````KEY_S```` or ````KEY_SUBTRACT```` is pressed, the camera will dolly backward. * * * ````"AND" Relation```` * To require all keys in a combination to be pressed: * * ````javascript * keyMap[cameraControl.DOLLY_BACKWARDS] = [[input.KEY_S, input.KEY_SUBTRACT]]; * ```` * * The camera will dolly backward if ````both KEY_S```` and ````KEY_SUBTRACT```` are pressed. * * * ````Mix "AND" and "OR" Relation```` * Use a combination of keys and groups for flexibility: * * ````javascript * keyMap[cameraControl.DOLLY_BACKWARDS] = [ * [input.KEY_S, input.KEY_SUBTRACT], // 'And' group * input.KEY_SHIFT // 'Or' with previous * ]; * ```` * * The camera will dolly backward if ````KEY_S```` + ````KEY_SUBTRACT```` are pressed together, or if ````KEY_SHIFT```` is pressed. * * ## Special Mouse Actions * Certain actions support combinations with specific mouse buttons or events: * * * ````MOUSE_PAN```` * For panning with a key and mouse movement: * * ````javascript * keyMap[cameraControl.MOUSE_PAN] = [[input.KEY_SHIFT, input.MOUSE_LEFT_BUTTON]]; * ```` * * Panning is triggered by pressing ````KEY_SHIFT```` + ````MOUSE_LEFT_BUTTON```` while moving the mouse. * * * ````MOUSE_ROTATE```` * Similar to panning, rotation can be configured with key and mouse button combinations: * * ````javascript * keyMap[cameraControl.MOUSE_ROTATE] = [[input.KEY_CTRL, input.MOUSE_LEFT_BUTTON]]; * ```` * * Rotation is triggered by pressing ````KEY_CTRL```` + ````MOUSE_LEFT_BUTTON```` during mouse movement. * * * ````MOUSE_DOLLY```` * Dolly with mouse wheel scrolling and optional key combinations: * * ````javascript * keyMap[cameraControl.MOUSE_DOLLY] = [[input.KEY_ALT]]; * ```` * * Dolly action occurs when scrolling the mouse wheel with ````KEY_ALT```` held (no need to specify MOUSE_WHEEL). * * ## Special Mouse Actions * Use ````input.MOUSE_LEFT_BUTTON````, ````input.MOUSE_MIDDLE_BUTTON````, and ````input.MOUSE_RIGHT_BUTTON```` in combinations only for camera movements involving the mouse. */ class CameraControl extends Component { /** * @private * @constructor */ constructor(owner, cfg = {}) { super(owner, cfg); /** * Identifies the XX action. * @final * @type {Number} */ this.PAN_LEFT = 0; /** * Identifies the XX action. * @final * @type {Number} */ this.PAN_RIGHT = 1; /** * Identifies the XX action. * @final * @type {Number} */ this.PAN_UP = 2; /** * Identifies the XX action. * @final * @type {Number} */ this.PAN_DOWN = 3; /** * Identifies the XX action. * @final * @type {Number} */ this.PAN_FORWARDS = 4; /** * Identifies the XX action. * @final * @type {Number} */ this.PAN_BACKWARDS = 5; /** * Identifies the XX action. * @final * @type {Number} */ this.ROTATE_X_POS = 6; /** * Identifies the XX action. * @final * @type {Number} */ this.ROTATE_X_NEG = 7; /** * Identifies the XX action. * @final * @type {Number} */ this.ROTATE_Y_POS = 8; /** * Identifies the XX action. * @final * @type {Number} */ this.ROTATE_Y_NEG = 9; /** * Identifies the XX action. * @final * @type {Number} */ this.DOLLY_FORWARDS = 10; /** * Identifies the XX action. * @final * @type {Number} */ this.DOLLY_BACKWARDS = 11; /** * Identifies the XX action. * @final * @type {Number} */ this.AXIS_VIEW_RIGHT = 12; /** * Identifies the XX action. * @final * @type {Number} */ this.AXIS_VIEW_BACK = 13; /** * Identifies the XX action. * @final * @type {Number} */ this.AXIS_VIEW_LEFT = 14; /** * Identifies the XX action. * @final * @type {Number} */ this.AXIS_VIEW_FRONT = 15; /** * Identifies the XX action. * @final * @type {Number} */ this.AXIS_VIEW_TOP = 16; /** * Identifies the XX action. * @final * @type {Number} */ this.AXIS_VIEW_BOTTOM = 17; /** * Identifies the XX action. * @final * @type {Number} */ this.MOUSE_PAN = 18; /** * Identifies the XX action. * @final * @type {Number} */ this.MOUSE_ROTATE = 19; /** * Identifies the XX action. * @final * @type {Number} */ this.MOUSE_DOLLY = 20; this._keyMap = {}; // Maps key codes to the above actions this.scene.canvas.canvas.oncontextmenu = (e) => { e.preventDefault(); }; // User-settable CameraControl configurations this._configs = { // Private longTapTimeout: 600, // Millisecs longTapRadius: 5, // Pixels // General active: true, keyboardLayout: "qwerty", navMode: "orbit", planView: false, firstPerson: false, followPointer: true, doublePickFlyTo: true, panRightClick: true, showPivot: false, pointerEnabled: true, constrainVertical: false, smartPivot: false, doubleClickTimeFrame: 250, zoomOnMouseWheel: true, snapToVertex: DEFAULT_SNAP_VERTEX, snapToEdge: DEFAULT_SNAP_EDGE, snapRadius: DEFAULT_SNAP_PICK_RADIUS, keyboardEnabledOnlyIfMouseover: true, // Rotation dragRotationRate: 360.0, keyboardRotationRate: 90.0, rotationInertia: 0.0, // Panning keyboardPanRate: 1.0, touchPanRate: 1.0, panInertia: 0.5, // Dollying keyboardDollyRate: 10, mouseWheelDollyRate: 100, touchDollyRate: 0.2, dollyInertia: 0, dollyProximityThreshold: 30.0, dollyMinSpeed: 0.04 }; // Current runtime state of the CameraControl this._states = { pointerCanvasPos: math.vec2(), mouseover: false, followPointerDirty: true, mouseDownClientX: 0, mouseDownClientY: 0, mouseDownCursorX: 0, mouseDownCursorY: 0, touchStartTime: null, activeTouches: [], tapStartPos: math.vec2(), tapStartTime: -1, lastTapTime: -1, longTouchTimeout: null }; // Updates for CameraUpdater to process on next Scene "tick" event this._updates = { rotateDeltaX: 0, rotateDeltaY: 0, panDeltaX: 0, panDeltaY: 0, panDeltaZ: 0, dollyDelta: 0 }; // Controllers to assist input event handlers with controlling the Camera const scene = this.scene; this._controllers = { cameraControl: this, pickController: new PickController(this, this._configs), pivotController: new PivotController(scene, this._configs), panController: new PanController(scene), cameraFlight: new CameraFlightAnimation(this, { duration: 0.5 }) }; // Input event handlers this._handlers = [ new MouseMiscHandler(this.scene, this._controllers, this._configs, this._states, this._updates), new TouchPanRotateAndDollyHandler(this.scene, this._controllers, this._configs, this._states, this._updates), new MousePanRotateDollyHandler(this.scene, this._controllers, this._configs, this._states, this._updates), new KeyboardAxisViewHandler(this.scene, this._controllers, this._configs, this._states, this._updates), new MousePickHandler(this.scene, this._controllers, this._configs, this._states, this._updates), new TouchPickHandler(this.scene, this._controllers, this._configs, this._states, this._updates), new KeyboardPanRotateDollyHandler(this.scene, this._controllers, this._configs, this._states, this._updates) ]; this._cursors = { dollyForward: "zoom-in", dollyBackward: "zoom-out", rotate: 'grabbing', pan: 'move', } // Applies scheduled updates to the Camera on each Scene "tick" event this._cameraUpdater = new CameraUpdater(this.scene, this._controllers, this._configs, this._states, this._updates); // Set initial user configurations this.navMode = cfg.navMode; if (cfg.planView) { this.planView = cfg.planView; } this.constrainVertical = cfg.constrainVertical; if (cfg.keyboardLayout) { this.keyboardLayout = cfg.keyboardLayout; // Deprecated } else { this.keyMap = cfg.keyMap; } this.doublePickFlyTo = cfg.doublePickFlyTo; this.panRightClick = cfg.panRightClick; this.active = cfg.active; this.followPointer = cfg.followPointer; this.rotationInertia = cfg.rotationInertia; this.keyboardPanRate = cfg.keyboardPanRate; this.touchPanRate = cfg.touchPanRate; this.keyboardRotationRate = cfg.keyboardRotationRate; this.dragRotationRate = cfg.dragRotationRate; this.touchDollyRate = cfg.touchDollyRate; this.dollyInertia = cfg.dollyInertia; this.dollyProximityThreshold = cfg.dollyProximityThreshold; this.dollyMinSpeed = cfg.dollyMinSpeed; this.panInertia = cfg.panInertia; this.pointerEnabled = true; this.keyboardDollyRate = cfg.keyboardDollyRate; this.mouseWheelDollyRate = cfg.mouseWheelDollyRate; } /** * Sets custom mappings of keys to ````CameraControl```` actions. * * See class docs for usage. * * @param {{Number:Number}|String} value Either a set of new key mappings, or a string to select a keyboard layout, * which causes ````CameraControl```` to use the default key mappings for that layout. */ set keyMap(value) { value = value || "qwerty"; if (utils.isString(value)) { const input = this.scene.input; const keyMap = {}; switch (value) { default: this.error("Unsupported value for 'keyMap': " + value + " defaulting to 'qwerty'"); // Intentional fall-through to "qwerty" case "qwerty": keyMap[this.PAN_LEFT] = [input.KEY_A]; keyMap[this.PAN_RIGHT] = [input.KEY_D]; keyMap[this.PAN_UP] = [input.KEY_Z]; keyMap[this.PAN_DOWN] = [input.KEY_X]; keyMap[this.PAN_BACKWARDS] = []; keyMap[this.PAN_FORWARDS] = []; keyMap[this.DOLLY_FORWARDS] = [input.KEY_W, input.KEY_ADD]; keyMap[this.DOLLY_BACKWARDS] = [input.KEY_S, input.KEY_SUBTRACT]; keyMap[this.ROTATE_X_POS] = [input.KEY_DOWN_ARROW]; keyMap[this.ROTATE_X_NEG] = [input.KEY_UP_ARROW]; keyMap[this.ROTATE_Y_POS] = [input.KEY_Q, input.KEY_LEFT_ARROW]; keyMap[this.ROTATE_Y_NEG] = [input.KEY_E, input.KEY_RIGHT_ARROW]; keyMap[this.AXIS_VIEW_RIGHT] = [input.KEY_NUM_1]; keyMap[this.AXIS_VIEW_BACK] = [input.KEY_NUM_2]; keyMap[this.AXIS_VIEW_LEFT] = [input.KEY_NUM_3]; keyMap[this.AXIS_VIEW_FRONT] = [input.KEY_NUM_4]; keyMap[this.AXIS_VIEW_TOP] = [input.KEY_NUM_5]; keyMap[this.AXIS_VIEW_BOTTOM] = [input.KEY_NUM_6]; keyMap[this.MOUSE_PAN] = [ [input.MOUSE_LEFT_BUTTON, input.KEY_SHIFT], this._configs.panRightClick ? input.MOUSE_RIGHT_BUTTON : input.MOUSE_MIDDLE_BUTTON ] keyMap[this.MOUSE_ROTATE] = [input.MOUSE_LEFT_BUTTON]; keyMap[this.MOUSE_DOLLY] = []; break; case "azerty": keyMap[this.PAN_LEFT] = [input.KEY_Q]; keyMap[this.PAN_RIGHT] = [input.KEY_D]; keyMap[this.PAN_UP] = [input.KEY_W]; keyMap[this.PAN_DOWN] = [input.KEY_X]; keyMap[this.PAN_BACKWARDS] = []; keyMap[this.PAN_FORWARDS] = []; keyMap[this.DOLLY_FORWARDS] = [input.KEY_Z, input.KEY_ADD]; keyMap[this.DOLLY_BACKWARDS] = [input.KEY_S, input.KEY_SUBTRACT]; keyMap[this.ROTATE_X_POS] = [input.KEY_DOWN_ARROW]; keyMap[this.ROTATE_X_NEG] = [input.KEY_UP_ARROW]; keyMap[this.ROTATE_Y_POS] = [input.KEY_A, input.KEY_LEFT_ARROW]; keyMap[this.ROTATE_Y_NEG] = [input.KEY_E, input.KEY_RIGHT_ARROW]; keyMap[this.AXIS_VIEW_RIGHT] = [input.KEY_NUM_1]; keyMap[this.AXIS_VIEW_BACK] = [input.KEY_NUM_2]; keyMap[this.AXIS_VIEW_LEFT] = [input.KEY_NUM_3]; keyMap[this.AXIS_VIEW_FRONT] = [input.KEY_NUM_4]; keyMap[this.AXIS_VIEW_TOP] = [input.KEY_NUM_5]; keyMap[this.AXIS_VIEW_BOTTOM] = [input.KEY_NUM_6]; keyMap[this.MOUSE_PAN] = [ [input.MOUSE_LEFT_BUTTON, input.KEY_SHIFT], this._configs.panRightClick ? input.MOUSE_RIGHT_BUTTON : input.MOUSE_MIDDLE_BUTTON ] keyMap[this.MOUSE_ROTATE] = [input.MOUSE_LEFT_BUTTON]; keyMap[this.MOUSE_DOLLY] = []; break; } this._keyMap = keyMap; } else { const keyMap = value; this._keyMap = keyMap; } } /** * Gets custom mappings of keys to {@link CameraControl} actions. * * @returns {{Number:Number}} Current key mappings. */ get keyMap() { return this._keyMap; } _areAllKeysDown(keyDownMap, keys) { if (!keys || keys.length <= 0) { return true; } if (!keyDownMap) { return false; } for (let i = 0, len = keys.length; i < len; i++) { const key = keys[i]; if (!keyDownMap[key]) return false; } return true; } _isAnyOtherKeyDown(keyDownMap, keyMap) { for (let i = 0, len = keyDownMap.length; i < len; i++) { if (keyDownMap[i]) { if (Array.isArray(keyMap)) { if (keyMap.indexOf(i) < 0) return true; } else if (i !== keyMap) return true; } } return false; } _isMouseAction(action) { switch (action) { case this.MOUSE_ROTATE: case this.MOUSE_DOLLY: case this.MOUSE_PAN: return true; default: return false; } } /** * Returns true if any keys configured for the given action are down. * @param action * @param keyDownMap * @private */ _isKeyDownForAction(action, keyDownMap) { const keys = this._keyMap[action]; if (!keys) { return false; } if (keys.length === 0 && this._isMouseAction(action)) return true; if (!keyDownMap) { keyDownMap = this.scene.input.keyDown; } for (let i = 0, len = keys.length; i < len; i++) { const key = keys[i]; if (!Array.isArray(key)) { if (keyDownMap[key] && !this._isAnyOtherKeyDown(keyDownMap, key)) return true; } else { if (this._areAllKeysDown(keyDownMap, key) && !this._isAnyOtherKeyDown(keyDownMap, key)) return true; } } return false; } /** * Sets the HTMl element to represent the pivot point when {@link CameraControl#followPointer} is true. * * See class comments for an example. * * @param {HTMLElement} element HTML element representing the pivot point. */ set pivotElement(element) { this._controllers.pivotController.setPivotElement(element); } /** * Sets if this ````CameraControl```` is active or not. * * When inactive, the ````CameraControl```` will not react to input. * * Default is ````true````. * * @param {Boolean} value Set ````true```` to activate this ````CameraControl````. */ set active(value) { value = value !== false; this._configs.active = value; this._handlers[1]._active = value; this._handlers[5]._active = value; } /** * Gets if this ````CameraControl```` is active or not. * * When inactive, the ````CameraControl```` will not react to input. * * Default is ````true````. * * @returns {Boolean} Returns ````true```` if this ````CameraControl```` is active. */ get active() { return this._configs.active; } /** * Sets whether the pointer snap to vertex. * * @param {boolean} snapToVertex */ set snapToVertex(snapToVertex) { this._configs.snapToVertex = !!snapToVertex; } /** * Gets whether the pointer snap to vertex. * * @returns {boolean} */ get snapToVertex() { return this._configs.snapToVertex; } /** * Sets whether the pointer snap to edge. * * @param {boolean} snapToEdge */ set snapToEdge(snapToEdge) { this._configs.snapToEdge = !!snapToEdge; } /** * Gets whether the pointer snap to edge. * * @returns {boolean} */ get snapToEdge() { return this._configs.snapToEdge; } /** * Sets the current snap radius for "hoverSnapOrSurface" events, to specify whether the radius * within which the pointer snaps to the nearest vertex or the nearest edge. * * Default value is 30 pixels. * * @param {Number} snapRadius The snap radius. */ set snapRadius(snapRadius) { snapRadius = snapRadius || DEFAULT_SNAP_PICK_RADIUS; this._configs.snapRadius = snapRadius; } /** * Gets the current snap radius. * * @returns {Number} The snap radius. */ get snapRadius() { return this._configs.snapRadius; } /** * If `true`, the keyboard shortcuts are enabled ONLY if the mouse is over the canvas. * * @param {boolean} value */ set keyboardEnabledOnlyIfMouseover(value) { this._configs.keyboardEnabledOnlyIfMouseover = !!value; } /** * Gets whether the keyboard shortcuts are enabled ONLY if the mouse is over the canvas or ALWAYS. * * @returns {boolean} */ get keyboardEnabledOnlyIfMouseover() { return this._configs.keyboardEnabledOnlyIfMouseover; } /** * Sets the current navigation mode. * * Accepted values are: * * * "orbit" - rotation orbits about the current target or pivot point, * * "firstPerson" - rotation is about the current eye position, * * "planView" - rotation is disabled. * * See class comments for more info. * * @param {String} navMode The navigation mode: "orbit", "firstPerson" or "planView". */ set navMode(navMode) { navMode = navMode || "orbit"; if (navMode !== "firstPerson" && navMode !== "orbit" && navMode !== "planView") { this.error("Unsupported value for navMode: " + navMode + " - supported values are 'orbit', 'firstPerson' and 'planView' - defaulting to 'orbit'"); navMode = "orbit"; } this._configs.firstPerson = (navMode === "firstPerson"); this._configs.planView = (navMode === "planView"); if (this._configs.firstPerson || this._configs.planView) { this._controllers.pivotController.hidePivot(); this._controllers.pivotController.endPivot(); } this._configs.navMode = navMode; } /** * Gets the current navigation mode. * * @returns {String} The navigation mode: "orbit", "firstPerson" or "planView". */ get navMode() { return this._configs.navMode; } /** * Sets whether mouse and touch input is enabled. * * Default is ````true````. * * Disabling mouse and touch input on ````CameraControl```` is useful when we want to temporarily use mouse or * touch input to interact with some other 3D control, without disturbing the {@link Camera}. * * @param {Boolean} value Set ````true```` to enable mouse and touch input. */ set pointerEnabled(value) { this._reset(); this._configs.pointerEnabled = !!value; } /** * Sets the cursor to be used when a particular action is being performed. * * Accepted actions are: * * * "dollyForward" - when the camera is dollying in the forward direction * * "dollyBackward" - when the camera is dollying in the backward direction * * "pan" - when the camera is being panned * * "rotate" - when the camera is being rotated * * @param {String} action * @param {String} style */ setCursorStyle(action, style) { if (Object.prototype.hasOwnProperty.call(this._cursors, action)) { this._cursors = { ...this._cursors, [action]: style }; } else console.warn(`Action '${action}' is not valid for cursor styles.`); } /** * Gets the current style for a particular action. * * @param {String} action To get the style for * @returns {String} style set on the cursor for action */ getCursorStyle(action) { return this._cursors[action] || null; } _reset() { for (let i = 0, len = this._handlers.length; i < len; i++) { const handler = this._handlers[i]; if (handler.reset) { handler.reset(); } } this._updates.panDeltaX = 0; this._updates.panDeltaY = 0; this._updates.rotateDeltaX = 0; this._updates.rotateDeltaY = 0; this._updates.dolyDelta = 0; } /** * Gets whether mouse and touch input is enabled. * * Default is ````true````. * * Disabling mouse and touch input on ````CameraControl```` is desirable when we want to temporarily use mouse or * touch input to interact with some other 3D control, without interfering with the {@link Camera}. * * @returns {Boolean} Returns ````true```` if mouse and touch input is enabled. */ get pointerEnabled() { return this._configs.pointerEnabled; } /** * Sets whether the {@link Camera} follows the mouse/touch pointer. * * In orbiting mode, the Camera will orbit about the pointer, and will dolly to and from the pointer. * * In fly-to mode, the Camera will dolly to and from the pointer, however the World will always rotate about the Camera position. * * In plan-view mode, the Camera will dolly to and from the pointer, however the Camera will not rotate. * * Default is ````true````. * * See class comments for more info. * * @param {Boolean} value Set ````true```` to enable the Camera to follow the pointer. */ set followPointer(value) { this._configs.followPointer = (value !== false); } /** * Sets whether the {@link Camera} follows the mouse/touch pointer. * * In orbiting mode, the Camera will orbit about the pointer, and will dolly to and from the pointer. * * In fly-to mode, the Camera will dolly to and from the pointer, however the World will always rotate about the Camera position. * * In plan-view mode, the Camera will dolly to and from the pointer, however the Camera will not rotate. * * Default is ````true````. * * See class comments for more info. * * @returns {Boolean} Returns ````true```` if the Camera follows the pointer. */ get followPointer() { return this._configs.followPointer; } /** * Sets the current World-space 3D target position. * * Only applies when {@link CameraControl#followPointer} is ````true````. * * @param {Number[]} worldPos The new World-space 3D target position. */ set pivotPos(worldPos) { this._controllers.pivotController.setPivotPos(worldPos); } /** * Gets the current World-space 3D pivot position. * * Only applies when {@link CameraControl#followPointer} is ````true````. * * @return {Number[]} worldPos The current World-space 3D pivot position. */ get pivotPos() { return this._controllers.pivotController.getPivotPos(); } /** * @deprecated * @param {Boolean} value Set ````true```` to enable dolly-to-pointer behaviour. */ set dollyToPointer(value) { this.warn("dollyToPointer property is deprecated - replaced with followPointer"); this.followPointer = value; } /** * @deprecated * @returns {Boolean} Returns ````true```` if dolly-to-pointer behaviour is enabled. */ get dollyToPointer() { this.warn("dollyToPointer property is deprecated - replaced with followPointer"); return this.followPointer; } /** * @deprecated * @param {Boolean} value Set ````true```` to enable dolly-to-pointer behaviour. */ set panToPointer(value) { this.warn("panToPointer property is deprecated - replaced with followPointer"); } /** * @deprecated * @returns {Boolean} Returns ````true```` if dolly-to-pointer behaviour is enabled. */ get panToPointer() { this.warn("panToPointer property is deprecated - replaced with followPointer"); return false; } /** * Sets whether this ````CameraControl```` is in plan-view mode. * * When in plan-view mode, rotation is disabled. * * Default is ````false````. * * Deprecated - use {@link CameraControl#navMode} instead. * * @param {Boolean} value Set ````true```` to enable plan-view mode. * @deprecated */ set planView(value) { this._configs.planView = !!value; this._configs.firstPerson = false; if (this._configs.planView) { this._controllers.pivotController.hidePivot(); this._controllers.pivotController.endPivot(); } this.warn("planView property is deprecated - replaced with navMode"); } /** * Gets whether this ````CameraControl```` is in plan-view mode. * * When in plan-view mode, rotation is disabled. * * Default is ````false````. * * Deprecated - use {@link CameraControl#navMode} instead. * * @returns {Boolean} Returns ````true```` if plan-view mode is enabled. * @deprecated */ get planView() { this.warn("planView property is deprecated - replaced with navMode"); return this._configs.planView; } /** * Sets whether this ````CameraControl```` is in first-person mode. * * In "first person" mode (disabled by default) the look position rotates about the eye position. Otherwise, {@link Camera#eye} rotates about {@link Camera#look}. * * Default is ````false````. * * Deprecated - use {@link CameraControl#navMode} instead. * * @param {Boolean} value Set ````true```` to enable first-person mode. * @deprecated */ set firstPerson(value) { this.warn("firstPerson property is deprecated - replaced with navMode"); this._configs.firstPerson = !!value; this._configs.planView = false; if (this._configs.firstPerson) { this._controllers.pivotController.hidePivot(); this._controllers.pivotController.endPivot(); } } /** * Gets whether this ````CameraControl```` is in first-person mode. * * In "first person" mode (disabled by default) the look position rotates about the eye position. Otherwise, {@link Camera#eye} rotates about {@link Camera#look}. * * Default is ````false````. * * Deprecated - use {@link CameraControl#navMode} instead. * * @returns {Boolean} Returns ````true```` if first-person mode is enabled. * @deprecated */ get firstPerson() { this.warn("firstPerson property is deprecated - replaced with navMode"); return this._configs.firstPerson; } /** * Sets whether to vertically constrain the {@link Camera} position for first-person navigation. * * When set ````true````, this constrains {@link Camera#eye} to its current vertical position. * * Only applies when {@link CameraControl#navMode} is ````"firstPerson"````. * * Default is ````false````. * * @param {Boolean} value Set ````true```` to vertically constrain the Camera. */ set constrainVertical(value) { this._configs.constrainVertical = !!value; } /** * Gets whether to vertically constrain the {@link Camera} position for first-person navigation. * * When set ````true````, this constrains {@link Camera#eye} to its current vertical position. * * Only applies when {@link CameraControl#navMode} is ````"firstPerson"````. * * Default is ````false````. * * @returns {Boolean} ````true```` when Camera is vertically constrained. */ get constrainVertical() { return this._configs.constrainVertical; } /** * Sets whether double-picking an {@link Entity} causes the {@link Camera} to fly to its boundary. * * Default is ````false````. * * @param {Boolean} value Set ````true```` to enable double-pick-fly-to mode. */ set doublePickFlyTo(value) { this._configs.doublePickFlyTo = value !== false; } /** * Gets whether double-picking an {@link Entity} causes the {@link Camera} to fly to its boundary. * * Default is ````false````. * * @returns {Boolean} Returns ````true```` when double-pick-fly-to mode is enabled. */ get doublePickFlyTo() { return this._configs.doublePickFlyTo; } /** * Sets whether either right-clicking (true) or middle-clicking (false) pans the {@l