Source: objects/ClippingPlanes.js

import { MathUtils } from '../math/MathUtils';
import { BaseTickableObject3D } from './BaseTickableObject3D';

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

let _vec3_1 = MathUtils.createVec3();
let _vec3_2 = MathUtils.createVec3();
let _mat3 = MathUtils.createMat3();
let _mat4 = MathUtils.createMat4();

// #region Private Functions

// Convert clipping plane to world space.
function _convertPlaneToWorldSpace(plane, matrixWorld) {
	let dir_1 = MathUtils.vec3.copy(_vec3_1, plane.direction);
	let dir_2 = MathUtils.vec3.copy(_vec3_2, plane.direction);

	const normalMatrix = MathUtils.mat3.normalFromMat4(_mat3, matrixWorld);
	const referencePoint = MathUtils.vec3.transformMat4(dir_1, MathUtils.vec3.scale(dir_1, dir_1, -plane.height), matrixWorld);
	const normal = MathUtils.vec3.normalize(dir_2, MathUtils.vec3.transformMat3(dir_2, dir_2, normalMatrix));
	const constant = -MathUtils.vec3.dot(referencePoint, normal);

	return {
		direction: normal.slice(0),
		height: constant
	}
}

// #endregion

/**
 * @class ClippingPlanes
 * The clipping planes object.
 * @memberof THING
 * @extends THING.BaseTickableObject3D
 * @public
 */
class ClippingPlanes extends BaseTickableObject3D {

	/**
	 * The clipping planes that use with object style to clip object by planes.
	 * @param {Object} param The initial parameters.
	 */
	constructor(param) {
		super(param);

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

		_private.lastMat4 = null;

		_private.resources = [];
		_private.planes = null;
		_private.worldPlanes = null;

		let planes = param['planes'];
		if (planes) {
			this.planes = planes;
		}
	}

	// #region Private

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

		// Get the object's matrix world
		this.node.getMatrixWorld(_mat4);

		// Check whether matrix world has been changed
		if (!force) {
			if (_private.lastMat4) {
				if (MathUtils.mat4.equals(_mat4, _private.lastMat4)) {
					return;
				}
			}
			else {
				_private.lastMat4 = MathUtils.createMat4();
			}
		}

		// Keep matrix world to latest
		if (_private.lastMat4) {
			MathUtils.mat4.copy(_private.lastMat4, _mat4);
		}

		// Convert local planes to world planes
		_private.worldPlanes = _private.planes.map(plane => {
			return _convertPlaneToWorldSpace(plane, _mat4);
		});

		// Update styles's resource clipping planes
		_private.resources.forEach(resource => {
			resource.setClippingPlanes(_private.worldPlanes);
		});
	}

	// #endregion

	// #region BaseObject Interface

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

		let _private = this[__.private];

		if (!_private.planes) {
			return;
		}

		if (_private.resources.length) {
			this._updatePlanes();
		}
	}

	// #endregion

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

		if (_private.resources.indexOf(resource) !== -1) {
			return;
		}

		_private.resources.push(resource);

		resource.setClippingPlanes(_private.worldPlanes);
	}

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

		let index = _private.resources.indexOf(resource);
		if (index === -1) {
			return;
		}

		_private.resources.splice(index, 1);

		resource.setClippingPlanes(null);
	}

	// #endregion

	// #region Accessor

	/**
	 * @typedef {Object} ClippingPlaneResult
	 * @property {Array<Number>} direction The direction in world space.
	 * @property {Number} height The height.
	 */

	/**
	 * Get/Set the clipping planes.
	 * @type {Array<ClippingPlaneResult>}
	 * @public
	 */
	get planes() {
		let _private = this[__.private];

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

		_private.planes = value.map(plane => {
			return {
				direction: plane.direction.slice(0),
				height: plane.height
			}
		});

		this._updatePlanes(true);
	}

	// #endregion

	/**
	 * Check whether it's ClippingPlanes type or inherit from it.
	 * @type {Boolean}
	 * @public
	 */
	get isClippingPlanes() {
		return true;
	}

}

export { ClippingPlanes }