import { StringEncoder } from '@uino/base-thing';
import { Utils } from '../common/Utils'
import { MathUtils } from '../math/MathUtils';
import { Object3D } from './Object3D';
import { RenderType, PivotMode, SideType, ScaleMode } from '../const';
const __ = {
private: Symbol('private'),
}
const _spriteTypeName = StringEncoder.toText("<@secret Sprite>");
const _defaultPlaneOptions = {
renderLayer: 1,
envMap: false,
lights: false,
};
// #endregion
/**
* Marker - 标记
*
* 在场景中显示图像标记对象,支持精灵模式和平面模式,可自定义缩放和旋转。
* @class Marker
* @memberof THING
* @extends THING.Object3D
* @public
* @example
const marker = new THING.Marker({
image: './images/marker.png',
size: [1, 1],
position: [0, 1, 0],
});
*/
class Marker extends Object3D {
/**
* 构造方法
* </br>在场景中显示图像的标记对象。
* @param {object} param 初始化参数,除Object3D基础的初始化参数以外的参数
* @param {boolean} [param.autoFitBodyScale=false] 是否自适应body的缩放
* @param {number} [param.scaleFactor=0.01] 缩放系数
* @param {THING.RenderType} [param.renderType=THING.RenderType.Sprite] 渲染类型 平面或精灵
* @deprecated @param {THING.PivotMode} [param.pivotMode=THING.PivotMode.Auto] 轴心点类型
* @param {number} [param.spriteRotation=0] 精灵模式下的旋转角度
* @constructor
* @public
*/
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._scaleMode = Utils.parseValue(param['scaleMode'], ScaleMode.ScaleFactor);
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(() => {
if (this.destroyed) {
return;
}
// 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 });
}
let _imgSize = this._calcSize(width, height);
width = _imgSize[0];
height = _imgSize[1];
// 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);
if (this._autoFitBodyScale) {
this._fitBodyScale();
}
}
else {
if (this._autoFitBodyScale) {
this._fitBodyScale();
}
if (pivot) {
this.pivot = pivot;
}
}
}
_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, app: this.app });
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);
}
}
_calcSize(width, height) {
switch (this._scaleMode) {
case ScaleMode.ScaleRatio:
if (width >= height) {
const _ratio = width / height;
width = _ratio * this._scaleFactor;
height = width / _ratio;
}
else {
const _ratio = height / width;
height = _ratio * this._scaleFactor;
width = height / _ratio;
}
break;
case ScaleMode.ScaleFixed:
if (width >= height) {
height = height / width * this._scaleFactor;
width = this._scaleFactor;
}
else {
width = width / height * this._scaleFactor;
height = this._scaleFactor;
}
break;
default:
width *= this._scaleFactor;
height *= this._scaleFactor;
break;
}
return [width, height]
}
// #endregion
// #region Overrides
onBeforeSetup(param) {
super.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:
let extras = param['extras'] || param['external'];
extras = extras ? Utils.cloneObject(extras) : {};
renderableNode = Utils.createObject(_spriteTypeName, extras);
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();
}
superOnRefresh() {
super.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) {
if (!this.isDynamicLoad) {
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, app: this.app });
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()) {
value = value || [0.5, 0.5, 0.5];
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
}
onExportExternalData(options) {
let data = super.onExportExternalData(options) || {};
data['autoFitBodyScale'] = this._autoFitBodyScale;
data['scaleFactor'] = this._scaleFactor;
data['renderType'] = this._renderType;
data['pivotMode'] = this._pivotMode;
data['spriteRotation'] = this._spriteRotation;
return data;
}
/**
* 修正 keepSize 计算得到的缩放值。
* 当 Marker 开启 autoFitBodyScale 时,可以在此对缩放做额外处理。
* 当前默认实现保持缩放不变,仅作为子类扩展点。
* @param {Array<number>} scale 由 keepSize 计算得到的缩放值。
* @returns {Array<number>} 修正后的缩放值。
* @protected
*/
onFixKeepSizeScale(scale) {
// 未开启自适应图片尺寸,直接返回原始缩放
if (!this._autoFitBodyScale) {
return scale;
}
// 根据图片的原始宽高来确定宽高比,确保 Marker 在 keepSize 模式下仍保持
// 与图片一致的宽高比例,不会被变形。
const style = this.style;
const image = style && style.image;
if (!image) {
return scale;
}
const imgWidth = image.width;
const imgHeight = image.height;
if (!imgWidth || !imgHeight) {
return scale;
}
// 使用 keepSize 计算出的 scale 作为“最大包围盒”,在该范围内按图片宽高比自适应缩放,
// 保证宽高比正确,且不会超过 keepSize 计算得到的缩放值。
const maxWidth = scale[0];
const maxHeight = scale[1];
const ratio = imgWidth / imgHeight; // 图片宽高比(宽/高)
let fixedWidth;
let fixedHeight;
// 先假设高度占满,按宽高比算出宽度
fixedHeight = maxHeight;
fixedWidth = fixedHeight * ratio;
// 如果宽度超过最大可用宽度,则改为宽度占满,再反推高度
if (fixedWidth > maxWidth) {
fixedWidth = maxWidth;
fixedHeight = fixedWidth / ratio;
}
return [
fixedWidth,
fixedHeight,
scale[2],
];
}
// #endregion
// #region Accessor
/**
* 创建Marker是否自适应图片的宽高比 如果为false,图片会变形
* @default false
* @type {boolean}
* @example
* marker.autoFitBodyScale = true;
* @public
*/
get autoFitBodyScale() {
return this._autoFitBodyScale;
}
set autoFitBodyScale(value) {
this._autoFitBodyScale = value;
if (this._autoFitBodyScale) {
this._fitBodyScale();
}
}
/**
* 设置/获取 自适应图片宽高比时的缩放倍数 仅autoFitBodyScale=true时生效
* @default 0.01
* @type {number|Array<number>}
* @example
* marker.scaleFactor = 0.1;
* @public
*/
get scaleFactor() {
if (Utils.isArray(this._scaleFactor)) {
return this._scaleFactor.concat();
}
return this._scaleFactor;
}
set scaleFactor(value) {
if (Utils.isArray(value)) {
this._scaleFactor = value.concat();
}
else {
this._scaleFactor = value;
}
if (this._autoFitBodyScale) {
this._fitBodyScale();
}
}
/**
* 设置/获取 渲染类型 支持精灵模式和平面模式
* @type {RenderType}
* @default THING.RenderType.Sprite
* @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 });
}
/**
* 设置/获取 轴心点模式
* @type {PivotMode}
* @default THING.PivotMode.Auto
* @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;
}
/**
* 精灵模式下的旋转角度
* @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));
}
}
}
/**
* 当适应图片尺寸时调用的函数。
* @callback OnFitBodyScaleCallback
* @param {object} info 参数
* @param {THING.Marker} info.object 对象。Marker对象
* @param {object} info.image 图片。图片对象
* @public
*/
/**
* 获取/设置在适应图片尺寸之前调用的回调函数。
* @type {OnFitBodyScaleCallback}
* @public
*/
get onBeforeFitBodyScale() {
return this._onBeforeFitBodyScale;
}
set onBeforeFitBodyScale(value) {
this._onBeforeFitBodyScale = value;
}
/**
* 设置/获取 自适应图片尺寸结束后的回调
* @type {OnFitBodyScaleCallback}
* @public
*/
get onAfterFitBodyScale() {
return this._onAfterFitBodyScale;
}
set onAfterFitBodyScale(value) {
this._onAfterFitBodyScale = value;
}
/**
* 设置/获取 缩放模式 (仅 autoFitBodyScale=true 时生效)
* @type {THING.ScaleMode}
* @public
*/
get scaleMode() {
return this._scaleMode;
}
set scaleMode(value) {
this._scaleMode = value;
if (this._autoFitBodyScale) {
this._fitBodyScale();
}
}
// #endregion
get isMarker() {
return true;
}
}
export { Marker }