Source: components/CameraControlComponent.js

import { Flags, StateGroup, Timer } from '@uino/base-thing';
import { Utils } from '../common/Utils';
import { MathUtils } from '../math/MathUtils';
import { BaseComponent } from './BaseComponent';
import { EventType, ProjectionType, PickType } from '../const';

const __ = {
	private: Symbol('private'),
}

const Flag = {
	Damping: 1 << 0,
	Rotate: 1 << 1,
	Pan: 1 << 2,
	Zoom: 1 << 3,
	MapControl: 1 << 4,
	AutoMoveTargetForward: 1 << 5,
	AutoAdjustTargetPosition: 1 << 6,
	AutoAdjustNear: 1 << 7,
	AutoAdjustPanSpeed: 1 << 8,
	ZoomToMouseCursorOnWheel: 1 << 9,
	PickWhenChanging: 1 << 10,
	Changing: 1 << 11,
	EnableUpdateTargetByChanging: 1 << 12,
}

const cZoomToMouseCursorOnWheel = '__cZoomToMouseCursorOnWheel__';

const cZoomEventTag = '__cZoomEventTag__';
const cZoomToMouseCursorOnWheelEventTag = '__cZoomToMouseCursorOnWheelEventTag__';
const cAutoAdjustTargetPositionEventTag = '__cAutoAdjustTargetPositionEventTag__';
const cAppClickEventTag = '__cAppClickEventTag__';
const cAppDBLClickEventTag = '__cAppDBLClickEventTag__';

const cPickerGroupName = 'OrbitControls-start';
const cPickerPriority = 100 * 100;

let _vec3 = MathUtils.createVec3();
let _position = MathUtils.createVec3();
let _target = MathUtils.createVec3();
let _forward = MathUtils.createVec3();
let _direction = MathUtils.createVec3();

// #region Private Functions

// #endregion

/**
 * @class CameraControlComponent
 * The camera controller component.
 * @memberof THING
 * @extends THING.BaseComponent
 * @public
 */
class CameraControlComponent extends BaseComponent {

	/**
	 * The camera controller.
	 */
	constructor() {
		super();

		this[__.private] = {};
		let _private = this[__.private];

		_private.controller = null;
		_private.flags = new Flags();
		_private.stateGroup = new StateGroup({
			onChange: (ev) => {
				let controller = this.getController();
				controller.active(ev.state);
			}
		});

		_private.changeTickcount = 0;

		_private.crossPlanes = null;

		_private.intervalTimers = {};

		_private.adjustCallbacks = {
			panSpeed: null,
		};

		_private.distanceLimited = null;

		_private.boundary = null;

		_private.zoomToMouseCursorOnWheelSpeed = 0.05;
	}

	// #region Private Functions

	// Unproject screen to world position.
	_unprojectScreenToWorld(x, y, factor = -1) {
		let app = this.app;

		// Fix by pixel ratio
		let pixelRatio = app.pixelRatio;
		x *= pixelRatio;
		y *= pixelRatio;

		// Convert position into [-1, 1] range
		let size = app.size;
		let pX = (x / size[0]) * 2 - 1;
		let pY = -(y / size[1]) * 2 + 1;

		// Convert to world position
		this.object.node.unproject(_position, MathUtils.vec3.set(_position, pX, pY, factor));

		return _position;
	}

	// Clear picker state.
	_clearPickerState() {
		this.app.picker.stateGroup.enable(true, cPickerGroupName, -cPickerPriority);
	}

	// Register events.
	_registerEvents() {
		let app = this.app;

		app.on('click', (ev) => {
			app.picker.stateGroup.enable(true, cPickerGroupName, -cPickerPriority);
		}, cAppClickEventTag);

		app.on('dblclick', (ev) => {
			app.picker.stateGroup.enable(true, cPickerGroupName, -cPickerPriority);
		}, cAppDBLClickEventTag);
	}

	// Unregister events.
	_unregisterEvents() {
		let app = this.app;

		app.off('click', cAppClickEventTag);
		app.off('dblclick', cAppDBLClickEventTag);
	}

	_createCrossPlanes() {
		let _private = this[__.private];

		// Create cross planes to pick
		let plane = this.app.global.cache.models['plane'];
		const size = 1000 * 1000 * 1000;
		const planeSize = [size, size, size];

		// Create horz plane
		let horzPlane = plane.clone();
		horzPlane.setVisible(false);
		horzPlane.setScale(planeSize);
		horzPlane.setQuaternion(MathUtils.getQuatFromAngles([-90, 0, 0]));

		// Create vert plane
		let vertPlane = plane.clone();
		vertPlane.setVisible(false);
		vertPlane.setScale(planeSize);

		// Build cross planes as scene
		let crossPlanes = Utils.createObject('Node');
		crossPlanes.setVisible(false);
		crossPlanes.add(horzPlane);
		crossPlanes.add(vertPlane);
		crossPlanes.horzPlane = horzPlane;
		crossPlanes.vertPlane = vertPlane;

		_private.crossPlanes = crossPlanes;

		// Update cross planes
		_private.intervalTimers['crossPlanes'] = new Timer({
			interval: 0.25,
			onInterval: () => {
				let object = this.object;
				if (this.isChanging) {
					let position = object.getWorldPosition(_position);
					let target = object.control.getTarget(_target);

					vertPlane.lookAt(position);

					crossPlanes.setPosition(target);
					crossPlanes.updateMatrixWorld();
				}
			}
		});
	}

	_dispatchChangingEvent() {
		let _private = this[__.private];

		_private.changeTickcount = 0;
		_private.flags.enable(Flag.Changing, true);
	}

	_createController() {
		let controller = Utils.createObject('OrbitControls', {
			camera: this.object.node,
			appDelegate: this.app.delegate
		});

		controller.active(false);

		return controller;
	}

	/**
	 * Check whether need to look at target
	 * @returns {Boolean}
	 * @private
	 */
	_isNeedLookAtTarget() {
		if (this.enableZoomToMouseCursorOnWheel) {
			return false;
		}

		let _private = this[__.private];

		if (_private.controller.isActivated()) {
			return false;
		}

		return true;
	}

	_getSourcePositionByDistance(distance) {
		let target = this.getTarget();
		let source = this.object.position;

		let direction = MathUtils.getDirection(source, target);
		let offset = MathUtils.scaleVector(direction, distance);

		return MathUtils.addVector(source, offset);
	}

	_getTargetPositionByDistance(distance) {
		let target = this.getTarget();
		let source = this.object.position;

		let direction = MathUtils.getDirection(source, target);
		let offset = MathUtils.scaleVector(direction, distance);

		return MathUtils.addVector(target, offset);
	}

	_canTargetMoveForwardByDistanceLimited(ev) {
		let _private = this[__.private];

		if (ev.delta > 0) {
			return false;
		}

		if (!this.enableAutoMoveTargetForward) {
			return false;
		}

		if (_private.distanceLimited && Utils.isValid(_private.distanceLimited[0])) {
			return false;
		}

		return true;
	}

	_updateTargetByMoveForwardAction(minDistance) {
		// Check distance
		let distance = this.distance;
		if (distance > minDistance) {
			return;
		}

		// Get the forward target position
		let targetPosition = this._getTargetPositionByDistance(minDistance * 2);

		if (this._canTargetMoveForwardByBoundary(targetPosition)) {
			// Update target position
			this.setTarget(targetPosition);
		}
	}

	_canTargetMoveForwardByBoundary(targetPosition) {
		let _private = this[__.private];

		let boundary = _private.boundary;
		if (boundary) {
			if (targetPosition[0] <= boundary.min[0] || targetPosition[1] <= boundary.min[1] || targetPosition[2] <= boundary.min[2]) {
				return false;
			}

			if (targetPosition[0] >= boundary.max[0] || targetPosition[1] >= boundary.max[1] || targetPosition[2] >= boundary.max[2]) {
				return false;
			}
		}

		return true;
	}

	_pickFromCrossPlanes(x, y, isVertical) {
		let _private = this[__.private];

		// Try to pick from cross planes
		let crossPlanes = _private.crossPlanes;
		if (!crossPlanes) {
			return null;
		}

		const vertPlane = crossPlanes.vertPlane;
		const horzPlane = crossPlanes.horzPlane;
		const picker = this.object.picker;

		let vertPlaneDistance = this.object.distance;
		let vertPlaneNormal = vertPlane.getForward(_direction);
		let horzPlaneDistance = vertPlaneDistance;
		if (!isVertical) {
			// The face facing the camera and positioned at the camera's target
			vertPlaneNormal = THING.Math.subVector(this.object.position, this.object.target);
			vertPlaneDistance = -THING.Math.dotVector(vertPlaneNormal, this.object.target);

			// The horizontal plane is always at y=0
			horzPlaneDistance = 0;
		}
		let results = [];

		let vertResult = picker.intersectPlane(x, y, vertPlaneNormal, vertPlaneDistance);
		if (vertResult) {
			results.push(vertResult);
		}

		let horzResult = picker.intersectPlane(x, y, horzPlane.getForward(_direction), horzPlaneDistance);
		if (horzResult) {
			results.push(horzResult);
		}

		return results;
	}

	_pickInWorld(x, y, testCrossPlanes = true, fastMode = false) {
		let result;

		// Try to pick from scene
		if (fastMode) {
			let prevPickType = this.object.pickType;
			this.object.pickType = PickType.GPUFast;
			result = this.object.pick(x, y);
			this.object.pickType = prevPickType;
		}
		else {
			result = this.object.pick(x, y);
		}

		if (result) {
			return result.position;
		}

		// Try to pick from cross planes
		if (testCrossPlanes) {
			result = this.pickFromCrossPlanes(x, y);
			if (result) {
				return result;
			}
		}

		return null;
	}

	_pick(x, y) {
		let pickedPosition = this._pickInWorld(x, y);
		if (pickedPosition) {
			return pickedPosition;
		}

		_position[0] = x;
		_position[1] = y;
		_position[2] = 0.5;

		let object = this.object;
		let position = object.position;
		let direction = MathUtils.normalizeVector(MathUtils.subVector(position, object.screenToWorld(_position)));
		return MathUtils.addVector(position, MathUtils.scaleVector(direction, -50));
	}

	_getCenterPickedPosition() {
		let size = this.app.size;
		let halfWidth = size[0] / 2;
		let halfHeight = size[1] / 2;

		return this._pick(halfWidth, halfHeight);
	}

	/**
	 * Update zoom to mouse cursor on wheel (delta < 0: forward, delta > 0: backward).
	 * @param {Object} ev The wheel event object.
	 * @private
	 */
	_updateZoomToMouseCursorOnWheel(ev) {
		let _private = this[__.private];

		// Get the mouse picked position
		let mousePickedPosition = this._pick(ev.x, ev.y);

		// Get the screen center picked position
		let centerPickedPosition = this._getCenterPickedPosition();

		// Get the factor for calculating offset
		let object = this.object;
		let distance = object.distanceTo(mousePickedPosition);
		let factor = distance * _private.zoomToMouseCursorOnWheelSpeed;
		factor *= this.zoomSpeed;

		// Get the offset from center to mouse picked position
		let direction = MathUtils.normalizeVector(MathUtils.subVector(mousePickedPosition, object.position));
		let offset = MathUtils.scaleVector(direction, factor);

		// Update camera position
		let cameraNode = object.node;
		cameraNode.getPosition(_position);
		if (ev.delta < 0) {
			_position = MathUtils.addVector(_position, offset);
		}
		else {
			_position = MathUtils.subVector(_position, offset);
		}
		cameraNode.setPosition(_position);

		// Update camera target
		this.setTarget(centerPickedPosition);
	}

	_enableAdjustTargetPosition(value) {
		let _private = this[__.private];

		let app = this.app;

		// Enable
		if (value) {
			app.on('mousedown', (ev) => {
				// Only works for pan move action
				if (ev.button != 2) {
					return;
				}

				// If pan is disable then skip to adjust target position
				if (!this.enablePan) {
					return;
				}

				// Get object and app size
				let object = this.object;
				let size = app.size;

				// Try to update target poistion by picking from center position of screen
				let pickedPosition = this._pickInWorld(size[0] / 2, size[1] / 2, false, true);
				if (!pickedPosition) {
					return;
				}

				// Get camera info
				let target = object.control.getTarget(_target);
				let position = object.getWorldPosition(_position);
				let forward = object.getForward(_forward);

				// Get the distances
				let curDistance = object.distance;
				let distanceToPickedPosition = MathUtils.getDistance(pickedPosition, position);
				let distanceFromTargetToPickedPosition = MathUtils.getDistance(pickedPosition, target);

				// Check for moving forward
				if (curDistance >= distanceToPickedPosition) {
					distanceFromTargetToPickedPosition *= -1;
				}

				// Get the target result
				let targetResult = MathUtils.addVector(target, MathUtils.scaleVector(forward, distanceFromTargetToPickedPosition));

				// Check limited distance
				let distanceLimited = _private.distanceLimited;
				if (distanceLimited) {
					let distanceToTarget = MathUtils.getDistance(position, targetResult);

					if (distanceToTarget <= distanceLimited[0]) {
						return;
					}

					if (distanceToTarget >= distanceLimited[1]) {
						return;
					}
				}

				// Make sure the target position is in the boundary region
				if (_private.boundary) {
					if (!MathUtils.intersectsPoint(targetResult, _private.boundary.min, _private.boundary.max)) {
						return;
					}
				}

				// Update the target
				this.setTarget(targetResult);
			}, cAutoAdjustTargetPositionEventTag);
		}
		// Disable
		else {
			app.off('mousedown', cAutoAdjustTargetPositionEventTag);
		}
	}

	_enableZoomToMouseCursorOnWheel(value) {
		let _private = this[__.private];

		let stateGroup = _private.stateGroup;

		let app = this.app;

		// Enable
		if (value) {
			this.getController().enable('Zoom', false);

			// We must wait for global cache finished
			app.global.waitForComplete().then(() => {
				// Register events
				app.on('wheel', (ev) => {
					if (stateGroup.getValue('default') === false) {
						return;
					}

					if (this.enableZoom) {
						stateGroup.enable(false, cZoomToMouseCursorOnWheel, 100000);

						this._updateZoomToMouseCursorOnWheel(ev);
					}
				}, cZoomToMouseCursorOnWheelEventTag);

				app.on('mousedown', (ev) => {
					stateGroup.enable(true, cZoomToMouseCursorOnWheel, 0);
				}, cZoomToMouseCursorOnWheelEventTag);
			});
		}
		// Disable
		else {
			this.getController().enable('Zoom', this.enableZoom);

			stateGroup.enable(true, cZoomToMouseCursorOnWheel, 0);

			// Unregister events
			app.off('wheel', cZoomToMouseCursorOnWheelEventTag);
			app.off('mousedown', cZoomToMouseCursorOnWheelEventTag);
		}
	}

	_getDistanceToTargetInPickingMode() {
		let object = this.object;

		// Get the screen position of target
		let cameraTarget = object.control.getTarget(_target);
		let screenPosition = object.worldToScreen(cameraTarget, _position);

		// Try to pick world position of target in screen
		let pickedPosition = this._pickInWorld(screenPosition[0], screenPosition[1], false, true);
		if (!pickedPosition) {
			return null;
		}

		// Get the camera distance to picked position
		let cameraPosition = object.getWorldPosition(_vec3);
		return MathUtils.getDistance(pickedPosition, cameraPosition);
	}

	_processAdjustPanSpeed() {
		let _private = this[__.private];

		if (_private.adjustCallbacks['onPanSpeed']) {
			let panSpeed = _private.adjustCallbacks['onPanSpeed']();
			if (panSpeed) {
				this.panSpeed = panSpeed;
			}
		}
		else {
			// Get the distance to target
			let distance = this.object.distance;
			if (!distance) {
				return;
			}

			// Update pan speed of render camera
			const factor = 30;
			this.panSpeed = MathUtils.clamp(1.0, 0.5, distance / factor);
		}
	}

	_processControllerEndEvent() {
		let _private = this[__.private];

		this._clearPickerState();

		_private.flags.enable(Flag.Changing, false);
	}

	/**
	 * Notify the outside world of camera changes.
	 * @param {String} eventType Event type.
	 * @private
	 */
	_change(eventType) {
		const object = this.object;

		object.getWorldPosition(_position);
		object.control.getTarget(_target);

		this.app.trigger(eventType, {
			position: _position,
			target: _target
		});
	}

	// #endregion

	// #region BaseComponent Interface

	onUpdate(deltaTime) {
		let _private = this[__.private];

		let object = this.object;

		// Update changing flag
		if (_private.changeTickcount != -1) {
			// Check pre 10ms
			if (_private.changeTickcount >= 0.01) {
				_private.changeTickcount = -1;
				_private.flags.enable(Flag.Changing, false);
			}
			else {
				_private.changeTickcount += deltaTime;
			}
		}

		// Update controller
		let controller = _private.controller;
		if (controller && controller.isActivated()) {
			this.getController().update(deltaTime);
		}

		// Update adjust near interval timer
		for (let key in _private.intervalTimers) {
			let timer = _private.intervalTimers[key];

			timer.update(deltaTime);
		}
	}

	onAdd(object) {
		super.onAdd(object);

		this._createCrossPlanes();

		this.enableDamping = true;
		this.enableRotate = true;
		this.enablePan = true;
		this.enableZoom = true;
		this.enableAutoMoveTargetForward = true;
		this.enableAdjustTargetPosition = true;
		this.enableAdjustNear = true;
		this.enableAdjustPanSpeed = true;
		this.enablePickWhenChanging = false; // Disable pick when changing to speed up
		this.enableUpdateTargetByChanging = true;

		this._registerEvents();
	}

	onRemove() {
		let _private = this[__.private];

		this._unregisterEvents();

		this.enableDamping = false;
		this.enableRotate = false;
		this.enablePan = false;
		this.enableZoom = false;
		this.enableAutoMoveTargetForward = false;
		this.enableAdjustTargetPosition = false;
		this.enableAdjustNear = false;
		this.enableAdjustPanSpeed = false;
		this.enablePickWhenChanging = true;
		this.enableUpdateTargetByChanging = false;

		if (_private.crossPlanes) {
			_private.crossPlanes.dispose();
			_private.crossPlanes = null;
		}

		if (_private.controller) {
			_private.controller.dispose();
			_private.controller = null;
		}

		if (_private.stateGroup) {
			_private.stateGroup.clear();
			_private.stateGroup = null;
		}

		super.onRemove();
	}

	onCopy(component) {
		this.target = component.target;

		this.enableDamping = component.enableDamping;
		this.enableRotate = component.enableRotate;
		this.enablePan = component.enablePan;
		this.enableZoom = component.enableZoom;
		this.enableAutoMoveTargetForward = component.enableAutoMoveTargetForward;
		this.enableAdjustTargetPosition = component.enableAdjustTargetPosition;
		this.enableAdjustNear = component.enableAdjustNear;
		this.enableAdjustPanSpeed = component.enableAdjustPanSpeed;
		this.enablePickWhenChanging = component.enablePickWhenChanging;

		this.dampingFactor = component.dampingFactor;
		this.zoomSpeed = component.zoomSpeed;
		this.rotateSpeed = component.rotateSpeed;
		this.panSpeed = component.panSpeed;
		this.keyPanSpeed = component.keyPanSpeed;
		this.zoomToMouseCursorOnWheelSpeed = component.zoomToMouseCursorOnWheelSpeed;
		this.distanceLimited = component.distanceLimited;
		this.vertAngleLimit = component.vertAngleLimit;
		this.horzAngleLimit = component.horzAngleLimit;
		this.boundary = component.boundary;
		this.target = component.target;
	}

	// #endregion

	getTarget(target = [0, 0, 0]) {
		return this.getController().getTargetWorldPosition(target);
	}

	setTarget(value) {
		if (this._isNeedLookAtTarget()) {
			this.object.node.lookAt(value);
		}

		this.getController().setTargetWorldPosition(value);
	}

	getController() {
		let _private = this[__.private];

		if (!_private.controller) {
			let controller = this._createController();
			this.setController(controller);
		}

		return _private.controller;
	}

	setController(controller) {
		let _private = this[__.private];

		if (controller) {
			// Set default options
			controller.setKeyPanSpeed(0.5);
			controller.setPanSpeed(0.5);
			controller.setRotateSpeed(0.95);
			controller.setDampingFactor(0.09);

			// Register controller event
			controller.addEventListener('preStart', (ev) => {
				this._change(EventType.CameraChangePreStart);
			});

			controller.addEventListener('start', (ev) => {
				if (!this.enablePickWhenChanging) {
					this.app.picker.stateGroup.enable(false, cPickerGroupName, cPickerPriority);
				}

				_private.flags.enable(Flag.Changing, true);

				this._change(EventType.CameraChangeStart);
			});

			controller.addEventListener('preEnd', (ev) => {
				this._processControllerEndEvent();

				this._change(EventType.CameraChangePreEnd);
			});

			controller.addEventListener('end', (ev) => {
				this._processControllerEndEvent();

				this._change(EventType.CameraChangeEnd);
			});

			controller.addEventListener('wheel', (ev) => {
				if (this._canTargetMoveForwardByDistanceLimited(ev)) {
					this._updateTargetByMoveForwardAction(5);
				}

				this._dispatchChangingEvent();

				this._change(EventType.CameraChange);
			});

			controller.addEventListener('change', (ev) => {
				this._dispatchChangingEvent();

				this._change(EventType.CameraChange);
			});

			_private.controller = controller;
		}
		else {
			if (_private.controller) {
				_private.controller.dispose();
				_private.controller = null;
			}
		}
	}

	stopZoom() {
		let object = this.object;

		object.lerp.stop(cZoomEventTag);
	}

	/**
	 * Pan in screen by pixel.
	 * @param {Number} deltaX The delta X in pixel.
	 * @param {Number} deltaY The delta Y in pixel.
	 * @param {Number} [duration=500] The time in milliseconds.
	 * @example
	 * 	// Move camera with bottom direction
	 * 	app.camera.pan(0, -50);
	 * @public
	 */
	pan(deltaX, deltaY, duration = 500) {
		let size = this.app.size;
		let object = this.object;
		let distance = object.distance;

		// half of the fov is center to top of screen
		distance *= Math.tan((object.fov / 2) * Math.PI / 180.0);

		// we use only client height here so aspect ratio does not distort speed
		let offset = [0, 0, 0];
		let leftColumn = MathUtils.getVec3FromMatrixColumn(object.matrix, 0);
		offset = MathUtils.addVector(offset, MathUtils.scaleVector(leftColumn, 2 * deltaX * distance / size[1]));
		let upColumn = MathUtils.getVec3FromMatrixColumn(object.matrix, 1);
		offset = MathUtils.addVector(offset, MathUtils.scaleVector(upColumn, 2 * deltaY * distance / size[1]));

		// Get the final position and target
		let position = MathUtils.addVector(object.position, offset);
		let target = MathUtils.addVector(object.target, offset);

		// Lerp to pan
		if (duration) {
			object.lerp.stopFlying();

			object.lerp.to({
				from: {
					position: object.position,
					target: object.target,
				},
				to: {
					position,
					target,
				},
				duration,
			}, cZoomEventTag);
		}
		// Zoom directly
		else {
			object.position = position;
			object.target = target;
		}
	}

	/**
	 * Move forward/backward.
	 * @param {Number} distance The distance (+: forward, -: backward).
	 * @param {Object} param The options.
	 * @param {Number} [param.duration=500] The time in milliseconds.
	 * @param {Boolean} [param.updateTarget=true] True indicates update target's position.
	 */
	zoom(distance, options = {}) {
		if (!distance) {
			return;
		}

		let duration = Utils.parseValue(options['duration'], Utils.parseValue(options['time'], 500));
		let updateTarget = Utils.parseValue(options['updateTarget'], true);

		let object = this.object;

		object.stopFlying();

		switch (object.projectionType) {
			case ProjectionType.Orthographic:
				let orthoDistance = Math.abs(object.orthoDistance - distance);

				// Lerp to zoom
				if (duration) {
					object.lerp.to({
						to: {
							orthoDistance: orthoDistance
						},
						duration,
					}, cZoomEventTag);
				}
				// Zoom directly
				else {
					object.orthoDistance = orthoDistance;
				}

				// Move target also to make Orhot<->Perspective much smoothly, but it's optional
				object.position = this._getSourcePositionByDistance(distance);
				if (updateTarget) {
					let direction = MathUtils.getDirection(object.position, object.target);
					object.target = MathUtils.getPositionOnDirection(object.position, direction, object.orthoDistance);
				}
				break;

			case ProjectionType.Perspective:
				let position = this._getSourcePositionByDistance(distance);

				if (updateTarget) {
					let target = this._getTargetPositionByDistance(distance);

					// Lerp to zoom
					if (duration) {
						object.lerp.to({
							from: {
								position: object.position,
								target: object.target,
							},
							to: {
								position,
								target,
							},
							duration,
						}, cZoomEventTag);
					}
					// Zoom directly
					else {
						object.position = position;
						object.target = target;
					}
				}
				else {
					// Lerp to zoom
					if (duration) {
						object.lerp.to({
							from: {
								position: object.position,
							},
							to: {
								position,
							},
							duration,
						}, cZoomEventTag);
					}
					// Zoom directly
					else {
						object.position = position;
					}
				}
				break;

			default:
				break;
		}
	}

	/**
	 * Stop current control.
	 */
	stop() {
		let _private = this[__.private];

		_private.controller.stop();
	}

	processAdjustNear() {
		const object = this.object;
		if (!object) {
			return;
		}

		const minDistance = 10;

		// Only check when it's far away from target position
		let distance = object.distance;
		if (distance <= minDistance && distance > object.near) {
			return;
		}

		// Get the distance to target by picking
		let distanceByPicking = this._getDistanceToTargetInPickingMode() || distance;

		// Update near of render camera
		const factor = 120;
		object.near = MathUtils.max(0.1, distanceByPicking / factor);
	}

	pickFromCrossPlanes(x, y, isVertical = true) {
		let results = this._pickFromCrossPlanes(x, y, isVertical);
		if (!results.length) {
			return null;
		}

		let object = this.object;

		results.sort((p1, p2) => {
			let d1 = object.distanceTo(p1);
			let d2 = object.distanceTo(p2);

			return d1 - d2;
		});

		return results[0];
	}

	/**
	 * Check whether is changing or not.
	 * @type {Boolean}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.pan(0, -50);
	 * // @expect(camera.isChanging == true)
	 * @public
	 */
	get isChanging() {
		let _private = this[__.private];

		return _private.flags.has(Flag.Changing);
	}

	/**
	 * Get the state group to active/deactive controller.
	 * @type {Object}
	 * @private
	 */
	get stateGroup() {
		return this[__.private].stateGroup;
	}

	/**
	 * Enable/Disable control.
	 * @type {Boolean}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.enable = false;
	 * // @expect(camera.enable == false)
	 * @public
	 */
	get enable() {
		// Many operations cloud disable camera control, so we just check 'default' state here
		let result = this[__.private].stateGroup.getValue('default');
		if (result === false) {
			return false;
		}

		return true;
	}
	set enable(value) {
		let _private = this[__.private];

		if (this.enableUpdateTargetByChanging) {
			this._updateTargetByChangeCameraEnable(value);
		}

		_private.stateGroup.enable(value, 'default', 1000);

		this._clearPickerState();
	}

	_updateTargetByChangeCameraEnable(value) {
		let _private = this[__.private];

		if (value === false) {
			if (!_private.changeCameraTransform) {
				_private.changeCameraTransform = true;
			}
		}
		else {
			if (_private.changeCameraTransform) {
				_private.changeCameraTransform = null;

				const vec3 = MathUtils.scaleVector(this.object.forward, this.distance);
				const target = MathUtils.addVector(this.object.position, vec3);

				this.object.target = target;
			}
		}
	}

	/**
	 * enable update target by changing camera transform.
	 * @type {Boolean}
	 * @private
	 */
	get enableUpdateTargetByChanging() {
		let _private = this[__.private];

		return _private.flags.has(Flag.EnableUpdateTargetByChanging);
	}

	set enableUpdateTargetByChanging(value) {
		let _private = this[__.private];

		_private.flags.enable(Flag.EnableUpdateTargetByChanging, value);
	}

	/**
	 * Enable/Disable auto move target forward.
	 * @type {Boolean}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.enableAutoMoveTargetForward = false;
	 * // @expect(camera.enableAutoMoveTargetForward == false)
	 * @public
	 */
	get enableAutoMoveTargetForward() {
		let _private = this[__.private];

		return _private.flags.has(Flag.AutoMoveTargetForward);
	}
	set enableAutoMoveTargetForward(value) {
		let _private = this[__.private];

		_private.flags.enable(Flag.AutoMoveTargetForward, value);
	}

	/**
	 * Enable/Disable adjust target position.
	 * @type {Boolean}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.enableAdjustTargetPosition = false;
	 * // @expect(camera.enableAdjustTargetPosition == false)
	 * @public
	 */
	get enableAdjustTargetPosition() {
		let _private = this[__.private];

		return _private.flags.has(Flag.AutoAdjustTargetPosition);
	}
	set enableAdjustTargetPosition(value) {
		let _private = this[__.private];

		if (_private.flags.enable(Flag.AutoAdjustTargetPosition, value)) {
			this._enableAdjustTargetPosition(value);
		}
	}

	/**
	 * Enable/Disable adjust near.
	 * @type {Boolean}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.enableAdjustNear = false;
	 * // @expect(camera.enableAdjustNear == false)
	 * @public
	 */
	get enableAdjustNear() {
		let _private = this[__.private];

		return _private.flags.has(Flag.AutoAdjustNear);
	}
	set enableAdjustNear(value) {
		let _private = this[__.private];

		if (_private.flags.enable(Flag.AutoAdjustNear, value)) {
			if (value) {
				let object = this.object;

				_private.intervalTimers['adjustNear'] = new Timer({
					interval: 0.2,
					onInterval: () => {
						if (object.isFlying || this.isChanging) {
							// Here we should look at target first to prevent angles change before adjust near
							let target = object.control.getTarget(_target);
							object.node.lookAt(target, object.up);

							// Here we also need to make vert plane to look at camera to make sure we can pick correct position from it
							let position = object.getWorldPosition(_position);
							_private.crossPlanes.vertPlane.lookAt(position);

							this.processAdjustNear();
						}
					}
				});
			}
			else {
				delete _private.intervalTimers['adjustNear'];
			}
		}
	}

	/**
	 * Enable/Disable adjust pan speed.
	 * @type {Boolean}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.enableAdjustPanSpeed = false;
	 * // @expect(camera.enableAdjustPanSpeed == false);
	 * @public
	 */
	get enableAdjustPanSpeed() {
		let _private = this[__.private];

		return _private.flags.has(Flag.AutoAdjustPanSpeed);
	}
	set enableAdjustPanSpeed(value) {
		let _private = this[__.private];

		if (_private.flags.enable(Flag.AutoAdjustPanSpeed, value)) {
			if (value) {
				let object = this.object;

				_private.intervalTimers['adjustPanSpeed'] = new Timer({
					interval: 0.5,
					onInterval: () => {
						if (object.isFlying || this.isChanging) {
							this._processAdjustPanSpeed();
						}
					}
				});
			}
			else {
				delete _private.intervalTimers['adjustPanSpeed'];
			}
		}
	}

	/**
	 * The function to call when start to adjust pan speed in camera control.
	 * @callback onPanSpeedCallback
	 * @returns {Number} The pan speed value.
	 * @private
	 */

	/**
	 * The auto adjust callbacks of camera control.
	 * @typedef {Object} CameraControlComponentAdjustCallbacks
	 * @property {onPanSpeedCallback} onPanSpeed When auto adjust pan speed value.
	 * @private
	 */

	/**
	 * Get/Set adjust callbacks.
	 * @type {CameraControlComponentAdjustCallbacks}
	 * @example
	 *	THING.App.current.camera.control.adjustCallbacks['onPanSpeed'] = function() {
	 *		return THING.App.current.camera.distance / 100;
	 *	}
	 * @example
	 * let camera = THING.App.current.camera;
	 * let func = function() {}
	 * camera.adjustCallbacks['onPanSpeed'] = func;
	 * // @expect(camera.adjustCallbacks['onPanSpeed'] == func);
	 * @private
	 */
	get adjustCallbacks() {
		let _private = this[__.private];

		return _private.adjustCallbacks;
	}
	set adjustCallbacks(value) {
		let _private = this[__.private];

		_private.adjustCallbacks = value;
	}

	/**
	 * Enable/Disable zoom to mouse cursor on wheel.
	 * @type {Boolean}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.enableZoomToMouseCursorOnWheel = false;
	 * // @expect(camera.enableZoomToMouseCursorOnWheel == false);
	 * @public
	 */
	get enableZoomToMouseCursorOnWheel() {
		let _private = this[__.private];

		return _private.flags.has(Flag.ZoomToMouseCursorOnWheel);
	}
	set enableZoomToMouseCursorOnWheel(value) {
		let _private = this[__.private];

		if (_private.flags.enable(Flag.ZoomToMouseCursorOnWheel, value)) {
			this._enableZoomToMouseCursorOnWheel(value);
		}
	}

	/**
	 * Enable/Disable pick when changing.
	 * @type {Boolean}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.enablePickWhenChanging = false;
	 * // @expect(camera.enablePickWhenChanging == false);
	 * @public
	 */
	get enablePickWhenChanging() {
		let _private = this[__.private];

		return _private.flags.has(Flag.PickWhenChanging);
	}
	set enablePickWhenChanging(value) {
		let _private = this[__.private];

		_private.flags.enable(Flag.PickWhenChanging, value);

		if (value) {
			this.object.app.picker.stateGroup.enable(true, cPickerGroupName, -cPickerPriority);
		}
	}

	/**
	 * Enable/Disable damping.
	 * @type {Boolean}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.enableDamping = false;
	 * // @expect(camera.enableDamping == false);
	 * @public
	 */
	get enableDamping() {
		let _private = this[__.private];

		return _private.flags.has(Flag.Damping);
	}
	set enableDamping(value) {
		let _private = this[__.private];

		if (_private.flags.enable(Flag.Damping, value)) {
			this.getController().enable('Damping', value);
		}
	}

	/**
	 * Enable/Disable rotate.
	 * @type {Boolean}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.enableRotate = false;
	 * // @expect(camera.enableRotate == false);
	 * @public
	 */
	get enableRotate() {
		let _private = this[__.private];

		return _private.flags.has(Flag.Rotate);
	}
	set enableRotate(value) {
		let _private = this[__.private];

		if (_private.flags.enable(Flag.Rotate, value)) {
			this.getController().enable('Rotate', value);
		}
	}

	/**
	 * Enable/Disable pan.
	 * @type {Boolean}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.enablePan = false;
	 * // @expect(camera.enablePan == false);
	 * @public
	 */
	get enablePan() {
		let _private = this[__.private];

		return _private.flags.has(Flag.Pan);
	}
	set enablePan(value) {
		let _private = this[__.private];

		if (_private.flags.enable(Flag.Pan, value)) {
			this.getController().enable('Pan', value);
		}
	}

	/**
	 * Enable/Disable zoom.
	 * @type {Boolean}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.enableZoom = false;
	 * // @expect(camera.enableZoom == false);
	 * @public
	 */
	get enableZoom() {
		let _private = this[__.private];

		return _private.flags.has(Flag.Zoom);
	}
	set enableZoom(value) {
		let _private = this[__.private];

		if (_private.flags.enable(Flag.Zoom, value)) {
			this.getController().enable('Zoom', value);
		}
	}

	/**
	 * Enable/Disable map control.
	 * @type {Boolean}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.enableMapControl = false;
	 * // @expect(camera.enableMapControl == false);
	 * @public
	 */
	get enableMapControl() {
		let _private = this[__.private];

		return _private.flags.has(Flag.MapControl);
	}
	set enableMapControl(value) {
		let _private = this[__.private];

		if (_private.flags.enable(Flag.MapControl, value)) {
			this.getController().enable('MapControl', value);
		}
	}

	/**
	 * Get/Set damping factor.
	 * @type {Number}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.dampingFactor = 10;
	 * // @expect(camera.dampingFactor == 10);
	 * @public
	 */
	get dampingFactor() {
		return this.getController().getDampingFactor();
	}
	set dampingFactor(value) {
		this.getController().setDampingFactor(value);
	}

	/**
	 * Get/Set zoom speed.
	 * @type {Number}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.zoomSpeed = 10;
	 * // @expect(camera.zoomSpeed == 10);
	 * @public
	 */
	get zoomSpeed() {
		return this.getController().getZoomSpeed();
	}
	set zoomSpeed(value) {
		this.getController().setZoomSpeed(value);
	}

	/**
	 * Get/Set rotate speed.
	 * @type {Number}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.rotateSpeed = 10;
	 * // @expect(camera.rotateSpeed == 10);
	 * @public
	 */
	get rotateSpeed() {
		return this.getController().getRotateSpeed();
	}
	set rotateSpeed(value) {
		this.getController().setRotateSpeed(value);
	}

	/**
	 * Get/Set pan speed.
	 * @type {Number}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.panSpeed = 10;
	 * // @expect(camera.panSpeed == 10);
	 * @public
	 */
	get panSpeed() {
		return this.getController().getPanSpeed();
	}
	set panSpeed(value) {
		this.getController().setPanSpeed(value);
	}

	/**
	 * Get/Set key pan speed.
	 * @type {Number}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.keyPanSpeed = 10;
	 * // @expect(camera.keyPanSpeed == 10);
	 * @public
	 */
	get keyPanSpeed() {
		return this.getController().getKeyPanSpeed();
	}
	set keyPanSpeed(value) {
		this.getController().setKeyPanSpeed(value);
	}

	/**
	 * Get/Set the factor of zoom to mouse cursor on wheel.
	 * @type {Number}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.zoomToMouseCursorOnWheelSpeed = 10;
	 * // @expect(camera.zoomToMouseCursorOnWheelSpeed == 10);
	 * @public
	 */
	get zoomToMouseCursorOnWheelSpeed() {
		let _private = this[__.private];

		return _private.zoomToMouseCursorOnWheelSpeed;
	}
	set zoomToMouseCursorOnWheelSpeed(value) {
		let _private = this[__.private];

		_private.zoomToMouseCursorOnWheelSpeed = value;
	}

	/**
	 * Set/Get distance limited range[min, max], null indicates it's unlimited.
	 * @type {Array<Number|null>}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.distanceLimited = [10,1000];
	 * // @expect(camera.distanceLimited[0] == 10 && camera.distanceLimited[1] == 1000);
	 * @public
	 */
	get distanceLimited() {
		let _private = this[__.private];

		return _private.distanceLimited;
	}
	set distanceLimited(value) {
		let _private = this[__.private];

		let controller = _private.controller;

		if (value) {
			_private.distanceLimited = value.slice(0);
			controller.setMinDistance(Utils.parseNumber(value[0], 0.00001));
			controller.setMaxDistance(Utils.parseNumber(value[1], Infinity));
		}
		else {
			_private.distanceLimited = null;

			controller.setMinDistance(0.00001);
			controller.setMaxDistance(Infinity);
		}
	}

	/**
	 * Set/Get vert angle limited range[min, max], default is [0, 180].
	 * @type {Array<Number>}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.vertAngleLimit = [10,100];
	 * // @expect(camera.vertAngleLimit[0] == 10 && camera.vertAngleLimit[1] == 100);
	 * @public
	 */
	get vertAngleLimit() {
		let _private = this[__.private];

		let controller = _private.controller;
		let minValue = MathUtils.radToDeg(controller.getMinPolarAngle());
		let maxValue = MathUtils.radToDeg(controller.getMaxPolarAngle());

		return [Math.min(minValue, maxValue), Math.max(minValue, maxValue)];
	}
	set vertAngleLimit(value) {
		let _private = this[__.private];

		let minValue = value[0];
		let maxValue = value[1];

		let controller = _private.controller;
		controller.setMinPolarAngle(MathUtils.degToRad(Math.min(minValue, maxValue)));
		controller.setMaxPolarAngle(MathUtils.degToRad(Math.max(minValue, maxValue)));
	}

	/**
	 * Set/Get horz angle limited range[min, max], default is [0, 180].
	 * @type {Array<Number>}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.horzAngleLimit = [10,100];
	 * // @expect(camera.horzAngleLimit[0] == 10 && camera.horzAngleLimit[1] == 100);
	 * @public
	 */
	get horzAngleLimit() {
		let _private = this[__.private];

		let controller = _private.controller;
		let minValue = MathUtils.radToDeg(controller.getMinAzimuthAngle());
		let maxValue = MathUtils.radToDeg(controller.getMaxAzimuthAngle());

		return [Math.min(minValue, maxValue), Math.max(minValue, maxValue)];
	}
	set horzAngleLimit(value) {
		let _private = this[__.private];

		let minValue = value[0];
		let maxValue = value[1];

		let controller = _private.controller;
		controller.setMinAzimuthAngle(MathUtils.degToRad(Math.min(minValue, maxValue)));
		controller.setMaxAzimuthAngle(MathUtils.degToRad(Math.max(minValue, maxValue)));
	}

	/**
	 * @typedef {Object} BoundaryResult
	 * @property {Array<Number>} center The center position.
	 * @property {Array<Number>} halfSize The half size.
	 */

	/**
	 * Get/Set boundary to limit target position(null indicates clear it).
	 * @type {BoundaryResult}
	 * @example
	 * let camera = THING.App.current.camera;
	 * let center = [10,10,10];
	 * let halfSize = [20,20,20];
	 * let boundary = {center, halfSize};
	 * camera.boundary = boundary;
	 * // @expect(camera.boundary.center[0] == 10 && camera.boundary.halfSize[0] == 20);
	 * @public
	 */
	get boundary() {
		let controller = this.getController();
		if (!controller) {
			return null;
		}

		let target = {
			center: [0, 0, 0],
			halfSize: [0, 0, 0]
		};

		if (!controller.getBoundary(target)) {
			return null;
		}

		return target;
	}
	set boundary(value) {
		let _private = this[__.private];

		let controller = this.getController();
		if (!controller) {
			return;
		}

		if (value) {
			let halfSize = value['halfSize'];
			if (!halfSize) {
				Utils.error(`Set camera control boundary failed, due to halfSize is invalid`);
				return;
			}

			let center = value['center'] || this.getTarget();

			controller.setBoundary({
				center,
				halfSize
			});

			_private.boundary = {
				min: MathUtils.subVector(center, halfSize),
				max: MathUtils.addVector(center, halfSize)
			};
		}
		else {
			_private.boundary = null;

			controller.setBoundary(null);
		}
	}

	/**
	 * Get/Set up direction.
	 * @type {Array<Number>}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.upDirection = [10,10,10];
	 * // @expect(camera.upDirection[0] == 10 && camera.upDirection[1] == 10 && camera.upDirection[2] == 10);
	 * @public
	 */
	get upDirection() {
		let controller = this.getController();
		if (!controller) {
			return null;
		}

		let target = [];
		return controller.getUpDirection(target);
	}
	set upDirection(value) {
		let controller = this.getController();
		if (!controller) {
			return;
		}

		controller.setUpDirection(value);
	}

	/**
	 * Get/Set target of the world space.
	 * @type {Array<Number>}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.target = [10,10,10];
	 * // @expect(camera.target[0] == 10 && camera.target[1] == 10 && camera.target[2] == 10);
	 * @public
	 */
	get target() {
		return this.getTarget();
	}
	set target(value) {
		this.setTarget(value);
	}

	/**
	 * Get the distance from position to target.
	 * @type {Number}
	 */
	get distance() {
		this.object.getWorldPosition(_position);
		this.getTarget(_target);

		return MathUtils.vec3.distance(_position, _target);
	}

	/**
	 * Get/Set space panning of the screen.
	 * True indicates pan action will base on screen, otherwise indicates will base on camera's Y axis direction.
	 * Default value is true.
	 * @type {Boolean}
	 * @example
	 * let camera = THING.App.current.camera;
	 * camera.screenSpacePanning = false;
	 * // @expect(camera.screenSpacePanning == false);
	 * @public
	 */
	get screenSpacePanning() {
		return this.getController().getScreenSpacePanning();
	}
	set screenSpacePanning(value) {
		this.getController().setScreenSpacePanning(value);
	}

}

CameraControlComponent.exportProperties = [
	'enable',
	'enableAutoMoveTargetForward',
	'enableAdjustTargetPosition',
	'enableAdjustNear',
	'enableAdjustPanSpeed',
	'enableZoomToMouseCursorOnWheel',
	'enableDamping',
	'enableRotate',
	'enablePan',
	'enableZoom',
	'enableMapControl',
	'dampingFactor',
	'zoomSpeed',
	'rotateSpeed',
	'panSpeed',
	'keyPanSpeed',
	'zoomToMouseCursorOnWheelSpeed',
	'distanceLimited',
	'vertAngleLimit',
	'horzAngleLimit',
	'boundary',
	'target',
	'distance',
	'screenSpacePanning',
	'enableUpdateTargetByChanging',
];

CameraControlComponent.exportFunctions = [
	'pan',
	'zoom'
];

export { CameraControlComponent }