@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
254 lines • 12.2 kB
JavaScript
import { __decorate } from "../../tslib.es6.js";
import { serialize } from "../../Misc/decorators.js";
import { CameraInputTypes } from "../../Cameras/cameraInputsManager.js";
import { PointerEventTypes } from "../../Events/pointerEvents.js";
import { Plane } from "../../Maths/math.plane.js";
import { Vector3, Matrix, TmpVectors } from "../../Maths/math.vector.js";
import { Epsilon } from "../../Maths/math.constants.js";
import { EventConstants } from "../../Events/deviceInputEvents.js";
import { Clamp } from "../../Maths/math.scalar.functions.js";
import { Tools } from "../../Misc/tools.js";
/**
* Firefox uses a different scheme to report scroll distances to other
* browsers. Rather than use complicated methods to calculate the exact
* multiple we need to apply, let's just cheat and use a constant.
* https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode
* https://stackoverflow.com/questions/20110224/what-is-the-height-of-a-line-in-a-wheel-event-deltamode-dom-delta-line
*/
const ffMultiplier = 40;
/**
* Manage the mouse wheel inputs to control an arc rotate camera.
* @see https://doc.babylonjs.com/features/featuresDeepDive/cameras/customizingCameraInputs
*/
export class ArcRotateCameraMouseWheelInput {
constructor() {
/**
* Gets or Set the mouse wheel precision or how fast is the camera zooming.
*/
this.wheelPrecision = 3.0;
/**
* Gets or Set the boolean value that controls whether or not the mouse wheel
* zooms to the location of the mouse pointer or not. The default is false.
*/
this.zoomToMouseLocation = false;
/**
* wheelDeltaPercentage will be used instead of wheelPrecision if different from 0.
* It defines the percentage of current camera.radius to use as delta when wheel is used.
*/
this.wheelDeltaPercentage = 0;
/**
* If set, this function will be used to set the radius delta that will be added to the current camera radius
*/
this.customComputeDeltaFromMouseWheel = null;
this._viewOffset = new Vector3(0, 0, 0);
this._globalOffset = new Vector3(0, 0, 0);
this._inertialPanning = Vector3.Zero();
}
_computeDeltaFromMouseWheelLegacyEvent(mouseWheelDelta, radius) {
let delta = 0;
const wheelDelta = mouseWheelDelta * 0.01 * this.wheelDeltaPercentage * radius;
if (mouseWheelDelta > 0) {
delta = wheelDelta / (1.0 + this.wheelDeltaPercentage);
}
else {
delta = wheelDelta * (1.0 + this.wheelDeltaPercentage);
}
return delta;
}
/**
* Attach the input controls to a specific dom element to get the input from.
* @param noPreventDefault Defines whether event caught by the controls should call preventdefault() (https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault)
*/
attachControl(noPreventDefault) {
noPreventDefault = Tools.BackCompatCameraNoPreventDefault(arguments);
this._wheel = (p) => {
//sanity check - this should be a PointerWheel event.
if (p.type !== PointerEventTypes.POINTERWHEEL) {
return;
}
const event = p.event;
let delta = 0;
const platformScale = event.deltaMode === EventConstants.DOM_DELTA_LINE ? ffMultiplier : 1; // If this happens to be set to DOM_DELTA_LINE, adjust accordingly
const wheelDelta = -(event.deltaY * platformScale);
if (this.customComputeDeltaFromMouseWheel) {
delta = this.customComputeDeltaFromMouseWheel(wheelDelta, this, event);
}
else {
if (this.wheelDeltaPercentage) {
delta = this._computeDeltaFromMouseWheelLegacyEvent(wheelDelta, this.camera.radius);
// If zooming in, estimate the target radius and use that to compute the delta for inertia
// this will stop multiple scroll events zooming in from adding too much inertia
if (delta > 0) {
let estimatedTargetRadius = this.camera.radius;
let targetInertia = this.camera.inertialRadiusOffset + delta;
for (let i = 0; i < 20; i++) {
// 20 iterations should be enough to converge
if (estimatedTargetRadius <= targetInertia) {
// We do not want a negative radius, so we break out of the loop
break;
}
if (Math.abs(targetInertia * this.camera.inertia) < 0.001) {
// We do not want to go below a certain threshold, so we break out of the loop
break;
}
estimatedTargetRadius -= targetInertia;
targetInertia *= this.camera.inertia;
}
estimatedTargetRadius = Clamp(estimatedTargetRadius, 0, Number.MAX_VALUE);
delta = this._computeDeltaFromMouseWheelLegacyEvent(wheelDelta, estimatedTargetRadius);
}
}
else {
delta = wheelDelta / (this.wheelPrecision * 40);
}
}
if (delta) {
if (this.zoomToMouseLocation) {
// If we are zooming to the mouse location, then we need to get the hit plane at the start of the zoom gesture if it doesn't exist
// The hit plane is normally calculated after the first motion and each time there's motion so if we don't do this first,
// the first zoom will be to the center of the screen
if (!this._hitPlane) {
this._updateHitPlane();
}
this._zoomToMouse(delta);
}
else {
this.camera.inertialRadiusOffset += delta;
}
}
if (event.preventDefault) {
if (!noPreventDefault) {
event.preventDefault();
}
}
};
this._observer = this.camera.getScene()._inputManager._addCameraPointerObserver(this._wheel, PointerEventTypes.POINTERWHEEL);
if (this.zoomToMouseLocation) {
this._inertialPanning.setAll(0);
}
}
/**
* Detach the current controls from the specified dom element.
*/
detachControl() {
if (this._observer) {
this.camera.getScene()._inputManager._removeCameraPointerObserver(this._observer);
this._observer = null;
this._wheel = null;
}
}
/**
* Update the current camera state depending on the inputs that have been used this frame.
* This is a dynamically created lambda to avoid the performance penalty of looping for inputs in the render loop.
*/
checkInputs() {
if (!this.zoomToMouseLocation) {
return;
}
const camera = this.camera;
const motion = 0.0 + camera.inertialAlphaOffset + camera.inertialBetaOffset + camera.inertialRadiusOffset;
if (motion) {
// if zooming is still happening as a result of inertia, then we also need to update
// the hit plane.
this._updateHitPlane();
// Note we cannot use arcRotateCamera.inertialPlanning here because arcRotateCamera panning
// uses a different panningInertia which could cause this panning to get out of sync with
// the zooming, and for this to work they must be exactly in sync.
camera.target.addInPlace(this._inertialPanning);
this._inertialPanning.scaleInPlace(camera.inertia);
this._zeroIfClose(this._inertialPanning);
}
}
/**
* Gets the class name of the current input.
* @returns the class name
*/
getClassName() {
return "ArcRotateCameraMouseWheelInput";
}
/**
* Get the friendly name associated with the input class.
* @returns the input friendly name
*/
getSimpleName() {
return "mousewheel";
}
_updateHitPlane() {
const camera = this.camera;
const direction = camera.target.subtract(camera.position);
this._hitPlane = Plane.FromPositionAndNormal(camera.target, direction);
}
// Get position on the hit plane
_getPosition() {
const camera = this.camera;
const scene = camera.getScene();
// since the _hitPlane is always updated to be orthogonal to the camera position vector
// we don't have to worry about this ray shooting off to infinity. This ray creates
// a vector defining where we want to zoom to.
const ray = scene.createPickingRay(scene.pointerX, scene.pointerY, Matrix.Identity(), camera, false);
// Since the camera is the origin of the picking ray, we need to offset it by the camera's offset manually
// Because the offset is in view space, we need to convert it to world space first
if (camera.targetScreenOffset.x !== 0 || camera.targetScreenOffset.y !== 0) {
this._viewOffset.set(camera.targetScreenOffset.x, camera.targetScreenOffset.y, 0);
camera.getViewMatrix().invertToRef(camera._cameraTransformMatrix);
this._globalOffset = Vector3.TransformNormal(this._viewOffset, camera._cameraTransformMatrix);
ray.origin.addInPlace(this._globalOffset);
}
let distance = 0;
if (this._hitPlane) {
distance = ray.intersectsPlane(this._hitPlane) ?? 0;
}
// not using this ray again, so modifying its vectors here is fine
return ray.origin.addInPlace(ray.direction.scaleInPlace(distance));
}
_zoomToMouse(delta) {
const camera = this.camera;
const inertiaComp = 1 - camera.inertia;
if (camera.lowerRadiusLimit) {
const lowerLimit = camera.lowerRadiusLimit ?? 0;
if (camera.radius - (camera.inertialRadiusOffset + delta) / inertiaComp < lowerLimit) {
delta = (camera.radius - lowerLimit) * inertiaComp - camera.inertialRadiusOffset;
}
}
if (camera.upperRadiusLimit) {
const upperLimit = camera.upperRadiusLimit ?? 0;
if (camera.radius - (camera.inertialRadiusOffset + delta) / inertiaComp > upperLimit) {
delta = (camera.radius - upperLimit) * inertiaComp - camera.inertialRadiusOffset;
}
}
const zoomDistance = delta / inertiaComp;
const ratio = zoomDistance / camera.radius;
const vec = this._getPosition();
// Now this vector tells us how much we also need to pan the camera
// so the targeted mouse location becomes the center of zooming.
const directionToZoomLocation = TmpVectors.Vector3[6];
vec.subtractToRef(camera.target, directionToZoomLocation);
directionToZoomLocation.scaleInPlace(ratio);
directionToZoomLocation.scaleInPlace(inertiaComp);
this._inertialPanning.addInPlace(directionToZoomLocation);
camera.inertialRadiusOffset += delta;
}
// Sets x y or z of passed in vector to zero if less than Epsilon.
_zeroIfClose(vec) {
if (Math.abs(vec.x) < Epsilon) {
vec.x = 0;
}
if (Math.abs(vec.y) < Epsilon) {
vec.y = 0;
}
if (Math.abs(vec.z) < Epsilon) {
vec.z = 0;
}
}
}
__decorate([
serialize()
], ArcRotateCameraMouseWheelInput.prototype, "wheelPrecision", void 0);
__decorate([
serialize()
], ArcRotateCameraMouseWheelInput.prototype, "zoomToMouseLocation", void 0);
__decorate([
serialize()
], ArcRotateCameraMouseWheelInput.prototype, "wheelDeltaPercentage", void 0);
CameraInputTypes["ArcRotateCameraMouseWheelInput"] = ArcRotateCameraMouseWheelInput;
//# sourceMappingURL=arcRotateCameraMouseWheelInput.js.map