Source: components/CameraPickerComponent.js

import { Utils } from '../common/Utils';
import { MathUtils } from '../math/MathUtils';
import { BaseComponent } from './BaseComponent';
import { PickType } from '../const';

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

let _position = MathUtils.createVec3();
let _origin = MathUtils.createVec3();

// #region Private Functions

// #endregion

/**
 * @class CameraPickerComponent
 * The camera picker compnent.
 * @memberof THING
 * @extends THING.BaseComponent
 */
class CameraPickerComponent extends BaseComponent {

	/**
	 * The picker by camera, user can pick object(s) from it.
	 */
	constructor() {
		super();

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

		_private.pickFuncName = 'pickFromGPU';

		_private.onPickObject = null;
	}

	// #region Private

	// Get adjusted position of screen to pick.
	_getAdjustedPosition(x, y) {
		let style = this.app.delegate.getAttribute('Style');
		if (style) {
			let transform = style.transform;
			if (transform) {
				let scale = transform.scale;
				if (scale) {
					x /= scale[0];
					y /= scale[1];
				}
			}
		}

		return [x, y];
	}

	// #endregion

	/**
	 * 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}
	 */
	intersectObjectInRegion(region, object) {
		let x = region[0];
		let y = region[1];
		let width = region[2];
		let height = region[3];

		return this.object.node.intersectNodeInRegion([x, y, width, height], object.bodyNode);
	}

	/**
	 * @typedef {Object} PickResult
	 * @property {THING.BaseObject} object The object.
	 * @property {Array<Number>} position The picked position.
	 * @property {Number} pickedId The picked Id.
	 * @property {Object} external The external info.
	 */

	/**
	 * 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}
	 */
	pick(x, y, root) {
		let _private = this[__.private];

		let app = this.app;

		let cameraNode = this.object.node;
		let pickFuncName = _private.pickFuncName;

		// Get screen position in pixel
		let position = this._getAdjustedPosition(x, y);

		// Get the pick scene
		let pickScene = root ? root : app.scene.root;

		// Pick object in scene
		let result = cameraNode[pickFuncName](position, pickScene);
		if (!result) {
			return null;
		}

		// Get the picked object
		let object = app.objectManager.getBaseObjectFromNode(result.node);

		// Notify outside we had picked object
		if (_private.onPickObject) {
			object = _private.onPickObject(object);
			if (!object) {
				return null; // Pick nothing
			}
		}

		// Build picked result
		let ev = {
			object,
			subNodeName: result.subNodeName,
			external: {}
		};

		// Get the picked Id
		if (result.pickedId !== undefined) {
			ev.pickedId = result.pickedId;
		}

		// To prevent BIG deal for these actions, we make property here to delay some operations
		Object.defineProperties(ev, {
			'position': {
				get() {
					return result.position;
				}
			},
			'normal': {
				get() {
					return result.normal;
				}
			}
		});

		return ev;
	}

	/**
	 * 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}
	 */
	pickFromCrossPlanes(x, y, isVertical = true) {
		let position = this.object.control.pickFromCrossPlanes(x, y, isVertical);

		return {
			position,
			object: null,
			pickedId: null,
			external: null
		};
	}

	/**
	 * 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>}
	 */
	intersectPlane(x, y, normal = [0, 1, 0], constant = 0) {
		let object = this.object;

		let origin, direction;
		if (object.projectionType === 'Orthographic') {
			origin = object.screenToWorld([x, y, (object.near + object.far) / (object.near - object.far)]);
			direction = MathUtils.transformDirection([0, 0, -1], object.matrixWorld);
		}
		else {
			let position = object.screenToWorld([x, y, 0], _position);

			origin = object.getWorldPosition(_origin);
			direction = MathUtils.normalizeVector(MathUtils.subVector(position, origin));
		}

		return MathUtils.intersectPlane(origin, direction, normal, constant);
	}

	/**
	 * Get/Set pick type.
	 * @type {PickType}
	 * @example
	 * THING.App.current.camera.pickType = THING.PickType.GPU;
	 * @public
	 */
	get pickType() {
		let _private = this[__.private];

		switch (_private.pickFuncName) {
			case 'pickFromGPU':
				return PickType.GPU;

			case 'pickFromGPUFast':
				return PickType.GPUFast;

			case 'pick':
				return PickType.Raycaster;

			default:
				break;
		}

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

		switch (value) {
			case PickType.GPU:
				_private.pickFuncName = 'pickFromGPU';
				break;

			case PickType.GPUFast:
				_private.pickFuncName = 'pickFromGPUFast';
				break;

			case PickType.Raycaster:
				_private.pickFuncName = 'pick';
				break;

			default:
				break;
		}
	}

	/**
	 * The function to call when pick object.
	 * @callback onPickObjectCallback
	 * @param {THING.Object3D} object The object.
	 * @returns {THING.Object3D} The latest picked object, if it's null indicates pick nothing
	 */

	/**
	 * Get/Set the callback function when pick object.
	 * @type {onPickObjectCallback}
	 * @private
	 */
	get onPickObject() {
		let _private = this[__.private];

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

		_private.onPickObject = value;
	}

}

CameraPickerComponent.exportProperties = [
	'pickType'
];

export { CameraPickerComponent }