@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
1,743 lines (1,538 loc) • 123 kB
JavaScript
import BoundingSphere from "../Core/BoundingSphere.js";
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 Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import EasingFunction from "../Core/EasingFunction.js";
import Ellipsoid from "../Core/Ellipsoid.js";
import EllipsoidGeodesic from "../Core/EllipsoidGeodesic.js";
import Event from "../Core/Event.js";
import getTimestamp from "../Core/getTimestamp.js";
import HeadingPitchRange from "../Core/HeadingPitchRange.js";
import HeadingPitchRoll from "../Core/HeadingPitchRoll.js";
import Intersect from "../Core/Intersect.js";
import IntersectionTests from "../Core/IntersectionTests.js";
import CesiumMath from "../Core/Math.js";
import Matrix3 from "../Core/Matrix3.js";
import Matrix4 from "../Core/Matrix4.js";
import OrthographicFrustum from "../Core/OrthographicFrustum.js";
import OrthographicOffCenterFrustum from "../Core/OrthographicOffCenterFrustum.js";
import PerspectiveFrustum from "../Core/PerspectiveFrustum.js";
import Quaternion from "../Core/Quaternion.js";
import Ray from "../Core/Ray.js";
import Rectangle from "../Core/Rectangle.js";
import Transforms from "../Core/Transforms.js";
import CameraFlightPath from "./CameraFlightPath.js";
import MapMode2D from "./MapMode2D.js";
import SceneMode from "./SceneMode.js";
/**
* @typedef {object} DirectionUp
*
* An orientation given by a pair of unit vectors
*
* @property {Cartesian3} direction The unit "direction" vector
* @property {Cartesian3} up The unit "up" vector
**/
/**
* @typedef {object} HeadingPitchRollValues
*
* An orientation given by numeric heading, pitch, and roll
*
* @property {number} [heading=0.0] The heading in radians
* @property {number} [pitch=-CesiumMath.PI_OVER_TWO] The pitch in radians
* @property {number} [roll=0.0] The roll in radians
**/
/**
* The camera is defined by a position, orientation, and view frustum.
* <br /><br />
* The orientation forms an orthonormal basis with a view, up and right = view x up unit vectors.
* <br /><br />
* The viewing frustum is defined by 6 planes.
* Each plane is represented by a {@link Cartesian4} object, where the x, y, and z components
* define the unit vector normal to the plane, and the w component is the distance of the
* plane from the origin/camera position.
*
* @alias Camera
*
* @constructor
*
* @param {Scene} scene The scene.
*
* @demo {@link https://sandcastle.cesium.com/index.html?src=Camera.html|Cesium Sandcastle Camera Demo}
* @demo {@link https://sandcastle.cesium.com/index.html?src=Camera%20Tutorial.html|Cesium Sandcastle Camera Tutorial Example}
* @demo {@link https://cesium.com/learn/cesiumjs-learn/cesiumjs-camera|Camera Tutorial}
*
* @example
* // Create a camera looking down the negative z-axis, positioned at the origin,
* // with a field of view of 60 degrees, and 1:1 aspect ratio.
* const camera = new Cesium.Camera(scene);
* camera.position = new Cesium.Cartesian3();
* camera.direction = Cesium.Cartesian3.negate(Cesium.Cartesian3.UNIT_Z, new Cesium.Cartesian3());
* camera.up = Cesium.Cartesian3.clone(Cesium.Cartesian3.UNIT_Y);
* camera.frustum.fov = Cesium.Math.PI_OVER_THREE;
* camera.frustum.near = 1.0;
* camera.frustum.far = 2.0;
*/
function Camera(scene) {
//>>includeStart('debug', pragmas.debug);
if (!defined(scene)) {
throw new DeveloperError("scene is required.");
}
//>>includeEnd('debug');
this._scene = scene;
this._transform = Matrix4.clone(Matrix4.IDENTITY);
this._invTransform = Matrix4.clone(Matrix4.IDENTITY);
this._actualTransform = Matrix4.clone(Matrix4.IDENTITY);
this._actualInvTransform = Matrix4.clone(Matrix4.IDENTITY);
this._transformChanged = false;
/**
* The position of the camera.
*
* @type {Cartesian3}
*/
this.position = new Cartesian3();
this._position = new Cartesian3();
this._positionWC = new Cartesian3();
this._positionCartographic = new Cartographic();
this._oldPositionWC = undefined;
/**
* The position delta magnitude.
*
* @private
*/
this.positionWCDeltaMagnitude = 0.0;
/**
* The position delta magnitude last frame.
*
* @private
*/
this.positionWCDeltaMagnitudeLastFrame = 0.0;
/**
* How long in seconds since the camera has stopped moving
*
* @private
*/
this.timeSinceMoved = 0.0;
this._lastMovedTimestamp = 0.0;
/**
* The view direction of the camera.
*
* @type {Cartesian3}
*/
this.direction = new Cartesian3();
this._direction = new Cartesian3();
this._directionWC = new Cartesian3();
/**
* The up direction of the camera.
*
* @type {Cartesian3}
*/
this.up = new Cartesian3();
this._up = new Cartesian3();
this._upWC = new Cartesian3();
/**
* The right direction of the camera.
*
* @type {Cartesian3}
*/
this.right = new Cartesian3();
this._right = new Cartesian3();
this._rightWC = new Cartesian3();
/**
* The region of space in view.
*
* @type {PerspectiveFrustum|PerspectiveOffCenterFrustum|OrthographicFrustum}
* @default PerspectiveFrustum()
*
* @see PerspectiveFrustum
* @see PerspectiveOffCenterFrustum
* @see OrthographicFrustum
*/
this.frustum = new PerspectiveFrustum();
this.frustum.aspectRatio =
scene.drawingBufferWidth / scene.drawingBufferHeight;
this.frustum.fov = CesiumMath.toRadians(60.0);
/**
* The default amount to move the camera when an argument is not
* provided to the move methods.
* @type {number}
* @default 100000.0;
*/
this.defaultMoveAmount = 100000.0;
/**
* The default amount to rotate the camera when an argument is not
* provided to the look methods.
* @type {number}
* @default Math.PI / 60.0
*/
this.defaultLookAmount = Math.PI / 60.0;
/**
* The default amount to rotate the camera when an argument is not
* provided to the rotate methods.
* @type {number}
* @default Math.PI / 3600.0
*/
this.defaultRotateAmount = Math.PI / 3600.0;
/**
* The default amount to move the camera when an argument is not
* provided to the zoom methods.
* @type {number}
* @default 100000.0;
*/
this.defaultZoomAmount = 100000.0;
/**
* If set, the camera will not be able to rotate past this axis in either direction.
* @type {Cartesian3 | undefined}
* @default undefined
*/
this.constrainedAxis = undefined;
/**
* The factor multiplied by the the map size used to determine where to clamp the camera position
* when zooming out from the surface. The default is 1.5. Only valid for 2D and the map is rotatable.
* @type {number}
* @default 1.5
*/
this.maximumZoomFactor = 1.5;
this._moveStart = new Event();
this._moveEnd = new Event();
this._changed = new Event();
this._changedPosition = undefined;
this._changedDirection = undefined;
this._changedFrustum = undefined;
this._changedHeading = undefined;
this._changedRoll = undefined;
/**
* The amount the camera has to change before the <code>changed</code> event is raised. The value is a percentage in the [0, 1] range.
* @type {number}
* @default 0.5
*/
this.percentageChanged = 0.5;
this._viewMatrix = new Matrix4();
this._invViewMatrix = new Matrix4();
updateViewMatrix(this);
this._mode = SceneMode.SCENE3D;
this._modeChanged = true;
const projection = scene.mapProjection;
this._projection = projection;
this._maxCoord = projection.project(
new Cartographic(Math.PI, CesiumMath.PI_OVER_TWO),
);
this._max2Dfrustum = undefined;
// set default view
rectangleCameraPosition3D(
this,
Camera.DEFAULT_VIEW_RECTANGLE,
this.position,
true,
);
let mag = Cartesian3.magnitude(this.position);
mag += mag * Camera.DEFAULT_VIEW_FACTOR;
Cartesian3.normalize(this.position, this.position);
Cartesian3.multiplyByScalar(this.position, mag, this.position);
}
/**
* @private
*/
Camera.TRANSFORM_2D = new Matrix4(
0.0,
0.0,
1.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
1.0,
);
/**
* @private
*/
Camera.TRANSFORM_2D_INVERSE = Matrix4.inverseTransformation(
Camera.TRANSFORM_2D,
new Matrix4(),
);
/**
* The default rectangle the camera will view on creation.
* @type Rectangle
*/
Camera.DEFAULT_VIEW_RECTANGLE = Rectangle.fromDegrees(
-95.0,
-20.0,
-70.0,
90.0,
);
/**
* A scalar to multiply to the camera position and add it back after setting the camera to view the rectangle.
* A value of zero means the camera will view the entire {@link Camera#DEFAULT_VIEW_RECTANGLE}, a value greater than zero
* will move it further away from the extent, and a value less than zero will move it close to the extent.
* @type {number}
*/
Camera.DEFAULT_VIEW_FACTOR = 0.5;
/**
* The default heading/pitch/range that is used when the camera flies to a location that contains a bounding sphere.
* @type HeadingPitchRange
*/
Camera.DEFAULT_OFFSET = new HeadingPitchRange(
0.0,
-CesiumMath.PI_OVER_FOUR,
0.0,
);
function updateViewMatrix(camera) {
Matrix4.computeView(
camera._position,
camera._direction,
camera._up,
camera._right,
camera._viewMatrix,
);
Matrix4.multiply(
camera._viewMatrix,
camera._actualInvTransform,
camera._viewMatrix,
);
Matrix4.inverseTransformation(camera._viewMatrix, camera._invViewMatrix);
}
function updateCameraDeltas(camera) {
if (!defined(camera._oldPositionWC)) {
camera._oldPositionWC = Cartesian3.clone(
camera.positionWC,
camera._oldPositionWC,
);
} else {
camera.positionWCDeltaMagnitudeLastFrame = camera.positionWCDeltaMagnitude;
const delta = Cartesian3.subtract(
camera.positionWC,
camera._oldPositionWC,
camera._oldPositionWC,
);
camera.positionWCDeltaMagnitude = Cartesian3.magnitude(delta);
camera._oldPositionWC = Cartesian3.clone(
camera.positionWC,
camera._oldPositionWC,
);
// Update move timers
if (camera.positionWCDeltaMagnitude > 0.0) {
camera.timeSinceMoved = 0.0;
camera._lastMovedTimestamp = getTimestamp();
} else {
camera.timeSinceMoved =
Math.max(getTimestamp() - camera._lastMovedTimestamp, 0.0) / 1000.0;
}
}
}
/**
* Checks if there's a camera flight with preload for this camera.
*
* @returns {boolean} Whether or not this camera has a current flight with a valid preloadFlightCamera in scene.
*
* @private
*
*/
Camera.prototype.canPreloadFlight = function () {
return defined(this._currentFlight) && this._mode !== SceneMode.SCENE2D;
};
Camera.prototype._updateCameraChanged = function () {
const camera = this;
updateCameraDeltas(camera);
if (camera._changed.numberOfListeners === 0) {
return;
}
const percentageChanged = camera.percentageChanged;
// check heading
const currentHeading = camera.heading;
if (!defined(camera._changedHeading)) {
camera._changedHeading = currentHeading;
}
let headingDelta =
Math.abs(camera._changedHeading - currentHeading) % CesiumMath.TWO_PI;
headingDelta =
headingDelta > CesiumMath.PI
? CesiumMath.TWO_PI - headingDelta
: headingDelta;
// Since delta is computed as the shortest distance between two angles
// the percentage is relative to the half circle.
const headingChangedPercentage = headingDelta / Math.PI;
if (headingChangedPercentage > percentageChanged) {
camera._changedHeading = currentHeading;
}
// check roll
const currentRoll = camera.roll;
if (!defined(camera._changedRoll)) {
camera._changedRoll = currentRoll;
}
let rollDelta =
Math.abs(camera._changedRoll - currentRoll) % CesiumMath.TWO_PI;
rollDelta =
rollDelta > CesiumMath.PI ? CesiumMath.TWO_PI - rollDelta : rollDelta;
// Since delta is computed as the shortest distance between two angles
// the percentage is relative to the half circle.
const rollChangedPercentage = rollDelta / Math.PI;
if (rollChangedPercentage > percentageChanged) {
camera._changedRoll = currentRoll;
}
if (
rollChangedPercentage > percentageChanged ||
headingChangedPercentage > percentageChanged
) {
camera._changed.raiseEvent(
Math.max(rollChangedPercentage, headingChangedPercentage),
);
}
if (camera._mode === SceneMode.SCENE2D) {
if (!defined(camera._changedFrustum)) {
camera._changedPosition = Cartesian3.clone(
camera.position,
camera._changedPosition,
);
camera._changedFrustum = camera.frustum.clone();
return;
}
const position = camera.position;
const lastPosition = camera._changedPosition;
const frustum = camera.frustum;
const lastFrustum = camera._changedFrustum;
const x0 = position.x + frustum.left;
const x1 = position.x + frustum.right;
const x2 = lastPosition.x + lastFrustum.left;
const x3 = lastPosition.x + lastFrustum.right;
const y0 = position.y + frustum.bottom;
const y1 = position.y + frustum.top;
const y2 = lastPosition.y + lastFrustum.bottom;
const y3 = lastPosition.y + lastFrustum.top;
const leftX = Math.max(x0, x2);
const rightX = Math.min(x1, x3);
const bottomY = Math.max(y0, y2);
const topY = Math.min(y1, y3);
let areaPercentage;
if (leftX >= rightX || bottomY >= y1) {
areaPercentage = 1.0;
} else {
let areaRef = lastFrustum;
if (x0 < x2 && x1 > x3 && y0 < y2 && y1 > y3) {
areaRef = frustum;
}
areaPercentage =
1.0 -
((rightX - leftX) * (topY - bottomY)) /
((areaRef.right - areaRef.left) * (areaRef.top - areaRef.bottom));
}
if (areaPercentage > percentageChanged) {
camera._changed.raiseEvent(areaPercentage);
camera._changedPosition = Cartesian3.clone(
camera.position,
camera._changedPosition,
);
camera._changedFrustum = camera.frustum.clone(camera._changedFrustum);
}
return;
}
if (!defined(camera._changedDirection)) {
camera._changedPosition = Cartesian3.clone(
camera.positionWC,
camera._changedPosition,
);
camera._changedDirection = Cartesian3.clone(
camera.directionWC,
camera._changedDirection,
);
return;
}
const dirAngle = CesiumMath.acosClamped(
Cartesian3.dot(camera.directionWC, camera._changedDirection),
);
let dirPercentage;
if (defined(camera.frustum.fovy)) {
dirPercentage = dirAngle / (camera.frustum.fovy * 0.5);
} else {
dirPercentage = dirAngle;
}
const distance = Cartesian3.distance(
camera.positionWC,
camera._changedPosition,
);
const heightPercentage = distance / camera.positionCartographic.height;
if (
dirPercentage > percentageChanged ||
heightPercentage > percentageChanged
) {
camera._changed.raiseEvent(Math.max(dirPercentage, heightPercentage));
camera._changedPosition = Cartesian3.clone(
camera.positionWC,
camera._changedPosition,
);
camera._changedDirection = Cartesian3.clone(
camera.directionWC,
camera._changedDirection,
);
}
};
function convertTransformForColumbusView(camera) {
Transforms.basisTo2D(
camera._projection,
camera._transform,
camera._actualTransform,
);
}
const scratchCartographic = new Cartographic();
const scratchCartesian3Projection = new Cartesian3();
const scratchCartesian3 = new Cartesian3();
const scratchCartesian4Origin = new Cartesian4();
const scratchCartesian4NewOrigin = new Cartesian4();
const scratchCartesian4NewXAxis = new Cartesian4();
const scratchCartesian4NewYAxis = new Cartesian4();
const scratchCartesian4NewZAxis = new Cartesian4();
function convertTransformFor2D(camera) {
const projection = camera._projection;
const ellipsoid = projection.ellipsoid;
const origin = Matrix4.getColumn(
camera._transform,
3,
scratchCartesian4Origin,
);
const cartographic = ellipsoid.cartesianToCartographic(
origin,
scratchCartographic,
);
const projectedPosition = projection.project(
cartographic,
scratchCartesian3Projection,
);
const newOrigin = scratchCartesian4NewOrigin;
newOrigin.x = projectedPosition.z;
newOrigin.y = projectedPosition.x;
newOrigin.z = projectedPosition.y;
newOrigin.w = 1.0;
const newZAxis = Cartesian4.clone(
Cartesian4.UNIT_X,
scratchCartesian4NewZAxis,
);
const xAxis = Cartesian4.add(
Matrix4.getColumn(camera._transform, 0, scratchCartesian3),
origin,
scratchCartesian3,
);
ellipsoid.cartesianToCartographic(xAxis, cartographic);
projection.project(cartographic, projectedPosition);
const newXAxis = scratchCartesian4NewXAxis;
newXAxis.x = projectedPosition.z;
newXAxis.y = projectedPosition.x;
newXAxis.z = projectedPosition.y;
newXAxis.w = 0.0;
Cartesian3.subtract(newXAxis, newOrigin, newXAxis);
newXAxis.x = 0.0;
const newYAxis = scratchCartesian4NewYAxis;
if (Cartesian3.magnitudeSquared(newXAxis) > CesiumMath.EPSILON10) {
Cartesian3.cross(newZAxis, newXAxis, newYAxis);
} else {
const yAxis = Cartesian4.add(
Matrix4.getColumn(camera._transform, 1, scratchCartesian3),
origin,
scratchCartesian3,
);
ellipsoid.cartesianToCartographic(yAxis, cartographic);
projection.project(cartographic, projectedPosition);
newYAxis.x = projectedPosition.z;
newYAxis.y = projectedPosition.x;
newYAxis.z = projectedPosition.y;
newYAxis.w = 0.0;
Cartesian3.subtract(newYAxis, newOrigin, newYAxis);
newYAxis.x = 0.0;
if (Cartesian3.magnitudeSquared(newYAxis) < CesiumMath.EPSILON10) {
Cartesian4.clone(Cartesian4.UNIT_Y, newXAxis);
Cartesian4.clone(Cartesian4.UNIT_Z, newYAxis);
}
}
Cartesian3.cross(newYAxis, newZAxis, newXAxis);
Cartesian3.normalize(newXAxis, newXAxis);
Cartesian3.cross(newZAxis, newXAxis, newYAxis);
Cartesian3.normalize(newYAxis, newYAxis);
Matrix4.setColumn(
camera._actualTransform,
0,
newXAxis,
camera._actualTransform,
);
Matrix4.setColumn(
camera._actualTransform,
1,
newYAxis,
camera._actualTransform,
);
Matrix4.setColumn(
camera._actualTransform,
2,
newZAxis,
camera._actualTransform,
);
Matrix4.setColumn(
camera._actualTransform,
3,
newOrigin,
camera._actualTransform,
);
}
const scratchCartesian = new Cartesian3();
function updateMembers(camera) {
const mode = camera._mode;
let heightChanged = false;
let height = 0.0;
if (mode === SceneMode.SCENE2D) {
height = camera.frustum.right - camera.frustum.left;
heightChanged = height !== camera._positionCartographic.height;
}
let position = camera._position;
const positionChanged =
!Cartesian3.equals(position, camera.position) || heightChanged;
if (positionChanged) {
position = Cartesian3.clone(camera.position, camera._position);
}
let direction = camera._direction;
const directionChanged = !Cartesian3.equals(direction, camera.direction);
if (directionChanged) {
Cartesian3.normalize(camera.direction, camera.direction);
direction = Cartesian3.clone(camera.direction, camera._direction);
}
let up = camera._up;
const upChanged = !Cartesian3.equals(up, camera.up);
if (upChanged) {
Cartesian3.normalize(camera.up, camera.up);
up = Cartesian3.clone(camera.up, camera._up);
}
let right = camera._right;
const rightChanged = !Cartesian3.equals(right, camera.right);
if (rightChanged) {
Cartesian3.normalize(camera.right, camera.right);
right = Cartesian3.clone(camera.right, camera._right);
}
const transformChanged = camera._transformChanged || camera._modeChanged;
camera._transformChanged = false;
if (transformChanged) {
Matrix4.inverseTransformation(camera._transform, camera._invTransform);
if (
camera._mode === SceneMode.COLUMBUS_VIEW ||
camera._mode === SceneMode.SCENE2D
) {
if (Matrix4.equals(Matrix4.IDENTITY, camera._transform)) {
Matrix4.clone(Camera.TRANSFORM_2D, camera._actualTransform);
} else if (camera._mode === SceneMode.COLUMBUS_VIEW) {
convertTransformForColumbusView(camera);
} else {
convertTransformFor2D(camera);
}
} else {
Matrix4.clone(camera._transform, camera._actualTransform);
}
Matrix4.inverseTransformation(
camera._actualTransform,
camera._actualInvTransform,
);
camera._modeChanged = false;
}
const transform = camera._actualTransform;
if (positionChanged || transformChanged) {
camera._positionWC = Matrix4.multiplyByPoint(
transform,
position,
camera._positionWC,
);
// Compute the Cartographic position of the camera.
if (mode === SceneMode.SCENE3D || mode === SceneMode.MORPHING) {
camera._positionCartographic =
camera._projection.ellipsoid.cartesianToCartographic(
camera._positionWC,
camera._positionCartographic,
);
} else {
// The camera position is expressed in the 2D coordinate system where the Y axis is to the East,
// the Z axis is to the North, and the X axis is out of the map. Express them instead in the ENU axes where
// X is to the East, Y is to the North, and Z is out of the local horizontal plane.
const positionENU = scratchCartesian;
positionENU.x = camera._positionWC.y;
positionENU.y = camera._positionWC.z;
positionENU.z = camera._positionWC.x;
// In 2D, the camera height is always 12.7 million meters.
// The apparent height is equal to half the frustum width.
if (mode === SceneMode.SCENE2D) {
positionENU.z = height;
}
camera._projection.unproject(positionENU, camera._positionCartographic);
}
}
if (directionChanged || upChanged || rightChanged) {
const det = Cartesian3.dot(
direction,
Cartesian3.cross(up, right, scratchCartesian),
);
if (Math.abs(1.0 - det) > CesiumMath.EPSILON2) {
//orthonormalize axes
const invUpMag = 1.0 / Cartesian3.magnitudeSquared(up);
const scalar = Cartesian3.dot(up, direction) * invUpMag;
const w0 = Cartesian3.multiplyByScalar(
direction,
scalar,
scratchCartesian,
);
up = Cartesian3.normalize(
Cartesian3.subtract(up, w0, camera._up),
camera._up,
);
Cartesian3.clone(up, camera.up);
right = Cartesian3.cross(direction, up, camera._right);
Cartesian3.clone(right, camera.right);
}
}
if (directionChanged || transformChanged) {
camera._directionWC = Matrix4.multiplyByPointAsVector(
transform,
direction,
camera._directionWC,
);
Cartesian3.normalize(camera._directionWC, camera._directionWC);
}
if (upChanged || transformChanged) {
camera._upWC = Matrix4.multiplyByPointAsVector(transform, up, camera._upWC);
Cartesian3.normalize(camera._upWC, camera._upWC);
}
if (rightChanged || transformChanged) {
camera._rightWC = Matrix4.multiplyByPointAsVector(
transform,
right,
camera._rightWC,
);
Cartesian3.normalize(camera._rightWC, camera._rightWC);
}
if (
positionChanged ||
directionChanged ||
upChanged ||
rightChanged ||
transformChanged
) {
updateViewMatrix(camera);
}
}
function getHeading(direction, up) {
let heading;
if (
!CesiumMath.equalsEpsilon(Math.abs(direction.z), 1.0, CesiumMath.EPSILON3)
) {
heading = Math.atan2(direction.y, direction.x) - CesiumMath.PI_OVER_TWO;
} else {
heading = Math.atan2(up.y, up.x) - CesiumMath.PI_OVER_TWO;
}
return CesiumMath.TWO_PI - CesiumMath.zeroToTwoPi(heading);
}
function getPitch(direction) {
return CesiumMath.PI_OVER_TWO - CesiumMath.acosClamped(direction.z);
}
function getRoll(direction, up, right) {
let roll = 0.0;
if (
!CesiumMath.equalsEpsilon(Math.abs(direction.z), 1.0, CesiumMath.EPSILON3)
) {
roll = Math.atan2(-right.z, up.z);
roll = CesiumMath.zeroToTwoPi(roll + CesiumMath.TWO_PI);
}
return roll;
}
const scratchHPRMatrix1 = new Matrix4();
const scratchHPRMatrix2 = new Matrix4();
Object.defineProperties(Camera.prototype, {
/**
* Gets the camera's reference frame. The inverse of this transformation is appended to the view matrix.
* @memberof Camera.prototype
*
* @type {Matrix4}
* @readonly
*
* @default {@link Matrix4.IDENTITY}
*/
transform: {
get: function () {
return this._transform;
},
},
/**
* Gets the inverse camera transform.
* @memberof Camera.prototype
*
* @type {Matrix4}
* @readonly
*
* @default {@link Matrix4.IDENTITY}
*/
inverseTransform: {
get: function () {
updateMembers(this);
return this._invTransform;
},
},
/**
* Gets the view matrix.
* @memberof Camera.prototype
*
* @type {Matrix4}
* @readonly
*
* @see Camera#inverseViewMatrix
*/
viewMatrix: {
get: function () {
updateMembers(this);
return this._viewMatrix;
},
},
/**
* Gets the inverse view matrix.
* @memberof Camera.prototype
*
* @type {Matrix4}
* @readonly
*
* @see Camera#viewMatrix
*/
inverseViewMatrix: {
get: function () {
updateMembers(this);
return this._invViewMatrix;
},
},
/**
* Gets the {@link Cartographic} position of the camera, with longitude and latitude
* expressed in radians and height in meters. In 2D and Columbus View, it is possible
* for the returned longitude and latitude to be outside the range of valid longitudes
* and latitudes when the camera is outside the map.
* @memberof Camera.prototype
*
* @type {Cartographic}
* @readonly
*/
positionCartographic: {
get: function () {
updateMembers(this);
return this._positionCartographic;
},
},
/**
* Gets the position of the camera in world coordinates.
* @memberof Camera.prototype
*
* @type {Cartesian3}
* @readonly
*/
positionWC: {
get: function () {
updateMembers(this);
return this._positionWC;
},
},
/**
* Gets the view direction of the camera in world coordinates.
* @memberof Camera.prototype
*
* @type {Cartesian3}
* @readonly
*/
directionWC: {
get: function () {
updateMembers(this);
return this._directionWC;
},
},
/**
* Gets the up direction of the camera in world coordinates.
* @memberof Camera.prototype
*
* @type {Cartesian3}
* @readonly
*/
upWC: {
get: function () {
updateMembers(this);
return this._upWC;
},
},
/**
* Gets the right direction of the camera in world coordinates.
* @memberof Camera.prototype
*
* @type {Cartesian3}
* @readonly
*/
rightWC: {
get: function () {
updateMembers(this);
return this._rightWC;
},
},
/**
* Gets the camera heading in radians.
* @memberof Camera.prototype
*
* @type {number}
* @readonly
*/
heading: {
get: function () {
if (this._mode !== SceneMode.MORPHING) {
const ellipsoid = this._projection.ellipsoid;
const oldTransform = Matrix4.clone(this._transform, scratchHPRMatrix1);
const transform = Transforms.eastNorthUpToFixedFrame(
this.positionWC,
ellipsoid,
scratchHPRMatrix2,
);
this._setTransform(transform);
const heading = getHeading(this.direction, this.up);
this._setTransform(oldTransform);
return heading;
}
return undefined;
},
},
/**
* Gets the camera pitch in radians.
* @memberof Camera.prototype
*
* @type {number}
* @readonly
*/
pitch: {
get: function () {
if (this._mode !== SceneMode.MORPHING) {
const ellipsoid = this._projection.ellipsoid;
const oldTransform = Matrix4.clone(this._transform, scratchHPRMatrix1);
const transform = Transforms.eastNorthUpToFixedFrame(
this.positionWC,
ellipsoid,
scratchHPRMatrix2,
);
this._setTransform(transform);
const pitch = getPitch(this.direction);
this._setTransform(oldTransform);
return pitch;
}
return undefined;
},
},
/**
* Gets the camera roll in radians.
* @memberof Camera.prototype
*
* @type {number}
* @readonly
*/
roll: {
get: function () {
if (this._mode !== SceneMode.MORPHING) {
const ellipsoid = this._projection.ellipsoid;
const oldTransform = Matrix4.clone(this._transform, scratchHPRMatrix1);
const transform = Transforms.eastNorthUpToFixedFrame(
this.positionWC,
ellipsoid,
scratchHPRMatrix2,
);
this._setTransform(transform);
const roll = getRoll(this.direction, this.up, this.right);
this._setTransform(oldTransform);
return roll;
}
return undefined;
},
},
/**
* Gets the event that will be raised at when the camera starts to move.
* @memberof Camera.prototype
* @type {Event}
* @readonly
*/
moveStart: {
get: function () {
return this._moveStart;
},
},
/**
* Gets the event that will be raised when the camera has stopped moving.
* @memberof Camera.prototype
* @type {Event}
* @readonly
*/
moveEnd: {
get: function () {
return this._moveEnd;
},
},
/**
* Gets the event that will be raised when the camera has changed by <code>percentageChanged</code>.
* @memberof Camera.prototype
* @type {Event}
* @readonly
*/
changed: {
get: function () {
return this._changed;
},
},
});
/**
* @private
*/
Camera.prototype.update = function (mode) {
//>>includeStart('debug', pragmas.debug);
if (!defined(mode)) {
throw new DeveloperError("mode is required.");
}
if (
mode === SceneMode.SCENE2D &&
!(this.frustum instanceof OrthographicOffCenterFrustum)
) {
throw new DeveloperError(
"An OrthographicOffCenterFrustum is required in 2D.",
);
}
if (
(mode === SceneMode.SCENE3D || mode === SceneMode.COLUMBUS_VIEW) &&
!(this.frustum instanceof PerspectiveFrustum) &&
!(this.frustum instanceof OrthographicFrustum)
) {
throw new DeveloperError(
"A PerspectiveFrustum or OrthographicFrustum is required in 3D and Columbus view",
);
}
//>>includeEnd('debug');
let updateFrustum = false;
if (mode !== this._mode) {
this._mode = mode;
this._modeChanged = mode !== SceneMode.MORPHING;
updateFrustum = this._mode === SceneMode.SCENE2D;
}
if (updateFrustum) {
const frustum = (this._max2Dfrustum = this.frustum.clone());
//>>includeStart('debug', pragmas.debug);
if (!(frustum instanceof OrthographicOffCenterFrustum)) {
throw new DeveloperError(
"The camera frustum is expected to be orthographic for 2D camera control.",
);
}
//>>includeEnd('debug');
const maxZoomOut = 2.0;
const ratio = frustum.top / frustum.right;
frustum.right = this._maxCoord.x * maxZoomOut;
frustum.left = -frustum.right;
frustum.top = ratio * frustum.right;
frustum.bottom = -frustum.top;
}
if (this._mode === SceneMode.SCENE2D) {
clampMove2D(this, this.position);
}
};
const setTransformPosition = new Cartesian3();
const setTransformUp = new Cartesian3();
const setTransformDirection = new Cartesian3();
Camera.prototype._setTransform = function (transform) {
const position = Cartesian3.clone(this.positionWC, setTransformPosition);
const up = Cartesian3.clone(this.upWC, setTransformUp);
const direction = Cartesian3.clone(this.directionWC, setTransformDirection);
Matrix4.clone(transform, this._transform);
this._transformChanged = true;
updateMembers(this);
const inverse = this._actualInvTransform;
Matrix4.multiplyByPoint(inverse, position, this.position);
Matrix4.multiplyByPointAsVector(inverse, direction, this.direction);
Matrix4.multiplyByPointAsVector(inverse, up, this.up);
Cartesian3.cross(this.direction, this.up, this.right);
updateMembers(this);
};
const scratchAdjustOrthographicFrustumMousePosition = new Cartesian2();
const scratchPickRay = new Ray();
const scratchRayIntersection = new Cartesian3();
const scratchDepthIntersection = new Cartesian3();
function calculateOrthographicFrustumWidth(camera) {
// Camera is fixed to an object, so keep frustum width constant.
if (!Matrix4.equals(Matrix4.IDENTITY, camera.transform)) {
return Cartesian3.magnitude(camera.position);
}
const scene = camera._scene;
const globe = scene.globe;
const mousePosition = scratchAdjustOrthographicFrustumMousePosition;
mousePosition.x = scene.drawingBufferWidth / scene.pixelRatio / 2.0;
mousePosition.y = scene.drawingBufferHeight / scene.pixelRatio / 2.0;
let rayIntersection;
if (defined(globe)) {
const ray = camera.getPickRay(mousePosition, scratchPickRay);
rayIntersection = globe.pickWorldCoordinates(
ray,
scene,
true,
scratchRayIntersection,
);
}
let depthIntersection;
if (scene.pickPositionSupported) {
depthIntersection = scene.pickPositionWorldCoordinates(
mousePosition,
scratchDepthIntersection,
);
}
let distance;
if (defined(rayIntersection) || defined(depthIntersection)) {
const depthDistance = defined(depthIntersection)
? Cartesian3.distance(depthIntersection, camera.positionWC)
: Number.POSITIVE_INFINITY;
const rayDistance = defined(rayIntersection)
? Cartesian3.distance(rayIntersection, camera.positionWC)
: Number.POSITIVE_INFINITY;
distance = Math.min(depthDistance, rayDistance);
} else {
distance = Math.max(camera.positionCartographic.height, 0.0);
}
return distance;
}
Camera.prototype._adjustOrthographicFrustum = function (zooming) {
if (!(this.frustum instanceof OrthographicFrustum)) {
return;
}
if (!zooming && this._positionCartographic.height < 150000.0) {
return;
}
this.frustum.width = calculateOrthographicFrustumWidth(this);
};
const scratchSetViewCartesian = new Cartesian3();
const scratchSetViewTransform1 = new Matrix4();
const scratchSetViewTransform2 = new Matrix4();
const scratchSetViewQuaternion = new Quaternion();
const scratchSetViewMatrix3 = new Matrix3();
const scratchSetViewCartographic = new Cartographic();
function setView3D(camera, position, hpr) {
//>>includeStart('debug', pragmas.debug);
if (isNaN(position.x) || isNaN(position.y) || isNaN(position.z)) {
throw new DeveloperError("position has a NaN component");
}
//>>includeEnd('debug');
const currentTransform = Matrix4.clone(
camera.transform,
scratchSetViewTransform1,
);
const localTransform = Transforms.eastNorthUpToFixedFrame(
position,
camera._projection.ellipsoid,
scratchSetViewTransform2,
);
camera._setTransform(localTransform);
Cartesian3.clone(Cartesian3.ZERO, camera.position);
hpr.heading = hpr.heading - CesiumMath.PI_OVER_TWO;
const rotQuat = Quaternion.fromHeadingPitchRoll(
hpr,
scratchSetViewQuaternion,
);
const rotMat = Matrix3.fromQuaternion(rotQuat, scratchSetViewMatrix3);
Matrix3.getColumn(rotMat, 0, camera.direction);
Matrix3.getColumn(rotMat, 2, camera.up);
Cartesian3.cross(camera.direction, camera.up, camera.right);
camera._setTransform(currentTransform);
camera._adjustOrthographicFrustum(true);
}
function setViewCV(camera, position, hpr, convert) {
const currentTransform = Matrix4.clone(
camera.transform,
scratchSetViewTransform1,
);
camera._setTransform(Matrix4.IDENTITY);
if (!Cartesian3.equals(position, camera.positionWC)) {
if (convert) {
const projection = camera._projection;
const cartographic = projection.ellipsoid.cartesianToCartographic(
position,
scratchSetViewCartographic,
);
position = projection.project(cartographic, scratchSetViewCartesian);
}
Cartesian3.clone(position, camera.position);
}
hpr.heading = hpr.heading - CesiumMath.PI_OVER_TWO;
const rotQuat = Quaternion.fromHeadingPitchRoll(
hpr,
scratchSetViewQuaternion,
);
const rotMat = Matrix3.fromQuaternion(rotQuat, scratchSetViewMatrix3);
Matrix3.getColumn(rotMat, 0, camera.direction);
Matrix3.getColumn(rotMat, 2, camera.up);
Cartesian3.cross(camera.direction, camera.up, camera.right);
camera._setTransform(currentTransform);
camera._adjustOrthographicFrustum(true);
}
function setView2D(camera, position, hpr, convert) {
const currentTransform = Matrix4.clone(
camera.transform,
scratchSetViewTransform1,
);
camera._setTransform(Matrix4.IDENTITY);
if (!Cartesian3.equals(position, camera.positionWC)) {
if (convert) {
const projection = camera._projection;
const cartographic = projection.ellipsoid.cartesianToCartographic(
position,
scratchSetViewCartographic,
);
position = projection.project(cartographic, scratchSetViewCartesian);
}
Cartesian2.clone(position, camera.position);
const newLeft = -position.z * 0.5;
const newRight = -newLeft;
const frustum = camera.frustum;
if (newRight > newLeft) {
const ratio = frustum.top / frustum.right;
frustum.right = newRight;
frustum.left = newLeft;
frustum.top = frustum.right * ratio;
frustum.bottom = -frustum.top;
}
}
if (camera._scene.mapMode2D === MapMode2D.ROTATE) {
hpr.heading = hpr.heading - CesiumMath.PI_OVER_TWO;
hpr.pitch = -CesiumMath.PI_OVER_TWO;
hpr.roll = 0.0;
const rotQuat = Quaternion.fromHeadingPitchRoll(
hpr,
scratchSetViewQuaternion,
);
const rotMat = Matrix3.fromQuaternion(rotQuat, scratchSetViewMatrix3);
Matrix3.getColumn(rotMat, 2, camera.up);
Cartesian3.cross(camera.direction, camera.up, camera.right);
}
camera._setTransform(currentTransform);
}
const scratchToHPRDirection = new Cartesian3();
const scratchToHPRUp = new Cartesian3();
const scratchToHPRRight = new Cartesian3();
function directionUpToHeadingPitchRoll(camera, position, orientation, result) {
const direction = Cartesian3.clone(
orientation.direction,
scratchToHPRDirection,
);
const up = Cartesian3.clone(orientation.up, scratchToHPRUp);
if (camera._scene.mode === SceneMode.SCENE3D) {
const ellipsoid = camera._projection.ellipsoid;
const transform = Transforms.eastNorthUpToFixedFrame(
position,
ellipsoid,
scratchHPRMatrix1,
);
const invTransform = Matrix4.inverseTransformation(
transform,
scratchHPRMatrix2,
);
Matrix4.multiplyByPointAsVector(invTransform, direction, direction);
Matrix4.multiplyByPointAsVector(invTransform, up, up);
}
const right = Cartesian3.cross(direction, up, scratchToHPRRight);
result.heading = getHeading(direction, up);
result.pitch = getPitch(direction);
result.roll = getRoll(direction, up, right);
return result;
}
const scratchSetViewOptions = {
destination: undefined,
orientation: {
direction: undefined,
up: undefined,
heading: undefined,
pitch: undefined,
roll: undefined,
},
convert: undefined,
endTransform: undefined,
};
const scratchHpr = new HeadingPitchRoll();
/**
* Sets the camera position, orientation and transform.
*
* @param {object} options Object with the following properties:
* @param {Cartesian3|Rectangle} [options.destination] The final position of the camera in world coordinates or a rectangle that would be visible from a top-down view.
* @param {HeadingPitchRollValues|DirectionUp} [options.orientation] An object that contains either direction and up properties or heading, pitch and roll properties. By default, the direction will point
* towards the center of the frame in 3D and in the negative z direction in Columbus view. The up direction will point towards local north in 3D and in the positive
* y direction in Columbus view. Orientation is not used in 2D when in infinite scrolling mode.
* @param {Matrix4} [options.endTransform] Transform matrix representing the reference frame of the camera.
* @param {boolean} [options.convert] Whether to convert the destination from world coordinates to scene coordinates (only relevant when not using 3D). Defaults to <code>true</code>.
*
* @example
* // 1. Set position with a top-down view
* viewer.camera.setView({
* destination : Cesium.Cartesian3.fromDegrees(-117.16, 32.71, 15000.0)
* });
*
* // 2 Set view with heading, pitch and roll
* viewer.camera.setView({
* destination : cartesianPosition,
* orientation: {
* heading : Cesium.Math.toRadians(90.0), // east, default value is 0.0 (north)
* pitch : Cesium.Math.toRadians(-90), // default value (looking down)
* roll : 0.0 // default value
* }
* });
*
* // 3. Change heading, pitch and roll with the camera position remaining the same.
* viewer.camera.setView({
* orientation: {
* heading : Cesium.Math.toRadians(90.0), // east, default value is 0.0 (north)
* pitch : Cesium.Math.toRadians(-90), // default value (looking down)
* roll : 0.0 // default value
* }
* });
*
*
* // 4. View rectangle with a top-down view
* viewer.camera.setView({
* destination : Cesium.Rectangle.fromDegrees(west, south, east, north)
* });
*
* // 5. Set position with an orientation using unit vectors.
* viewer.camera.setView({
* destination : Cesium.Cartesian3.fromDegrees(-122.19, 46.25, 5000.0),
* orientation : {
* direction : new Cesium.Cartesian3(-0.04231243104240401, -0.20123236049443421, -0.97862924300734),
* up : new Cesium.Cartesian3(-0.47934589305293746, -0.8553216253114552, 0.1966022179118339)
* }
* });
*/
Camera.prototype.setView = function (options) {
options = options ?? Frozen.EMPTY_OBJECT;
let orientation = options.orientation ?? Frozen.EMPTY_OBJECT;
const mode = this._mode;
if (mode === SceneMode.MORPHING) {
return;
}
if (defined(options.endTransform)) {
this._setTransform(options.endTransform);
}
let convert = options.convert ?? true;
let destination =
options.destination ??
Cartesian3.clone(this.positionWC, scratchSetViewCartesian);
if (defined(destination) && defined(destination.west)) {
destination = this.getRectangleCameraCoordinates(
destination,
scratchSetViewCartesian,
);
//>>includeStart('debug', pragmas.debug);
// destination.z may be null in 2D, but .x and .y should be numeric
if (isNaN(destination.x) || isNaN(destination.y)) {
throw new DeveloperError(`destination has a NaN component`);
}
//>>includeEnd('debug');
convert = false;
}
if (defined(orientation.direction)) {
orientation = directionUpToHeadingPitchRoll(
this,
destination,
orientation,
scratchSetViewOptions.orientation,
);
}
scratchHpr.heading = orientation.heading ?? 0.0;
scratchHpr.pitch = orientation.pitch ?? -CesiumMath.PI_OVER_TWO;
scratchHpr.roll = orientation.roll ?? 0.0;
if (mode === SceneMode.SCENE3D) {
setView3D(this, destination, scratchHpr);
} else if (mode === SceneMode.SCENE2D) {
setView2D(this, destination, scratchHpr, convert);
} else {
setViewCV(this, destination, scratchHpr, convert);
}
};
const pitchScratch = new Cartesian3();
/**
* Fly the camera to the home view. Use {@link Camera#.DEFAULT_VIEW_RECTANGLE} to set
* the default view for the 3D scene. The home view for 2D and columbus view shows the
* entire map.
*
* @param {number} [duration] The duration of the flight in seconds. If omitted, Cesium attempts to calculate an ideal duration based on the distance to be traveled by the flight. See {@link Camera#flyTo}
*/
Camera.prototype.flyHome = function (duration) {
const mode = this._mode;
if (mode === SceneMode.MORPHING) {
this._scene.completeMorph();
}
if (mode === SceneMode.SCENE2D) {
this.flyTo({
destination: Camera.DEFAULT_VIEW_RECTANGLE,
duration: duration,
endTransform: Matrix4.IDENTITY,
});
} else if (mode === SceneMode.SCENE3D) {
const destination = this.getRectangleCameraCoordinates(
Camera.DEFAULT_VIEW_RECTANGLE,
);
let mag = Cartesian3.magnitude(destination);
mag += mag * Camera.DEFAULT_VIEW_FACTOR;
Cartesian3.normalize(destination, destination);
Cartesian3.multiplyByScalar(destination, mag, destination);
this.flyTo({
destination: destination,
duration: duration,
endTransform: Matrix4.IDENTITY,
});
} else if (mode === SceneMode.COLUMBUS_VIEW) {
const maxRadii = this._projection.ellipsoid.maximumRadius;
let position = new Cartesian3(0.0, -1.0, 1.0);
position = Cartesian3.multiplyByScalar(
Cartesian3.normalize(position, position),
5.0 * maxRadii,
position,
);
this.flyTo({
destination: position,
duration: duration,
orientation: {
heading: 0.0,
pitch: -Math.acos(Cartesian3.normalize(position, pitchScratch).z),
roll: 0.0,
},
endTransform: Matrix4.IDENTITY,
convert: false,
});
}
};
/**
* Transform a vector or point from world coordinates to the camera's reference frame.
*
* @param {Cartesian4} cartesian The vector or point to transform.
* @param {Cartesian4} [result] The object onto which to store the result.
* @returns {Cartesian4} The transformed vector or point.
*/
Camera.prototype.worldToCameraCoordinates = function (cartesian, result) {
//>>includeStart('debug', pragmas.debug);
if (!defined(cartesian)) {
throw new DeveloperError("cartesian is required.");
}
//>>includeEnd('debug');
if (!defined(result)) {
result = new Cartesian4();
}
updateMembers(this);
return Matrix4.multiplyByVector(this._actualInvTransform, cartesian, result);
};
/**
* Transform a point from world coordinates to the camera's reference frame.
*
* @param {Cartesian3} cartesian The point to transform.
* @param {Cartesian3} [result] The object onto which to store the result.
* @returns {Cartesian3} The transformed point.
*/
Camera.prototype.worldToCameraCoordinatesPoint = function (cartesian, result) {
//>>includeStart('debug', pragmas.debug);
if (!defined(cartesian)) {
throw new DeveloperError("cartesian is required.");
}
//>>includeEnd('debug');
if (!defined(result)) {
result = new Cartesian3();
}
updateMembers(this);
return Matrix4.multiplyByPoint(this._actualInvTransform, cartesian, result);
};
/**
* Transform a vector from world coordinates to the camera's reference frame.
*
* @param {Cartesian3} cartesian The vector to transform.
* @param {Cartesian3} [result] The object onto which to store the result.
* @returns {Cartesian3} The transformed vector.
*/
Camera.prototype.worldToCameraCoordinatesVector = function (cartesian, result) {
//>>includeStart('debug', pragmas.debug);
if (!defined(cartesian)) {
throw new DeveloperError("cartesian is required.");
}
//>>includeEnd('debug');
if (!defined(result)) {
result = new Cartesian3();
}
updateMembers(this);
return Matrix4.multiplyByPointAsVector(
this._actualInvTransform,
cartesian,
result,
);
};
/**
* Transform a vector or point from the camera's reference frame to world coordinates.
*
* @param {Cartesian4} cartesian The vector or point to transform.
* @param {Cartesian4} [result] The object onto which to store the result.
* @returns {Cartesian4} The transformed vector or point.
*/
Camera.prototype.cameraToWorldCoordinates = function (cartesian, result) {
//>>includeStart('debug', pragmas.debug);
if (!defined(cartesian)) {
throw new DeveloperError("cartesian is required.");
}
//>>includeEnd('debug');
if (!defined(result)) {
result = new Cartesian4();
}
updateMembers(this);
return Matrix4.multiplyByVector(this._actualTransform, cartesian, result);
};
/**
* Transform a point from the camera's reference frame to world coordinates.
*
* @param {Cartesian3} cartesian The point to transform.
* @param {Cartesian3} [result] The object onto which to store the result.
* @returns {Cartesian3} The transformed point.
*/
Camera.prototype.cameraToWorldCoordinatesPoint = function (cartesian, result) {
//>>includeStart('debug', pragmas.debug);
if (!defined(cartesian)) {
throw new DeveloperError("cartesian is required.");
}
//>>includeEnd('debug');
if (!defined(result)) {
result = new Cartesian3();
}
updateMembers(this);
return Matrix4.multiplyByPoint(this._actualTransform, cartesian, result);
};
/**
* Transform a vector from the camera's reference frame to world coordinates.
*
* @param {Cartesian3} cartesian The vector to transform.
* @param {Cartesian3} [result] The object onto which to store the result.
* @returns {Cartesian3} The transformed vector.
*/
Camera.prototype.cameraToWorldCoordinatesVector = function (cartesian, result) {
//>>includeStart('debug', pragmas.debug);
if (!defined(cartesian)) {
throw new DeveloperError("cartesian is required.");
}
//>>includeEnd('debug');
if (!defined(result)) {
result = new Cartesian3();
}
updateMembers(this);
return Matrix4.multiplyByPointAsVector(
this._actualTransform,
cartesian,
result,
);
};
function clampMove2D(camera, position) {
const rotatable2D = camera._scene.mapMode2D === MapMode2D.ROTATE;
const maxProjectedX = camera._maxCoord.x;
const maxProjectedY = camera._maxCoord.y;
let minX;
let maxX;
if (rotatable2D) {
maxX = maxProjectedX;
minX = -maxX;
} else {