Source: objects/Camera.js

import { StringEncoder, Flags } from '@uino/base-thing';
import { Utils } from '../common/Utils';
import { MathUtils } from '../math/MathUtils';
import { CameraTransformComponent } from '../components/CameraTransformComponent';
import { CameraLerpComponent } from '../components/CameraLerpComponent';
import { CameraHelperComponent } from '../components/CameraHelperComponent';
import { CameraBoundingComponent } from '../components/CameraBoundingComponent';
import { CameraEffectComponent } from '../components/CameraEffectComponent';
import { CameraPostEffectComponent } from '../components/CameraPostEffectComponent';
import { CameraFogComponent } from '../components/CameraFogComponent';
import { CameraPickerComponent } from '../components/CameraPickerComponent';
import { CameraControlComponent } from '../components/CameraControlComponent';
import { CameraControlEventDispatcherComponent } from '../components/CameraControlEventDispatcherComponent';
import { CameraResourceControllerComponent } from '../components/CameraResourceControllerComponent';
import { RenderTexture } from '../resources/RenderTexture';
import { Object3D } from './Object3D';
import { NodeUserDataType, InheritType, ProjectionType, ViewMode, LerpType, EventType } from '../const';
import { CameraDynamicSkyComponent } from '../components/CameraDynamicSkyComponent';

const _renderOverlayFuncName = StringEncoder.toText("<@secret renderOverlay>");
const _ClearColorBuffer = StringEncoder.toText("<@secret ClearColorBuffer>");

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

const Flag = {
	Created: 1 << 0,
	EnableViewport: 1 << 1,
	Ticking: 1 << 2
};

const registerComponentParam = { isResident: true };

/**
 * @class Camera
 * The camera object.
 * @memberof THING
 * @extends THING.Object3D
 * @public
 */
class Camera extends Object3D {

	/**
	 * The camera object in scene.
	 * @param {Object} param The initial parameters.
	 */
	constructor(param = {}) {
		super(param);

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

		_private.flags = new Flags();

		_private.projectionType = ProjectionType.Perspective;
		_private.projectionTween = null;

		_private.viewModeInfo = {
			type: null,
			target: null,
			position: null,
			lookAt: null
		};

		_private.viewport = null;
		_private.captureRenderTexture = null;
		_private.renderTexture = null;

		_private.renderableRefCount = 0;

		// Get default options
		_private.fov = Utils.parseValue(param['fov'], 60);
		_private.near = Utils.parseValue(param['near'], 0.1);
		_private.far = Utils.parseValue(param['far'], 10000);

		// Refresh attributes
		this.onRefresh();

		// Enable effect component
		this.effect.enable = true;

		// Disable render to viewport as default
		this.enableViewport = false;

		// Hook control event dispatcher
		this.controlEventDispatcher.enable = true;

		// Resize
		let size = this.app.size;
		this.resize(size[0], size[1]);

		// Set default background color
		this.background = 0xFFFFFF;

		// Setup completed
		_private.flags.enable(Flag.Created, true);
	}

	// #region Private

	/**
	 * Get position, target and up for view mode in perspective
	 * @param {ViewMode} viewMode The view mode type.
	 * @param {THING.Object3D} target The target object.
	 * @returns {Object}
	 * @private
	 */
	_getLocationOfViewModeInPerspective(viewMode, target) {
		let lookAt, position, up;

		let boundingBox = target.boundingBox;
		let distance = -(boundingBox.radius / 0.5) / (2 * Math.tan(THING.Math.degToRad(this.fov) / 2));

		switch (viewMode) {
			case ViewMode.Top:
				lookAt = boundingBox.center;
				position = MathUtils.addVector(lookAt, [0, -distance, 0.01]);
				up = [0, 0, -1];
				break;

			case ViewMode.Bottom:
				lookAt = boundingBox.center;
				position = MathUtils.addVector(lookAt, [0, distance, 0.01]);
				up = [0, 0, 1];
				break;

			case ViewMode.Left:
				lookAt = boundingBox.center;
				position = MathUtils.addVector(lookAt, [-distance, 0, 0]);
				up = [0, 1, 0];
				break;

			case ViewMode.Right:
				lookAt = boundingBox.center;
				position = MathUtils.addVector(lookAt, [distance, 0, 0]);
				up = [0, 1, 0];
				break;

			case ViewMode.Front:
				lookAt = boundingBox.center;
				position = MathUtils.addVector(lookAt, [0, 0, -distance]);
				up = [0, 1, 0];
				break;

			case ViewMode.Back:
				lookAt = boundingBox.center;
				position = MathUtils.addVector(lookAt, [0, 0, distance]);
				up = [0, 1, 0];
				break;

			default:
				return;
		}

		return { lookAt, position, up };
	}

	/**
	 * Get position, target and up for view mode in orthographic
	 * @param {ViewMode} viewMode The view mode type.
	 * @param {THING.Object3D} target The target object.
	 * @returns {Object}
	 * @private
	 */
	_getLocationOfViewModeInOrthographic(viewMode, target) {
		let { lookAt, position, up } = this._getLocationOfViewModeInPerspective(viewMode, target);

		let z = MathUtils.getDistance(position, lookAt);
		let depth = Math.tan(this.fov / 2.0 * Math.PI / 180.0) * 2.0;
		let distance = depth * z * 0.5;

		let direction = MathUtils.getDirection(position, lookAt);
		lookAt = MathUtils.getPositionOnDirection(position, direction, distance);

		return { lookAt, position, up, distance };
	}

	/**
	 * Get position, target and up for view mode
	 * @param {ViewMode} viewMode The view mode type.
	 * @param {THING.Object3D} target The target object.
	 * @returns {Object}
	 * @private
	 */
	_getLocationOfViewMode(viewMode, target) {
		let _private = this[__.private];

		switch (_private.projectionType) {
			case ProjectionType.Orthographic:
				return this._getLocationOfViewModeInOrthographic(viewMode, target);

			case ProjectionType.Perspective:
				return this._getLocationOfViewModeInPerspective(viewMode, target);

			default:
				return null;
		}
	}

	_setViewMode(target, viewMode, duration) {
		let _private = this[__.private];

		let viewModeInfo = _private.viewModeInfo;
		viewModeInfo.type = viewMode;
		viewModeInfo.target = target;
		viewModeInfo.position = viewModeInfo.position || this.position;
		viewModeInfo.lookAt = viewModeInfo.lookAt || this.target;

		// Enable view mode
		if (viewMode) {
			let { lookAt, position, up, distance } = this._getLocationOfViewMode(viewMode, target);

			// Start to lerp
			this.flyTo({
				target: lookAt,
				position,
				up,
				duration,
			});

			// Update distance for ortho
			if (distance !== undefined) {
				this.orthoDistance = distance;
			}
		}
		// Clear view mode
		else {
			let lookAt, position;

			if (viewModeInfo.position) {
				position = viewModeInfo.position;
				viewModeInfo.position = null;
			}

			if (viewModeInfo.lookAt) {
				lookAt = viewModeInfo.lookAt;
				viewModeInfo.lookAt = null;
			}

			// Start to lerp
			this.flyTo({
				target: lookAt,
				position,
				up: [0, 1, 0],
				duration,
			});
		}
	}

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

		let scene = this.app.scene;

		if (value) {
			_private.renderableRefCount++;
			scene.addRenderCamera(this);
		}
		else {
			_private.renderableRefCount--;
			if (_private.renderableRefCount) {
				return;
			}

			scene.removeRenderCamera(this);
		}
	}

	_getSize(width, height) {
		let currentSize = [];
		this.node.getSize(currentSize);

		return [
			Utils.parseValue(width, currentSize[0]),
			Utils.parseValue(height, currentSize[1]),
		];
	}

	// #endregion

	// #region Common

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

		if (!_private) {
			return false;
		}

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

	// #endregion

	// #region BaseObject Interface

	onBeforeSetup(param) {
		param['renderableNode'] = param['renderableNode'] || Utils.createObject('Camera');

		super.onBeforeSetup(param);
	}

	onSetupComponent(param) {
		super.onSetupComponent(param);

		this.addComponent(CameraTransformComponent, 'transform', registerComponentParam);
		this.addComponent(CameraControlComponent, 'control', registerComponentParam);
		this.addComponent(CameraLerpComponent, 'lerp', registerComponentParam);
		this.addComponent(CameraHelperComponent, 'helper', registerComponentParam);
		this.addComponent(CameraBoundingComponent, 'bounding', registerComponentParam);
		this.addComponent(CameraEffectComponent, 'effect', registerComponentParam);
		this.addComponent(CameraPostEffectComponent, 'postEffect', registerComponentParam);
		this.addComponent(CameraFogComponent, 'fog', registerComponentParam);
		this.addComponent(CameraPickerComponent, 'picker', registerComponentParam);
		this.addComponent(CameraControlEventDispatcherComponent, 'controlEventDispatcher', registerComponentParam);
		this.addComponent(CameraDynamicSkyComponent, 'dynamicSky', registerComponentParam);
		this.addComponent(CameraResourceControllerComponent, 'resourceController', registerComponentParam);
	}

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

		// Update camera options
		let node = this.node;
		node.setFov(_private.fov);
		node.setNear(_private.near);
		node.setFar(_private.far);

		// Never affect bounding box for camera
		node.getUserData()[NodeUserDataType.BoundingBoxInheritType] = InheritType.Stop;
	}

	onUpdate(deltaTime) {
		super.onUpdate(deltaTime);

		// Get camera node
		let cameraNode = this.node;

		// Update camera
		cameraNode.updateMatrixWorld();
		cameraNode.update(deltaTime);
	}

	onBeforeDestroy() {
		if (!super.onBeforeDestroy()) {
			return false;
		}

		if (this.app.objectManager.keepAliveObjects.includes(this)) {
			return false;
		}

		let scene = this.app.scene;
		scene.removeRenderCamera(this);

		let _private = this[__.private];

		if (_private.renderTexture) {
			_private.renderTexture.release();
			_private.renderTexture = null;
		}

		return true;
	}

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

		// Check whether it's ticking now
		if (_private.flags.has(Flag.Ticking)) {
			return;
		}

		// The camera must update first, so we insert it to the front of tickable objects
		this.app.objectManager.insertTickableObject(0, this);

		// Prevent to add tickable object duplicatly
		_private.flags.enable(Flag.Ticking, true);
	}

	onCopy(object) {
		super.onCopy(object);

		let viewport = [], size = [];
		let node = object.node;
		node.getViewport(viewport);
		node.getSize(size);

		this.node.setViewport(viewport);
		this.node.setSize(size);

		this.orthoDistance = object.orthoDistance;
		this.fov = object.fov;
		this.near = object.near;
		this.far = object.far;
	}

	// #endregion

	// #region Coordinate Translation

	/**
	 * Convert world position to screen position.
	 * @param {Array<Number>} position The world position.
	 * @returns {Array<Number>} The screen position in [x, y, z] format, [z] indicates the sort order for 2D element.
	 * @example
	 * 	let screen0 = THING.App.current.camera.worldToScreen([100, 100, 90]);
	 * 	let screen1 = THING.App.current.camera.worldToScreen([100, 100, 100]);
	 * 	if (screen0 < screen1) {
	 * 		console.log('screen0 is closer to the camera than screen1');
	 * 	}
	 * @public
	 */
	worldToScreen(position, target = []) {
		return this.node.worldToScreen(position, target);
	}

	/**
	 * Convert screen position to world position.
	 * @param {Array<Number>} screenPosition The screen position.
	 * @returns {Array<Number>}
	 * @example
	 * 	let position = THING.App.current.camera.screenToWorld([100, 100]);
	 * @public
	 */
	screenToWorld(screenPosition, target = []) {
		return this.node.screenToWorld(screenPosition, target);
	}

	// #endregion

	// #region Resources

	onUnloadResource() {
		this.control.setController(null);

		super.onUnloadResource();
	}

	onCopyResource(object) {
		this.onCopyComponents(object);
	}

	copy(object) {
		this.onBeforeCopy(object);
		this.onAfterCopy(object);

		return this;
	}

	// #endregion

	// #region Buffers

	/**
	 * Enable/Disable auto clear color buffer.
	 * @type {Boolean}
	 * @private
	 */
	get autoClearColorBuffer() {
		return this.node.getAttribute('ClearColorBuffer');
	}
	set autoClearColorBuffer(value) {
		this.node.setAttribute('ClearColorBuffer', value);
	}

	/**
	 * Enable/Disable auto clear depth buffer.
	 * @type {Boolean}
	 * @private
	 */
	get autoClearDepthBuffer() {
		return this.node.getAttribute('ClearDepthBuffer');
	}
	set autoClearDepthBuffer(value) {
		this.node.setAttribute('ClearDepthBuffer', value);
	}

	/**
	 * Enable/Disable auto clear stencil buffer.
	 * @type {Boolean}
	 * @private
	 */
	get autoClearStencilBuffer() {
		return this.node.getAttribute('ClearStencilBuffer');
	}
	set autoClearStencilBuffer(value) {
		this.node.setAttribute('ClearStencilBuffer', value);
	}

	/**
	 * Capture to render texture.
	 * @param {Number} width The image width in pixel, if it not provide then use the current width.
	 * @param {Number} height The image height in pixel, if it not provide then use the current height.
	 * @returns {THING.RenderTexture}
	 * @example
	 * let renderTexture = THING.App.current.camera.captureToRenderTexture();
	 * renderTexture.download('myScreenshot');
	 * @public
	 */
	captureToRenderTexture(width, height) {
		let _private = this[__.private];

		let node = this.node;

		let size = this._getSize(width, height);

		let currentSize = [];
		node.getSize(currentSize);

		let currentViewport = [];
		node.getViewport(currentViewport);

		if (!_private.captureRenderTexture) {
			_private.captureRenderTexture = new RenderTexture({ size });
		}
		else {
			_private.captureRenderTexture.size = size;
		}

		let renderTexture = _private.captureRenderTexture.getTextureResource();

		// We are going to capture screen
		this.app.trigger(EventType.BeforeCameraCapture, { width: size[0], height: size[1] });

		// Prepare to render
		node.setViewport([0, 0, size[0], size[1]]);
		node.setSize(size);

		// Render scene
		node.render(renderTexture);

		// Render overlay
		let clearColorBuffer = node.getAttribute(_ClearColorBuffer);
		node.setAttribute(_ClearColorBuffer, false);
		node[_renderOverlayFuncName](renderTexture);
		node.setAttribute(_ClearColorBuffer, clearColorBuffer);

		// Finish to render
		node.setSize(currentSize);
		node.setViewport(currentViewport);

		// We have finished to capture screen
		this.app.trigger(EventType.AfterCameraCapture);

		return _private.captureRenderTexture;
	}

	/**
	 * Capture render result to pixel buffer in RGBA color format.
	 * @param {Number} width The image width in pixel, if it not provide then use the current width.
	 * @param {Number} height The image height in pixel, if it not provide then use the current height.
	 * @returns {Uint8Array}
	 * @example
	 * let data = THING.App.current.camera.captureToData(1024, 768);
	 * @public
	 */
	captureToData(width, height) {
		let renderTexture = this.captureToRenderTexture(width, height);

		return renderTexture.pixelBuffer;
	}

	/**
	 * Capture render result into image.
	 * @param {Number} width The image width in pixel, if it not provide then use the current width.
	 * @param {Number} height The image height in pixel, if it not provide then use the current height.
	 * @returns {Object}
	 * @example
	 * let image = THING.App.current.camera.captureToImage(1024, 768);
	 * @public
	 */
	captureToImage(width, height) {
		let size = this._getSize(width, height);
		let pixelBuffer = this.captureToData(size[0], size[1]);

		return Utils.saveAsImage(size[0], size[1], pixelBuffer);
	}

	/**
	 * Capture render result to file(PNG format) in download mode.
	 * @param {String} fileName The file name.
	 * @param {Number} width The image width in pixel, if it not provide then use the current width.
	 * @param {Number} height The image height in pixel, if it not provide then use the current height.
	 * @example
	 * THING.App.current.camera.captureToFile('myScreenshot');
	 * @public
	 */
	captureToFile(fileName, width, height) {
		let renderTexture = this.captureToRenderTexture(width, height);
		renderTexture.download(fileName);
	}

	// #endregion

	// #region Render

	/**
	 * Resize.
	 * @param {Number} width The width in pixel.
	 * @param {Number} height The height in pixel.
	 * @private
	 */
	resize(width, height) {
		let _private = this[__.private];

		let node = this.node;

		let viewport = _private.viewport;
		if (viewport) {
			node.setViewport(viewport);
			node.setSize([viewport[2], viewport[3]]);
		}
		else {
			node.setViewport([0, 0, width, height]);
			node.setSize([width, height]);
		}
	}

	/**
	 * Enable/Disable viewport.
	 * @type {Boolean}
	 * @example
	 * let camera = new THING.Camera();
	 * camera.enableViewport = true;
	 * @public
	 */
	get enableViewport() {
		let _private = this[__.private];

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

		if (_private.flags.enable(Flag.EnableViewport, value)) {
			this.trigger(EventType.EnableViewport, { value });
		}
	}

	/**
	 * Get/Set viewport of screen [x, y, width, height].
	 * @type {Array<Number>}
	 * @example
	 * let camera = new THING.Camera();
	 * camera.viewport = [0, 0, 100, 100];
	 * @public
	 */
	get viewport() {
		let _private = this[__.private];

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

		if (value) {
			_private.viewport = value.slice(0);

			this.node.setViewport(_private.viewport);
			this.node.setSize([value[2], value[3]]);
		}
		else {
			_private.viewport = null;

			this.node.setViewport([0, 0, 0, 0]);
		}

		this._updateRenderableRefCount(value);
	}

	/**
	 * Get/Set render texture.
	 * @type {THING.RenderTexture}
	 * @example
	 * 	let renderTexture = new THING.RenderTexture({
	 * 		size: THING.App.current.size
	 * 	});
	 *
	 * 	let camera = new THING.Camera();
	 * 	camera.renderTexture = renderTexture;
	 * @public
	 */
	get renderTexture() {
		let _private = this[__.private];

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

		if (_private.renderTexture) {
			_private.renderTexture.release();
		}

		// Make sure it's render texture
		if (value && !value.isRenderTexture) {
			Utils.error(`Set camera render texture failed, due to value is not RenderTexture type`);
			return;
		}

		_private.renderTexture = value;
		_private.renderTexture.addRef();

		this._updateRenderableRefCount(value);
	}

	/**
	 * Get projection matrix.
	 * @type {Array<Number>}
	 * @private
	 */
	get projectionMatrix() {
		let target = [];
		return this.node.getProjectionMatrix(target);
	}

	// #endregion

	// #region Control

	/**
	 * process adjust near
	 * @public
	 */
	processAdjustNear() {
		this.control.processAdjustNear();
	}

	/**
	 * Get projection type.
	 * @returns {ProjectionType}
	 * @example
	 * let projectionType = THING.App.current.camera.getProjectionType();
	 * @public
	 */
	getProjectionType() {
		let _private = this[__.private];

		return _private.projectionType;
	}

	/**
	 * Set projection type.
	 * @param {ProjectionType} value The projection value.
	 * @param {Number} duration The projection matrix lerping time in milliseconds.
	 * @example
	 * THING.App.current.camera.setProjectionType(THING.ProjectionType.Orthographic);
	 * @public
	 */
	setProjectionType(value, duration = 1000) {
		let _private = this[__.private];

		if (_private.projectionType == value) {
			return;
		}

		let tweenManager = this.app.tweenManager;
		let cameraNode = this.node;
		let weight = cameraNode.getProjectionMatrixWeight();

		// Stop current lerping
		if (_private.projectionTween) {
			_private.projectionTween.stop();
		}

		if (_private.projectionType == value) {
			return;
		}

		// 2D -> 3D
		if (value == ProjectionType.Perspective) {
			if (duration) {
				_private.projectionTween = tweenManager.lerpTo(weight, 1, duration).easing(LerpType.Exponential.In).onUpdate(ev => {
					cameraNode.setProjectionMatrixWeight(ev.value);
				});
			}
			else {
				cameraNode.setProjectionMatrixWeight(1);
			}
		}
		// 3D -> 2D
		else if (value == ProjectionType.Orthographic) {
			if (duration) {
				_private.projectionTween = tweenManager.lerpTo(weight, 0, duration).easing(LerpType.Quartic.Out).onUpdate(ev => {
					this.orthoDistance = this.distance;

					cameraNode.setProjectionMatrixWeight(ev.value);
				});
			}
			else {
				this.orthoDistance = this.distance;

				cameraNode.setProjectionMatrixWeight(0);
			}
		}

		_private.projectionType = value;
	}

	/**
	 * Get view mode type.
	 * @returns {ViewMode}
	 * @example
	 * let viewMode = THING.App.current.camera.getViewMode();
	 * @public
	 */
	getViewMode() {
		let _private = this[__.private];

		return _private.viewModeInfo.type;
	}

	/**
	 * Set view mode type.
	 * @param {THING.Object3D} target The target object, if do not provide then would use level's current object or root object.
	 * @param {ViewMode} value The view mode value.
	 * @param {Number} duration The projection matrix lerping time in milliseconds.
	 * @example
	 * THING.App.current.camera.setViewMode(THING.App.current.root, THING.ViewModeType.Top);
	 * @public
	 */
	setViewMode(target, value, duration = 1000) {
		if (Utils.isString(target)) {
			duration = value || duration;
			value = target;
			target = null;
		}

		let object = target || this.app.levelManager.current || this.app.root;

		this._setViewMode(object, value, duration);
	}

	/**
	 * Get view mode type.
	 * @returns {ViewMode}
	 * @deprecated 2.7
	 * @private
	 */
	getViewModeType() {
		console.warn('Please use getViewMode(), getViewModeType() has been abandoned, but will be compatible with version 2.7');
		return this.getViewMode();
	}

	/**
	 * Set view mode type.
	 * @param {THING.Object3D} target The target object, if do not provide then would use level's current object or root object.
	 * @param {ViewMode} value The view mode value.
	 * @param {Number} duration The projection matrix lerping time in milliseconds.
	 * @deprecated 2.7
	 * @private
	 */
	setViewModeType(target, value, duration = 1000) {
		console.warn('Please use setViewMode(), setViewModeType() has been abandoned, but will be compatible with version 2.7');
		this.setViewMode(target, value, duration);
	}

	/**
	 * Get ortho rect [left, right, bottom, top].
	 * @type {Number}
	 * @private
	 */
	get orthoRect() {
		let target = [];
		return this.node.getOrthographicRect(target);
	}

	/**
	 * Get/Set ortho distance.
	 * @type {Number}
	 * @private
	 */
	get orthoDistance() {
		return this.node.getAttribute('OrthoDistance');
	}
	set orthoDistance(value) {
		this.node.setAttribute('OrthoDistance', value);
	}

	/**
	 * Get/Set fov.
	 * @type {Number}
	 * @example
	 * 	THING.App.current.camera.fov = 60;
	 * @public
	 */
	get fov() {
		let _private = this[__.private];

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

		_private.fov = value;

		this.node.setFov(value);
	}

	/**
	 * Get/Set near.
	 * @type {Number}
	 * @example
	 * 	THING.App.current.camera.near = 0.001;
	 * @public
	 */
	get near() {
		let _private = this[__.private];

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

		_private.near = value;

		this.node.setNear(value);
	}

	/**
	 * Get/Set far.
	 * @type {Number}
	 * @example
	 * 	THING.App.current.camera.far = 1000000;
	 * @public
	 */
	get far() {
		let _private = this[__.private];

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

		_private.far = value;

		this.node.setFar(value);
	}

	/**
	 * Get aspect.
	 * @type {Number}
	 * @readonly
	 * @example
	 * 	let aspect = THING.App.current.camera.aspect;
	 * @public
	 */
	get aspect() {
		return this.node.getAspect();
	}

	/**
	 * Get/Set projection type.
	 * @type {ProjectionType}
	 * @example
	 * THING.App.current.camera.projectionType = THING.ProjectionType.Orthographic;
	 * @public
	 */
	get projectionType() {
		return this.getProjectionType();
	}
	set projectionType(value) {
		this.setProjectionType(value, 0);
	}

	/**
	 * Get/Set view mode type.
	 * @type {ViewMode}
	 * @example
	 * THING.App.current.camera.viewMode = THING.ViewModeType.Top;
	 * @public
	 */
	get viewMode() {
		return this.getViewMode();
	}
	set viewMode(value) {
		this.setViewMode(null, value, 0);
	}

	/**
	 * Get/Set view mode type.
	 * @type {ViewMode}
	 * @deprecated 2.7
	 * @private
	 */
	get viewModeType() {
		console.warn('Please use viewMode, viewModeType has been abandoned, but will be compatible with version 2.7');
		return this.viewMode;
	}
	set viewModeType(value) {
		console.warn('Please use viewMode, viewModeType has been abandoned, but will be compatible with version 2.7');
		this.viewMode = value;
	}

	// #endregion

	// #region Picker

	/**
	 * Check intersection between object and region.
	 * @param {Array<Number>} region The [left, top, width, height] region in screen.
	 * @param {THING.Object3D} object The object.
	 * @returns {Boolean}
	 * @example
	 * if (THING.App.current.camera.intersectObjectInRegion([0, 0, 100, 100], object)) {
	 * 	console.log('intersect OK');
	 * }
	 * @public
	 */
	intersectObjectInRegion(region, object) {
		return this.picker.intersectObjectInRegion(region, object);
	}

	/**
	 * Pick node.
	 * @param {Number} x The x coordinate in screen.
	 * @param {Number} y The y coordinate in screen.
	 * @param {THING.BaseObject} root? The root object to pick.
	 * @returns {PickResult}
	 * @example
	 * 	let result = THING.App.current.camera.pick(100, 100);
	 * 	if (result) {
	 * 		console.log('picked result: ', result.object);
	 * 	}
	 * @public
	 */
	pick(x, y, root) {
		return this.picker.pick(x, y, root);
	}

	/**
	 * Pick from cross planes.
	 * @param {Number} x The x coordinate in screen.
	 * @param {Number} y The y coordinate in screen.
	 * @param {Boolean} isVertical Whether vertical and horizontal planes will always be vertical.
	 * @returns {PickResult}
	 * @example
	 * 	let result = THING.App.current.camera.pickFromCrossPlanes(100, 100);
	 * 	if (result) {
	 * 		console.log('picked result: ', result.object);
	 * 	}
	 * @public
	 */
	pickFromCrossPlanes(x, y, isVertical = true) {
		return this.picker.pickFromCrossPlanes(x, y, isVertical);
	}

	/**
	 * Calculate the points where the plane intersects.
	 * @param {Number} x The x coordinate in screen.
	 * @param {Number} y The y coordinate in screen.
	 * @param {Array<Number>} normal The plane normal. (default [0,1,0])
	 * @param {Number} constant Distance of plane. (default 0)
	 * @returns {Array<Number>}
	 * @private
	 */
	intersectPlane(x, y, normal = [0, 1, 0], constant = 0) {
		return this.picker.intersectPlane(x, y, normal, constant);
	}
	/**
	 * Add an effect.
	 * @param {String} name The effect name.
	 * @param {Effect} effect The effect.
	 * @public
	 */
	addEffect(name, effect) {
		this.node.addEffect(name, effect.resource);
	}

	/**
	 * Remove an effect.
	 * @param {String} name The effect name.
	 * @public
	 */
	removeEffect(name) {
		this.node.removeEffect(name);
	}

	// #endregion

	get isCamera() {
		return true;
	}

}

export { Camera }