From a76a136b0e5250ced8b73ecb9f7544f986cbe044 Mon Sep 17 00:00:00 2001 From: ggetz Date: Tue, 30 Jun 2026 10:16:28 -0400 Subject: [PATCH 01/10] Camera controllers for asset inspection --- packages/engine/Source/Core/Math.js | 59 ++ packages/engine/Source/Core/getTimestamp.js | 6 +- packages/engine/Source/Scene/Camera.js | 66 ++- .../Source/Scene/Controllers/Controller.js | 62 ++ .../Scene/Controllers/ControllerHost.js | 72 +++ .../HybridScreenspacePanCameraController.js | 99 ++++ .../Source/Scene/Controllers/InputBinding.js | 125 +++++ .../Source/Scene/Controllers/MouseButton.js | 32 ++ .../OrbitCameraAnimationController.js | 62 ++ .../ScreenspaceElevatorCameraController.js | 240 ++++++++ .../ScreenspaceMapCameraController.js | 256 +++++++++ .../ScreenspaceTiltOrbitCameraController.js | 529 ++++++++++++++++++ packages/engine/Source/Scene/Scene.js | 14 + packages/engine/Source/Widget/CesiumWidget.js | 14 + packages/engine/Specs/Scene/CameraSpec.js | 100 ++++ .../gallery/camera-controllers/index.html | 8 + .../gallery/camera-controllers/main.js | 28 + .../camera-controllers/sandcastle.yaml | 5 + .../gallery/camera-controllers/thumbnail.jpg | Bin 0 -> 32658 bytes packages/widgets/Source/Viewer/Viewer.js | 14 + 20 files changed, 1784 insertions(+), 7 deletions(-) create mode 100644 packages/engine/Source/Scene/Controllers/Controller.js create mode 100644 packages/engine/Source/Scene/Controllers/ControllerHost.js create mode 100644 packages/engine/Source/Scene/Controllers/HybridScreenspacePanCameraController.js create mode 100644 packages/engine/Source/Scene/Controllers/InputBinding.js create mode 100644 packages/engine/Source/Scene/Controllers/MouseButton.js create mode 100644 packages/engine/Source/Scene/Controllers/OrbitCameraAnimationController.js create mode 100644 packages/engine/Source/Scene/Controllers/ScreenspaceElevatorCameraController.js create mode 100644 packages/engine/Source/Scene/Controllers/ScreenspaceMapCameraController.js create mode 100644 packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js create mode 100644 packages/sandcastle/gallery/camera-controllers/index.html create mode 100644 packages/sandcastle/gallery/camera-controllers/main.js create mode 100644 packages/sandcastle/gallery/camera-controllers/sandcastle.yaml create mode 100644 packages/sandcastle/gallery/camera-controllers/thumbnail.jpg diff --git a/packages/engine/Source/Core/Math.js b/packages/engine/Source/Core/Math.js index 2ea709c7d076..4c49cc6f8154 100644 --- a/packages/engine/Source/Core/Math.js +++ b/packages/engine/Source/Core/Math.js @@ -339,6 +339,64 @@ CesiumMath.lerp = function (p, q, time) { return (1.0 - time) * p + time * q; }; +/** @typedef {Object} SmoothDampResult + * @property {number} value The new value after applying the smooth damp. + * @property {number} velocity The updated current velocity. + */ + +/** + * Gradually changes a value towards a target value over time. The smoothing function uses a spring-damping algorithm based on Game Programming Gems 4 Chapter 1.10. + * @param {number} p The current value. + * @param {number} q The target value. + * @param {number} velocity The current velocity. + * @param {number} [deltaTime=0.0] The time since the last call to this function. Value must be greater than or equal to 0.0. + * @param {number} [maximumSpeed=Number.POSITIVE_INFINITY] Optionally allows clamping to the specified maximum speed. + * @param {number} [smoothTime=0.0001] Approximately the time it will take to reach the target. A smaller value will reach the target faster. This value must be greater than or equal to 0.0001. + * @param {SmoothDampResult} [result] An object to store the result. If not provided, a new object will be created and returned. + * @returns {SmoothDampResult} An object containing the new value and the updated current velocity. + */ +CesiumMath.smoothDamp = function ( + p, + q, + velocity, + deltaTime = 0.0, + maximumSpeed = Number.POSITIVE_INFINITY, + smoothTime = 0.0001, + result = {}, +) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number("p", p); + Check.typeOf.number("q", q); + Check.typeOf.number("velocity", velocity); + Check.typeOf.number.greaterThanOrEquals("deltaTime", deltaTime, 0.0); + Check.typeOf.number.greaterThanOrEquals("maximumSpeed", maximumSpeed, 0.0); + Check.typeOf.number.greaterThanOrEquals("smoothTime", smoothTime, 0.0001); + Check.typeOf.object("result", result); + //>>includeEnd('debug'); + + // As a fallback, prevent crashes even if smoothTime is too small + smoothTime = Math.max(0.0001, smoothTime); + const omega = 2.0 / smoothTime; + + const x = omega * deltaTime; + const exp = 1.0 / (1.0 + x + 0.48 * x * x + 0.235 * x * x * x); + + const maxChange = maximumSpeed * smoothTime; + let change = p - q; + change = CesiumMath.clamp(change, -maxChange, maxChange); + + const target = p - change; + + const temp = (velocity + omega * change) * deltaTime; + + velocity = (velocity - omega * temp) * exp; + + result.value = target + (change + temp) * exp; + result.velocity = velocity; + + return result; +}; + /** * pi * @@ -1122,4 +1180,5 @@ CesiumMath.fastApproximateAtan2 = function (x, y) { t = y < 0.0 ? -t : t; return t; }; + export default CesiumMath; diff --git a/packages/engine/Source/Core/getTimestamp.js b/packages/engine/Source/Core/getTimestamp.js index 6c1ce26a1874..5dc46bbfab2c 100644 --- a/packages/engine/Source/Core/getTimestamp.js +++ b/packages/engine/Source/Core/getTimestamp.js @@ -1,11 +1,12 @@ +// @ts-check + /** * Gets a timestamp that can be used in measuring the time between events. Timestamps * are expressed in milliseconds, but it is not specified what the milliseconds are * measured from. This function uses performance.now() if it is available, or Date.now() * otherwise. - * + * @type {Function} * @function getTimestamp - * * @returns {number} The timestamp in milliseconds since some unspecified reference time. */ let getTimestamp; @@ -23,4 +24,5 @@ if ( return Date.now(); }; } + export default getTimestamp; diff --git a/packages/engine/Source/Scene/Camera.js b/packages/engine/Source/Scene/Camera.js index d29567035520..d5e6f22fc19a 100644 --- a/packages/engine/Source/Scene/Camera.js +++ b/packages/engine/Source/Scene/Camera.js @@ -3,6 +3,7 @@ import Cartesian2 from "../Core/Cartesian2.js"; import Cartesian3 from "../Core/Cartesian3.js"; import Cartesian4 from "../Core/Cartesian4.js"; import Cartographic from "../Core/Cartographic.js"; +import Check from "../Core/Check.js"; import Frozen from "../Core/Frozen.js"; import defined from "../Core/defined.js"; import DeveloperError from "../Core/DeveloperError.js"; @@ -2356,7 +2357,7 @@ const scratchLookAtHeadingPitchRangeQuaternion1 = new Quaternion(); const scratchLookAtHeadingPitchRangeQuaternion2 = new Quaternion(); const scratchHeadingPitchRangeMatrix3 = new Matrix3(); -function offsetFromHeadingPitchRange(heading, pitch, range) { +function offsetFromHeadingPitchRange(heading, pitch, range, result) { pitch = CesiumMath.clamp( pitch, -CesiumMath.PI_OVER_TWO, @@ -2380,10 +2381,7 @@ function offsetFromHeadingPitchRange(heading, pitch, range) { scratchHeadingPitchRangeMatrix3, ); - const offset = Cartesian3.clone( - Cartesian3.UNIT_X, - scratchLookAtHeadingPitchRangeOffset, - ); + const offset = Cartesian3.clone(Cartesian3.UNIT_X, result); Matrix3.multiplyByVector(rotMatrix, offset, offset); Cartesian3.negate(offset, offset); Cartesian3.multiplyByScalar(offset, range, offset); @@ -2441,6 +2439,7 @@ Camera.prototype.lookAtTransform = function (transform, offset) { offset.heading, offset.pitch, offset.range, + scratchLookAtHeadingPitchRangeOffset, ); } else { cartesianOffset = offset; @@ -2492,6 +2491,62 @@ Camera.prototype.lookAtTransform = function (transform, offset) { this._adjustOrthographicFrustum(true); }; +const scratchLookAtWorldPositionTransform = new Matrix4(); +const scratchLookAtWorldPositionDirection = new Cartesian3(); +const scratchLookAtWorldPositionWorldUp = new Cartesian3(); +const scratchLookAtWorldPositionRight = new Cartesian3(); + +/** + * Sets the camera orientation to look at a target position in world coordinates. The camera's up vector will be oriented to the world up vector at the target position. + * If the camera is at the target position, the camera will be oriented to the world up vector at the target position. + * @param {Cartesian3} target The target position in world coordinates. + * @param {Ellipsoid} [ellipsoid=Ellipsoid.default] The ellipsoid to use for determining the world up. + */ +Camera.prototype.lookAtWorldPosition = function ( + target, + ellipsoid = Ellipsoid.default, +) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("target", target); + Check.typeOf.object("ellipsoid", ellipsoid); + //>>includeEnd('debug'); + + const transform = Matrix4.clone(this._transform, scratchLookAtWorldPositionTransform); + + this._setTransform(Matrix4.IDENTITY); + + // Get direction to look at target + let direction = Cartesian3.subtract(target, this.positionWC, scratchLookAtWorldPositionDirection); + + // If the camera is at the target position, we can't look at it, but we should still continue to re-orient the camera to the world up vector at the target position. + if (Cartesian3.magnitudeSquared(direction) < CesiumMath.EPSILON8) { + direction = Cartesian3.clone(this.directionWC, scratchLookAtWorldPositionDirection); + } + + direction = Cartesian3.normalize(direction, this.direction); + + // Orient the camera to the world up vector at the target position + const worldUp = ellipsoid.geodeticSurfaceNormal( + target, + scratchLookAtWorldPositionWorldUp, + ); + + let right = Cartesian3.cross( + direction, + worldUp, + scratchLookAtWorldPositionRight, + ); + if (Cartesian3.magnitudeSquared(right) < CesiumMath.EPSILON8) { + right = Cartesian3.clone(this.rightWC, scratchLookAtWorldPositionRight); + } + Cartesian3.normalize(right, this.right); + + const up = Cartesian3.cross(right, direction, this.up); + Cartesian3.normalize(up, this.up); + + this._setTransform(transform); +}; + const viewRectangle3DCartographic1 = new Cartographic(); const viewRectangle3DCartographic2 = new Cartographic(); const viewRectangle3DNorthEast = new Cartesian3(); @@ -3621,6 +3676,7 @@ Camera.prototype.flyToBoundingSphere = function (boundingSphere, options) { offset.heading, offset.pitch, offset.range, + scratchflyToBoundingSphereDestination, ); } diff --git a/packages/engine/Source/Scene/Controllers/Controller.js b/packages/engine/Source/Scene/Controllers/Controller.js new file mode 100644 index 000000000000..ff78169f9069 --- /dev/null +++ b/packages/engine/Source/Scene/Controllers/Controller.js @@ -0,0 +1,62 @@ +// @ts-check +/** @import JulianDate from '../../Core/JulianDate.js'; */ + +import DeveloperError from "../../Core/DeveloperError.js"; + +/** + * TODO: Docs + * This type describes an + * interface and is not intended to be instantiated directly. + * @abstract + */ +export default class Controller { + /** + * TODO + * @type {boolean} + */ + get enabled() { + return DeveloperError.throwInstantiationError(); + } + set enabled(value) { + DeveloperError.throwInstantiationError(); + } + + + /** + * Invoked when the controller is added to the DOM. Implement connectedCallback 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 disconnectedCallback 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 update 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 update is called. Implement firstUpdate to perform one-time work after the relevant scene has begun it's 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(); + } +} diff --git a/packages/engine/Source/Scene/Controllers/ControllerHost.js b/packages/engine/Source/Scene/Controllers/ControllerHost.js new file mode 100644 index 000000000000..2fabe506e98d --- /dev/null +++ b/packages/engine/Source/Scene/Controllers/ControllerHost.js @@ -0,0 +1,72 @@ +// @ts-check +/** @import Controller from './Controller.js'; */ +/** @import Scene from '../Scene.js'; */ +/** @import JulianDate from '../../Core/JulianDate.js'; */ + +/** + * TODO: Docs + */ +export default class ControllerHost { + constructor() { + /** + * @type {Controller[]} + * @private + * @readonly + */ + this._controllers = []; + this._needsUpdate = new Set(); + } + + /** + * The number of controllers registered to this host. + * @type {number} + */ + get controllerCount() { + return this._controllers.length; + } + + /** + * @param {Controller} controller + * @param {HTMLElement} element + * @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 0 would mean the new controller would apply it's 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., it's 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); + // TODO: disconnect automatically? + } + + /** + * @param {Controller} controller + * @param {HTMLElement} element + */ + unregisterController(controller, element) { + const controllers = this._controllers; + const index = controllers.indexOf(controller); + if (index !== -1) { + controllers.splice(index, 1); + controller.disconnectedCallback(element); + } + } + + /** + * @param {Scene} 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); + } + } +} diff --git a/packages/engine/Source/Scene/Controllers/HybridScreenspacePanCameraController.js b/packages/engine/Source/Scene/Controllers/HybridScreenspacePanCameraController.js new file mode 100644 index 000000000000..6ced951c5cb8 --- /dev/null +++ b/packages/engine/Source/Scene/Controllers/HybridScreenspacePanCameraController.js @@ -0,0 +1,99 @@ +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 panning and screenspace map controls. + * Based on the camera angle, either a screenspace pan or screenspace map controller is used: + * - If the camera is looking mostly down (within angleThreshold of nadir), the screenspace map controller is used. + * - If the camera is looking more horizontally (beyond angleThreshold from nadir), the screenspace pan controller is used. + * @implements Controller + * @example + * const hybridController = new HybridScreenspacePanCameraController(); + * viewer.addController(hybridController); + */ +export default 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. + */ + this.angleThreshold = CesiumMath.toRadians(180 - 55); + } + + 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} + */ + get elevatorController() { + return this._elevatorController; + } + + /** + * The controller that is used when the camera is looking mostly down (within angleThreshold of nadir). + * @type {ScreenspaceMapCameraController} + */ + 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 + ? this._elevatorController + : this._mapController; + + activeController.update(scene); + } +} diff --git a/packages/engine/Source/Scene/Controllers/InputBinding.js b/packages/engine/Source/Scene/Controllers/InputBinding.js new file mode 100644 index 000000000000..643c066ff402 --- /dev/null +++ b/packages/engine/Source/Scene/Controllers/InputBinding.js @@ -0,0 +1,125 @@ +// @ts-check + +import Check from "../../Core/Check.js"; +import defined from "../../Core/defined.js"; +import ScreenSpaceEventType from "../../Core/ScreenSpaceEventType.js"; +import MouseButton from "./MouseButton.js"; + +/** + * @ignore + * @typedef {object} ScreenSpaceEventHandler + * @property {function} setInputAction + */ + +/** + * @ignore + * @typedef {number} ScreenSpaceEventType + */ + +/** + * @ignore + * @typedef {number} MouseButton + */ + +/** + * @typedef {object} ScreenspaceInputBinding + * @property {MouseButton} button The mouse button used for drag start/stop. + * @property {number} [modifier] The optional keyboard modifier to register. + */ + +/** + * @typedef {object} DragInputActions + * @property {Function} start Called on drag start. + * @property {Function} end Called on drag stop. + * @property {Function} move Called on drag move. + */ + +/** + * @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; +} + +/** + * @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; +} + +export default class InputBinding { + /** + * Registers drag input bindings on a screen space event handler. + * @param {ScreenSpaceEventHandler} handler The screen space event handler. + * @param {ScreenspaceInputBinding[]} inputBindings The drag bindings to register. + * @param {DragInputActions} dragInputActions The callbacks to invoke for drag actions. + */ + 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 moveModifiers = new Set(); + + for (const binding of inputBindings) { + const downEventType = getDownEventType(binding.button); + const upEventType = getUpEventType(binding.button); + + if (defined(downEventType)) { + handler.setInputAction( + dragInputActions.start, + downEventType, + binding.modifier, + ); + } + + if (defined(upEventType)) { + handler.setInputAction( + dragInputActions.end, + upEventType, + binding.modifier, + ); + } + + moveModifiers.add(binding.modifier); + } + + + for (const modifier of moveModifiers) { + handler.setInputAction( + dragInputActions.move, + ScreenSpaceEventType.MOUSE_MOVE, + modifier, + ); + } + } +} diff --git a/packages/engine/Source/Scene/Controllers/MouseButton.js b/packages/engine/Source/Scene/Controllers/MouseButton.js new file mode 100644 index 000000000000..332ec69a0d97 --- /dev/null +++ b/packages/engine/Source/Scene/Controllers/MouseButton.js @@ -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; diff --git a/packages/engine/Source/Scene/Controllers/OrbitCameraAnimationController.js b/packages/engine/Source/Scene/Controllers/OrbitCameraAnimationController.js new file mode 100644 index 000000000000..c815530a91c9 --- /dev/null +++ b/packages/engine/Source/Scene/Controllers/OrbitCameraAnimationController.js @@ -0,0 +1,62 @@ +import Cartesian2 from "../../Core/Cartesian2.js"; +import Cartesian3 from "../../Core/Cartesian3.js"; +import getTimestamp from "../../Core/getTimestamp.js"; +import HeadingPitchRange from "../../Core/HeadingPitchRange.js"; +import TimeConstants from "../../Core/TimeConstants.js"; + +const scratchPixelSize = new Cartesian2(); + +export default class OrbitCameraAnimationController { + constructor() { + this.enabled = true; + this.speed = 100.0; + this.target = new Cartesian3(); + + this._lastUpdateTime = undefined; + this._headingPitchRange = new HeadingPitchRange(); + } + + get range() { + return this._headingPitchRange.range; + } + + set range(value) { + this._headingPitchRange.range = value; + } + + connectedCallback(element) {} + disconnectedCallback(element) {} + + firstUpdate(scene, time) { + this._lastUpdateTime = getTimestamp(); + } + + update(scene, time) { + const now = getTimestamp(); + const ds = + (now - this._lastUpdateTime) * TimeConstants.SECONDS_PER_MILLISECOND; + + const camera = scene.camera; + const distance = Cartesian3.distance(camera.positionWC, this.target); + + const { drawingBufferWidth, drawingBufferHeight, pixelRatio } = scene; + const pixelSize = camera.frustum.getPixelDimensions( + drawingBufferWidth, + drawingBufferHeight, + distance, + pixelRatio, + scratchPixelSize, + ); + + const p = Math.max(pixelSize.x, pixelSize.y); + + const moveRate = this.speed * p * ds; + + this._headingPitchRange.range = distance; + + camera.moveForward(moveRate); + //camera.lookAt(this.target, this._headingPitchRange); + + this._lastUpdateTime = getTimestamp(); + } +} diff --git a/packages/engine/Source/Scene/Controllers/ScreenspaceElevatorCameraController.js b/packages/engine/Source/Scene/Controllers/ScreenspaceElevatorCameraController.js new file mode 100644 index 000000000000..f0ce0f994517 --- /dev/null +++ b/packages/engine/Source/Scene/Controllers/ScreenspaceElevatorCameraController.js @@ -0,0 +1,240 @@ +// @ts-check +/** @import Controller from './Controller.js'; */ +/** @import { ScreenspaceInputBinding } from './InputBinding.js'; */ + +import Cartesian2 from "../../Core/Cartesian2.js"; +import Cartesian3 from "../../Core/Cartesian3.js"; +import defined from "../../Core/defined.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 InputBinding from "./InputBinding.js"; +import MouseButton from "./MouseButton.js"; + +/** + * 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. + * @implements Controller + * @example + * const elevatorCameraController = new ScreenspaceElevatorCameraController(); + * viewer.addController(elevatorCameraController); + */ +export default class ScreenspaceElevatorCameraController { + constructor() { + 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 {ScreenspaceInputBinding[]} + * @see ScreenSpaceEventHandler + */ + this.dragBindings = [ + { + button: MouseButton.LEFT, + }, + ]; + + this._isPanning = false; + this._panDelta = new Cartesian2(); + this._panPosition = new Cartesian2(); + + 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; + + /** + * 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 [0, 1) 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 { + this._isPanning = false; + } + } + + /** + * @inheritdoc + * @param {HTMLElement} element The DOM element containing the Cesium scene. + */ + connectedCallback(element) { + const handler = new ScreenSpaceEventHandler(element); + this._handler = handler; + + InputBinding.registerDragInputBindings(handler, this.dragBindings, { + start: this._handleStartPan.bind(this), + end: this._handleStopPan.bind(this), + move: 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; + const { clientWidth, clientHeight } = canvas; + if (dt === 0 || clientWidth === 0 || clientHeight === 0) { + // Reset for next frame + this._lastUpdateTime = getTimestamp(); + this._panDelta.x = 0; + this._panDelta.y = 0; + return; + } + + let surface = camera.pickEllipsoid( + this._panPosition, + ellipsoid, + 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, + ); + + let dx = -this._panDelta.x; + let dy = this._panDelta.y; + + if (!this._isPanning) { + 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 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._isPanning = true; + this._panDelta.x = 0; + this._panDelta.y = 0; + } + + _handleStopPan() { + this._isPanning = false; + } + + /** + * @typedef {object} DragEvent + * @property {Cartesian2} startPosition The position of the mouse when the drag started. + * @property {Cartesian2} endPosition The position of the mouse when the drag ended. + */ + + /** + * @param {DragEvent} event + * @private + */ + _handlePan(event) { + if (!this._isPanning) { + return; + } + + 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; + } +} diff --git a/packages/engine/Source/Scene/Controllers/ScreenspaceMapCameraController.js b/packages/engine/Source/Scene/Controllers/ScreenspaceMapCameraController.js new file mode 100644 index 000000000000..b2b5cb7b0cec --- /dev/null +++ b/packages/engine/Source/Scene/Controllers/ScreenspaceMapCameraController.js @@ -0,0 +1,256 @@ +// @ts-check +/** @import Controller from './Controller.js'; */ +/** @import { ScreenspaceInputBinding } from './InputBinding.js'; */ + +import Cartesian2 from "../../Core/Cartesian2.js"; +import Cartesian3 from "../../Core/Cartesian3.js"; +import defined from "../../Core/defined.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 InputBinding from "./InputBinding.js"; +import MouseButton from "./MouseButton.js"; + +/** + * A camera controller that allows panning the camera tangential to the ellipsoid in screen space + * by clicking and dragging the mouse. + * @implements Controller + * @example + * const mapCameraController = new ScreenspaceMapCameraController(); + * viewer.addController(mapCameraController); + */ +export default class ScreenspaceMapCameraController { + constructor() { + 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 {ScreenspaceInputBinding[]} + * @see ScreenSpaceEventHandler + */ + this.dragBindings = [ + { + button: MouseButton.LEFT, + }, + ]; + + this._isPanning = false; + this._panDelta = new Cartesian2(); + this._panPosition = new Cartesian2(); + + 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; + + /** + * 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 [0, 1) 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 { + this._isPanning = false; + } + } + + /** + * @inheritdoc + * @param {HTMLElement} element The DOM element containing the Cesium scene. + */ + connectedCallback(element) { + const handler = new ScreenSpaceEventHandler(element); + this._handler = handler; + + InputBinding.registerDragInputBindings(handler, this.dragBindings, { + start: this._handleStartPan.bind(this), + end: this._handleStopPan.bind(this), + move: 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; + const { clientWidth, clientHeight } = canvas; + if (dt === 0 || clientWidth === 0 || clientHeight === 0) { + // Reset for next frame + this._lastUpdateTime = getTimestamp(); + this._panDelta.x = 0; + this._panDelta.y = 0; + return; + } + + let surface = camera.pickEllipsoid( + this._panPosition, + ellipsoid, + 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, + ); + + let dx = -this._panDelta.x; + let dy = this._panDelta.y; + + if (!this._isPanning) { + 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 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._isPanning = true; + this._panDelta.x = 0; + this._panDelta.y = 0; + } + + _handleStopPan() { + this._isPanning = false; + } + + /** + * @typedef {object} DragEvent + * @property {Cartesian2} startPosition The position of the mouse when the drag started. + * @property {Cartesian2} endPosition The position of the mouse when the drag ended. + */ + + /** + * @param {DragEvent} event + * @private + */ + _handlePan(event) { + if (!this._isPanning) { + return; + } + + 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; + } +} diff --git a/packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js b/packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js new file mode 100644 index 000000000000..ba9310365f7a --- /dev/null +++ b/packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js @@ -0,0 +1,529 @@ +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 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 InputBinding from "./InputBinding.js"; +import MouseButton from "./MouseButton.js"; + +/** + * 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. + * @implements Controller + */ +export default class ScreenspaceTiltOrbitCameraController { + /** + * Creates a new ScreenspaceTiltOrbitCameraController instance. + * @constructor + * @example + * const tiltOrbitController = new Cesium.ScreenspaceTiltOrbitCameraController(); + * viewer.addController(tiltOrbitController); + */ + constructor() { + 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; + + /** + * The drag input bindings that control tilting. Each binding is a combination of the mouse button + * and an optional keyboard modifier. + * @type {ScreenspaceInputBinding[]} + * @see ScreenSpaceEventHandler + */ + this.dragBindings = [ + { + button: MouseButton.LEFT, + modifier: KeyboardEventModifier.CTRL, + }, + { + button: MouseButton.RIGHT, + }, + ]; + + this._isDragging = false; + this._dragDelta = new Cartesian2(); + this._dragOrigin = new Cartesian2(); + this._screenOrigin = new Cartesian2(); + + 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; + + /** + * Specifies the length of time in seconds in which a single tilt animation completes. + * @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 minimum tilt angle in radians from the zenith, or the ellipsoid surface normal, at the tilt origin at which the camera can orbit. + * @type {number} + * @default CesiumMath.toRadians(5) + */ + this.minimumOrbitTiltAngle = CesiumMath.toRadians(5); + + /** + * 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._tiltDampenedResults = { + velocity: 0.0, + value: 0.0, + }; + + this._orbitTargetEnu = new Matrix4(); + this._orbitTargetEast = new Cartesian3(); + this._orbitQuaternion = new Quaternion(); + this._orbitOffset = new Cartesian3(); + this._orbitDampenedResults = { + velocity: 0.0, + value: 0.0, + }; + + /** + * A parameter in the range [0, 1) 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; + this._dragDelta.y = 0; + } else { + this._isDragging = false; + } + } + + /** + * @inheritdoc + * @param {HTMLElement} element The DOM element containing the Cesium scene. + */ + connectedCallback(element) { + const handler = new ScreenSpaceEventHandler(element); + this._handler = handler; + + InputBinding.registerDragInputBindings(handler, this.dragBindings, { + start: this._handleStartDrag.bind(this), + end: this._handleStopDrag.bind(this), + move: 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; + this._dragDelta.y = 0; + } + + /** + * @typedef {object} StartDragEvent + * @property {Cartesian2} position The position of the mouse when the drag started. + */ + + /** + * @private + * @param {StartDragEvent} event + */ + _handleStartDrag(event) { + if (!this.enabled) { + return; + } + + this._isDragging = true; + this._dragOrigin.x = event.position.x; + this._dragOrigin.y = event.position.y; + this._dragDelta.x = 0; + this._dragDelta.y = 0; + } + + _handleStopDrag() { + this._isDragging = false; + } + + /** + * @typedef {object} DragEvent + * @property {Cartesian2} startPosition The position of the mouse when the drag started. + * @property {Cartesian2} endPosition The position of the mouse when the drag ended. + */ + + /** + * @private + * @param {DragEvent} event + */ + _handleDrag(event) { + if (!this._isDragging) { + return; + } + + 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; + } + + /** @typedef {any} Camera */ + + /** + * 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 target 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 currentTiltAngle = Cartesian3.angleBetween(camera.direction, axis); + if (currentTiltAngle < this.minimumOrbitTiltAngle) { + return; + } + + const offset = Cartesian3.subtract( + camera.positionWC, + target, + this._orbitOffset, + ); + const enu = Transforms.eastNorthUpToFixedFrame( + target, + ellipsoid, + this._orbitTargetEnu, + ); + const east = Matrix4.multiplyByPointAsVector( + enu, + Cartesian3.UNIT_X, + this._orbitTargetEast, + ); + const currentOrbitAngle = Cartesian3.angleBetween(offset, east); + + if (Math.abs(this.orbitVelocity) < this.minimumOrbitVelocity) { + this.orbitVelocity = 0.0; + } + + // Apply inertia + if (!this._isDragging) { + amount += this.orbitVelocity * dt; + } + + if (amount === 0.0) { + return; + } + + const targetOrbitAngle = currentOrbitAngle + amount; + + // Apply critical dampening + this._orbitDampenedResults = CesiumMath.smoothDamp( + currentOrbitAngle, + targetOrbitAngle, + this.orbitVelocity, + dt, + this.maximumOrbitVelocity * this.orbitMagnitude, + this.orbitAnimationDuration, + this._orbitDampenedResults, + ); + + const theta = this.orbitAngle - currentOrbitAngle; + const rotation = Matrix3.fromQuaternion( + Quaternion.fromAxisAngle(camera.upWC, -theta, this._orbitQuaternion), + ); + + const rotatedOffset = Matrix3.multiplyByVector( + rotation, + offset, + this._orbitOffset, + ); + + Cartesian3.add(target, rotatedOffset, camera.position); + camera.lookAtWorldPosition(target, 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 target 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'); + + const currentTiltAngle = Cartesian3.angleBetween(camera.direction, axis); + + if (Math.abs(this.tiltVelocity) < this.minimumTiltVelocity) { + this.tiltVelocity = 0.0; + } + + // Apply inertia + if (!this._isDragging) { + amount += this.tiltVelocity * dt; + } + + if (amount === 0.0) { + return; + } + + const targetRotationAngle = currentTiltAngle + amount; + + CesiumMath.smoothDamp( + currentTiltAngle, + targetRotationAngle, + this.tiltVelocity, + dt, + this.maximumTiltVelocity * this.tiltMagnitude, + this.tiltAnimationDuration, + this._tiltDampenedResults, + ); + + const theta = this.tiltAngle - currentTiltAngle; + const rotation = Matrix3.fromQuaternion( + Quaternion.fromAxisAngle(camera.rightWC, -theta, this._tiltQuaternion), + ); + + const offset = Cartesian3.subtract( + camera.positionWC, + target, + this._tiltOffset, + ); + + const rotatedOffset = Matrix3.multiplyByVector( + rotation, + offset, + this._tiltOffset, + ); + Cartesian3.add(target, rotatedOffset, camera.position); + camera.lookAtWorldPosition(target, ellipsoid); + } + + /** + * @inheritdoc + * @param {Scene} scene + */ + update(scene) { + const dt = + (getTimestamp() - this._lastUpdateTime) * + TimeConstants.SECONDS_PER_MILLISECOND; + + const { camera, ellipsoid, 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; + } + + const screenOrigin = this._screenOrigin; + screenOrigin.x = clientWidth / 2.0; + screenOrigin.y = clientHeight / 2.0; + + let target = camera.pickEllipsoid(screenOrigin, ellipsoid, this._target); + + // Fallback to the cursor position if the center of the screen is not on the ellipsoid + if (!defined(target)) { + const dragOrigin = this._dragOrigin; + target = camera.pickEllipsoid(dragOrigin, ellipsoid, this._target); + } + + if (defined(target)) { + 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; + } +} diff --git a/packages/engine/Source/Scene/Scene.js b/packages/engine/Source/Scene/Scene.js index 6e3feb1d1748..66b56561ecaa 100644 --- a/packages/engine/Source/Scene/Scene.js +++ b/packages/engine/Source/Scene/Scene.js @@ -45,6 +45,7 @@ import BrdfLutGenerator from "./BrdfLutGenerator.js"; import Camera from "./Camera.js"; import Cesium3DTilePass from "./Cesium3DTilePass.js"; import Cesium3DTilePassState from "./Cesium3DTilePassState.js"; +import ControllerHost from "./Controllers/ControllerHost.js"; import CreditDisplay from "./CreditDisplay.js"; import DebugCameraPrimitive from "./DebugCameraPrimitive.js"; import DepthPlane from "./DepthPlane.js"; @@ -241,6 +242,8 @@ function Scene(options) { this._preRender = new Event(); this._postRender = new Event(); + this._controllerHost = new ControllerHost(); + this._minimumDisableDepthTestDistance = 0.0; this._debugInspector = new DebugInspector(); @@ -1079,6 +1082,15 @@ Object.defineProperties(Scene.prototype, { }, }, + /** + * TODO + */ + controllerHost: { + get: function() { + return this._controllerHost; + } + }, + /** * Gets the controller for camera input handling. * @memberof Scene.prototype @@ -4419,6 +4431,8 @@ Scene.prototype.render = function (time) { time = JulianDate.now(); } + this._controllerHost.update(this, time); + const cameraChanged = this._view.checkForCameraUpdates(this); if (cameraChanged) { this._globeHeightDirty = true; diff --git a/packages/engine/Source/Widget/CesiumWidget.js b/packages/engine/Source/Widget/CesiumWidget.js index 9444cdb4b663..564003165a25 100644 --- a/packages/engine/Source/Widget/CesiumWidget.js +++ b/packages/engine/Source/Widget/CesiumWidget.js @@ -1227,6 +1227,20 @@ CesiumWidget.prototype._onDataSourceRemoved = function ( } }; +/** + * TODO + */ +CesiumWidget.prototype.addController = function (controller) { + return this.scene.controllerHost.registerController(controller, this.container); +} + +/** + * TODO + */ +CesiumWidget.prototype.removeController = function (controller) { + return this.scene.controllerHost.unregisterController(controller, this.container); +} + /** * Asynchronously sets the camera to view the provided entity, entities, or data source. * If the data source is still in the process of loading or the visualization is otherwise still loading, diff --git a/packages/engine/Specs/Scene/CameraSpec.js b/packages/engine/Specs/Scene/CameraSpec.js index 584aacd4997a..0e5563e12a73 100644 --- a/packages/engine/Specs/Scene/CameraSpec.js +++ b/packages/engine/Specs/Scene/CameraSpec.js @@ -2193,6 +2193,106 @@ describe("Scene/Camera", function () { }).toThrowDeveloperError(); }); + it("lookAtWorldPosition", function () { + const tempCamera = Camera.clone(camera); + const target = Cartesian3.fromDegrees(-75.0, 40.0, 0.0); + + tempCamera.lookAtWorldPosition(target); + + const expectedDirection = new Cartesian3(0.19881574, -0.74199046, 0.64025187); + const expectedRight = new Cartesian3(-0.96592583, -0.25881905, 0.0); + const expectedUp = new Cartesian3(-0.16570938, 0.61843582, 0.76816505); + + expect(tempCamera.directionWC).toEqualEpsilon( + expectedDirection, + CesiumMath.EPSILON8, + ); + expect(tempCamera.rightWC).toEqualEpsilon(expectedRight, CesiumMath.EPSILON8); + expect(tempCamera.upWC).toEqualEpsilon(expectedUp, CesiumMath.EPSILON8); + }); + + it("lookAtWorldPosition uses Ellipsoid.default when ellipsoid is omitted", function () { + const originalDefaultEllipsoid = Ellipsoid.default; + Ellipsoid.default = Ellipsoid.UNIT_SPHERE; + + try { + const target = new Cartesian3(0.0, 1.0, 0.0); + const cameraWithDefault = Camera.clone(camera); + const cameraWithExplicit = Camera.clone(camera); + + cameraWithDefault.lookAtWorldPosition(target); + cameraWithExplicit.lookAtWorldPosition(target, Ellipsoid.UNIT_SPHERE); + + expect(cameraWithDefault.directionWC).toEqualEpsilon( + cameraWithExplicit.directionWC, + CesiumMath.EPSILON12, + ); + expect(cameraWithDefault.rightWC).toEqualEpsilon( + cameraWithExplicit.rightWC, + CesiumMath.EPSILON12, + ); + expect(cameraWithDefault.upWC).toEqualEpsilon( + cameraWithExplicit.upWC, + CesiumMath.EPSILON12, + ); + } finally { + Ellipsoid.default = originalDefaultEllipsoid; + } + }); + + it("lookAtWorldPosition when target equals camera position", function () { + const tempCamera = Camera.clone(camera); + const target = Cartesian3.clone(tempCamera.positionWC, new Cartesian3()); + const directionBefore = Cartesian3.clone(tempCamera.directionWC, new Cartesian3()); + + tempCamera.lookAtWorldPosition(target); + + expect(tempCamera.directionWC).toEqualEpsilon(directionBefore, CesiumMath.EPSILON12); + expect(Cartesian3.magnitude(tempCamera.directionWC)).toEqualEpsilon( + 1.0, + CesiumMath.EPSILON12, + ); + expect(Cartesian3.magnitude(tempCamera.upWC)).toEqualEpsilon( + 1.0, + CesiumMath.EPSILON12, + ); + expect(Cartesian3.magnitude(tempCamera.rightWC)).toEqualEpsilon( + 1.0, + CesiumMath.EPSILON12, + ); + }); + + it("lookAtWorldPosition with non-identity transform", function () { + const tempCamera = Camera.clone(camera); + const ellipsoid = Ellipsoid.WGS84; + const transform = Transforms.eastNorthUpToFixedFrame( + Cartesian3.fromDegrees(-75.0, 40.0, 0.0), + ellipsoid, + ); + tempCamera._setTransform(transform); + + const target = Cartesian3.fromDegrees(-75.1, 40.0, 150.0); + tempCamera.lookAtWorldPosition(target, ellipsoid); + + const expectedDirectionWC = new Cartesian3(0.19752041, -0.74233628, 0.64025193); + expect(tempCamera.directionWC).toEqualEpsilon( + expectedDirectionWC, + CesiumMath.EPSILON8, + ); + }); + + it("lookAtWorldPosition throws with no target parameter", function () { + expect(function () { + camera.lookAtWorldPosition(undefined, Ellipsoid.WGS84); + }).toThrowDeveloperError(); + }); + + it("lookAtWorldPosition throws with no ellipsoid parameter", function () { + expect(function () { + camera.lookAtWorldPosition(Cartesian3.ZERO, undefined); + }).toThrowDeveloperError(); + }); + it("lookAtTransform", function () { const target = new Cartesian3(-1.0, -1.0, 0.0); const offset = new Cartesian3(1.0, 1.0, 0.0); diff --git a/packages/sandcastle/gallery/camera-controllers/index.html b/packages/sandcastle/gallery/camera-controllers/index.html new file mode 100644 index 000000000000..923148147ef0 --- /dev/null +++ b/packages/sandcastle/gallery/camera-controllers/index.html @@ -0,0 +1,8 @@ + +
+

Loading...

+
+ TODO: Note controller inputs +
diff --git a/packages/sandcastle/gallery/camera-controllers/main.js b/packages/sandcastle/gallery/camera-controllers/main.js new file mode 100644 index 000000000000..da3fbf29ed95 --- /dev/null +++ b/packages/sandcastle/gallery/camera-controllers/main.js @@ -0,0 +1,28 @@ +import * as Cesium from "cesium"; + +const viewer = new Cesium.Viewer("cesiumContainer"); +const scene = viewer.scene; + +scene.camera.flyTo({ + duration: 0, + destination: Cesium.Rectangle.fromDegrees( + // Philly + -75.280266, + 39.867004, + -74.955763, + 40.137992, + ), +}); + +scene.screenSpaceCameraController.enableInputs = false; +scene.screenSpaceCameraController.enableCollisionDetection = false; + +// TODO: Zoom controller + +const panController = new Cesium.HybridScreenspacePanCameraController(); +viewer.addController(panController); + +const tiltController = new Cesium.ScreenspaceTiltOrbitCameraController(); +viewer.addController(tiltController); + +// TODO: Instructions panel \ No newline at end of file diff --git a/packages/sandcastle/gallery/camera-controllers/sandcastle.yaml b/packages/sandcastle/gallery/camera-controllers/sandcastle.yaml new file mode 100644 index 000000000000..93ebc2e8f38f --- /dev/null +++ b/packages/sandcastle/gallery/camera-controllers/sandcastle.yaml @@ -0,0 +1,5 @@ +title: Camera Controllers for Asset Inspection +description: TODO +labels: + - Camera +thumbnail: thumbnail.jpg diff --git a/packages/sandcastle/gallery/camera-controllers/thumbnail.jpg b/packages/sandcastle/gallery/camera-controllers/thumbnail.jpg new file mode 100644 index 0000000000000000000000000000000000000000..23dadd4bacd470c3864df2d28d386e0ed947814e GIT binary patch literal 32658 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 literal 0 HcmV?d00001 diff --git a/packages/widgets/Source/Viewer/Viewer.js b/packages/widgets/Source/Viewer/Viewer.js index 530f256ac38f..f61b31a261f3 100644 --- a/packages/widgets/Source/Viewer/Viewer.js +++ b/packages/widgets/Source/Viewer/Viewer.js @@ -1973,6 +1973,20 @@ Viewer.prototype._onDataSourceRemoved = function ( this._dataSourceChangedListeners[id] = undefined; }; +/** + * TODO + */ +Viewer.prototype.addController = function (controller) { + return this._cesiumWidget.addController(controller); +} + +/** + * TODO + */ +Viewer.prototype.removeController = function (controller) { + return this._cesiumWidget.removeController(controller); +} + /** * Asynchronously sets the camera to view the provided entity, entities, or data source. * If the data source is still in the process of loading or the visualization is otherwise still loading, From f3562ffea0fec7dd2d3d555e734f6ecba0666858 Mon Sep 17 00:00:00 2001 From: ggetz Date: Tue, 30 Jun 2026 12:23:17 -0400 Subject: [PATCH 02/10] Type check and ref doc fixes --- .../Source/Scene/Controllers/Controller.js | 19 ++--- .../Scene/Controllers/ControllerHost.js | 36 ++++++---- .../HybridScreenspacePanCameraController.js | 23 +++--- .../OrbitCameraAnimationController.js | 62 ---------------- .../ScreenspaceElevatorCameraController.js | 59 +++++++++++---- ...Binding.js => ScreenspaceInputBindings.js} | 35 ++++----- .../ScreenspaceMapCameraController.js | 71 +++++++++++++------ .../ScreenspaceTiltOrbitCameraController.js | 68 +++++++++++++----- packages/engine/Source/Scene/Scene.js | 16 ++++- packages/engine/Source/Widget/CesiumWidget.js | 28 ++++++-- .../gallery/camera-controllers/index.html | 18 ++++- .../gallery/camera-controllers/main.js | 39 ++++++---- .../camera-controllers/sandcastle.yaml | 2 +- packages/widgets/Source/Viewer/Viewer.js | 18 +++-- 14 files changed, 297 insertions(+), 197 deletions(-) delete mode 100644 packages/engine/Source/Scene/Controllers/OrbitCameraAnimationController.js rename packages/engine/Source/Scene/Controllers/{InputBinding.js => ScreenspaceInputBindings.js} (87%) diff --git a/packages/engine/Source/Scene/Controllers/Controller.js b/packages/engine/Source/Scene/Controllers/Controller.js index ff78169f9069..69c8f4b67eaf 100644 --- a/packages/engine/Source/Scene/Controllers/Controller.js +++ b/packages/engine/Source/Scene/Controllers/Controller.js @@ -1,17 +1,19 @@ -// @ts-check -/** @import JulianDate from '../../Core/JulianDate.js'; */ - import DeveloperError from "../../Core/DeveloperError.js"; /** - * TODO: Docs + * 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} */ -export default class Controller { +class Controller { /** - * TODO + * Determines if the controller is enabled and should be updated by the host scene. * @type {boolean} */ get enabled() { @@ -21,10 +23,8 @@ export default class Controller { DeveloperError.throwInstantiationError(); } - /** * Invoked when the controller is added to the DOM. Implement connectedCallback to set up any DOM event listeners. - * * @param {HTMLElement} element The DOM element containing the Cesium scene. */ connectedCallback(element) { @@ -33,7 +33,6 @@ export default class Controller { /** * Invoked when the controller is removed from the DOM. Implement disconnectedCallback to tear down any DOM event listeners. - * * @param {HTMLElement} element The DOM element containing the Cesium scene. */ disconnectedCallback(element) { @@ -60,3 +59,5 @@ export default class Controller { DeveloperError.throwInstantiationError(); } } + +export default Controller; diff --git a/packages/engine/Source/Scene/Controllers/ControllerHost.js b/packages/engine/Source/Scene/Controllers/ControllerHost.js index 2fabe506e98d..5562126188a8 100644 --- a/packages/engine/Source/Scene/Controllers/ControllerHost.js +++ b/packages/engine/Source/Scene/Controllers/ControllerHost.js @@ -1,17 +1,20 @@ -// @ts-check -/** @import Controller from './Controller.js'; */ -/** @import Scene from '../Scene.js'; */ -/** @import JulianDate from '../../Core/JulianDate.js'; */ - /** - * TODO: Docs + * 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} */ -export default class ControllerHost { +class ControllerHost { + /** + * Creates an instance of a ControllerHost. Typically, a ControllerHost is created by the Scene constructor and accessed via {@link Scene#controllerHost}. + * @constructor + * @alias ControllerHost + * @see {@link Scene#controllerHost} + */ constructor() { /** * @type {Controller[]} * @private - * @readonly */ this._controllers = []; this._needsUpdate = new Set(); @@ -20,14 +23,16 @@ export default class ControllerHost { /** * The number of controllers registered to this host. * @type {number} + * @readonly */ get controllerCount() { return this._controllers.length; } /** - * @param {Controller} controller - * @param {HTMLElement} element + * 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 0 would mean the new controller would apply it's 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., it's updates are applied after all other controllers. */ registerController(controller, element, priority) { @@ -35,12 +40,12 @@ export default class ControllerHost { this._controllers.splice(index, 0, controller); this._needsUpdate.add(controller); controller.connectedCallback(element); - // TODO: disconnect automatically? } /** - * @param {Controller} controller - * @param {HTMLElement} 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; @@ -52,7 +57,8 @@ export default class ControllerHost { } /** - * @param {Scene} scene + * 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) { @@ -70,3 +76,5 @@ export default class ControllerHost { } } } + +export default ControllerHost; diff --git a/packages/engine/Source/Scene/Controllers/HybridScreenspacePanCameraController.js b/packages/engine/Source/Scene/Controllers/HybridScreenspacePanCameraController.js index 6ced951c5cb8..5573abfe76fb 100644 --- a/packages/engine/Source/Scene/Controllers/HybridScreenspacePanCameraController.js +++ b/packages/engine/Source/Scene/Controllers/HybridScreenspacePanCameraController.js @@ -4,16 +4,17 @@ import Cartesian3 from "../../Core/Cartesian3.js"; import CesiumMath from "../../Core/Math.js"; /** - * A contextual camera controller that combines screenspace panning and screenspace map controls. - * Based on the camera angle, either a screenspace pan or screenspace map controller is used: - * - If the camera is looking mostly down (within angleThreshold of nadir), the screenspace map controller is used. - * - If the camera is looking more horizontally (beyond angleThreshold from nadir), the screenspace pan controller is used. + * 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), ScreenspaceMapCameraController is used. + * If the camera is looking towards the horizon (beyond angleThreshold from nadir), the ScreenspaceElevatorCameraController is used. * @implements Controller * @example + * viewer.scene.screenSpaceCameraController.enableInputs = false; + * viewer.scene.screenSpaceCameraController.enableCollisionDetection = false; + * * const hybridController = new HybridScreenspacePanCameraController(); * viewer.addController(hybridController); */ -export default class HybridScreenspacePanCameraController { +class HybridScreenspacePanCameraController { constructor() { this._elevatorController = new ScreenspaceElevatorCameraController(); this._mapController = new ScreenspaceMapCameraController(); @@ -38,6 +39,7 @@ export default class HybridScreenspacePanCameraController { /** * The controller that is used when the camera is looking more horizontally (beyond angleThreshold from nadir). * @type {ScreenspaceElevatorCameraController} + * @readonly */ get elevatorController() { return this._elevatorController; @@ -46,12 +48,13 @@ export default class HybridScreenspacePanCameraController { /** * 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. */ @@ -60,7 +63,7 @@ export default class HybridScreenspacePanCameraController { this._mapController.connectedCallback(element); } - /** + /** * @inheritdoc * @param {HTMLElement} element The DOM element containing the Cesium scene. */ @@ -69,7 +72,7 @@ export default class HybridScreenspacePanCameraController { this._mapController.disconnectedCallback(element); } - /** + /** * @inheritdoc */ firstUpdate() { @@ -77,7 +80,7 @@ export default class HybridScreenspacePanCameraController { this._mapController.firstUpdate(); } - /** + /** * @inheritdoc * @param {Scene} scene */ @@ -97,3 +100,5 @@ export default class HybridScreenspacePanCameraController { activeController.update(scene); } } + +export default HybridScreenspacePanCameraController; diff --git a/packages/engine/Source/Scene/Controllers/OrbitCameraAnimationController.js b/packages/engine/Source/Scene/Controllers/OrbitCameraAnimationController.js deleted file mode 100644 index c815530a91c9..000000000000 --- a/packages/engine/Source/Scene/Controllers/OrbitCameraAnimationController.js +++ /dev/null @@ -1,62 +0,0 @@ -import Cartesian2 from "../../Core/Cartesian2.js"; -import Cartesian3 from "../../Core/Cartesian3.js"; -import getTimestamp from "../../Core/getTimestamp.js"; -import HeadingPitchRange from "../../Core/HeadingPitchRange.js"; -import TimeConstants from "../../Core/TimeConstants.js"; - -const scratchPixelSize = new Cartesian2(); - -export default class OrbitCameraAnimationController { - constructor() { - this.enabled = true; - this.speed = 100.0; - this.target = new Cartesian3(); - - this._lastUpdateTime = undefined; - this._headingPitchRange = new HeadingPitchRange(); - } - - get range() { - return this._headingPitchRange.range; - } - - set range(value) { - this._headingPitchRange.range = value; - } - - connectedCallback(element) {} - disconnectedCallback(element) {} - - firstUpdate(scene, time) { - this._lastUpdateTime = getTimestamp(); - } - - update(scene, time) { - const now = getTimestamp(); - const ds = - (now - this._lastUpdateTime) * TimeConstants.SECONDS_PER_MILLISECOND; - - const camera = scene.camera; - const distance = Cartesian3.distance(camera.positionWC, this.target); - - const { drawingBufferWidth, drawingBufferHeight, pixelRatio } = scene; - const pixelSize = camera.frustum.getPixelDimensions( - drawingBufferWidth, - drawingBufferHeight, - distance, - pixelRatio, - scratchPixelSize, - ); - - const p = Math.max(pixelSize.x, pixelSize.y); - - const moveRate = this.speed * p * ds; - - this._headingPitchRange.range = distance; - - camera.moveForward(moveRate); - //camera.lookAt(this.target, this._headingPitchRange); - - this._lastUpdateTime = getTimestamp(); - } -} diff --git a/packages/engine/Source/Scene/Controllers/ScreenspaceElevatorCameraController.js b/packages/engine/Source/Scene/Controllers/ScreenspaceElevatorCameraController.js index f0ce0f994517..711123f6f68f 100644 --- a/packages/engine/Source/Scene/Controllers/ScreenspaceElevatorCameraController.js +++ b/packages/engine/Source/Scene/Controllers/ScreenspaceElevatorCameraController.js @@ -1,27 +1,58 @@ -// @ts-check -/** @import Controller from './Controller.js'; */ -/** @import { ScreenspaceInputBinding } from './InputBinding.js'; */ - 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 InputBinding from "./InputBinding.js"; +import InputBinding from "./ScreenspaceInputBindings.js"; import MouseButton from "./MouseButton.js"; +/** + * @typedef {object} ControllerOptions + * @memberOf ScreenspaceElevatorCameraController + * @property {ScreenspaceInputBinding[]} [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 + * @alias ScreenspaceElevatorCameraController * @implements Controller * @example - * const elevatorCameraController = new ScreenspaceElevatorCameraController(); + * 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); */ -export default class ScreenspaceElevatorCameraController { - constructor() { +class ScreenspaceElevatorCameraController { + /** + * @private + * @returns {ScreenspaceInputBinding[]} The default drag input bindings. + */ + static _getDefaultDragInputs() { + return [ + Object.freeze({ + button: MouseButton.LEFT, + }), + ]; + } + + /** + * Creates an instance of a ScreenspaceElevatorCameraController. + * @param {ControllerOptions} [options] The options for configuring the controller. + */ + constructor(options = Frozen.EMPTY_OBJECT) { this._enabled = true; this._handler = undefined; this._lastUpdateTime = undefined; @@ -32,11 +63,9 @@ export default class ScreenspaceElevatorCameraController { * @type {ScreenspaceInputBinding[]} * @see ScreenSpaceEventHandler */ - this.dragBindings = [ - { - button: MouseButton.LEFT, - }, - ]; + this.dragInputs = + options.dragInputs ?? + ScreenspaceElevatorCameraController._getDefaultDragInputs(); this._isPanning = false; this._panDelta = new Cartesian2(); @@ -99,7 +128,7 @@ export default class ScreenspaceElevatorCameraController { const handler = new ScreenSpaceEventHandler(element); this._handler = handler; - InputBinding.registerDragInputBindings(handler, this.dragBindings, { + InputBinding.registerDragInputBindings(handler, this.dragInputs, { start: this._handleStartPan.bind(this), end: this._handleStopPan.bind(this), move: this._handlePan.bind(this), @@ -238,3 +267,5 @@ export default class ScreenspaceElevatorCameraController { this._panPosition.y = event.endPosition.y; } } + +export default ScreenspaceElevatorCameraController; diff --git a/packages/engine/Source/Scene/Controllers/InputBinding.js b/packages/engine/Source/Scene/Controllers/ScreenspaceInputBindings.js similarity index 87% rename from packages/engine/Source/Scene/Controllers/InputBinding.js rename to packages/engine/Source/Scene/Controllers/ScreenspaceInputBindings.js index 643c066ff402..78dfe5871266 100644 --- a/packages/engine/Source/Scene/Controllers/InputBinding.js +++ b/packages/engine/Source/Scene/Controllers/ScreenspaceInputBindings.js @@ -1,40 +1,25 @@ -// @ts-check - import Check from "../../Core/Check.js"; import defined from "../../Core/defined.js"; import ScreenSpaceEventType from "../../Core/ScreenSpaceEventType.js"; import MouseButton from "./MouseButton.js"; /** - * @ignore - * @typedef {object} ScreenSpaceEventHandler - * @property {function} setInputAction - */ - -/** - * @ignore - * @typedef {number} ScreenSpaceEventType - */ - -/** - * @ignore - * @typedef {number} MouseButton - */ - -/** - * @typedef {object} ScreenspaceInputBinding + * @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} move Called on drag move. */ /** + * @private * @param {MouseButton} button The mouse button. * @returns {ScreenSpaceEventType|undefined} The corresponding down event type. */ @@ -55,6 +40,7 @@ function getDownEventType(button) { } /** + * @private * @param {MouseButton} button The mouse button. * @returns {ScreenSpaceEventType|undefined} The corresponding down event type. */ @@ -74,11 +60,15 @@ function getUpEventType(button) { return undefined; } -export default class InputBinding { +/** + * @namespace + * @alias ScreenspaceInputBindings + */ +class ScreenspaceInputBindings { /** * Registers drag input bindings on a screen space event handler. * @param {ScreenSpaceEventHandler} handler The screen space event handler. - * @param {ScreenspaceInputBinding[]} inputBindings The drag bindings to register. + * @param {InputBinding[]} inputBindings The drag bindings to register. * @param {DragInputActions} dragInputActions The callbacks to invoke for drag actions. */ static registerDragInputBindings(handler, inputBindings, dragInputActions) { @@ -113,7 +103,6 @@ export default class InputBinding { moveModifiers.add(binding.modifier); } - for (const modifier of moveModifiers) { handler.setInputAction( dragInputActions.move, @@ -123,3 +112,5 @@ export default class InputBinding { } } } + +export default ScreenspaceInputBindings; diff --git a/packages/engine/Source/Scene/Controllers/ScreenspaceMapCameraController.js b/packages/engine/Source/Scene/Controllers/ScreenspaceMapCameraController.js index b2b5cb7b0cec..118fd51d4817 100644 --- a/packages/engine/Source/Scene/Controllers/ScreenspaceMapCameraController.js +++ b/packages/engine/Source/Scene/Controllers/ScreenspaceMapCameraController.js @@ -1,27 +1,57 @@ -// @ts-check -/** @import Controller from './Controller.js'; */ -/** @import { ScreenspaceInputBinding } from './InputBinding.js'; */ - 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 InputBinding from "./InputBinding.js"; +import InputBinding from "./ScreenspaceInputBindings.js"; import MouseButton from "./MouseButton.js"; +/** + * @typedef {object} ControllerOptions + * @memberOf ScreenspaceMapCameraController + * @property {ScreenspaceInputBinding[]} [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 + * @alias ScreenspaceMapCameraController * @implements Controller * @example - * const mapCameraController = new ScreenspaceMapCameraController(); + * 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); */ -export default class ScreenspaceMapCameraController { - constructor() { +class ScreenspaceMapCameraController { + /** + * @private + * @returns {ScreenspaceInputBinding[]} The default drag input bindings. + */ + static _getDefaultDragInputs() { + return [ + Object.freeze({ + button: MouseButton.LEFT, + }), + ]; + } + + /** + * Creates an instance of a ScreenspaceMapCameraController. + * @param {ControllerOptions} [options] The options for configuring the controller. + */ + constructor(options = Frozen.EMPTY_OBJECT) { this._enabled = true; this._handler = undefined; this._lastUpdateTime = undefined; @@ -32,11 +62,9 @@ export default class ScreenspaceMapCameraController { * @type {ScreenspaceInputBinding[]} * @see ScreenSpaceEventHandler */ - this.dragBindings = [ - { - button: MouseButton.LEFT, - }, - ]; + this.dragInputs = + options.dragInputs ?? + ScreenspaceMapCameraController._getDefaultDragInputs(); this._isPanning = false; this._panDelta = new Cartesian2(); @@ -98,7 +126,7 @@ export default class ScreenspaceMapCameraController { const handler = new ScreenSpaceEventHandler(element); this._handler = handler; - InputBinding.registerDragInputBindings(handler, this.dragBindings, { + InputBinding.registerDragInputBindings(handler, this.dragInputs, { start: this._handleStartPan.bind(this), end: this._handleStopPan.bind(this), move: this._handlePan.bind(this), @@ -135,7 +163,7 @@ export default class ScreenspaceMapCameraController { TimeConstants.SECONDS_PER_MILLISECOND; const { camera, ellipsoid, canvas } = scene; - const { clientWidth, clientHeight } = canvas; + const { clientWidth, clientHeight } = canvas; if (dt === 0 || clientWidth === 0 || clientHeight === 0) { // Reset for next frame this._lastUpdateTime = getTimestamp(); @@ -196,16 +224,17 @@ export default class ScreenspaceMapCameraController { dy = this._panVelocity.y * dt; } - const maxPixels = this.maximumMovementRatio * Math.max(clientWidth, clientHeight); - + 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); @@ -233,7 +262,7 @@ export default class ScreenspaceMapCameraController { this._isPanning = false; } - /** + /** * @typedef {object} DragEvent * @property {Cartesian2} startPosition The position of the mouse when the drag started. * @property {Cartesian2} endPosition The position of the mouse when the drag ended. @@ -254,3 +283,5 @@ export default class ScreenspaceMapCameraController { this._panPosition.y = event.endPosition.y; } } + +export default ScreenspaceMapCameraController; diff --git a/packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js b/packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js index ba9310365f7a..e38ca6a8256a 100644 --- a/packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js +++ b/packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js @@ -3,6 +3,7 @@ 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"; @@ -12,22 +13,57 @@ 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 InputBinding from "./InputBinding.js"; +import InputBinding from "./ScreenspaceInputBindings.js"; import MouseButton from "./MouseButton.js"; +/** + * @typedef {object} ControllerOptions + * @memberOf ScreenspaceTiltOrbitCameraController + * @property {ScreenspaceInputBinding[]} [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 + * @alias ScreenspaceTiltOrbitCameraController * @implements Controller + * @example + * viewer.scene.screenSpaceCameraController.enableInputs = false; + * viewer.scene.screenSpaceCameraController.enableCollisionDetection = false; + * + * const tiltOrbitController = new Cesium.ScreenspaceTiltOrbitCameraController(); + * 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); */ -export default class ScreenspaceTiltOrbitCameraController { +class ScreenspaceTiltOrbitCameraController { /** - * Creates a new ScreenspaceTiltOrbitCameraController instance. + * @private + * @returns {ScreenspaceInputBinding[]} 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 ScreenspaceTiltOrbitCameraController. + * @param {ControllerOptions} [options] The options for configuring the controller. * @constructor - * @example - * const tiltOrbitController = new Cesium.ScreenspaceTiltOrbitCameraController(); - * viewer.addController(tiltOrbitController); */ - constructor() { + constructor(options = Frozen.EMPTY_OBJECT) { this._enabled = true; this._handler = undefined; this._lastUpdateTime = undefined; @@ -52,15 +88,9 @@ export default class ScreenspaceTiltOrbitCameraController { * @type {ScreenspaceInputBinding[]} * @see ScreenSpaceEventHandler */ - this.dragBindings = [ - { - button: MouseButton.LEFT, - modifier: KeyboardEventModifier.CTRL, - }, - { - button: MouseButton.RIGHT, - }, - ]; + this.dragInputs = + options.dragInputs ?? + ScreenspaceTiltOrbitCameraController._getDefaultDragInputs(); this._isDragging = false; this._dragDelta = new Cartesian2(); @@ -186,7 +216,7 @@ export default class ScreenspaceTiltOrbitCameraController { const handler = new ScreenSpaceEventHandler(element); this._handler = handler; - InputBinding.registerDragInputBindings(handler, this.dragBindings, { + InputBinding.registerDragInputBindings(handler, this.dragInputs, { start: this._handleStartDrag.bind(this), end: this._handleStopDrag.bind(this), move: this._handleDrag.bind(this), @@ -354,7 +384,7 @@ export default class ScreenspaceTiltOrbitCameraController { ); const currentOrbitAngle = Cartesian3.angleBetween(offset, east); - if (Math.abs(this.orbitVelocity) < this.minimumOrbitVelocity) { + if (Math.abs(this.orbitVelocity) < this.minimumOrbitVelocity) { this.orbitVelocity = 0.0; } @@ -527,3 +557,5 @@ export default class ScreenspaceTiltOrbitCameraController { this._dragDelta.y = 0; } } + +export default ScreenspaceTiltOrbitCameraController; diff --git a/packages/engine/Source/Scene/Scene.js b/packages/engine/Source/Scene/Scene.js index 66b56561ecaa..d23ebe97077f 100644 --- a/packages/engine/Source/Scene/Scene.js +++ b/packages/engine/Source/Scene/Scene.js @@ -1083,12 +1083,22 @@ Object.defineProperties(Scene.prototype, { }, /** - * TODO + * Collects an array of Controller objects that can be registered with the scene to handle input events, camera animations, and other interactions. + * @see {@link Controller} + * @type {ControllerHost} + * @memberof Scene.prototype + * @readonly + * @example + * scene.screenSpaceCameraController.enableInputs = false; + * scene.screenSpaceCameraController.enableCollisionDetection = false; + * + * const tiltOrbitController = new Cesium.ScreenspaceTiltOrbitCameraController(); + * scene.controllerHost.registerController(tiltOrbitController, scene.canvas.parentNode); */ controllerHost: { - get: function() { + get: function () { return this._controllerHost; - } + }, }, /** diff --git a/packages/engine/Source/Widget/CesiumWidget.js b/packages/engine/Source/Widget/CesiumWidget.js index 564003165a25..929e8282a6d7 100644 --- a/packages/engine/Source/Widget/CesiumWidget.js +++ b/packages/engine/Source/Widget/CesiumWidget.js @@ -1228,18 +1228,34 @@ CesiumWidget.prototype._onDataSourceRemoved = function ( }; /** - * TODO + * Adds a controller— an implementation of the {@link Controller} interface used to handle input events, camera animations, and other interactions— to the widget's scene. + * @param {Controller} controller An implementation of the Controller interface. + * @example + * widget.scene.screenSpaceCameraController.enableInputs = false; + * widget.scene.screenSpaceCameraController.enableCollisionDetection = false; + * + * const tiltOrbitController = new Cesium.ScreenspaceTiltOrbitCameraController(); + * widget.addController(tiltOrbitController); */ CesiumWidget.prototype.addController = function (controller) { - return this.scene.controllerHost.registerController(controller, this.container); -} + return this.scene.controllerHost.registerController( + controller, + this.container, + ); +}; /** - * TODO + * Removes a controller— an implementation of the {@link Controller} interface used to handle input events, camera animations, and other interactions— from the widget's scene. + * @param {Controller} controller An implementation of the Controller interface. + * @example + * widget.removeController(tiltOrbitController); */ CesiumWidget.prototype.removeController = function (controller) { - return this.scene.controllerHost.unregisterController(controller, this.container); -} + return this.scene.controllerHost.unregisterController( + controller, + this.container, + ); +}; /** * Asynchronously sets the camera to view the provided entity, entities, or data source. diff --git a/packages/sandcastle/gallery/camera-controllers/index.html b/packages/sandcastle/gallery/camera-controllers/index.html index 923148147ef0..776ff57f4a5a 100644 --- a/packages/sandcastle/gallery/camera-controllers/index.html +++ b/packages/sandcastle/gallery/camera-controllers/index.html @@ -4,5 +4,21 @@

Loading...

- TODO: Note controller inputs + + + + + + + + + + + + + + + + +
Camera function
Pan
Tilt
Orbit
Zoom
diff --git a/packages/sandcastle/gallery/camera-controllers/main.js b/packages/sandcastle/gallery/camera-controllers/main.js index da3fbf29ed95..0626bc58edc6 100644 --- a/packages/sandcastle/gallery/camera-controllers/main.js +++ b/packages/sandcastle/gallery/camera-controllers/main.js @@ -3,26 +3,37 @@ import * as Cesium from "cesium"; const viewer = new Cesium.Viewer("cesiumContainer"); const scene = viewer.scene; -scene.camera.flyTo({ - duration: 0, - destination: Cesium.Rectangle.fromDegrees( - // Philly - -75.280266, - 39.867004, - -74.955763, - 40.137992, - ), -}); - +// Disable the default camera controls scene.screenSpaceCameraController.enableInputs = false; scene.screenSpaceCameraController.enableCollisionDetection = false; -// TODO: Zoom controller - +// Set up the modular camera controllers const panController = new Cesium.HybridScreenspacePanCameraController(); viewer.addController(panController); const tiltController = new Cesium.ScreenspaceTiltOrbitCameraController(); viewer.addController(tiltController); -// TODO: Instructions panel \ No newline at end of file +const zoomController = new Cesium.ScreenspaceZoomCameraController(); +viewer.addController(zoomController); + +// Load a 3D Tiles power plant asset +try { + const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(2464651); + scene.primitives.add(tileset); + + viewer.clock.currentTime = Cesium.JulianDate.fromIso8601( + "2022-08-01T00:00:00Z", + ); + + viewer.zoomTo( + tileset, + new Cesium.HeadingPitchRange( + 0.5, + -0.2, + tileset.boundingSphere.radius * 4.0, + ), + ); +} catch (error) { + console.log(`Error loading tileset: ${error}`); +} diff --git a/packages/sandcastle/gallery/camera-controllers/sandcastle.yaml b/packages/sandcastle/gallery/camera-controllers/sandcastle.yaml index 93ebc2e8f38f..b0b970fb79f3 100644 --- a/packages/sandcastle/gallery/camera-controllers/sandcastle.yaml +++ b/packages/sandcastle/gallery/camera-controllers/sandcastle.yaml @@ -1,5 +1,5 @@ title: Camera Controllers for Asset Inspection -description: TODO +description: Configure modular camera controllers (pan, orbit, tilt, and zoom) to inspect 3D assets in a scene. This example includes contextual panning that allows the camera to move either horizontally or vertically based on the camera's orientation. labels: - Camera thumbnail: thumbnail.jpg diff --git a/packages/widgets/Source/Viewer/Viewer.js b/packages/widgets/Source/Viewer/Viewer.js index f61b31a261f3..8322e773b4ee 100644 --- a/packages/widgets/Source/Viewer/Viewer.js +++ b/packages/widgets/Source/Viewer/Viewer.js @@ -1974,18 +1974,28 @@ Viewer.prototype._onDataSourceRemoved = function ( }; /** - * TODO + * Adds a controller— an implementation of the {@link Controller} interface used to handle input events, camera animations, and other interactions— to the viewer's scene. + * @param {Controller} controller An implementation of the Controller interface. + * @example + * viewer.scene.screenSpaceCameraController.enableInputs = false; + * viewer.scene.screenSpaceCameraController.enableCollisionDetection = false; + * + * const tiltOrbitController = new Cesium.ScreenspaceTiltOrbitCameraController(); + * viewer.addController(tiltOrbitController); */ Viewer.prototype.addController = function (controller) { return this._cesiumWidget.addController(controller); -} +}; /** - * TODO + * Removes a controller— an implementation of the {@link Controller} interface used to handle input events, camera animations, and other interactions— from the viewer's scene. + * @param {Controller} controller An implementation of the Controller interface. + * @example + * viewer.removeController(tiltOrbitController); */ Viewer.prototype.removeController = function (controller) { return this._cesiumWidget.removeController(controller); -} +}; /** * Asynchronously sets the camera to view the provided entity, entities, or data source. From c322b638b30542505952513a23262929be45339c Mon Sep 17 00:00:00 2001 From: ggetz Date: Tue, 30 Jun 2026 13:30:01 -0400 Subject: [PATCH 03/10] ts fixes --- .../HybridScreenspacePanCameraController.js | 4 ++- .../ScreenspaceElevatorCameraController.js | 27 +++++++++------- .../ScreenspaceMapCameraController.js | 27 +++++++++------- .../ScreenspaceTiltOrbitCameraController.js | 32 +++++++++++-------- .../gallery/camera-controllers/main.js | 4 +-- 5 files changed, 56 insertions(+), 38 deletions(-) diff --git a/packages/engine/Source/Scene/Controllers/HybridScreenspacePanCameraController.js b/packages/engine/Source/Scene/Controllers/HybridScreenspacePanCameraController.js index 5573abfe76fb..e579f920eebc 100644 --- a/packages/engine/Source/Scene/Controllers/HybridScreenspacePanCameraController.js +++ b/packages/engine/Source/Scene/Controllers/HybridScreenspacePanCameraController.js @@ -24,8 +24,10 @@ class HybridScreenspacePanCameraController { /** * 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(180 - 55); + this.angleThreshold = CesiumMath.toRadians(125); } get enabled() { diff --git a/packages/engine/Source/Scene/Controllers/ScreenspaceElevatorCameraController.js b/packages/engine/Source/Scene/Controllers/ScreenspaceElevatorCameraController.js index 711123f6f68f..dfd86065d10e 100644 --- a/packages/engine/Source/Scene/Controllers/ScreenspaceElevatorCameraController.js +++ b/packages/engine/Source/Scene/Controllers/ScreenspaceElevatorCameraController.js @@ -6,13 +6,13 @@ 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 InputBinding from "./ScreenspaceInputBindings.js"; +import ScreenspaceInputBindings from "./ScreenspaceInputBindings.js"; import MouseButton from "./MouseButton.js"; /** * @typedef {object} ControllerOptions - * @memberOf ScreenspaceElevatorCameraController - * @property {ScreenspaceInputBinding[]} [dragInputs] The drag input bindings that control panning. + * @memberof ScreenspaceElevatorCameraController + * @property {ScreenspaceInputBindings.InputBinding[]} [dragInputs] The drag input bindings that control panning. */ /** @@ -38,7 +38,7 @@ import MouseButton from "./MouseButton.js"; class ScreenspaceElevatorCameraController { /** * @private - * @returns {ScreenspaceInputBinding[]} The default drag input bindings. + * @returns {ScreenspaceInputBindings.InputBinding[]} The default drag input bindings. */ static _getDefaultDragInputs() { return [ @@ -50,7 +50,7 @@ class ScreenspaceElevatorCameraController { /** * Creates an instance of a ScreenspaceElevatorCameraController. - * @param {ControllerOptions} [options] The options for configuring the controller. + * @param {ScreenspaceElevatorCameraController.ControllerOptions} [options] The options for configuring the controller. */ constructor(options = Frozen.EMPTY_OBJECT) { this._enabled = true; @@ -60,7 +60,7 @@ class ScreenspaceElevatorCameraController { /** * The drag input bindings that control vertical panning. Each binding is a combination of the mouse button * and an optional keyboard modifier. - * @type {ScreenspaceInputBinding[]} + * @type {ScreenspaceInputBindings.InputBinding[]} * @see ScreenSpaceEventHandler */ this.dragInputs = @@ -128,11 +128,15 @@ class ScreenspaceElevatorCameraController { const handler = new ScreenSpaceEventHandler(element); this._handler = handler; - InputBinding.registerDragInputBindings(handler, this.dragInputs, { - start: this._handleStartPan.bind(this), - end: this._handleStopPan.bind(this), - move: this._handlePan.bind(this), - }); + ScreenspaceInputBindings.registerDragInputBindings( + handler, + this.dragInputs, + { + start: this._handleStartPan.bind(this), + end: this._handleStopPan.bind(this), + move: this._handlePan.bind(this), + }, + ); } /** @@ -248,6 +252,7 @@ class ScreenspaceElevatorCameraController { /** * @typedef {object} DragEvent + * @memberof ScreenspaceElevatorCameraController * @property {Cartesian2} startPosition The position of the mouse when the drag started. * @property {Cartesian2} endPosition The position of the mouse when the drag ended. */ diff --git a/packages/engine/Source/Scene/Controllers/ScreenspaceMapCameraController.js b/packages/engine/Source/Scene/Controllers/ScreenspaceMapCameraController.js index 118fd51d4817..98f6b3ea8c3c 100644 --- a/packages/engine/Source/Scene/Controllers/ScreenspaceMapCameraController.js +++ b/packages/engine/Source/Scene/Controllers/ScreenspaceMapCameraController.js @@ -6,13 +6,13 @@ 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 InputBinding from "./ScreenspaceInputBindings.js"; +import ScreenspaceInputBindings from "./ScreenspaceInputBindings.js"; import MouseButton from "./MouseButton.js"; /** * @typedef {object} ControllerOptions - * @memberOf ScreenspaceMapCameraController - * @property {ScreenspaceInputBinding[]} [dragInputs] The drag input bindings that control panning. + * @memberof ScreenspaceMapCameraController + * @property {ScreenspaceInputBindings.InputBinding[]} [dragInputs] The drag input bindings that control panning. */ /** @@ -37,7 +37,7 @@ import MouseButton from "./MouseButton.js"; class ScreenspaceMapCameraController { /** * @private - * @returns {ScreenspaceInputBinding[]} The default drag input bindings. + * @returns {ScreenspaceInputBindings.InputBinding[]} The default drag input bindings. */ static _getDefaultDragInputs() { return [ @@ -49,7 +49,7 @@ class ScreenspaceMapCameraController { /** * Creates an instance of a ScreenspaceMapCameraController. - * @param {ControllerOptions} [options] The options for configuring the controller. + * @param {ScreenspaceMapCameraController.ControllerOptions} [options] The options for configuring the controller. */ constructor(options = Frozen.EMPTY_OBJECT) { this._enabled = true; @@ -59,7 +59,7 @@ class ScreenspaceMapCameraController { /** * The drag input bindings that map panning. Each binding is a combination of the mouse button * and an optional keyboard modifier. - * @type {ScreenspaceInputBinding[]} + * @type {ScreenspaceInputBindings.InputBinding[]} * @see ScreenSpaceEventHandler */ this.dragInputs = @@ -126,11 +126,15 @@ class ScreenspaceMapCameraController { const handler = new ScreenSpaceEventHandler(element); this._handler = handler; - InputBinding.registerDragInputBindings(handler, this.dragInputs, { - start: this._handleStartPan.bind(this), - end: this._handleStopPan.bind(this), - move: this._handlePan.bind(this), - }); + ScreenspaceInputBindings.registerDragInputBindings( + handler, + this.dragInputs, + { + start: this._handleStartPan.bind(this), + end: this._handleStopPan.bind(this), + move: this._handlePan.bind(this), + }, + ); } /** @@ -264,6 +268,7 @@ class ScreenspaceMapCameraController { /** * @typedef {object} DragEvent + * @memberof ScreenspaceMapCameraController * @property {Cartesian2} startPosition The position of the mouse when the drag started. * @property {Cartesian2} endPosition The position of the mouse when the drag ended. */ diff --git a/packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js b/packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js index e38ca6a8256a..14c56eb485c3 100644 --- a/packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js +++ b/packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js @@ -13,13 +13,13 @@ 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 InputBinding from "./ScreenspaceInputBindings.js"; +import ScreenspaceInputBindings from "./ScreenspaceInputBindings.js"; import MouseButton from "./MouseButton.js"; /** * @typedef {object} ControllerOptions - * @memberOf ScreenspaceTiltOrbitCameraController - * @property {ScreenspaceInputBinding[]} [dragInputs] The drag input bindings that control tilting and orbiting. + * @memberof ScreenspaceTiltOrbitCameraController + * @property {ScreenspaceInputBindings.InputBinding[]} [dragInputs] The drag input bindings that control tilting and orbiting. */ /** @@ -44,7 +44,7 @@ import MouseButton from "./MouseButton.js"; class ScreenspaceTiltOrbitCameraController { /** * @private - * @returns {ScreenspaceInputBinding[]} The default drag input bindings. + * @returns {ScreenspaceInputBindings.InputBinding[]} The default drag input bindings. */ static _getDefaultDragInputs() { return [ @@ -60,7 +60,7 @@ class ScreenspaceTiltOrbitCameraController { /** * Creates a new instance of ScreenspaceTiltOrbitCameraController. - * @param {ControllerOptions} [options] The options for configuring the controller. + * @param {ScreenspaceTiltOrbitCameraController.ControllerOptions} [options] The options for configuring the controller. * @constructor */ constructor(options = Frozen.EMPTY_OBJECT) { @@ -68,6 +68,8 @@ class ScreenspaceTiltOrbitCameraController { this._handler = undefined; this._lastUpdateTime = undefined; + // TODO: Option to use center of screen or cursor position for tilt/orbit origin + /** * Enabled dragging to tilt the camera. * @type {boolean} @@ -85,7 +87,7 @@ class ScreenspaceTiltOrbitCameraController { /** * The drag input bindings that control tilting. Each binding is a combination of the mouse button * and an optional keyboard modifier. - * @type {ScreenspaceInputBinding[]} + * @type {ScreenspaceInputBindings.InputBinding[]} * @see ScreenSpaceEventHandler */ this.dragInputs = @@ -216,11 +218,15 @@ class ScreenspaceTiltOrbitCameraController { const handler = new ScreenSpaceEventHandler(element); this._handler = handler; - InputBinding.registerDragInputBindings(handler, this.dragInputs, { - start: this._handleStartDrag.bind(this), - end: this._handleStopDrag.bind(this), - move: this._handleDrag.bind(this), - }); + ScreenspaceInputBindings.registerDragInputBindings( + handler, + this.dragInputs, + { + start: this._handleStartDrag.bind(this), + end: this._handleStopDrag.bind(this), + move: this._handleDrag.bind(this), + }, + ); } /** @@ -245,6 +251,7 @@ class ScreenspaceTiltOrbitCameraController { /** * @typedef {object} StartDragEvent + * @memberof ScreenspaceTiltOrbitCameraController * @property {Cartesian2} position The position of the mouse when the drag started. */ @@ -270,6 +277,7 @@ class ScreenspaceTiltOrbitCameraController { /** * @typedef {object} DragEvent + * @memberOf ScreenspaceTiltOrbitCameraController * @property {Cartesian2} startPosition The position of the mouse when the drag started. * @property {Cartesian2} endPosition The position of the mouse when the drag ended. */ @@ -341,8 +349,6 @@ class ScreenspaceTiltOrbitCameraController { this._orbitDampenedResults.velocity = value; } - /** @typedef {any} Camera */ - /** * 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. diff --git a/packages/sandcastle/gallery/camera-controllers/main.js b/packages/sandcastle/gallery/camera-controllers/main.js index 0626bc58edc6..5638f6396fd1 100644 --- a/packages/sandcastle/gallery/camera-controllers/main.js +++ b/packages/sandcastle/gallery/camera-controllers/main.js @@ -14,8 +14,8 @@ viewer.addController(panController); const tiltController = new Cesium.ScreenspaceTiltOrbitCameraController(); viewer.addController(tiltController); -const zoomController = new Cesium.ScreenspaceZoomCameraController(); -viewer.addController(zoomController); +// const zoomController = new Cesium.ScreenspaceZoomCameraController(); +// viewer.addController(zoomController); // Load a 3D Tiles power plant asset try { From 70c4764475863f33cd0244128ec526865a7b2070 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Wed, 1 Jul 2026 09:39:59 -0400 Subject: [PATCH 04/10] Update packages/engine/Source/Scene/Controllers/Controller.js Co-authored-by: Marco Hutter --- packages/engine/Source/Scene/Controllers/Controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/engine/Source/Scene/Controllers/Controller.js b/packages/engine/Source/Scene/Controllers/Controller.js index 69c8f4b67eaf..26c6892c8177 100644 --- a/packages/engine/Source/Scene/Controllers/Controller.js +++ b/packages/engine/Source/Scene/Controllers/Controller.js @@ -50,7 +50,7 @@ class Controller { } /** - * Invoked when the controller is being updated the first time, immediately before update is called. Implement firstUpdate to perform one-time work after the relevant scene has begun it's render loop. Some examples might include initializing simulation time values or adding a primitive to the scene. + * Invoked when the controller is being updated the first time, immediately before update is called. Implement firstUpdate 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. From 6c2384fcc2fb730bbccddc2126d88bddd3d49fa9 Mon Sep 17 00:00:00 2001 From: Gabby Getz Date: Wed, 1 Jul 2026 09:40:07 -0400 Subject: [PATCH 05/10] Update packages/engine/Source/Scene/Controllers/ControllerHost.js Co-authored-by: Marco Hutter --- packages/engine/Source/Scene/Controllers/ControllerHost.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/engine/Source/Scene/Controllers/ControllerHost.js b/packages/engine/Source/Scene/Controllers/ControllerHost.js index 5562126188a8..aed2140c75f6 100644 --- a/packages/engine/Source/Scene/Controllers/ControllerHost.js +++ b/packages/engine/Source/Scene/Controllers/ControllerHost.js @@ -33,7 +33,7 @@ class ControllerHost { * 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 0 would mean the new controller would apply it's 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., it's updates are applied after all other controllers. + * @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 0 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; From 2c6652b3bedd7373fc86f1196f7a341c563517d9 Mon Sep 17 00:00:00 2001 From: ggetz Date: Tue, 30 Jun 2026 15:51:09 -0400 Subject: [PATCH 06/10] Add zoom camera controller --- .../ScreenspaceZoomCameraController.js | 192 ++++++++++++++++++ .../gallery/camera-controllers/main.js | 4 +- 2 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 packages/engine/Source/Scene/Controllers/ScreenspaceZoomCameraController.js diff --git a/packages/engine/Source/Scene/Controllers/ScreenspaceZoomCameraController.js b/packages/engine/Source/Scene/Controllers/ScreenspaceZoomCameraController.js new file mode 100644 index 000000000000..55ff4dbbe5fd --- /dev/null +++ b/packages/engine/Source/Scene/Controllers/ScreenspaceZoomCameraController.js @@ -0,0 +1,192 @@ +// @ts-check +/** @import Controller from './Controller.js'; */ + +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 TimeConstants from "../../Core/TimeConstants.js"; + +/** + * @typedef {object} ControllerOptions + * @memberof ScreenspaceZoomCameraController + */ + + +/** + * A camera controller that allows zooming the camera in and out based on the pointer location in screen space. + * @class + * @alias ScreenspaceZoomCameraController + * @implements Controller + * @example + * TODO + */ +class ScreenspaceZoomCameraController { + /** + * Creates a new instance of ScreenspaceTiltOrbitCameraController. + * @param {ScreenspaceZoomCameraController.ControllerOptions} [options] The options for configuring the controller. + * @constructor + */ + constructor(options = Frozen.EMPTY_OBJECT) { + this._enabled = true; + this._handler = undefined; + this._lastUpdateTime = undefined; + + // TODO: Input maps + + /** + * The rate at which the camera zooms in and out based on the mouse wheel delta. + * @type {number} + * @default 0.1 + */ + this.zoomSensitivity = 0.1; + + /** + * 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.2 + */ + this.zoomDistanceRatio = 0.2; + + /** + * The rate at which the camera's zoom velocity decays over time. + * @type {number} + * @default 6.0 + */ + this.inertiaDecay = 6.0; + + // TODO: Maximum distance + + + this._zoomDelta = 0.0; + this._zoomPosition = new Cartesian2(); + this._target = new Cartesian3(); + this._zoomVelocity = 0.0; + } + + /** + * @inheritdoc + */ + get enabled() { + return this._enabled; + } + set enabled(value) { + this._enabled = value; + + if (value) { + this._lastUpdateTime = getTimestamp(); + } + } + + /** + * @inheritdoc + * @param {HTMLElement} element The DOM element containing the Cesium scene. + */ + connectedCallback(element) { + const handler = new ScreenSpaceEventHandler(element); + this._handler = handler; + + handler.setInputAction( + this._handleZoom.bind(this), + ScreenSpaceEventType.WHEEL, + ); + + handler.setInputAction( + this._handleZoomPosition.bind(this), + ScreenSpaceEventType.MOUSE_MOVE, + ); + } + + /** + * @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._zoomDelta = 0.0; + } + + /** + * @inheritdoc + * @param {Scene} scene + */ + update(scene) { + const now = getTimestamp(); + const dt = + (now - this._lastUpdateTime) * TimeConstants.SECONDS_PER_MILLISECOND; + + //let zoomDelta = this._zoomDelta * dt; + + //this.zoomVelocity *= Math.exp(-this.inertiaDecay * dt); + //zoomDelta += this.zoomVelocity * dt; + + if (dt === 0 || Cartesian2.magnitude(this._zoomPosition) <= 0.0) { + this._lastUpdateTime = getTimestamp(); + this._zoomDelta = 0.0; + return; + } + + const { camera, ellipsoid } = scene; + const target = camera.pickEllipsoid( + this._zoomPosition, + ellipsoid, + this._target, + ); + let distance = + Cartesian3.magnitude(camera.positionWC) - ellipsoid.maximumRadius; + + if (defined(target)) { + distance = Cartesian3.distance(camera.positionWC, target); + } + + const zoom = this._zoomDelta * distance * this.zoomDistanceRatio; + this._zoomVelocity = zoom / dt; + + // TODO: To target + camera.move(camera.direction, zoom); + + // Reset for next frame + this._lastUpdateTime = getTimestamp(); + this._zoomDelta = 0.0; + } + + /** + * @private + * @param {number} amount + */ + _handleZoom(amount) { + this._zoomDelta += amount * this.zoomSensitivity; + } + + /** + * @typedef {object} DragEvent + * @memberof ScreenspaceZoomCameraController + * @property {Cartesian2} startPosition The position of the mouse when the drag started. + * @property {Cartesian2} endPosition The position of the mouse when the drag ended. + */ + + /** + * @param {DragEvent} event + * @private + */ + _handleZoomPosition(event) { + this._zoomPosition.x = event.endPosition.x; + this._zoomPosition.y = event.endPosition.y; + } +} + +export default ScreenspaceZoomCameraController; diff --git a/packages/sandcastle/gallery/camera-controllers/main.js b/packages/sandcastle/gallery/camera-controllers/main.js index 5638f6396fd1..0626bc58edc6 100644 --- a/packages/sandcastle/gallery/camera-controllers/main.js +++ b/packages/sandcastle/gallery/camera-controllers/main.js @@ -14,8 +14,8 @@ viewer.addController(panController); const tiltController = new Cesium.ScreenspaceTiltOrbitCameraController(); viewer.addController(tiltController); -// const zoomController = new Cesium.ScreenspaceZoomCameraController(); -// viewer.addController(zoomController); +const zoomController = new Cesium.ScreenspaceZoomCameraController(); +viewer.addController(zoomController); // Load a 3D Tiles power plant asset try { From f3613037b00f7b5bb6a6aa4ff07588380dd93556 Mon Sep 17 00:00:00 2001 From: ggetz Date: Wed, 1 Jul 2026 11:08:13 -0400 Subject: [PATCH 07/10] Adjust zoom controller defaults --- .../ScreenspaceTiltOrbitCameraController.js | 11 ++++++-- .../ScreenspaceZoomCameraController.js | 26 +++++++------------ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js b/packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js index 14c56eb485c3..22c93969bb8d 100644 --- a/packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js +++ b/packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js @@ -68,8 +68,6 @@ class ScreenspaceTiltOrbitCameraController { this._handler = undefined; this._lastUpdateTime = undefined; - // TODO: Option to use center of screen or cursor position for tilt/orbit origin - /** * Enabled dragging to tilt the camera. * @type {boolean} @@ -102,6 +100,15 @@ class ScreenspaceTiltOrbitCameraController { this._target = new Cartesian3(); this._axis = new Cartesian3(); + /** + * Controls the screenspace position used to determine the location on the ellipsoid that the camera orbit and tilts around. + *

If true, the origin is preferred to be determined by the position at the center of the screen, and will fall back to the position under the mouse cursor when dragging starts. If false, the origin is always determined by the position under the mouse cursor when dragging starts.

+ *

Regardless of the preference, if the origin is not on the ellipsoid, no tilt or orbit is applied.

+ * @type {boolean} + * @default true + */ + this.targetScreenCenter = true; // TODO + /** * 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} diff --git a/packages/engine/Source/Scene/Controllers/ScreenspaceZoomCameraController.js b/packages/engine/Source/Scene/Controllers/ScreenspaceZoomCameraController.js index 55ff4dbbe5fd..73683def7ca0 100644 --- a/packages/engine/Source/Scene/Controllers/ScreenspaceZoomCameraController.js +++ b/packages/engine/Source/Scene/Controllers/ScreenspaceZoomCameraController.js @@ -1,12 +1,8 @@ -// @ts-check -/** @import Controller from './Controller.js'; */ - 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 TimeConstants from "../../Core/TimeConstants.js"; @@ -16,7 +12,6 @@ import TimeConstants from "../../Core/TimeConstants.js"; * @memberof ScreenspaceZoomCameraController */ - /** * A camera controller that allows zooming the camera in and out based on the pointer location in screen space. * @class @@ -26,8 +21,8 @@ import TimeConstants from "../../Core/TimeConstants.js"; * TODO */ class ScreenspaceZoomCameraController { - /** - * Creates a new instance of ScreenspaceTiltOrbitCameraController. + /** + * Creates a new instance of ScreenspaceZoomCameraController. * @param {ScreenspaceZoomCameraController.ControllerOptions} [options] The options for configuring the controller. * @constructor */ @@ -48,9 +43,9 @@ class ScreenspaceZoomCameraController { /** * 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.2 + * @default 0.06 */ - this.zoomDistanceRatio = 0.2; + this.zoomDistanceRatio = 0.06; /** * The rate at which the camera's zoom velocity decays over time. @@ -61,7 +56,6 @@ class ScreenspaceZoomCameraController { // TODO: Maximum distance - this._zoomDelta = 0.0; this._zoomPosition = new Cartesian2(); this._target = new Cartesian3(); @@ -129,17 +123,15 @@ class ScreenspaceZoomCameraController { const dt = (now - this._lastUpdateTime) * TimeConstants.SECONDS_PER_MILLISECOND; - //let zoomDelta = this._zoomDelta * dt; - - //this.zoomVelocity *= Math.exp(-this.inertiaDecay * dt); - //zoomDelta += this.zoomVelocity * dt; - if (dt === 0 || Cartesian2.magnitude(this._zoomPosition) <= 0.0) { this._lastUpdateTime = getTimestamp(); this._zoomDelta = 0.0; return; } + const dz = this._zoomDelta; + // TODO: Inertia + const { camera, ellipsoid } = scene; const target = camera.pickEllipsoid( this._zoomPosition, @@ -153,10 +145,10 @@ class ScreenspaceZoomCameraController { distance = Cartesian3.distance(camera.positionWC, target); } - const zoom = this._zoomDelta * distance * this.zoomDistanceRatio; + const zoom = dz * distance * this.zoomDistanceRatio; this._zoomVelocity = zoom / dt; - // TODO: To target + // TODO: To target, not center of screen camera.move(camera.direction, zoom); // Reset for next frame From 5c85da0ce7c02fa94a6778e6849681bbf1d8c138 Mon Sep 17 00:00:00 2001 From: ggetz Date: Wed, 1 Jul 2026 11:08:50 -0400 Subject: [PATCH 08/10] Prettier --- packages/engine/Source/Scene/Camera.js | 16 +++++++++++--- packages/engine/Specs/Scene/CameraSpec.js | 27 ++++++++++++++++++----- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/engine/Source/Scene/Camera.js b/packages/engine/Source/Scene/Camera.js index d5e6f22fc19a..1ec683b58aba 100644 --- a/packages/engine/Source/Scene/Camera.js +++ b/packages/engine/Source/Scene/Camera.js @@ -2511,16 +2511,26 @@ Camera.prototype.lookAtWorldPosition = function ( Check.typeOf.object("ellipsoid", ellipsoid); //>>includeEnd('debug'); - const transform = Matrix4.clone(this._transform, scratchLookAtWorldPositionTransform); + const transform = Matrix4.clone( + this._transform, + scratchLookAtWorldPositionTransform, + ); this._setTransform(Matrix4.IDENTITY); // Get direction to look at target - let direction = Cartesian3.subtract(target, this.positionWC, scratchLookAtWorldPositionDirection); + let direction = Cartesian3.subtract( + target, + this.positionWC, + scratchLookAtWorldPositionDirection, + ); // If the camera is at the target position, we can't look at it, but we should still continue to re-orient the camera to the world up vector at the target position. if (Cartesian3.magnitudeSquared(direction) < CesiumMath.EPSILON8) { - direction = Cartesian3.clone(this.directionWC, scratchLookAtWorldPositionDirection); + direction = Cartesian3.clone( + this.directionWC, + scratchLookAtWorldPositionDirection, + ); } direction = Cartesian3.normalize(direction, this.direction); diff --git a/packages/engine/Specs/Scene/CameraSpec.js b/packages/engine/Specs/Scene/CameraSpec.js index 0e5563e12a73..10ca587a5cdd 100644 --- a/packages/engine/Specs/Scene/CameraSpec.js +++ b/packages/engine/Specs/Scene/CameraSpec.js @@ -2199,7 +2199,11 @@ describe("Scene/Camera", function () { tempCamera.lookAtWorldPosition(target); - const expectedDirection = new Cartesian3(0.19881574, -0.74199046, 0.64025187); + const expectedDirection = new Cartesian3( + 0.19881574, + -0.74199046, + 0.64025187, + ); const expectedRight = new Cartesian3(-0.96592583, -0.25881905, 0.0); const expectedUp = new Cartesian3(-0.16570938, 0.61843582, 0.76816505); @@ -2207,7 +2211,10 @@ describe("Scene/Camera", function () { expectedDirection, CesiumMath.EPSILON8, ); - expect(tempCamera.rightWC).toEqualEpsilon(expectedRight, CesiumMath.EPSILON8); + expect(tempCamera.rightWC).toEqualEpsilon( + expectedRight, + CesiumMath.EPSILON8, + ); expect(tempCamera.upWC).toEqualEpsilon(expectedUp, CesiumMath.EPSILON8); }); @@ -2243,11 +2250,17 @@ describe("Scene/Camera", function () { it("lookAtWorldPosition when target equals camera position", function () { const tempCamera = Camera.clone(camera); const target = Cartesian3.clone(tempCamera.positionWC, new Cartesian3()); - const directionBefore = Cartesian3.clone(tempCamera.directionWC, new Cartesian3()); + const directionBefore = Cartesian3.clone( + tempCamera.directionWC, + new Cartesian3(), + ); tempCamera.lookAtWorldPosition(target); - expect(tempCamera.directionWC).toEqualEpsilon(directionBefore, CesiumMath.EPSILON12); + expect(tempCamera.directionWC).toEqualEpsilon( + directionBefore, + CesiumMath.EPSILON12, + ); expect(Cartesian3.magnitude(tempCamera.directionWC)).toEqualEpsilon( 1.0, CesiumMath.EPSILON12, @@ -2274,7 +2287,11 @@ describe("Scene/Camera", function () { const target = Cartesian3.fromDegrees(-75.1, 40.0, 150.0); tempCamera.lookAtWorldPosition(target, ellipsoid); - const expectedDirectionWC = new Cartesian3(0.19752041, -0.74233628, 0.64025193); + const expectedDirectionWC = new Cartesian3( + 0.19752041, + -0.74233628, + 0.64025193, + ); expect(tempCamera.directionWC).toEqualEpsilon( expectedDirectionWC, CesiumMath.EPSILON8, From e57380dc9a24462b6755f33742ffe98d1cb5c8aa Mon Sep 17 00:00:00 2001 From: ggetz Date: Mon, 20 Jul 2026 14:22:22 -0400 Subject: [PATCH 09/10] Screenspace -> ScreenSpace --- .../Source/Scene/Controllers/Controller.js | 8 ++--- ...> HybridScreenSpacePanCameraController.js} | 22 ++++++------- ...=> ScreenSpaceElevatorCameraController.js} | 30 ++++++++--------- ...indings.js => ScreenSpaceInputBindings.js} | 10 +++--- ...r.js => ScreenSpaceMapCameraController.js} | 30 ++++++++--------- ...> ScreenSpaceTiltOrbitCameraController.js} | 32 +++++++++---------- ....js => ScreenSpaceZoomCameraController.js} | 14 ++++---- packages/engine/Source/Scene/Scene.js | 2 +- packages/engine/Source/Widget/CesiumWidget.js | 2 +- .../gallery/camera-controllers/main.js | 6 ++-- packages/widgets/Source/Viewer/Viewer.js | 2 +- 11 files changed, 79 insertions(+), 79 deletions(-) rename packages/engine/Source/Scene/Controllers/{HybridScreenspacePanCameraController.js => HybridScreenSpacePanCameraController.js} (81%) rename packages/engine/Source/Scene/Controllers/{ScreenspaceElevatorCameraController.js => ScreenSpaceElevatorCameraController.js} (89%) rename packages/engine/Source/Scene/Controllers/{ScreenspaceInputBindings.js => ScreenSpaceInputBindings.js} (94%) rename packages/engine/Source/Scene/Controllers/{ScreenspaceMapCameraController.js => ScreenSpaceMapCameraController.js} (89%) rename packages/engine/Source/Scene/Controllers/{ScreenspaceTiltOrbitCameraController.js => ScreenSpaceTiltOrbitCameraController.js} (94%) rename packages/engine/Source/Scene/Controllers/{ScreenspaceZoomCameraController.js => ScreenSpaceZoomCameraController.js} (92%) diff --git a/packages/engine/Source/Scene/Controllers/Controller.js b/packages/engine/Source/Scene/Controllers/Controller.js index 26c6892c8177..72ae18165054 100644 --- a/packages/engine/Source/Scene/Controllers/Controller.js +++ b/packages/engine/Source/Scene/Controllers/Controller.js @@ -6,10 +6,10 @@ import DeveloperError from "../../Core/DeveloperError.js"; * interface and is not intended to be instantiated directly. * @class * @abstract - * @see {@link HybridScreenspacePanCameraController} - * @see {@link ScreenspaceElevatorCameraController} - * @see {@link ScreenspaceMapCameraController} - * @see {@link ScreenspaceTiltOrbitCameraController} + * @see {@link HybridScreenSpacePanCameraController} + * @see {@link ScreenSpaceElevatorCameraController} + * @see {@link ScreenSpaceMapCameraController} + * @see {@link ScreenSpaceTiltOrbitCameraController} */ class Controller { /** diff --git a/packages/engine/Source/Scene/Controllers/HybridScreenspacePanCameraController.js b/packages/engine/Source/Scene/Controllers/HybridScreenSpacePanCameraController.js similarity index 81% rename from packages/engine/Source/Scene/Controllers/HybridScreenspacePanCameraController.js rename to packages/engine/Source/Scene/Controllers/HybridScreenSpacePanCameraController.js index e579f920eebc..4971513113f7 100644 --- a/packages/engine/Source/Scene/Controllers/HybridScreenspacePanCameraController.js +++ b/packages/engine/Source/Scene/Controllers/HybridScreenSpacePanCameraController.js @@ -1,23 +1,23 @@ -import ScreenspaceElevatorCameraController from "./ScreenspaceElevatorCameraController.js"; -import ScreenspaceMapCameraController from "./ScreenspaceMapCameraController.js"; +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), ScreenspaceMapCameraController is used. - * If the camera is looking towards the horizon (beyond angleThreshold from nadir), the ScreenspaceElevatorCameraController is used. + * 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), ScreenSpaceMapCameraController is used. + * If the camera is looking towards the horizon (beyond angleThreshold from nadir), the ScreenSpaceElevatorCameraController is used. * @implements Controller * @example * viewer.scene.screenSpaceCameraController.enableInputs = false; * viewer.scene.screenSpaceCameraController.enableCollisionDetection = false; * - * const hybridController = new HybridScreenspacePanCameraController(); + * const hybridController = new HybridScreenSpacePanCameraController(); * viewer.addController(hybridController); */ -class HybridScreenspacePanCameraController { +class HybridScreenSpacePanCameraController { constructor() { - this._elevatorController = new ScreenspaceElevatorCameraController(); - this._mapController = new ScreenspaceMapCameraController(); + this._elevatorController = new ScreenSpaceElevatorCameraController(); + this._mapController = new ScreenSpaceMapCameraController(); this._enabled = true; this._ellipsoidNormal = new Cartesian3(); @@ -40,7 +40,7 @@ class HybridScreenspacePanCameraController { /** * The controller that is used when the camera is looking more horizontally (beyond angleThreshold from nadir). - * @type {ScreenspaceElevatorCameraController} + * @type {ScreenSpaceElevatorCameraController} * @readonly */ get elevatorController() { @@ -49,7 +49,7 @@ class HybridScreenspacePanCameraController { /** * The controller that is used when the camera is looking mostly down (within angleThreshold of nadir). - * @type {ScreenspaceMapCameraController} + * @type {ScreenSpaceMapCameraController} * @readonly */ get mapController() { @@ -103,4 +103,4 @@ class HybridScreenspacePanCameraController { } } -export default HybridScreenspacePanCameraController; +export default HybridScreenSpacePanCameraController; diff --git a/packages/engine/Source/Scene/Controllers/ScreenspaceElevatorCameraController.js b/packages/engine/Source/Scene/Controllers/ScreenSpaceElevatorCameraController.js similarity index 89% rename from packages/engine/Source/Scene/Controllers/ScreenspaceElevatorCameraController.js rename to packages/engine/Source/Scene/Controllers/ScreenSpaceElevatorCameraController.js index dfd86065d10e..309ca28a39b5 100644 --- a/packages/engine/Source/Scene/Controllers/ScreenspaceElevatorCameraController.js +++ b/packages/engine/Source/Scene/Controllers/ScreenSpaceElevatorCameraController.js @@ -6,39 +6,39 @@ 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 ScreenspaceInputBindings from "./ScreenspaceInputBindings.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. + * @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 - * @alias ScreenspaceElevatorCameraController + * @alias ScreenSpaceElevatorCameraController * @implements Controller * @example * viewer.scene.screenSpaceCameraController.enableInputs = false; * viewer.scene.screenSpaceCameraController.enableCollisionDetection = false; * - * const elevatorCameraController = new Cesium.ScreenspaceElevatorCameraController(); + * 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({ + * const elevatorCameraController = new Cesium.ScreenSpaceElevatorCameraController({ * dragInputs: [{ button: Cesium.MouseButton.RIGHT}] * }); * viewer.addController(elevatorCameraController); */ -class ScreenspaceElevatorCameraController { +class ScreenSpaceElevatorCameraController { /** * @private - * @returns {ScreenspaceInputBindings.InputBinding[]} The default drag input bindings. + * @returns {ScreenSpaceInputBindings.InputBinding[]} The default drag input bindings. */ static _getDefaultDragInputs() { return [ @@ -49,8 +49,8 @@ class ScreenspaceElevatorCameraController { } /** - * Creates an instance of a ScreenspaceElevatorCameraController. - * @param {ScreenspaceElevatorCameraController.ControllerOptions} [options] The options for configuring the controller. + * Creates an instance of a ScreenSpaceElevatorCameraController. + * @param {ScreenSpaceElevatorCameraController.ControllerOptions} [options] The options for configuring the controller. */ constructor(options = Frozen.EMPTY_OBJECT) { this._enabled = true; @@ -60,12 +60,12 @@ class ScreenspaceElevatorCameraController { /** * 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[]} + * @type {ScreenSpaceInputBindings.InputBinding[]} * @see ScreenSpaceEventHandler */ this.dragInputs = options.dragInputs ?? - ScreenspaceElevatorCameraController._getDefaultDragInputs(); + ScreenSpaceElevatorCameraController._getDefaultDragInputs(); this._isPanning = false; this._panDelta = new Cartesian2(); @@ -128,7 +128,7 @@ class ScreenspaceElevatorCameraController { const handler = new ScreenSpaceEventHandler(element); this._handler = handler; - ScreenspaceInputBindings.registerDragInputBindings( + ScreenSpaceInputBindings.registerDragInputBindings( handler, this.dragInputs, { @@ -252,7 +252,7 @@ class ScreenspaceElevatorCameraController { /** * @typedef {object} DragEvent - * @memberof ScreenspaceElevatorCameraController + * @memberof ScreenSpaceElevatorCameraController * @property {Cartesian2} startPosition The position of the mouse when the drag started. * @property {Cartesian2} endPosition The position of the mouse when the drag ended. */ @@ -273,4 +273,4 @@ class ScreenspaceElevatorCameraController { } } -export default ScreenspaceElevatorCameraController; +export default ScreenSpaceElevatorCameraController; diff --git a/packages/engine/Source/Scene/Controllers/ScreenspaceInputBindings.js b/packages/engine/Source/Scene/Controllers/ScreenSpaceInputBindings.js similarity index 94% rename from packages/engine/Source/Scene/Controllers/ScreenspaceInputBindings.js rename to packages/engine/Source/Scene/Controllers/ScreenSpaceInputBindings.js index 78dfe5871266..8ebb6357b103 100644 --- a/packages/engine/Source/Scene/Controllers/ScreenspaceInputBindings.js +++ b/packages/engine/Source/Scene/Controllers/ScreenSpaceInputBindings.js @@ -5,14 +5,14 @@ import MouseButton from "./MouseButton.js"; /** * @typedef {object} InputBinding - * @memberof ScreenspaceInputBindings + * @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 + * @memberof ScreenSpaceInputBindings * @property {Function} start Called on drag start. * @property {Function} end Called on drag stop. * @property {Function} move Called on drag move. @@ -62,9 +62,9 @@ function getUpEventType(button) { /** * @namespace - * @alias ScreenspaceInputBindings + * @alias ScreenSpaceInputBindings */ -class ScreenspaceInputBindings { +class ScreenSpaceInputBindings { /** * Registers drag input bindings on a screen space event handler. * @param {ScreenSpaceEventHandler} handler The screen space event handler. @@ -113,4 +113,4 @@ class ScreenspaceInputBindings { } } -export default ScreenspaceInputBindings; +export default ScreenSpaceInputBindings; diff --git a/packages/engine/Source/Scene/Controllers/ScreenspaceMapCameraController.js b/packages/engine/Source/Scene/Controllers/ScreenSpaceMapCameraController.js similarity index 89% rename from packages/engine/Source/Scene/Controllers/ScreenspaceMapCameraController.js rename to packages/engine/Source/Scene/Controllers/ScreenSpaceMapCameraController.js index 98f6b3ea8c3c..f2bf0acce957 100644 --- a/packages/engine/Source/Scene/Controllers/ScreenspaceMapCameraController.js +++ b/packages/engine/Source/Scene/Controllers/ScreenSpaceMapCameraController.js @@ -6,38 +6,38 @@ 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 ScreenspaceInputBindings from "./ScreenspaceInputBindings.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. + * @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 - * @alias ScreenspaceMapCameraController + * @alias ScreenSpaceMapCameraController * @implements Controller * @example * viewer.scene.screenSpaceCameraController.enableInputs = false; * - * const mapCameraController = new Cesium.ScreenspaceMapCameraController(); + * 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({ + * const mapCameraController = new Cesium.ScreenSpaceMapCameraController({ * dragInputs: [{ button: Cesium.MouseButton.RIGHT}] * }); * viewer.addController(mapCameraController); */ -class ScreenspaceMapCameraController { +class ScreenSpaceMapCameraController { /** * @private - * @returns {ScreenspaceInputBindings.InputBinding[]} The default drag input bindings. + * @returns {ScreenSpaceInputBindings.InputBinding[]} The default drag input bindings. */ static _getDefaultDragInputs() { return [ @@ -48,8 +48,8 @@ class ScreenspaceMapCameraController { } /** - * Creates an instance of a ScreenspaceMapCameraController. - * @param {ScreenspaceMapCameraController.ControllerOptions} [options] The options for configuring the controller. + * Creates an instance of a ScreenSpaceMapCameraController. + * @param {ScreenSpaceMapCameraController.ControllerOptions} [options] The options for configuring the controller. */ constructor(options = Frozen.EMPTY_OBJECT) { this._enabled = true; @@ -59,12 +59,12 @@ class ScreenspaceMapCameraController { /** * The drag input bindings that map panning. Each binding is a combination of the mouse button * and an optional keyboard modifier. - * @type {ScreenspaceInputBindings.InputBinding[]} + * @type {ScreenSpaceInputBindings.InputBinding[]} * @see ScreenSpaceEventHandler */ this.dragInputs = options.dragInputs ?? - ScreenspaceMapCameraController._getDefaultDragInputs(); + ScreenSpaceMapCameraController._getDefaultDragInputs(); this._isPanning = false; this._panDelta = new Cartesian2(); @@ -126,7 +126,7 @@ class ScreenspaceMapCameraController { const handler = new ScreenSpaceEventHandler(element); this._handler = handler; - ScreenspaceInputBindings.registerDragInputBindings( + ScreenSpaceInputBindings.registerDragInputBindings( handler, this.dragInputs, { @@ -268,7 +268,7 @@ class ScreenspaceMapCameraController { /** * @typedef {object} DragEvent - * @memberof ScreenspaceMapCameraController + * @memberof ScreenSpaceMapCameraController * @property {Cartesian2} startPosition The position of the mouse when the drag started. * @property {Cartesian2} endPosition The position of the mouse when the drag ended. */ @@ -289,4 +289,4 @@ class ScreenspaceMapCameraController { } } -export default ScreenspaceMapCameraController; +export default ScreenSpaceMapCameraController; diff --git a/packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js b/packages/engine/Source/Scene/Controllers/ScreenSpaceTiltOrbitCameraController.js similarity index 94% rename from packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js rename to packages/engine/Source/Scene/Controllers/ScreenSpaceTiltOrbitCameraController.js index 22c93969bb8d..ee214609b30f 100644 --- a/packages/engine/Source/Scene/Controllers/ScreenspaceTiltOrbitCameraController.js +++ b/packages/engine/Source/Scene/Controllers/ScreenSpaceTiltOrbitCameraController.js @@ -13,38 +13,38 @@ 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 ScreenspaceInputBindings from "./ScreenspaceInputBindings.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. + * @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 - * @alias ScreenspaceTiltOrbitCameraController + * @alias ScreenSpaceTiltOrbitCameraController * @implements Controller * @example * viewer.scene.screenSpaceCameraController.enableInputs = false; * viewer.scene.screenSpaceCameraController.enableCollisionDetection = false; * - * const tiltOrbitController = new Cesium.ScreenspaceTiltOrbitCameraController(); + * const tiltOrbitController = new Cesium.ScreenSpaceTiltOrbitCameraController(); * 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({ + * const tiltOrbitController = new Cesium.ScreenSpaceTiltOrbitCameraController({ * dragInputs: [{ button: Cesium.MouseButton.LEFT }] * }); * viewer.addController(tiltOrbitController); */ -class ScreenspaceTiltOrbitCameraController { +class ScreenSpaceTiltOrbitCameraController { /** * @private - * @returns {ScreenspaceInputBindings.InputBinding[]} The default drag input bindings. + * @returns {ScreenSpaceInputBindings.InputBinding[]} The default drag input bindings. */ static _getDefaultDragInputs() { return [ @@ -59,8 +59,8 @@ class ScreenspaceTiltOrbitCameraController { } /** - * Creates a new instance of ScreenspaceTiltOrbitCameraController. - * @param {ScreenspaceTiltOrbitCameraController.ControllerOptions} [options] The options for configuring the controller. + * Creates a new instance of ScreenSpaceTiltOrbitCameraController. + * @param {ScreenSpaceTiltOrbitCameraController.ControllerOptions} [options] The options for configuring the controller. * @constructor */ constructor(options = Frozen.EMPTY_OBJECT) { @@ -85,12 +85,12 @@ class ScreenspaceTiltOrbitCameraController { /** * The drag input bindings that control tilting. Each binding is a combination of the mouse button * and an optional keyboard modifier. - * @type {ScreenspaceInputBindings.InputBinding[]} + * @type {ScreenSpaceInputBindings.InputBinding[]} * @see ScreenSpaceEventHandler */ this.dragInputs = options.dragInputs ?? - ScreenspaceTiltOrbitCameraController._getDefaultDragInputs(); + ScreenSpaceTiltOrbitCameraController._getDefaultDragInputs(); this._isDragging = false; this._dragDelta = new Cartesian2(); @@ -225,7 +225,7 @@ class ScreenspaceTiltOrbitCameraController { const handler = new ScreenSpaceEventHandler(element); this._handler = handler; - ScreenspaceInputBindings.registerDragInputBindings( + ScreenSpaceInputBindings.registerDragInputBindings( handler, this.dragInputs, { @@ -258,7 +258,7 @@ class ScreenspaceTiltOrbitCameraController { /** * @typedef {object} StartDragEvent - * @memberof ScreenspaceTiltOrbitCameraController + * @memberof ScreenSpaceTiltOrbitCameraController * @property {Cartesian2} position The position of the mouse when the drag started. */ @@ -284,7 +284,7 @@ class ScreenspaceTiltOrbitCameraController { /** * @typedef {object} DragEvent - * @memberOf ScreenspaceTiltOrbitCameraController + * @memberOf ScreenSpaceTiltOrbitCameraController * @property {Cartesian2} startPosition The position of the mouse when the drag started. * @property {Cartesian2} endPosition The position of the mouse when the drag ended. */ @@ -571,4 +571,4 @@ class ScreenspaceTiltOrbitCameraController { } } -export default ScreenspaceTiltOrbitCameraController; +export default ScreenSpaceTiltOrbitCameraController; diff --git a/packages/engine/Source/Scene/Controllers/ScreenspaceZoomCameraController.js b/packages/engine/Source/Scene/Controllers/ScreenSpaceZoomCameraController.js similarity index 92% rename from packages/engine/Source/Scene/Controllers/ScreenspaceZoomCameraController.js rename to packages/engine/Source/Scene/Controllers/ScreenSpaceZoomCameraController.js index 73683def7ca0..091178152b93 100644 --- a/packages/engine/Source/Scene/Controllers/ScreenspaceZoomCameraController.js +++ b/packages/engine/Source/Scene/Controllers/ScreenSpaceZoomCameraController.js @@ -9,21 +9,21 @@ import TimeConstants from "../../Core/TimeConstants.js"; /** * @typedef {object} ControllerOptions - * @memberof ScreenspaceZoomCameraController + * @memberof ScreenSpaceZoomCameraController */ /** * A camera controller that allows zooming the camera in and out based on the pointer location in screen space. * @class - * @alias ScreenspaceZoomCameraController + * @alias ScreenSpaceZoomCameraController * @implements Controller * @example * TODO */ -class ScreenspaceZoomCameraController { +class ScreenSpaceZoomCameraController { /** - * Creates a new instance of ScreenspaceZoomCameraController. - * @param {ScreenspaceZoomCameraController.ControllerOptions} [options] The options for configuring the controller. + * Creates a new instance of ScreenSpaceZoomCameraController. + * @param {ScreenSpaceZoomCameraController.ControllerOptions} [options] The options for configuring the controller. * @constructor */ constructor(options = Frozen.EMPTY_OBJECT) { @@ -166,7 +166,7 @@ class ScreenspaceZoomCameraController { /** * @typedef {object} DragEvent - * @memberof ScreenspaceZoomCameraController + * @memberof ScreenSpaceZoomCameraController * @property {Cartesian2} startPosition The position of the mouse when the drag started. * @property {Cartesian2} endPosition The position of the mouse when the drag ended. */ @@ -181,4 +181,4 @@ class ScreenspaceZoomCameraController { } } -export default ScreenspaceZoomCameraController; +export default ScreenSpaceZoomCameraController; diff --git a/packages/engine/Source/Scene/Scene.js b/packages/engine/Source/Scene/Scene.js index 1638195074ed..4149a01de610 100644 --- a/packages/engine/Source/Scene/Scene.js +++ b/packages/engine/Source/Scene/Scene.js @@ -1092,7 +1092,7 @@ Object.defineProperties(Scene.prototype, { * scene.screenSpaceCameraController.enableInputs = false; * scene.screenSpaceCameraController.enableCollisionDetection = false; * - * const tiltOrbitController = new Cesium.ScreenspaceTiltOrbitCameraController(); + * const tiltOrbitController = new Cesium.ScreenSpaceTiltOrbitCameraController(); * scene.controllerHost.registerController(tiltOrbitController, scene.canvas.parentNode); */ controllerHost: { diff --git a/packages/engine/Source/Widget/CesiumWidget.js b/packages/engine/Source/Widget/CesiumWidget.js index 929e8282a6d7..4fc1b5e7dfdb 100644 --- a/packages/engine/Source/Widget/CesiumWidget.js +++ b/packages/engine/Source/Widget/CesiumWidget.js @@ -1234,7 +1234,7 @@ CesiumWidget.prototype._onDataSourceRemoved = function ( * widget.scene.screenSpaceCameraController.enableInputs = false; * widget.scene.screenSpaceCameraController.enableCollisionDetection = false; * - * const tiltOrbitController = new Cesium.ScreenspaceTiltOrbitCameraController(); + * const tiltOrbitController = new Cesium.ScreenSpaceTiltOrbitCameraController(); * widget.addController(tiltOrbitController); */ CesiumWidget.prototype.addController = function (controller) { diff --git a/packages/sandcastle/gallery/camera-controllers/main.js b/packages/sandcastle/gallery/camera-controllers/main.js index 0626bc58edc6..28959d0ca18f 100644 --- a/packages/sandcastle/gallery/camera-controllers/main.js +++ b/packages/sandcastle/gallery/camera-controllers/main.js @@ -8,13 +8,13 @@ scene.screenSpaceCameraController.enableInputs = false; scene.screenSpaceCameraController.enableCollisionDetection = false; // Set up the modular camera controllers -const panController = new Cesium.HybridScreenspacePanCameraController(); +const panController = new Cesium.HybridScreenSpacePanCameraController(); viewer.addController(panController); -const tiltController = new Cesium.ScreenspaceTiltOrbitCameraController(); +const tiltController = new Cesium.ScreenSpaceTiltOrbitCameraController(); viewer.addController(tiltController); -const zoomController = new Cesium.ScreenspaceZoomCameraController(); +const zoomController = new Cesium.ScreenSpaceZoomCameraController(); viewer.addController(zoomController); // Load a 3D Tiles power plant asset diff --git a/packages/widgets/Source/Viewer/Viewer.js b/packages/widgets/Source/Viewer/Viewer.js index 8322e773b4ee..3797e149d1ab 100644 --- a/packages/widgets/Source/Viewer/Viewer.js +++ b/packages/widgets/Source/Viewer/Viewer.js @@ -1980,7 +1980,7 @@ Viewer.prototype._onDataSourceRemoved = function ( * viewer.scene.screenSpaceCameraController.enableInputs = false; * viewer.scene.screenSpaceCameraController.enableCollisionDetection = false; * - * const tiltOrbitController = new Cesium.ScreenspaceTiltOrbitCameraController(); + * const tiltOrbitController = new Cesium.ScreenSpaceTiltOrbitCameraController(); * viewer.addController(tiltOrbitController); */ Viewer.prototype.addController = function (controller) { From 45d99f0000865f15a5509f6b6041d02c661094cf Mon Sep 17 00:00:00 2001 From: ggetz Date: Tue, 21 Jul 2026 09:44:54 -0400 Subject: [PATCH 10/10] Orbit around mouse position --- .../ScreenSpaceTiltOrbitCameraController.js | 136 ++++++++++++------ 1 file changed, 94 insertions(+), 42 deletions(-) diff --git a/packages/engine/Source/Scene/Controllers/ScreenSpaceTiltOrbitCameraController.js b/packages/engine/Source/Scene/Controllers/ScreenSpaceTiltOrbitCameraController.js index ee214609b30f..5d73cdd9ed91 100644 --- a/packages/engine/Source/Scene/Controllers/ScreenSpaceTiltOrbitCameraController.js +++ b/packages/engine/Source/Scene/Controllers/ScreenSpaceTiltOrbitCameraController.js @@ -25,7 +25,6 @@ import MouseButton from "./MouseButton.js"; /** * 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 - * @alias ScreenSpaceTiltOrbitCameraController * @implements Controller * @example * viewer.scene.screenSpaceCameraController.enableInputs = false; @@ -82,6 +81,13 @@ class ScreenSpaceTiltOrbitCameraController { */ 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. @@ -94,21 +100,14 @@ class ScreenSpaceTiltOrbitCameraController { this._isDragging = false; this._dragDelta = new Cartesian2(); - this._dragOrigin = new Cartesian2(); - this._screenOrigin = new Cartesian2(); + this._screenSpaceDragPosition = new Cartesian2(); + this._screenSpaceOrigin = new Cartesian2(); + this._origin = new Cartesian3(); + this._shouldPickTarget = true; this._target = new Cartesian3(); this._axis = new Cartesian3(); - /** - * Controls the screenspace position used to determine the location on the ellipsoid that the camera orbit and tilts around. - *

If true, the origin is preferred to be determined by the position at the center of the screen, and will fall back to the position under the mouse cursor when dragging starts. If false, the origin is always determined by the position under the mouse cursor when dragging starts.

- *

Regardless of the preference, if the origin is not on the ellipsoid, no tilt or orbit is applied.

- * @type {boolean} - * @default true - */ - this.targetScreenCenter = true; // TODO - /** * 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} @@ -175,6 +174,7 @@ class ScreenSpaceTiltOrbitCameraController { this._tiltAxis = new Cartesian3(); this._tiltQuaternion = new Quaternion(); this._tiltOffset = new Cartesian3(); + this._tiltOrigin = new Cartesian3(); this._tiltDampenedResults = { velocity: 0.0, value: 0.0, @@ -184,6 +184,8 @@ class ScreenSpaceTiltOrbitCameraController { 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, @@ -272,8 +274,9 @@ class ScreenSpaceTiltOrbitCameraController { } this._isDragging = true; - this._dragOrigin.x = event.position.x; - this._dragOrigin.y = event.position.y; + this._shouldPickTarget = true; + this._screenSpaceDragPosition.x = event.position.x; + this._screenSpaceDragPosition.y = event.position.y; this._dragDelta.x = 0; this._dragDelta.y = 0; } @@ -359,7 +362,7 @@ class ScreenSpaceTiltOrbitCameraController { /** * 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 target position to orbit around in world coordinates. + * @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. @@ -380,11 +383,6 @@ class ScreenSpaceTiltOrbitCameraController { return; } - const offset = Cartesian3.subtract( - camera.positionWC, - target, - this._orbitOffset, - ); const enu = Transforms.eastNorthUpToFixedFrame( target, ellipsoid, @@ -395,7 +393,7 @@ class ScreenSpaceTiltOrbitCameraController { Cartesian3.UNIT_X, this._orbitTargetEast, ); - const currentOrbitAngle = Cartesian3.angleBetween(offset, east); + const currentOrbitAngle = Cartesian3.angleBetween(camera.directionWC, east); if (Math.abs(this.orbitVelocity) < this.minimumOrbitVelocity) { this.orbitVelocity = 0.0; @@ -425,23 +423,52 @@ class ScreenSpaceTiltOrbitCameraController { const theta = this.orbitAngle - currentOrbitAngle; const rotation = Matrix3.fromQuaternion( - Quaternion.fromAxisAngle(camera.upWC, -theta, this._orbitQuaternion), + Quaternion.fromAxisAngle(axis, -theta, this._orbitQuaternion), ); - const rotatedOffset = Matrix3.multiplyByVector( - rotation, + 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, ); - Cartesian3.add(target, rotatedOffset, camera.position); - camera.lookAtWorldPosition(target, ellipsoid); + 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 target position to tilt around in world coordinates. + * @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. @@ -490,18 +517,31 @@ class ScreenSpaceTiltOrbitCameraController { ); const offset = Cartesian3.subtract( - camera.positionWC, + 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, - offset, + lookOffset, this._tiltOffset, ); - Cartesian3.add(target, rotatedOffset, camera.position); - camera.lookAtWorldPosition(target, ellipsoid); + + Cartesian3.add(lookTarget, rotatedOffset, camera.position); + camera.lookAtWorldPosition(lookTarget, ellipsoid); } /** @@ -523,19 +563,31 @@ class ScreenSpaceTiltOrbitCameraController { return; } - const screenOrigin = this._screenOrigin; - screenOrigin.x = clientWidth / 2.0; - screenOrigin.y = clientHeight / 2.0; - - let target = camera.pickEllipsoid(screenOrigin, ellipsoid, this._target); + const screenSpaceOrigin = this._screenSpaceOrigin; + screenSpaceOrigin.x = clientWidth / 2.0; + screenSpaceOrigin.y = clientHeight / 2.0; + const origin = camera.pickEllipsoid( + screenSpaceOrigin, + ellipsoid, + this._origin, + ); - // Fallback to the cursor position if the center of the screen is not on the ellipsoid - if (!defined(target)) { - const dragOrigin = this._dragOrigin; - target = camera.pickEllipsoid(dragOrigin, ellipsoid, this._target); - } + if (defined(origin)) { + let target = origin; + if (this._isDragging && this.useDragPosition) { + target = this._target; + if (this._shouldPickTarget) { + const dragPositionTarget = camera.pickEllipsoid( + this._screenSpaceDragPosition, + ellipsoid, + this._target, + ); + const picked = defined(dragPositionTarget); + this._shouldPickTarget = !picked; + target = picked ? dragPositionTarget : origin; + } + } - if (defined(target)) { const normal = ellipsoid.geodeticSurfaceNormal(target, this._axis); const axis = Cartesian3.negate(normal, this._axis);