Add existing to tracked

This commit is contained in:
Jay
2026-08-11 09:53:42 -04:00
parent afe07f3055
commit ffd6e3d73c
8531 changed files with 4396230 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
import DeveloperError from "../../Core/DeveloperError.js";
/**
* An interface for a camera controller that can be registered with the scene to handle input events, camera animations, and other interactions. Implementations of this interface are expected to be registered with the scene via a {@link ControllerHost}.
* This type describes an
* interface and is not intended to be instantiated directly.
* @class
* @abstract
* @see {@link HybridScreenSpacePanCameraController}
* @see {@link ScreenSpaceElevatorCameraController}
* @see {@link ScreenSpaceMapCameraController}
* @see {@link ScreenSpaceTiltOrbitCameraController}
*/
class Controller {
/**
* Determines if the controller is enabled and should be updated by the host scene.
* @type {boolean}
*/
get enabled() {
return DeveloperError.throwInstantiationError();
}
set enabled(value) {
DeveloperError.throwInstantiationError();
}
/**
* Invoked when the controller is added to the DOM. Implement <code>connectedCallback</code> to set up any DOM event listeners.
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
connectedCallback(element) {
DeveloperError.throwInstantiationError();
}
/**
* Invoked when the controller is removed from the DOM. Implement <code>disconnectedCallback</code> to tear down any DOM event listeners.
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
disconnectedCallback(element) {
DeveloperError.throwInstantiationError();
}
/**
* Invoked once per frame. Implement <code>update</code> to modify the camera or other parts of the scene.
* @see {@link https://cesium.com/blog/2018/01/24/cesium-scene-rendering-performance/#updaterender-cycle-events|Update/Render Cycle Events}
* @param {Scene} scene
* @param {JulianDate} time The current simulation time.
*/
update(scene, time) {
DeveloperError.throwInstantiationError();
}
/**
* Invoked when the controller is being updated the first time, immediately before <code>update</code> is called. Implement <code>firstUpdate</code> to perform one-time work after the relevant scene has begun its render loop. Some examples might include initializing simulation time values or adding a primitive to the scene.
* @see Controller#update
* @param {Scene} scene
* @param {JulianDate} time The current simulation time.
*/
firstUpdate(scene, time) {
DeveloperError.throwInstantiationError();
}
}
export default Controller;
+78
View File
@@ -0,0 +1,78 @@
/**
* Collects an array of Controller objects that can be registered with the scene to handle input events, camera animations, and other interactions.
* @class
* @see {@link Controller}
* @see {@link Scene#controllerHost}
*/
class ControllerHost {
/**
* Creates an instance of a <code>ControllerHost</code>. Typically, a <code>ControllerHost</code> is created by the Scene constructor and accessed via {@link Scene#controllerHost}.
* @see {@link Scene#controllerHost}
*/
constructor() {
/**
* @type {Controller[]}
* @private
*/
this._controllers = [];
this._needsUpdate = new Set();
}
/**
* The number of controllers registered to this host.
* @type {number}
* @readonly
*/
get controllerCount() {
return this._controllers.length;
}
/**
* Registers a controller implementation with this host.
* @param {Controller} controller An implementation of the Controller interface to register with this host.
* @param {HTMLElement} element The DOM element containing the Cesium scene.
* @param {number} [priority=0] An index, less than or equal to the current count of registed controllers, that defines the precedence of the new controller relative to those previously registered. A priority of <code>0</code> would mean the new controller would apply its updates before any other controller. As subsequent controllers are updated, their effects are applied on top of any previous update effects. If omitted, the new controller becomes the highest priority, i.e., its updates are applied after all other controllers.
*/
registerController(controller, element, priority) {
const index = priority ?? this.controllerCount;
this._controllers.splice(index, 0, controller);
this._needsUpdate.add(controller);
controller.connectedCallback(element);
}
/**
* Unregisters a controller implementation from this host.
* @param {Controller} controller An implementation of the Controller interface to unregister from this host.
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
unregisterController(controller, element) {
const controllers = this._controllers;
const index = controllers.indexOf(controller);
if (index !== -1) {
controllers.splice(index, 1);
controller.disconnectedCallback(element);
}
}
/**
* Invoked once per frame by the host scene. Updates all registered controllers in order of their priority.
* @param {Scene} scene The host scene.
* @param {JulianDate} time The current simulation time.
*/
update(scene, time) {
for (const controller of this._controllers) {
if (!controller.enabled) {
continue;
}
if (this._needsUpdate.has(controller)) {
controller.firstUpdate(scene, time);
this._needsUpdate.delete(controller);
}
controller.update(scene, time);
}
}
}
export default ControllerHost;
@@ -0,0 +1,106 @@
import ScreenSpaceElevatorCameraController from "./ScreenSpaceElevatorCameraController.js";
import ScreenSpaceMapCameraController from "./ScreenSpaceMapCameraController.js";
import Cartesian3 from "../../Core/Cartesian3.js";
import CesiumMath from "../../Core/Math.js";
/**
* A contextual camera controller that combines screenspace map panning and screenspace elevator panning. The controller automatically switches between the two based on the camera's angle relative to nadir. If the camera is looking mostly down (within angleThreshold of nadir), <code>ScreenSpaceMapCameraController</code> is used.
* If the camera is looking towards the horizon (beyond angleThreshold from nadir), the <code>ScreenSpaceElevatorCameraController</code> is used.
* @implements Controller
* @example
* viewer.scene.screenSpaceCameraController.enableInputs = false;
* viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
*
* const hybridController = new HybridScreenSpacePanCameraController();
* viewer.addController(hybridController);
*/
class HybridScreenSpacePanCameraController {
constructor() {
this._elevatorController = new ScreenSpaceElevatorCameraController();
this._mapController = new ScreenSpaceMapCameraController();
this._enabled = true;
this._ellipsoidNormal = new Cartesian3();
/**
* The angle threshold in radians that determines which controller is used. If the camera is looking within this angle of nadir, the map controller is used. Otherwise, the elevator controller is used.
* @type {number}
* @default CesiumMath.toRadians(125)
*/
this.angleThreshold = CesiumMath.toRadians(125);
}
get enabled() {
return this._enabled;
}
set enabled(value) {
this._enabled = value;
}
/**
* The controller that is used when the camera is looking more horizontally (beyond angleThreshold from nadir).
* @type {ScreenSpaceElevatorCameraController}
* @readonly
*/
get elevatorController() {
return this._elevatorController;
}
/**
* The controller that is used when the camera is looking mostly down (within angleThreshold of nadir).
* @type {ScreenSpaceMapCameraController}
* @readonly
*/
get mapController() {
return this._mapController;
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
connectedCallback(element) {
this._elevatorController.connectedCallback(element);
this._mapController.connectedCallback(element);
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
disconnectedCallback(element) {
this._elevatorController.disconnectedCallback(element);
this._mapController.disconnectedCallback(element);
}
/**
* @inheritdoc
*/
firstUpdate() {
this._elevatorController.firstUpdate();
this._mapController.firstUpdate();
}
/**
* @inheritdoc
* @param {Scene} scene
*/
update(scene) {
const camera = scene.camera;
const normal = scene.ellipsoid.geodeticSurfaceNormal(
camera.positionWC,
this._ellipsoidNormal,
);
const angle = Math.abs(Cartesian3.angleBetween(normal, camera.directionWC));
const activeController =
angle < this.angleThreshold && angle > Math.PI - this.angleThreshold
? this._elevatorController
: this._mapController;
activeController.update(scene);
}
}
export default HybridScreenSpacePanCameraController;
+32
View File
@@ -0,0 +1,32 @@
// @ts-check
/**
* This enumerated type is for classifying mouse buttons: left, middle, and right.
* @enum {number}
*/
const MouseButton = {
/**
* Represents a mouse left button.
* @type {number}
* @constant
*/
LEFT: 0,
/**
* Represents a mouse middle button.
* @type {number}
* @constant
*/
MIDDLE: 1,
/**
* Represents a mouse right button.
* @type {number}
* @constant
*/
RIGHT: 2,
};
Object.freeze(MouseButton);
export default MouseButton;
@@ -0,0 +1,295 @@
import Cartesian2 from "../../Core/Cartesian2.js";
import Cartesian3 from "../../Core/Cartesian3.js";
import defined from "../../Core/defined.js";
import Frozen from "../../Core/Frozen.js";
import getTimestamp from "../../Core/getTimestamp.js";
import CesiumMath from "../../Core/Math.js";
import ScreenSpaceEventHandler from "../../Core/ScreenSpaceEventHandler.js";
import TimeConstants from "../../Core/TimeConstants.js";
import defaultPickWorldPosition from "./defaultPickWorldPosition.js";
import ScreenSpaceInputBindings from "./ScreenSpaceInputBindings.js";
import MouseButton from "./MouseButton.js";
/**
* @typedef {object} ControllerOptions
* @memberof ScreenSpaceElevatorCameraController
* @property {ScreenSpaceInputBindings.InputBinding[]} [dragInputs] The drag input bindings that control panning.
*/
/**
* A camera controller that allows panning the camera tangential to the ellipsoid, i.e., up and down relative to the ellipsoid normal, in screen space
* by clicking and dragging the mouse.
* @class
* @implements Controller
* @example
* viewer.scene.screenSpaceCameraController.enableInputs = false;
* viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
*
* const elevatorCameraController = new Cesium.ScreenSpaceElevatorCameraController();
* viewer.addController(elevatorCameraController);
*
* @example
* // Configure the controller to use the right mouse button for panning instead of the default left mouse button.
* const elevatorCameraController = new Cesium.ScreenSpaceElevatorCameraController({
* dragInputs: [{ button: Cesium.MouseButton.RIGHT}]
* });
* viewer.addController(elevatorCameraController);
*/
class ScreenSpaceElevatorCameraController {
/**
* @private
* @returns {ScreenSpaceInputBindings.InputBinding[]} The default drag input bindings.
*/
static _getDefaultDragInputs() {
return [
Object.freeze({
button: MouseButton.LEFT,
}),
];
}
/**
* Creates an instance of a ScreenSpaceElevatorCameraController.
* @param {ScreenSpaceElevatorCameraController.ControllerOptions} [options] The options for configuring the controller.
*/
constructor(options = Frozen.EMPTY_OBJECT) {
this._enabled = true;
this._handler = undefined;
this._lastUpdateTime = undefined;
/**
* The drag input bindings that control vertical panning. Each binding is a combination of the mouse button
* and an optional keyboard modifier.
* @type {ScreenSpaceInputBindings.InputBinding[]}
* @see ScreenSpaceEventHandler
*/
this.dragInputs =
options.dragInputs ??
ScreenSpaceElevatorCameraController._getDefaultDragInputs();
this._dragInputState = undefined;
this._panDelta = new Cartesian2();
this._panPosition = new Cartesian2();
/**
* A callback function used to pick the world position from which to pan. The function is called with {@link Scene}, the {@link Cartesian2} screen space position, and a {@link Cartesian3} instance to store the result. The function should return the {@link Cartesian3} world position from which to pan, or <code>undefined</code> if no position could be picked. If <code>undefined</code> is returned, the camera will pan relative to the ellipsoid surface below the camera.
* @type {Function(Scene, Cartesian2, Cartesian3): Cartesian3|undefined}
* @default defaultPickWorldPosition
* @example
* const elevatorCameraController = new Cesium.ScreenSpaceElevatorCameraController();
* elevatorCameraController.pickWorldPosition = function (scene, windowPosition, result) {
* // Pick the world position from the depth buffer
* return scene.pickPosition(windowPosition, result);
* };
* viewer.addController(elevatorCameraController);
*/
this.pickWorldPosition = defaultPickWorldPosition;
this._ellipsoidNormal = new Cartesian3();
this._ellipsoidSurfacePosition = new Cartesian3();
this._panDirectionX = new Cartesian3();
this._panDirectionY = new Cartesian3();
this._pixelSize = new Cartesian2();
this._panVelocity = new Cartesian2();
/**
* The speed in meters per pixel at which the camera pans.
* @type {number}
* @default 1.0
*/
this.panSpeed = 1.0;
/**
* Enable or disable inertia when panning. When enabled, the camera will continue to move after the user stops dragging, gradually slowing down based on {@link ScreenSpaceMapCameraController#inertialDecay}.
* @type {boolean}
* @default true
*/
this.inertiaEnabled = true;
/**
* The rate at which the camera's pan velocity decays over time.
* @type {number}
* @default 6.0
*/
this.inertialDecay = 6.0;
/**
* A parameter in the range <code>[0, 1)</code> used to limit the range
* of inputs to a percentage of the window width/height per animation frame.
* This helps keep the camera under control in low-frame-rate situations.
* @type {number}
* @default 0.1
*/
this.maximumMovementRatio = 0.1;
}
/**
* @inheritdoc
*/
get enabled() {
return this._enabled;
}
set enabled(value) {
this._enabled = value;
if (value) {
this._lastUpdateTime = getTimestamp();
this._panDelta.x = 0;
this._panDelta.y = 0;
} else if (defined(this._dragInputState)) {
this._dragInputState.isDragging = false;
}
}
/**
* @private
* @type {boolean}
*/
get isDragging() {
return defined(this._dragInputState) && this._dragInputState.isDragging;
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
connectedCallback(element) {
const handler = new ScreenSpaceEventHandler(element);
this._handler = handler;
this._dragInputState = ScreenSpaceInputBindings.registerDragInputBindings(
handler,
this.dragInputs,
{
start: this._handleStartPan.bind(this),
change: this._handlePan.bind(this),
},
);
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
disconnectedCallback(element) {
const handler = this._handler;
if (defined(handler) && !handler.isDestroyed()) {
handler.destroy();
}
}
/**
* @inheritdoc
*/
firstUpdate() {
this._lastUpdateTime = getTimestamp();
this._panDelta.x = 0;
this._panDelta.y = 0;
}
/**
* @inheritdoc
* @param {any} scene
*/
update(scene) {
const dt =
(getTimestamp() - this._lastUpdateTime) *
TimeConstants.SECONDS_PER_MILLISECOND;
const { camera, ellipsoid, canvas } = scene;
let dx = -this._panDelta.x;
let dy = this._panDelta.y;
if (this.inertiaEnabled && !this.isDragging) {
const damping = Math.exp(-this.inertialDecay * dt);
this._panVelocity.x *= damping;
this._panVelocity.y *= damping;
dx = this._panVelocity.x * dt;
dy = this._panVelocity.y * dt;
}
const { clientWidth, clientHeight } = canvas;
if (
dt === 0 ||
clientWidth === 0 ||
clientHeight === 0 ||
(Math.abs(dx) <= CesiumMath.EPSILON3 &&
Math.abs(dy) <= CesiumMath.EPSILON3)
) {
// Reset for next frame
this._lastUpdateTime = getTimestamp();
this._panDelta.x = 0;
this._panDelta.y = 0;
return;
}
const windowPosition = this._panPosition;
let surface = this.pickWorldPosition(
scene,
windowPosition,
this._ellipsoidSurfacePosition,
);
if (!defined(surface)) {
surface = ellipsoid.scaleToGeodeticSurface(
camera.positionWC,
this._ellipsoidSurfacePosition,
);
}
let xAxis = Cartesian3.clone(camera.rightWC, this._panDirectionX);
xAxis = Cartesian3.normalize(xAxis, this._panDirectionX);
const zAxis = Cartesian3.normalize(surface, this._panDirectionY);
const distance = Cartesian3.distance(camera.positionWC, surface);
const { drawingBufferWidth, drawingBufferHeight, pixelRatio } = scene;
const pixelSize = camera.frustum.getPixelDimensions(
drawingBufferWidth,
drawingBufferHeight,
distance,
pixelRatio,
this._pixelSize,
);
const maxPixels =
this.maximumMovementRatio * Math.max(clientWidth, clientHeight);
dx = CesiumMath.clamp(dx, -maxPixels, maxPixels);
this._panVelocity.x = dx / dt;
dx *= this.panSpeed * pixelSize.x;
dy = CesiumMath.clamp(dy, -maxPixels, maxPixels);
this._panVelocity.y = dy / dt;
dy *= this.panSpeed * pixelSize.y;
camera.move(xAxis, dx);
camera.move(zAxis, dy);
// Reset for next frame
this._lastUpdateTime = getTimestamp();
this._panDelta.x = 0;
this._panDelta.y = 0;
}
_handleStartPan() {
if (!this.enabled) {
return;
}
this._panDelta.x = 0;
this._panDelta.y = 0;
}
/**
* @private
*/
_handlePan(event) {
this._panDelta.x += event.endPosition.x - event.startPosition.x;
this._panDelta.y += event.endPosition.y - event.startPosition.y;
this._panPosition.x = event.endPosition.x;
this._panPosition.y = event.endPosition.y;
}
}
export default ScreenSpaceElevatorCameraController;
@@ -0,0 +1,154 @@
import Check from "../../Core/Check.js";
import defined from "../../Core/defined.js";
import ScreenSpaceEventType from "../../Core/ScreenSpaceEventType.js";
import MouseButton from "./MouseButton.js";
/**
* @typedef {object} InputBinding
* @memberof ScreenSpaceInputBindings
* @property {MouseButton} button The mouse button used for drag start/stop.
* @property {number} [modifier] The optional keyboard modifier to register.
*/
/**
* @typedef {object} DragInputActions
* @memberof ScreenSpaceInputBindings
* @property {Function} [start] Called on drag start.
* @property {Function} [end] Called on drag stop.
* @property {Function} [change] Called on drag move.
*/
/**
* @typedef {object} DragInputState
* @memberof ScreenSpaceInputBindings
* @property {boolean} isDragging True if a drag is in progress, false otherwise.
*/
/**
* @private
* @param {MouseButton} button The mouse button.
* @returns {ScreenSpaceEventType|undefined} The corresponding down event type.
*/
function getDownEventType(button) {
if (button === MouseButton.LEFT) {
return ScreenSpaceEventType.LEFT_DOWN;
}
if (button === MouseButton.MIDDLE) {
return ScreenSpaceEventType.MIDDLE_DOWN;
}
if (button === MouseButton.RIGHT) {
return ScreenSpaceEventType.RIGHT_DOWN;
}
return undefined;
}
/**
* @private
* @param {MouseButton} button The mouse button.
* @returns {ScreenSpaceEventType|undefined} The corresponding down event type.
*/
function getUpEventType(button) {
if (button === MouseButton.LEFT) {
return ScreenSpaceEventType.LEFT_UP;
}
if (button === MouseButton.MIDDLE) {
return ScreenSpaceEventType.MIDDLE_UP;
}
if (button === MouseButton.RIGHT) {
return ScreenSpaceEventType.RIGHT_UP;
}
return undefined;
}
/**
* @namespace
*/
class ScreenSpaceInputBindings {
/**
* Registers drag input bindings on a screen space event handler.
* @param {ScreenSpaceEventHandler} handler The screen space event handler.
* @param {InputBinding[]} inputBindings The drag bindings to register.
* @param {DragInputActions} dragInputActions The callbacks to invoke for drag actions.
* @returns {DragInputState} The drag input state.
*/
static registerDragInputBindings(handler, inputBindings, dragInputActions) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("handler", handler);
Check.defined("inputBindings", inputBindings);
Check.typeOf.object("dragInputActions", dragInputActions);
//>>includeEnd('debug');
const changeModifiers = new Set();
const dragInputState = {
isDragging: false,
};
for (const binding of inputBindings) {
dragInputState.isDragging = false;
const downEventType = getDownEventType(binding.button);
const upEventType = getUpEventType(binding.button);
if (defined(downEventType)) {
handler.setInputAction(
(...e) => {
dragInputState.isDragging = true;
if (defined(dragInputActions.start)) {
dragInputActions.start(...e);
}
},
downEventType,
binding.modifier,
);
}
if (defined(upEventType)) {
handler.setInputAction(
(...e) => {
if (dragInputState.isDragging) {
dragInputState.isDragging = false;
if (defined(dragInputActions.end)) {
dragInputActions.end(...e);
}
}
},
upEventType,
binding.modifier,
);
// Register a global up event to ensure that the drag end callback is called even if the mouse is released outside of the canvas or the modifier key is released before the mouse button.
handler.setInputAction((...e) => {
if (dragInputState.isDragging) {
dragInputState.isDragging = false;
if (defined(dragInputActions.end)) {
dragInputActions.end(...e);
}
}
}, upEventType);
}
changeModifiers.add(binding.modifier);
}
for (const modifier of changeModifiers) {
handler.setInputAction(
(...e) => {
if (dragInputState.isDragging && defined(dragInputActions.change)) {
dragInputActions.change(...e);
}
},
ScreenSpaceEventType.MOUSE_MOVE,
modifier,
);
}
return dragInputState;
}
}
export default ScreenSpaceInputBindings;
@@ -0,0 +1,310 @@
import Cartesian2 from "../../Core/Cartesian2.js";
import Cartesian3 from "../../Core/Cartesian3.js";
import defined from "../../Core/defined.js";
import Frozen from "../../Core/Frozen.js";
import getTimestamp from "../../Core/getTimestamp.js";
import CesiumMath from "../../Core/Math.js";
import ScreenSpaceEventHandler from "../../Core/ScreenSpaceEventHandler.js";
import TimeConstants from "../../Core/TimeConstants.js";
import defaultPickWorldPosition from "./defaultPickWorldPosition.js";
import ScreenSpaceInputBindings from "./ScreenSpaceInputBindings.js";
import MouseButton from "./MouseButton.js";
/**
* @typedef {object} ControllerOptions
* @memberof ScreenSpaceMapCameraController
* @property {ScreenSpaceInputBindings.InputBinding[]} [dragInputs] The drag input bindings that control panning.
*/
/**
* A camera controller that allows panning the camera tangential to the ellipsoid in screen space
* by clicking and dragging the mouse.
* @class
* @implements Controller
* @example
* viewer.scene.screenSpaceCameraController.enableInputs = false;
*
* const mapCameraController = new Cesium.ScreenSpaceMapCameraController();
* viewer.addController(mapCameraController);
*
* @example
* // Configure the controller to use the right mouse button for panning instead of the default left mouse button.
* const mapCameraController = new Cesium.ScreenSpaceMapCameraController({
* dragInputs: [{ button: Cesium.MouseButton.RIGHT}]
* });
* viewer.addController(mapCameraController);
*/
class ScreenSpaceMapCameraController {
/**
* @private
* @returns {ScreenSpaceInputBindings.InputBinding[]} The default drag input bindings.
*/
static _getDefaultDragInputs() {
return [
Object.freeze({
button: MouseButton.LEFT,
}),
];
}
/**
* Creates an instance of a ScreenSpaceMapCameraController.
* @param {ScreenSpaceMapCameraController.ControllerOptions} [options] The options for configuring the controller.
*/
constructor(options = Frozen.EMPTY_OBJECT) {
this._enabled = true;
this._handler = undefined;
this._lastUpdateTime = undefined;
/**
* The drag input bindings that map panning. Each binding is a combination of the mouse button
* and an optional keyboard modifier.
* @type {ScreenSpaceInputBindings.InputBinding[]}
* @see ScreenSpaceEventHandler
*/
this.dragInputs =
options.dragInputs ??
ScreenSpaceMapCameraController._getDefaultDragInputs();
this._dragInputState = undefined;
this._panDelta = new Cartesian2();
this._panPosition = new Cartesian2();
/**
* A callback function used to pick the world position from which to pan. The function is called with {@link Scene}, the {@link Cartesian2} screen space position, and a {@link Cartesian3} instance to store the result. The function should return the {@link Cartesian3} world position from which to pan, or <code>undefined</code> if no position could be picked. If <code>undefined</code> is returned, the camera will pan relative to the ellipsoid surface below the camera.
* @type {Function(Scene, Cartesian2, Cartesian3): Cartesian3|undefined}
* @default defaultPickWorldPosition
* @example
* const mapCameraController = new Cesium.ScreenSpaceMapCameraController();
* mapCameraController.pickWorldPosition = function (scene, windowPosition, result) {
* // Pick the world position from the depth buffer
* return scene.pickPosition(windowPosition, result);
* };
* viewer.addController(mapCameraController);
*/
this.pickWorldPosition = defaultPickWorldPosition;
this._ellipsoidNormal = new Cartesian3();
this._ellipsoidSurfacePosition = new Cartesian3();
this._panDirectionX = new Cartesian3();
this._panDirectionY = new Cartesian3();
this._pixelSize = new Cartesian2();
this._panVelocity = new Cartesian2();
/**
* The speed in meters per pixel at which the camera pans.
* @type {number}
* @default 1.0
*/
this.panSpeed = 1.0;
/**
* Enable or disable inertia when panning. When enabled, the camera will continue to move after the user stops dragging, gradually slowing down based on {@link ScreenSpaceMapCameraController#inertialDecay}.
* @type {boolean}
* @default true
*/
this.inertiaEnabled = true;
/**
* The rate at which the camera's pan velocity decays over time.
* @type {number}
* @default 6.0
*/
this.inertialDecay = 6.0;
/**
* A parameter in the range <code>[0, 1)</code> used to limit the range
* of inputs to a percentage of the window width/height per animation frame.
* This helps keep the camera under control in low-frame-rate situations.
* @type {number}
* @default 0.1
*/
this.maximumMovementRatio = 0.1;
}
/**
* @inheritdoc
*/
get enabled() {
return this._enabled;
}
set enabled(value) {
this._enabled = value;
if (value) {
this._panDelta.x = 0;
this._panDelta.y = 0;
} else if (defined(this._dragInputState)) {
this._dragInputState.isDragging = false;
}
}
/**
* @private
* @type {boolean}
*/
get isDragging() {
return defined(this._dragInputState) && this._dragInputState.isDragging;
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
connectedCallback(element) {
const handler = new ScreenSpaceEventHandler(element);
this._handler = handler;
this._dragInputState = ScreenSpaceInputBindings.registerDragInputBindings(
handler,
this.dragInputs,
{
start: this._handleStartPan.bind(this),
change: this._handlePan.bind(this),
},
);
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
disconnectedCallback(element) {
const handler = this._handler;
if (defined(handler) && !handler.isDestroyed()) {
handler.destroy();
}
}
/**
* @inheritdoc
*/
firstUpdate() {
this._lastUpdateTime = getTimestamp();
this._panDelta.x = 0;
this._panDelta.y = 0;
}
/**
* @inheritdoc
* @param {any} scene
*/
update(scene) {
const dt =
(getTimestamp() - this._lastUpdateTime) *
TimeConstants.SECONDS_PER_MILLISECOND;
let dx = -this._panDelta.x;
let dy = this._panDelta.y;
if (this.inertiaEnabled && !this.isDragging) {
const damping = Math.exp(-this.inertialDecay * dt);
this._panVelocity.x *= damping;
this._panVelocity.y *= damping;
dx = this._panVelocity.x * dt;
dy = this._panVelocity.y * dt;
}
const { camera, ellipsoid, canvas } = scene;
const { clientWidth, clientHeight } = canvas;
if (
dt === 0 ||
clientWidth === 0 ||
clientHeight === 0 ||
(Math.abs(dx) <= CesiumMath.EPSILON3 &&
Math.abs(dy) <= CesiumMath.EPSILON3)
) {
// Reset for next frame
this._lastUpdateTime = getTimestamp();
this._panDelta.x = 0;
this._panDelta.y = 0;
return;
}
const windowPosition = this._panPosition;
let surface = this.pickWorldPosition(
scene,
windowPosition,
this._ellipsoidSurfacePosition,
);
if (!defined(surface)) {
surface = ellipsoid.scaleToGeodeticSurface(
camera.positionWC,
this._ellipsoidSurfacePosition,
);
}
const zAxis = ellipsoid.geodeticSurfaceNormal(
surface,
this._ellipsoidNormal,
);
let xAxis = Cartesian3.clone(camera.rightWC, this._panDirectionX);
xAxis = Cartesian3.normalize(xAxis, this._panDirectionX);
// If z-axis is parallel to camera forward, we use the camera up vector to compute the y-axis. Otherwise, we use the z-axis and x-axis to compute the y-axis.
let yAxis = Cartesian3.clone(camera.upWC, this._panDirectionY);
const theta = Math.abs(Cartesian3.dot(zAxis, camera.directionWC));
if (CesiumMath.lessThan(theta, 1.0, CesiumMath.EPSILON6)) {
yAxis = Cartesian3.cross(zAxis, xAxis, this._panDirectionY);
}
yAxis = Cartesian3.normalize(yAxis, this._panDirectionY);
const distance = Cartesian3.distance(camera.positionWC, surface);
const { drawingBufferWidth, drawingBufferHeight, pixelRatio } = scene;
const pixelSize = camera.frustum.getPixelDimensions(
drawingBufferWidth,
drawingBufferHeight,
distance,
pixelRatio,
this._pixelSize,
);
const maxPixels =
this.maximumMovementRatio * Math.max(clientWidth, clientHeight);
dx = CesiumMath.clamp(dx, -maxPixels, maxPixels);
this._panVelocity.x = dx / dt;
dx *= this.panSpeed * pixelSize.x;
dy = CesiumMath.clamp(dy, -maxPixels, maxPixels);
this._panVelocity.y = dy / dt;
dy *= this.panSpeed * pixelSize.y;
camera.move(xAxis, dx);
camera.move(yAxis, dy);
// Reset for next frame
this._lastUpdateTime = getTimestamp();
this._panDelta.x = 0;
this._panDelta.y = 0;
}
/**
* @private
* @param {Event} event
*/
_handleStartPan(event) {
if (!this.enabled) {
return;
}
this._panDelta.x = 0;
this._panDelta.y = 0;
}
/**
* @private
*/
_handlePan(event) {
this._panDelta.x += event.endPosition.x - event.startPosition.x;
this._panDelta.y += event.endPosition.y - event.startPosition.y;
this._panPosition.x = event.endPosition.x;
this._panPosition.y = event.endPosition.y;
}
}
export default ScreenSpaceMapCameraController;
@@ -0,0 +1,642 @@
import Cartesian2 from "../../Core/Cartesian2.js";
import Cartesian3 from "../../Core/Cartesian3.js";
import Check from "../../Core/Check.js";
import defined from "../../Core/defined.js";
import Ellipsoid from "../../Core/Ellipsoid.js";
import Frozen from "../../Core/Frozen.js";
import getTimestamp from "../../Core/getTimestamp.js";
import KeyboardEventModifier from "../../Core/KeyboardEventModifier.js";
import CesiumMath from "../../Core/Math.js";
import Matrix3 from "../../Core/Matrix3.js";
import Matrix4 from "../../Core/Matrix4.js";
import ScreenSpaceEventHandler from "../../Core/ScreenSpaceEventHandler.js";
import Quaternion from "../../Core/Quaternion.js";
import TimeConstants from "../../Core/TimeConstants.js";
import Transforms from "../../Core/Transforms.js";
import defaultPickWorldPosition from "./defaultPickWorldPosition.js";
import ScreenSpaceInputBindings from "./ScreenSpaceInputBindings.js";
import MouseButton from "./MouseButton.js";
/**
* @typedef {object} ControllerOptions
* @memberof ScreenSpaceTiltOrbitCameraController
* @property {ScreenSpaceInputBindings.InputBinding[]} [dragInputs] The drag input bindings that control tilting and orbiting.
*/
/**
* A camera controller that allows tilting and orbiting the camera around a target position in screen space by clicking and dragging the mouse or touching and dragging on a touch screen.
* @class
* @implements Controller
* @example
* viewer.scene.screenSpaceCameraController.enableInputs = false;
* viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
*
* const tiltOrbitController = new Cesium.ScreenSpaceTiltOrbitCameraController();
* viewer.addController(tiltOrbitController);
*
* @example
* // Tilt around the position under the cursor or tap when dragging starts instead of the position at the center of the screen.
* viewer.scene.screenSpaceCameraController.enableInputs = false;
* viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
*
* const tiltOrbitController = new Cesium.ScreenSpaceTiltOrbitCameraController();
* tiltOrbitController.useDragPosition = true;
* viewer.addController(tiltOrbitController);
*
* @example
* // Configure the controller to use the left mouse button for tilting and orbiting instead of the default right mouse button.
* const tiltOrbitController = new Cesium.ScreenSpaceTiltOrbitCameraController({
* dragInputs: [{ button: Cesium.MouseButton.LEFT }]
* });
* viewer.addController(tiltOrbitController);
*/
class ScreenSpaceTiltOrbitCameraController {
/**
* @private
* @returns {ScreenSpaceInputBindings.InputBinding[]} The default drag input bindings.
*/
static _getDefaultDragInputs() {
return [
Object.freeze({
button: MouseButton.LEFT,
modifier: KeyboardEventModifier.CTRL,
}),
Object.freeze({
button: MouseButton.RIGHT,
}),
];
}
/**
* Creates a new instance of <code>ScreenSpaceTiltOrbitCameraController</code>.
* @param {ScreenSpaceTiltOrbitCameraController.ControllerOptions} [options] The options for configuring the controller.
*/
constructor(options = Frozen.EMPTY_OBJECT) {
this._enabled = true;
this._handler = undefined;
this._lastUpdateTime = undefined;
/**
* Enabled dragging to tilt the camera.
* @type {boolean}
* @default true
*/
this.tiltEnabled = true;
/**
* Enabled dragging to orbit the camera.
* @type {boolean}
* @default true
*/
this.orbitEnabled = true;
/**
* If false, the camera will orbit and tilt around the position at the center of the screen. If true, the camera will orbit and tilt around the position under the cursor or tap when dragging starts.
* @type {boolean}
* @default false
*/
this.useDragPosition = false;
/**
* The drag input bindings that control tilting. Each binding is a combination of the mouse button
* and an optional keyboard modifier.
* @type {ScreenSpaceInputBindings.InputBinding[]}
* @see ScreenSpaceEventHandler
*/
this.dragInputs =
options.dragInputs ??
ScreenSpaceTiltOrbitCameraController._getDefaultDragInputs();
this._dragInputState = undefined;
this._dragDelta = new Cartesian2();
this._screenSpaceDragPosition = new Cartesian2();
this._screenSpaceOrigin = new Cartesian2();
/**
* A callback function used to pick the world position around which to tilt or orbit. The function is called with {@link Scene}, the {@link Cartesian2} screen space position, and a {@link Cartesian3} instance to store the result. The function should return the {@link Cartesian3} world position from which to tilt or orbit, or <code>undefined</code> if no position could be picked.
* @type {Function(Scene, Cartesian2, Cartesian3): Cartesian3|undefined}
* @default defaultPickWorldPosition
* @example
* const tiltOrbitCameraController = new Cesium.ScreenSpaceTiltOrbitCameraController();
* tiltOrbitCameraController.pickWorldPosition = function (scene, windowPosition, result) {
* // Pick the world position from the depth buffer
* return scene.pickPosition(windowPosition, result);
* };
* viewer.addController(tiltOrbitCameraController);
*/
this.pickWorldPosition = defaultPickWorldPosition;
this._hasTarget = false;
this._target = new Cartesian3();
this._axis = new Cartesian3();
/**
* The amount at which the camera tilts per dragged pixel. A value of 1.0 means that dragging the mouse across the entire canvas will tilt the camera by 90 degrees.
* @type {number}
* @default 2.0
*/
this.tiltMagnitude = 2.0;
/**
* Enables or disables damping for tilt and orbit animations. Damping smooths out the camera movement and makes it feel more natural or weighty, but it can also introduce a slight delay in the camera response. If damping is disabled, the camera will respond immediately to user input.
* @type {boolean}
* @default true
*/
this.dampingEnabled = true;
/**
* Specifies the length of time in seconds in which a single tilt animation is targeted to complete.
* @type {number}
* @default 0.0045
*/
this.tiltAnimationDuration = 0.0045;
/**
* The maximum tilt velocity in radians per second. A value of Number.POSITIVE_INFINITY means that the maximum tilt velocity is unbounded.
* @type {number}
* @default CesiumMath.PI
*/
this.maximumTiltVelocity = CesiumMath.PI;
/**
* @private
* @type {number}
* @default CesiumMath.EPSILON20
*/
this.minimumTiltVelocity = CesiumMath.EPSILON20;
/**
* The amount at which the camera orbits per dragged pixel. A value of 1.0 means that dragging the mouse across the entire canvas will orbit the camera by 180 degrees.
* @type {number}
* @default 2.0
*/
this.orbitMagnitude = 2.0;
/**
* Specifies the length of time in seconds in which a single orbit animation completes.
* @type {number}
* @default 0.0045
*/
this.orbitAnimationDuration = 0.0045;
/**
* The maximum orbit velocity in radians per second. A value of Number.POSITIVE_INFINITY means that the maximum orbit velocity is unbounded.
* @type {number}
* @default CesiumMath.TWO_PI
*/
this.maximumOrbitVelocity = CesiumMath.TWO_PI;
/**
* @private
* @type {number}
* @default CesiumMath.EPSILON20
*/
this.minimumOrbitVelocity = CesiumMath.EPSILON20;
this._tiltAxis = new Cartesian3();
this._tiltQuaternion = new Quaternion();
this._tiltOffset = new Cartesian3();
this._tiltOrigin = new Cartesian3();
this._tiltDampenedResults = {
velocity: 0.0,
value: 0.0,
};
this._orbitTargetEnu = new Matrix4();
this._orbitTargetEast = new Cartesian3();
this._orbitQuaternion = new Quaternion();
this._orbitOffset = new Cartesian3();
this._orbitLookOffset = new Cartesian3();
this._orbitOrigin = new Cartesian3();
this._orbitDampenedResults = {
velocity: 0.0,
value: 0.0,
};
/**
* A parameter in the range <code>[0, 1)</code> used to limit the range
* of inputs to a percentage of the window width/height per animation frame.
* This helps keep the camera under control in low-frame-rate situations.
* @type {number}
* @default 0.1
*/
this.maximumMovementRatio = 0.1;
}
/**
* @inheritdoc
*/
get enabled() {
return this._enabled;
}
set enabled(value) {
this._enabled = value;
if (value) {
this._lastUpdateTime = getTimestamp();
this._dragDelta.x = 0.0;
this._dragDelta.y = 0.0;
} else if (defined(this._dragInputState)) {
this._dragInputState.isDragging = false;
}
}
/**
* @private
* @type {boolean}
*/
get isDragging() {
return defined(this._dragInputState) && this._dragInputState.isDragging;
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
connectedCallback(element) {
const handler = new ScreenSpaceEventHandler(element);
this._handler = handler;
this._dragInputState = ScreenSpaceInputBindings.registerDragInputBindings(
handler,
this.dragInputs,
{
start: this._handleStartDrag.bind(this),
change: this._handleDrag.bind(this),
},
);
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
disconnectedCallback(element) {
const handler = this._handler;
if (defined(handler) && !handler.isDestroyed()) {
handler.destroy();
}
}
/**
* @inheritdoc
*/
firstUpdate() {
this._lastUpdateTime = getTimestamp();
this._dragDelta.x = 0.0;
this._dragDelta.y = 0.0;
}
/**
* @private
*/
_handleStartDrag(event) {
if (!this.enabled) {
return;
}
this._hasTarget = false;
this._screenSpaceDragPosition.x = event.position.x;
this._screenSpaceDragPosition.y = event.position.y;
this._dragDelta.x = 0.0;
this._dragDelta.y = 0.0;
}
/**
* @private
*/
_handleDrag(event) {
this._dragDelta.x += event.endPosition.x - event.startPosition.x;
this._dragDelta.y += event.endPosition.y - event.startPosition.y;
}
/**
* The current tilt angle of the camera in radians. A value of 0.0 means that the camera is looking straight down at the ellipsoid, and a value of PI/2 means that the camera is looking at the horizon.
* @type {number}
* @private
*/
get tiltAngle() {
return this._tiltDampenedResults.value;
}
/**
* The current tilt velocity of the camera in radians per second.
* @type {number}
* @private
*/
get tiltVelocity() {
return this._tiltDampenedResults.velocity;
}
/**
* The current tilt velocity of the camera in radians per second.
* @type {number}
* @private
*/
set tiltVelocity(value) {
this._tiltDampenedResults.velocity = value;
}
/**
* The current orbit angle of the camera in radians around the target. A value of 0.0 means that the camera is looking at the target from the east, and a value of PI/2 means that the camera is looking at the target from the north.
* @type {number}
* @private
*/
get orbitAngle() {
return this._orbitDampenedResults.value;
}
/**
* The current orbit velocity of the camera in radians per second.
* @type {number}
* @private
*/
get orbitVelocity() {
return this._orbitDampenedResults.velocity;
}
/**
* The current orbit velocity of the camera in radians per second.
* @type {number}
* @private
*/
set orbitVelocity(value) {
this._orbitDampenedResults.velocity = value;
}
/**
* Attempts to orbit the camera around the specified origin by the specified amount in radians. Positive values orbit the camera clockwise, negative values orbit the camera counterclockwise. If the drag origin is not on the ellipsoid, no orbit is applied.
* @param {Camera} camera The camera to orbit.
* @param {Cartesian3} target The origin position to orbit around in world coordinates.
* @param {Cartesian3} axis The axis to orbit around, typically the negative of the surface normal at the target position.
* @param {number} amount The amount to orbit the camera in radians. Positive values orbit the camera clockwise, negative values orbit the camera counterclockwise.
* @param {number} dt The time delta in seconds since the last update.
* @param {Ellipsoid} [ellipsoid=Ellipsoid.default] The ellipsoid to pick for the orbit origin. If undefined, the default ellipsoid is used.
*/
orbit(camera, target, axis, amount, dt, ellipsoid = Ellipsoid.default) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("camera", camera);
Check.typeOf.object("target", target);
Check.typeOf.object("axis", axis);
Check.typeOf.number("amount", amount);
Check.typeOf.number.greaterThan("dt", dt, 0);
Check.typeOf.object("ellipsoid", ellipsoid);
//>>includeEnd('debug');
const enu = Transforms.eastNorthUpToFixedFrame(
target,
ellipsoid,
this._orbitTargetEnu,
);
const east = Matrix4.multiplyByPointAsVector(
enu,
Cartesian3.UNIT_X,
this._orbitTargetEast,
);
const currentOrbitAngle = Cartesian3.angleBetween(camera.directionWC, east);
if (Math.abs(this.orbitVelocity) < this.minimumOrbitVelocity) {
this.orbitVelocity = 0.0;
}
// Apply inertia
if (!this.isDragging && this.dampingEnabled) {
amount += this.orbitVelocity * dt;
}
if (amount === 0.0) {
return;
}
const targetOrbitAngle = currentOrbitAngle + amount;
// Apply critical damping
const maxSpeed = this.dampingEnabled
? this.maximumOrbitVelocity * this.orbitMagnitude
: undefined;
const smoothTime = this.dampingEnabled
? this.orbitAnimationDuration
: undefined;
this._orbitDampenedResults = CesiumMath.smoothDamp(
currentOrbitAngle,
targetOrbitAngle,
this.orbitVelocity,
dt,
maxSpeed,
smoothTime,
this._orbitDampenedResults,
);
const rho = this.orbitAngle - currentOrbitAngle;
const rotation = Matrix3.fromQuaternion(
Quaternion.fromAxisAngle(axis, -rho, this._orbitQuaternion),
);
const targetOffset = Cartesian3.subtract(
camera.positionWC,
target,
this._orbitOffset,
);
const t = Cartesian3.dot(targetOffset, camera.directionWC);
const offset = Cartesian3.multiplyByScalar(
camera.directionWC,
t,
this._orbitLookOffset,
);
const lookOffset = Cartesian3.subtract(
targetOffset,
offset,
this._orbitLookOffset,
);
const rotatedTargetOffset = Matrix3.multiplyByVector(
rotation,
targetOffset,
this._orbitOffset,
);
const rotatedLookOffset = Matrix3.multiplyByVector(
rotation,
lookOffset,
this._orbitLookOffset,
);
Cartesian3.add(target, rotatedTargetOffset, camera.position);
const lookTarget = Cartesian3.add(
target,
rotatedLookOffset,
this._orbitOrigin,
);
camera.lookAtWorldPosition(lookTarget, ellipsoid);
}
/**
* Attempts to tilt the camera by the specified amount in radians. Positive values tilt the camera down, negative values tilt the camera up. If the drag origin is not on the ellipsoid, no tilt is applied.
* @param {Camera} camera The camera to tilt.
* @param {Cartesian3} target The origin position to tilt around in world coordinates.
* @param {Cartesian3} axis The axis to tilt around, typically the negative of the surface normal at the target position.
* @param {number} amount The amount to tilt the camera in radians. Positive values tilt the camera down, negative values tilt the camera up.
* @param {number} dt The time delta in seconds since the last update. Value must be greater than 0.
* @param {Ellipsoid} [ellipsoid=Ellipsoid.default] The ellipsoid to pick for the tilt origin. If undefined, the default ellipsoid is used.
*/
tilt(camera, target, axis, amount, dt, ellipsoid = Ellipsoid.default) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("camera", camera);
Check.typeOf.object("target", target);
Check.typeOf.object("axis", axis);
Check.typeOf.number("amount", amount);
Check.typeOf.number.greaterThan("dt", dt, 0);
Check.typeOf.object("ellipsoid", ellipsoid);
//>>includeEnd('debug');
if (Math.abs(this.tiltVelocity) < this.minimumTiltVelocity) {
this.tiltVelocity = 0.0;
}
// Apply inertia
if (!this.isDragging && this.dampingEnabled) {
amount += this.tiltVelocity * dt;
}
if (amount === 0.0) {
return;
}
const currentTiltAngle = Cartesian3.angleBetween(camera.direction, axis);
// Avoid large deltas when the sign is close to flipping, which can happen when the camera is looking straight down at the ellipsoid.
if (
(currentTiltAngle < CesiumMath.PI_OVER_TWO && amount > 0.0) ||
(currentTiltAngle > CesiumMath.PI_OVER_TWO && amount < 0.0)
) {
amount *= Math.abs(Math.sin(currentTiltAngle));
}
const targetTiltAngle = currentTiltAngle + amount;
const maxSpeed = this.dampingEnabled
? this.maximumTiltVelocity * this.tiltMagnitude
: undefined;
const smoothTime = this.dampingEnabled
? this.tiltAnimationDuration
: undefined;
CesiumMath.smoothDamp(
currentTiltAngle,
targetTiltAngle,
this.tiltVelocity,
dt,
maxSpeed,
smoothTime,
this._tiltDampenedResults,
);
const theta = this.tiltAngle - currentTiltAngle;
const rotation = Matrix3.fromQuaternion(
Quaternion.fromAxisAngle(camera.rightWC, -theta, this._tiltQuaternion),
);
const offset = Cartesian3.subtract(
camera.position,
target,
this._tiltOffset,
);
const t = Cartesian3.dot(offset, camera.directionWC);
const lookOffset = Cartesian3.multiplyByScalar(
camera.directionWC,
t,
this._tiltOffset,
);
const lookTarget = Cartesian3.subtract(
camera.position,
lookOffset,
this._tiltOrigin,
);
const rotatedOffset = Matrix3.multiplyByVector(
rotation,
lookOffset,
this._tiltOffset,
);
Cartesian3.add(lookTarget, rotatedOffset, camera.position);
camera.lookAtWorldPosition(lookTarget, ellipsoid);
}
/**
* @inheritdoc
* @param {Scene} scene
*/
update(scene) {
const dt =
(getTimestamp() - this._lastUpdateTime) *
TimeConstants.SECONDS_PER_MILLISECOND;
const { canvas } = scene;
const { clientWidth, clientHeight } = canvas;
if (dt === 0 || clientWidth === 0 || clientHeight === 0) {
// Reset for next frame
this._lastUpdateTime = getTimestamp();
this._dragDelta.x = 0;
this._dragDelta.y = 0;
return;
}
// Target position to orbit and tilt around. Pick the world position when dragging begins, and use that position for the duration of the drag.
let target = this._target;
if (this.isDragging && !this._hasTarget) {
let windowPosition = this._screenSpaceDragPosition;
if (!this.useDragPosition) {
windowPosition = this._screenSpaceOrigin;
windowPosition.x = clientWidth / 2.0;
windowPosition.y = clientHeight / 2.0;
}
const dragPositionTarget = this.pickWorldPosition(
scene,
windowPosition,
this._target,
);
const picked = defined(dragPositionTarget);
this._hasTarget = picked;
target = dragPositionTarget;
}
if (this._hasTarget) {
const { camera, ellipsoid } = scene;
const normal = ellipsoid.geodeticSurfaceNormal(target, this._axis);
const axis = Cartesian3.negate(normal, this._axis);
if (this.orbitEnabled) {
let dx = this._dragDelta.x / clientWidth;
dx = CesiumMath.clamp(
dx,
-this.maximumMovementRatio,
this.maximumMovementRatio,
);
dx *= this.orbitMagnitude * CesiumMath.TWO_PI;
this.orbit(camera, target, axis, dx, dt, ellipsoid);
}
if (this.tiltEnabled) {
let dy = this._dragDelta.y / clientHeight;
dy = CesiumMath.clamp(
dy,
-this.maximumMovementRatio,
this.maximumMovementRatio,
);
dy *= this.tiltMagnitude * CesiumMath.PI;
this.tilt(camera, target, axis, dy, dt, ellipsoid);
}
}
// Reset for next frame
this._lastUpdateTime = getTimestamp();
this._dragDelta.x = 0;
this._dragDelta.y = 0;
}
}
export default ScreenSpaceTiltOrbitCameraController;
@@ -0,0 +1,431 @@
import Cartesian2 from "../../Core/Cartesian2.js";
import Cartesian3 from "../../Core/Cartesian3.js";
import defined from "../../Core/defined.js";
import Frozen from "../../Core/Frozen.js";
import getTimestamp from "../../Core/getTimestamp.js";
import CesiumMath from "../../Core/Math.js";
import ScreenSpaceEventHandler from "../../Core/ScreenSpaceEventHandler.js";
import ScreenSpaceEventType from "../../Core/ScreenSpaceEventType.js";
import defaultPickWorldPosition from "./defaultPickWorldPosition.js";
import ScreenSpaceInputBindings from "./ScreenSpaceInputBindings.js";
import TimeConstants from "../../Core/TimeConstants.js";
import MouseButton from "./MouseButton.js";
/**
* @typedef {object} ControllerOptions
* @memberof ScreenSpaceZoomCameraController
* @property {ScreenSpaceInputBindings.InputBinding[]} [dragInputs] The drag input bindings that control zooming.
* @property {ScreenSpaceEventType[]} [scrollInputs] The scroll input bindings that control zooming.
*/
/**
* A camera controller that allows zooming the camera in and out based on the pointer location in screen space.
* @class
* @implements Controller
* @example
* viewer.scene.screenSpaceCameraController.enableInputs = false;
* viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
*
* const zoomCameraController = new Cesium.ScreenSpaceZoomCameraController();
* viewer.addController(zoomCameraController);
*/
class ScreenSpaceZoomCameraController {
/**
* @private
* @returns {ScreenSpaceInputBindings.InputBinding[]} The default drag input bindings.
*/
static _getDefaultDragInputs() {
return [
Object.freeze({
button: MouseButton.MIDDLE,
}),
];
}
/**
* @private
* @returns {ScreenSpaceEventType[]} The default scroll input bindings.
*/
static _getDefaultScrollInputs() {
return [ScreenSpaceEventType.WHEEL];
}
/**
* Creates a new instance of <code>ScreenSpaceZoomCameraController</code>.
* @param {ScreenSpaceZoomCameraController.ControllerOptions} [options] The options for configuring the controller.
*/
constructor(options = Frozen.EMPTY_OBJECT) {
this._enabled = true;
this._handler = undefined;
this._lastUpdateTime = undefined;
/**
* If false, the camera will zoom to the position at the center of the screen. If true, the camera will zoom to the position under the cursor or tap when dragging starts or when scrolling with the scroll wheel.
* @type {boolean}
* @default false
*/
this.usePointerPosition = false;
/**
* The drag input bindings that control zooming. Each binding is a combination of the mouse button
* and an optional keyboard modifier.
* @type {ScreenSpaceInputBindings.InputBinding[]}
* @see ScreenSpaceEventHandler
*/
this.dragInputs =
options.dragInputs ??
ScreenSpaceZoomCameraController._getDefaultDragInputs();
/**
* The scroll input bindings that control zooming.
* @type {ScreenSpaceEventType[]}
* @see ScreenSpaceEventHandler
* @default [ScreenSpaceEventType.WHEEL]
*/
this.scrollInputs =
options.scrollInputs ??
ScreenSpaceZoomCameraController._getDefaultScrollInputs();
this._dragInputState = undefined;
this._dragDelta = new Cartesian2();
this._scrollDelta = 0.0;
this._zoomInputVelocity = 0.0;
this._screenSpaceScrollPosition = new Cartesian2();
this._screenSpaceDragPosition = new Cartesian2();
this._screenSpaceOrigin = new Cartesian2();
/**
* The rate at which the camera zooms in and out based on the mouse wheel delta.
* @type {number}
* @default 0.2
*/
this.zoomSensitivity = 0.2;
/**
* A callback function used to pick the world position from which to zoom. The function is called with {@link Scene}, the {@link Cartesian2} screen space position, and a {@link Cartesian3} instance to store the result. The function should return the {@link Cartesian3} world position from which to zoom, or <code>undefined</code> if no position could be picked.
* @type {Function(Scene, Cartesian2, Cartesian3): Cartesian3|undefined}
* @default defaultPickWorldPosition
* @example
* const zoomCameraController = new Cesium.ScreenSpaceZoomCameraController();
* zoomCameraController.pickWorldPosition = function (scene, windowPosition, result) {
* // Pick the world position from the depth buffer
* return scene.pickPosition(windowPosition, result);
* };
* viewer.addController(zoomCameraController);
*/
this.pickWorldPosition = defaultPickWorldPosition;
/**
* The ratio of the camera's distance to the zoom target that defines how much the camera zooms in and out per second.
* @type {number}
* @default 0.4
*/
this.zoomDistanceRatio = 0.4;
/**
* Enable or disable inertia when zooming. When enabled, the camera will continue to move after the user input stops, gradually slowing down based on {@link ScreenSpaceZoomCameraController#inertialDecay}.
* @type {boolean}
* @default true
*/
this.inertiaEnabled = false;
/**
* The rate at which the camera's zoom velocity decays over time.
* @type {number}
* @default 6.0
*/
this.inertialDecay = 6.0;
/**
* @private
* @type number
* @default 0.0
*/
this.minimumZoomDistance = 0.0;
/**
* Maximum distance from the zoom target that the camera can move away.
* @type {number}
* @default 100000.0
*/
this.maximumZoomDistance = 100000.0;
/**
* @private
* @type {number}
* @default CesiumMath.EPSILON20
*/
this.minimumZoomVelocity = CesiumMath.EPSILON20;
/**
* The maximum zoom velocity in meters per second. This limits the speed at which the camera can zoom in and out.
* @type {number}
* @default 1.0
*/
this.maximumZoomVelocity = 1.0;
/**
* Enables or disables damping for zooming. Damping smooths out the camera movement and makes it feel more natural or weighty, but it can also introduce a slight delay in the camera response. If damping is disabled, the camera will respond immediately to user input.
* @type {boolean}
* @default true
*/
this.dampingEnabled = true;
/**
* Specifies the length of time in seconds in which a single zoom animation is targeted to complete.
* @type {number}
* @default 0.45
*/
this.zoomAnimationDuration = 0.45;
this._target = new Cartesian3();
this._zoomDirection = new Cartesian3();
this._zoomDampenedResults = {
velocity: 0.0,
value: 0.0,
};
}
/**
* @inheritdoc
*/
get enabled() {
return this._enabled;
}
set enabled(value) {
this._enabled = value;
if (value) {
this._lastUpdateTime = getTimestamp();
this._scrollDelta = 0.0;
this._dragDelta.x = 0.0;
this._dragDelta.y = 0.0;
} else if (defined(this._dragInputState)) {
this._dragInputState.isDragging = false;
}
}
/**
* @private
* @type {boolean}
*/
get isDragging() {
return defined(this._dragInputState) && this._dragInputState.isDragging;
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
connectedCallback(element) {
const handler = new ScreenSpaceEventHandler(element);
this._handler = handler;
for (const input of this.scrollInputs) {
handler.setInputAction(this._handleZoom.bind(this), input);
}
handler.setInputAction(
this._handleZoomPosition.bind(this),
ScreenSpaceEventType.MOUSE_MOVE,
);
this._dragInputState = ScreenSpaceInputBindings.registerDragInputBindings(
handler,
this.dragInputs,
{
start: this._handleStartDrag.bind(this),
change: this._handleDrag.bind(this),
},
);
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
disconnectedCallback(element) {
const handler = this._handler;
if (defined(handler) && !handler.isDestroyed()) {
handler.destroy();
}
}
/**
* @inheritdoc
*/
firstUpdate() {
this._lastUpdateTime = getTimestamp();
this._scrollDelta = 0.0;
this._dragDelta.x = 0.0;
this._dragDelta.y = 0.0;
}
/**
* The current zoom distance of the camera in meters. This is the distance from the camera to the zoom target.
* @type {number}
* @private
*/
get zoomDistance() {
return this._zoomDampenedResults.value;
}
/**
* The current zoom velocity of the camera in radians per second.
* @type {number}
* @private
*/
get zoomVelocity() {
return this._zoomDampenedResults.velocity;
}
/**
* The current zoom velocity of the camera in radians per second.
* @type {number}
* @private
*/
set zoomVelocity(value) {
this._zoomDampenedResults.velocity = value;
}
/**
* @inheritdoc
* @param {Scene} scene
*/
update(scene) {
const now = getTimestamp();
const dt =
(now - this._lastUpdateTime) * TimeConstants.SECONDS_PER_MILLISECOND;
const { canvas } = scene;
const { clientWidth, clientHeight } = canvas;
if (dt === 0 || clientWidth === 0 || clientHeight === 0) {
this._lastUpdateTime = getTimestamp();
this._scrollDelta = 0.0;
this._dragDelta.x = 0;
this._dragDelta.y = 0;
return;
}
let dz = this._scrollDelta + this._dragDelta.y;
if (dz === 0.0 && this.inertiaEnabled) {
const damping = Math.exp(-this.inertialDecay * dt);
this._zoomInputVelocity *= damping;
dz = this._zoomInputVelocity * dt;
}
if (
Math.abs(this.zoomVelocity) < this.minimumZoomVelocity &&
dz <= CesiumMath.EPSILON3 &&
dz >= -CesiumMath.EPSILON3 &&
this.zoomVelocity <= CesiumMath.EPSILON3
) {
this._lastUpdateTime = getTimestamp();
this._scrollDelta = 0.0;
this._dragDelta.x = 0;
this._dragDelta.y = 0;
this.zoomVelocity = 0.0;
return;
}
this._zoomInputVelocity = CesiumMath.clamp(
dz / dt,
-this.maximumZoomVelocity,
this.maximumZoomVelocity,
);
const { camera, ellipsoid } = scene;
let direction = camera.direction;
let distance =
Cartesian3.magnitude(camera.positionWC) - ellipsoid.maximumRadius;
let windowPosition = this.isDragging
? this._screenSpaceDragPosition
: this._screenSpaceScrollPosition;
if (!this.useDragPosition) {
windowPosition = this._screenSpaceOrigin;
windowPosition.x = clientWidth / 2.0;
windowPosition.y = clientHeight / 2.0;
}
const target = this.pickWorldPosition(scene, windowPosition, this._target);
if (defined(target)) {
direction = Cartesian3.subtract(
target,
camera.positionWC,
this._zoomDirection,
);
direction = Cartesian3.normalize(direction, this._zoomDirection);
distance = Cartesian3.distance(target, camera.positionWC);
}
distance = CesiumMath.clamp(
distance,
distance > 0.0 ? this.minimumZoomDistance : -this.maximumZoomDistance,
distance > 0.0 ? this.maximumZoomDistance : -this.minimumZoomDistance,
);
const zoom = dz * distance * this.zoomDistanceRatio;
const smoothTime = this.dampingEnabled
? this.zoomAnimationDuration
: undefined;
this._zoomDampenedResults = CesiumMath.smoothDamp(
0.0,
zoom,
this.zoomVelocity,
dt,
undefined,
smoothTime,
this._zoomDampenedResults,
);
camera.move(direction, this.zoomDistance);
// Reset for next frame
this._lastUpdateTime = getTimestamp();
this._scrollDelta = 0.0;
this._dragDelta.x = 0;
this._dragDelta.y = 0;
}
/**
* @private
* @param {number} amount
*/
_handleZoom(amount) {
this._scrollDelta += amount * this.zoomSensitivity;
}
/**
* @private
*/
_handleZoomPosition(event) {
this._screenSpaceScrollPosition.x = event.endPosition.x;
this._screenSpaceScrollPosition.y = event.endPosition.y;
}
/**
* @private
*/
_handleStartDrag(event) {
if (!this.enabled) {
return;
}
this._screenSpaceDragPosition.x = event.position.x;
this._screenSpaceDragPosition.y = event.position.y;
this._dragDelta.x = 0.0;
this._dragDelta.y = 0.0;
}
/**
* @private
*/
_handleDrag(event) {
this._dragDelta.x += event.endPosition.x - event.startPosition.x;
this._dragDelta.y += event.endPosition.y - event.startPosition.y;
}
}
export default ScreenSpaceZoomCameraController;
@@ -0,0 +1,84 @@
import Cartesian3 from "../../Core/Cartesian3.js";
import Cartesian2 from "../../Core/Cartesian2.js";
import Check from "../../Core/Check.js";
import defined from "../../Core/defined.js";
import IntersectionTests from "../../Core/IntersectionTests.js";
import Plane from "../../Core/Plane.js";
import Ray from "../../Core/Ray.js";
const scratchSurfaceCartesian = new Cartesian3();
const scratchPlane = new Plane(Cartesian3.UNIT_X, 0.0);
const scratchRay = new Ray();
const defaultTargetPixelSize = new Cartesian2(1.0, 1.0, 1.0);
/**
* Picks a cartesian worldspace position based on the specified window coordinates and the camera's current position and orientation.
* <ol>
* <li>If the camera is above the scene's defined ellipsoid, the position is picked on the ellipsoid.</li>
* <li> If the camera is below the ellipsoid, a temporary plane is created relative to the camera's position and orientation, and the position is picked on that plane.</li>
* </ol>
* @param {Scene} scene The scene to pick the world position from.
* @param {Cartesian2} windowPosition The x and y coordinates of a pixel.
* @param {Cartesian3} result The object onto which to store the result.
* @param {Cartesian2} targetPixelSize The pixel size at the target position, used to preserve relative camera distance from the target position when navigating.
* @returns {Cartesian3|undefined} The picked cartesian worldspace position, or <code>undefined</code> if no position could be picked.
* @see {@link ScreenSpaceMapCameraController#pickWorldPosition}
* @see {@link ScreenSpaceElevatorCameraController#pickWorldPosition}
* @see {@link ScreenSpaceTiltOrbitCameraController#pickWorldPosition}
* @see {@link ScreenSpaceZoomCameraController#pickWorldPosition}
*/
export default function (
scene,
windowPosition,
result,
targetPixelSize = defaultTargetPixelSize,
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("scene", scene);
Check.typeOf.object("windowPosition", windowPosition);
Check.typeOf.object("result", result);
//>>includeEnd('debug');
const { camera, ellipsoid } = scene;
const surface = ellipsoid.scaleToGeodeticSurface(
camera.positionWC,
scratchSurfaceCartesian,
);
// Camera is at the origin
if (!defined(surface)) {
return undefined;
}
const cameraMagnitude = Cartesian3.magnitude(camera.positionWC);
const surfaceMagnitude = Cartesian3.magnitude(surface);
const belowEllipsoid = cameraMagnitude <= surfaceMagnitude;
const normal = ellipsoid.geodeticSurfaceNormal(
camera.positionWC,
scratchSurfaceCartesian,
);
const dot = Cartesian3.dot(normal, camera.directionWC);
const lookingUp = dot > 0.0;
if (belowEllipsoid || lookingUp) {
// Camera is inside the ellipsoid. When underground, create a temporary plane beneath the ellipsoid surface to avoid picking a position on the inside and opposite side of the ellipsoid.
const plane = Plane.fromPointNormal(
camera.positionWC,
normal,
scratchPlane,
);
const { clientHeight } = scene.canvas;
const focusDistance =
(targetPixelSize.y * clientHeight) /
(2.0 * Math.tan(camera.frustum.fovy * 0.5));
plane.distance -= dot * focusDistance;
const ray = camera.getPickRay(windowPosition, scratchRay);
return IntersectionTests.rayPlane(ray, plane, result);
}
return camera.pickEllipsoid(windowPosition, ellipsoid, result);
}