Source: objects/Marker.js

import { StringEncoder } from '@uino/base-thing';
import { Utils } from '../common/Utils'
import { MathUtils } from '../math/MathUtils';
import { Object3D } from './Object3D';
import { RenderType, PivotMode, SideType } from '../const';

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

const _spriteTypeName = StringEncoder.toText("<@secret Sprite>");

const _defaultPlaneOptions = {
	renderLayer: 1,
	envMap: false,
	lights: false,
};

// #endregion

/**
 * @class Marker
 * The marker object.
 * @memberof THING
 * @extends THING.Object3D
 * @public
 */
class Marker extends Object3D {

	/**
	 * The marker object that show image in scene.
	 * @param {Object} param The initial parameters.
	 */
	constructor(param = {}) {
		super(Utils.cloneObject(param));

		this._autoFitBodyScale = Utils.parseValue(param['autoFitBodyScale'], false);
		this._scaleFactor = Utils.parseValue(param['scaleFactor'], 0.01);
		this._renderType = Utils.parseValue(param['renderType'], RenderType.Sprite);
		this._pivotMode = Utils.parseValue(param['pivotMode'] || param['pivotModeType'], PivotMode.Auto);
		this._spriteRotation = Utils.parseValue(param['spriteRotation'], 0);

		this._onBeforeFitBodyScale = null;
		this._onAfterFitBodyScale = null;

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

	// #region Private Functions

	_useSpriteCenterAsPivot() {
		if (this.renderType != RenderType.Sprite) {
			return false;
		}

		if (this.pivotMode != PivotMode.Auto) {
			return false;
		}

		return true;
	}

	_fitBodyScale() {
		let image = this.style.image;
		if (!image) {
			return;
		}

		image.waitForComplete().then(() => {
			// Get the image size
			let width = image.width;
			let height = image.height;
			if (!width || !height) {
				return;
			}

			// Notify we are going to set body scale
			if (this._onBeforeFitBodyScale) {
				this._onBeforeFitBodyScale({ object: this, image });
			}

			// Adjust body scale with scale factor
			const scaleFactor = this._scaleFactor;
			width *= scaleFactor;
			height *= scaleFactor;

			// Ignore parent scale
			let parent = this.parent;
			if (parent) {
				let parentScale = parent.scale;

				width /= parentScale[0];
				height /= parentScale[1];
			}

			// Update body scale to fit ratio
			this.body.localScale = [width, height, 1];

			// Notify we finished to set body scale
			if (this._onAfterFitBodyScale) {
				this._onAfterFitBodyScale({ object: this, image });
			}

			// Refresh keep size
			let keepSizeInfo = this.transform.keepSizeInfo;
			if (keepSizeInfo) {
				keepSizeInfo.refresh();
			}
		});
	}

	_updatePivot(pivot) {
		this.clearPivot();

		// Get body node
		let bodyNode = this.bodyNode;

		// It's sprite render type
		if (this._useSpriteCenterAsPivot()) {
			if (pivot) {
				bodyNode.setAttribute('Center', [pivot[0], pivot[1]]);
			}

			bodyNode.setAttribute('Rotation', MathUtils.degToRad(this._spriteRotation));

			bodyNode.setWorldPosition(this.position);
		}
		else {
			if (pivot) {
				this.pivot = pivot;
			}
		}

		if (this._autoFitBodyScale) {
			this._fitBodyScale();
		}
	}

	_refreshRenderableNode(options) {
		let renderType = this.renderType;

		// Create render node
		if (renderType == RenderType.Sprite) {
			// If user provide renderable node then skip to create it
			let renderableNode = options['renderableNode'];
			if (!renderableNode) {
				if (this.bodyNode.getType() != _spriteTypeName) {
					let node = Utils.createObject(_spriteTypeName, this.external);
					this.body.setNode(node);
				}
			}

			var spriteRotation = options['spriteRotation'];
			if (spriteRotation) {
				this.bodyNode.setAttribute('Rotation', MathUtils.degToRad(spriteRotation));
			}
		}
		else if (renderType == RenderType.Plane) {
			// If user provide renderable node then skip to create it
			let renderableNode = options['renderableNode'];
			if (!renderableNode) {
				if (this.bodyNode.getType() != 'Node') {
					let node = this.app.resourceManager.parseModel('Plane', _defaultPlaneOptions);
					this.body.setNode(node);
				}
			}
		}

		// Get the pivot and prepare to set it (in delay mode)
		let pivot = options['pivot'];
		if (pivot) {
			this._updatePivot(pivot);
		}
	}

	// #endregion

	// #region Overrides

	onBeforeSetup(param) {
		// If user provide renderable node then skip to create it
		let renderableNode = param['renderableNode'];
		if (renderableNode) {
			return;
		}

		// Create render node
		let renderType = Utils.parseValue(param['renderType'], RenderType.Sprite);
		switch (renderType) {
			case RenderType.Sprite:
				renderableNode = Utils.createObject(_spriteTypeName, param['extras'] || param['external']);
				break;

			case RenderType.Plane:
				renderableNode = this.app.resourceManager.parseModel('Plane', _defaultPlaneOptions);
				break;

			default:
				break;
		}

		param['renderableNode'] = renderableNode;
	}

	onSetupStyle(param) {
		let style = param['style'];
		if (style) {
			// Enable transparent mode as default
			if (style.transparent === undefined) {
				style.transparent = true;
			}

			// Enable double side mode as default
			if (style.sideType === undefined) {
				style.sideType = SideType.Double;
			}

			// Disable ligths mode as default
			if (style.lights === undefined) {
				style.lights = false;
			}
		}
		else {
			param['style'] = {
				transparent: true,
				sideType: SideType.Double
			}
		}

		super.onSetupStyle(param);
	}

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

		this._autoFitBodyScale = object.autoFitBodyScale;
		this._scaleFactor = object.scaleFactor;
		this._renderType = object.renderType;
		this._pivotMode = object.pivotMode;
		this._spriteRotation = object.spriteRotation;

		this.onRefresh();
	}

	onRefresh() {
		if (this._autoFitBodyScale) {
			this._fitBodyScale();
		}

		super.onRefresh();
	}

	/**
	 * When load reosurce.
	 * @param {Object} options The options to load.
	 * @param {Function} resolve The promise resolve callback function.
	 * @param {Function} reject The promise reject callback function.
	 * @private
	 */
	onLoadResource(options, resolve, reject) {
		let dynamic = Utils.parseValue(options['dynamic'], false);
		if (!dynamic) {
			Utils.setTimeout(() => {
				if (this.destroyed) {
					return;
				}

				this._refreshRenderableNode(options);

				resolve();
			});
		}
		else {
			Utils.setTimeout(() => {
				resolve();
			});
		}
	}

	onCreateBodyNode(type) {
		let node;

		switch (this.renderType) {
			case RenderType.Sprite:
				node = Utils.createObject(_spriteTypeName, this.external);
				break;

			case RenderType.Plane:
				node = this.app.resourceManager.parseModel('Plane', _defaultPlaneOptions);
				break;

			default:
				node = super.onCreateBodyNode(type);
				break;
		}

		return node;
	}

	onGetPivot() {
		if (this._useSpriteCenterAsPivot()) {
			let pivot = this.bodyNode.getAttribute('Center');
			return [pivot[0], pivot[1], 0.5];
		}
		else {
			return super.onGetPivot();
		}
	}

	onSetPivot(value) {
		if (this._useSpriteCenterAsPivot()) {
			this.bodyNode.setAttribute('Center', [value[0], value[1]]);
		}
		else {
			super.onSetPivot(value);
		}
	}

	onChangeImage() {
		this.waitForComplete().then(() => {
			if (this.destroyed) {
				return;
			}

			if (this._autoFitBodyScale) {
				this._fitBodyScale();
			}
		});
	}

	onSetupComplete(param) {
		super.onSetupComplete(param);

		this.level.config.ignoreStyle = true; // skip level outline
	}

	// #endregion

	// #region Accessor

	/**
	 * Enable/Disable auto fit body scale, true indicates it will use the ratio of style's image size to fit body scale.
	 * Default value is: false.
	 * @type {Boolean}
	 * @example
	 * 	marker.autoFitBodyScale = true;
	 * @public
	 */
	get autoFitBodyScale() {
		return this._autoFitBodyScale;
	}
	set autoFitBodyScale(value) {
		this._autoFitBodyScale = value;

		if (this._autoFitBodyScale) {
			this._fitBodyScale();
		}
	}

	/**
	 * Get/Set scale factor of autoFitBodyScale attribute(only works when [autoFitBodyScale=true]).
	 * Default value is: 0.01.
	 * @type {Number}
	 * @example
	 * 	marker.scaleFactor = 0.1;
	 * @public
	 */
	get scaleFactor() {
		return this._scaleFactor;
	}
	set scaleFactor(value) {
		this._scaleFactor = value;

		if (this._autoFitBodyScale) {
			this._fitBodyScale();
		}
	}

	/**
	 * Get/Set the render type.
	 * @type {RenderType}
	 * @public
	 */
	get renderType() {
		if (this._renderType !== undefined) {
			return this._renderType;
		}

		let type = this.bodyNode.getType();
		switch (type) {
			case 'Sprite': return RenderType.Sprite;
			case 'Plane': return RenderType.Plane;
			default:
				return RenderType.Plane;
		}
	}
	set renderType(value) {
		let pivot = this.pivot;

		this._renderType = value;

		this.reloadResource(false, { pivot });
	}

	/**
	 * Get/Set the pivot mode.
	 * @type {PivotMode}
	 * @private
	 */
	get pivotMode() {
		return this._pivotMode;
	}
	set pivotMode(value) {
		if (this._pivotMode != value) {
			this._pivotMode = value;

			this.onSetPivot(this.pivot);
		}
	}

	/**
	 * Get/Set the pivot mode type.
	 * @type {PivotModeType}
	 * @deprecated 2.7
	 * @private
	 */
	get pivotModeType() {
		return this.pivotMode;
	}
	set pivotModeType(value) {
		this.pivotMode = value;
	}

	/**
	 * Get/Set the sprite rotation in degree, only works on THING.RenderType.Sprite mode.
	 * @type {Number}
	 * @public
	 */
	get spriteRotation() {
		return this._spriteRotation;
	}
	set spriteRotation(value) {
		this._spriteRotation = value;

		if (this.loaded) {
			if (this.renderType == RenderType.Sprite) {
				this.bodyNode.setAttribute('Rotation', MathUtils.degToRad(value));
			}
		}
	}

	/**
	 * @typedef {Object} FitBodyScaleInfo
	 * @property {THING.Marker} object The object.
	 * @property {Object} image The image.
	 */

	/**
	 * The function to call when fit body scale.
	 * @callback OnFitBodyScaleCallback
	 * @param {FitBodyScaleInfo} info The info.
	 */

	/**
	 * Get/Set before fit body scale callback function.
	 * @type {OnFitBodyScaleCallback}
	 * @public
	 */
	get onBeforeFitBodyScale() {
		return this._onBeforeFitBodyScale;
	}
	set onBeforeFitBodyScale(value) {
		this._onBeforeFitBodyScale = value;
	}

	/**
	 * Get/Set after fit body scale callback function.
	 * @type {OnFitBodyScaleCallback}
	 * @public
	 */
	get onAfterFitBodyScale() {
		return this._onAfterFitBodyScale;
	}
	set onAfterFitBodyScale(value) {
		this._onAfterFitBodyScale = value;
	}

	// #endregion

	get isMarker() {
		return true;
	}

}

export { Marker }