import { ResolvablePromise, TypedObject, ObjectAttributes } from '@uino/base-thing';
import { BaseObject } from './BaseObject';
import { BodyObject } from './BodyObject';
import { MathUtils } from '../math/MathUtils';
import { Utils } from '../common/Utils';
import { Style } from '../resources/Style';
import { HelperComponent } from '../components/HelperComponent';
import { TransformComponent } from '../components/TransformComponent';
import { RenderComponent } from '../components/RenderComponent';
import { LerpComponent } from '../components/LerpComponent';
import { BoundingComponent } from '../components/BoundingComponent';
import { LevelComponent } from '../components/LevelComponent';
import { ActionGroupComponent } from '../components/ActionGroupComponent';
import { BlueprintComponent } from '../components/BlueprintComponent';
import { ColliderComponent } from '../components/ColliderComponent';
import { RelationshipComponent } from '../components/RelationshipComponent';
import { TaskExecutorComponent } from '../components/TaskExecutorComponent';
import { EventType, InheritType } from '../const';
import { MonitorDataComponent } from '../components/MonitorDataComponent';
const __ = {
private: Symbol('private'),
}
// [0-7] index is reserved for BaseObject, so index starts from 8
const Flag = {
// Common
Initializing: 1 << 8,
// State
Pickable: 1 << 9,
Active: 1 << 10,
// On/Off
KeepSize: 1 << 11
}
const registerComponentParam = { isResident: true };
let _emptyTags = [];
function _getStyleInheritance(object) {
if (!object.hasInherit()) {
return InheritType.Normal;
}
return object.inherit.style;
}
/**
* @class Object3D
* The base object 3D.
* @memberof THING
* @extends THING.BaseObject
* @public
*/
class Object3D extends BaseObject {
static defaultTagArray = ['Object3D'];
// The resource state
static ResourceState = {
Error: -1,
Ready: 1,
Loaded: 2,
Loading: 3,
};
/**
* @typedef {BaseObjectInitialOptions} Object3DInitialOptions
* @property {Array<Number>} localPosition? The local position.
* @property {Array<Number>} localScale? The local scale.
* @property {Array<Number>} localAngels? The local angles.
* @property {Array<Number>} position? The world position.
* @property {Array<Number>} scale? The world scale.
* @property {Array<Number>} angels? The world angles.
*/
/**
* The base object in scene.
* @param {Object3DInitialOptions} param The initial parameters.
*/
constructor(param = {}) {
super(param);
this[__.private] = {};
let _private = this[__.private];
// Common
_private.tags = null;
_private.external = null;
// Inheritance
_private.inherit = null;
// Render
_private.style = undefined; // null indicates it's not renderable object
_private.styleObjectAttributes = [];
_private.renderLayout = null;
// Resource
_private.copyPromise = null;
_private.resource = {
url: '',
};
_private.options = Object.assign({}, param);
// Setup callbacks
_private.onError = Utils.parseValue(param['onError'], param['error']);
_private.onComplete = Utils.parseValue(param['onComplete'], param['complete']);
_private.onSyncComplete = Utils.parseValue(param['onSyncComplete'], param['syncComplete']);
_private.onSyncBeforeDestroy = Utils.parseValue(param['onSyncBeforeDestroy'], param['syncBeforeDestroy']);
// Sync complete callbacks
_private.syncCompleteCallbacks = [];
// Setup flags
this._flags.enable(Flag.Initializing | Flag.Pickable | Flag.Active, true);
// Some members must be initialized in constructor, so would declarate here
this._resourceState = this._resourceState || Object3D.ResourceState.Ready;
this._loadPromise = this._loadPromise || null;
// Setup before add object
this.onSetupAttributes(param);
this.onSetupTransform(param);
this.onSetupState(param);
this.onSetupStyle(param);
this.onSetupResource(param);
// Setup completed
this.onSetupComplete(param);
// Notify completed
this.onCreate(param);
}
// #region Private
_createResourcePromise() {
// Object has resources, unload it first
if (this.loaded) {
return this.unloadResource(false);
}
// Object is loading resources
else if (this.loading) {
return this.waitForComplete();
}
// Object is in unloaded state
else {
return Promise.resolve();
}
}
_updateResource(value) {
this.setResource(value);
return this.reloadResource(false, { url: value.url });
}
_unloadResource(recursive) {
// Get the initial local bounding box
let initialLocalBoundingBox = this.initialLocalBoundingBox || this._body.getLocalBoundingBox();
// Notify all unloadable components
let unloadableComponents = this.unloadableComponents;
if (unloadableComponents) {
unloadableComponents.forEach(component => {
component.onUnloadResource();
});
}
// When unload resource
this.onUnloadResource();
// Unload body object resource
this._body.unloadResource((type) => {
return this.onCreateBodyNode(type);
});
// Continue to unload children resources
if (recursive) {
if (this.hasChildren()) {
this.children.forEach(child => {
child.unloadResource(recursive);
});
}
}
// Resume the initial local bounding box
this.initialLocalBoundingBox = initialLocalBoundingBox;
}
_onLoadResource(options, callback, error) {
// Had loaded resource
if (this.loaded) {
callback();
}
else {
// Prevent object had been destroyed
if (this.destroyed) {
error({ object: this, desc: 'The object had been destroyed' });
}
else {
this.onLoadResource(options, () => {
if (!this.destroyed) {
this.onSetupState(options);
this.taskExecutor.then(callback, error);
}
}, error);
}
}
}
_loadSelfResource(options, resolve, reject) {
if (this.loading) {
this.waitForComplete().then(() => {
resolve();
}).catch(ev => {
reject(ev);
});
}
else {
this._onLoadResource(options, resolve, reject);
}
}
_setChildrenAttributeState(inheritName, name, value, recursive) {
let children = this.children;
for (let i = 0; i < children.length; i++) {
let child = children[i];
if (child.hasInherit()) {
let inheritType = child.inherit[inheritName];
switch (inheritType) {
case InheritType.Normal:
child['set' + name](value, recursive);
break;
case InheritType.Break:
child['set' + name](value, false);
break;
case InheritType.Jump:
child.children.traverse(object => {
object['set' + name](value, recursive);
});
break;
default:
break;
}
}
else {
child['set' + name](value, recursive);
}
}
}
_setAttributeState(inheritName, name, value, recursive = false) {
let bodyNode = this.bodyNode;
bodyNode.setAttribute(name, value);
// Change all children if needed
if (recursive && this.hasChildren()) {
this._setChildrenAttributeState(inheritName, name, value, recursive);
}
}
_syncAttributes() {
let flags = this._flags;
// Sync pickable state
let pickable = flags.has(Flag.Pickable);
if (!pickable) {
this._setAttributeState('pickable', 'Pickable', pickable, false);
}
// Sync keep size state
let keepSize = flags.has(Flag.KeepSize);
if (keepSize) {
this.transform.keepSize(true, this.app.camera);
}
// Sync renderable node state
if (this.hasComponent('render')) {
this.render.syncAttributes();
}
}
_getTempData() {
return super._getTempData((tempData) => {
let basePivot = [0.5, 0.5, 0.5];
if (this.hasComponent('transform')) {
basePivot = this.transform.getPivot();
}
tempData['pivot'] = basePivot.slice(0);
tempData['basePivot'] = basePivot;
});
}
_isSameResource(value) {
let _private = this[__.private];
if (Utils.isString(value)) {
value = { url: value };
}
let resource = _private.resource;
let children = resource.children;
if (children && children.length) {
return false;
}
if (resource.url != value.url) {
return false;
}
return true;
}
_beforeSetAttribute(callback) {
// Check whether skip to set attribute
if (Utils.isFunction(callback)) {
if (callback(this) === false) {
return false;
}
}
return true;
}
_needCopyComponent(isResident, hasInstance, classType) {
if (!hasInstance) {
// Skip for not resident component
if (!isResident) {
return false;
}
// Skip for not copy with instance component
if (classType.mustCopyWithInstance) {
return false;
}
}
return true;
}
_copyStyleValues(values) {
let style = this.body.style;
for (let key in values) {
let value = values[key];
switch (key) {
case 'attributes':
for (let attributeKey in value) {
style.setAttribute(attributeKey, value[attributeKey]);
}
break;
case 'uniforms':
for (let uniformKey in value) {
style.setUniform(uniformKey, value[uniformKey]);
}
break;
case 'macros':
for (let macroKey in value) {
style.setMacro(macroKey, value[macroKey]);
}
break;
default:
style[key] = value;
break;
}
}
}
_renderComponents() {
let renderableComponents = this.renderableComponents;
if (renderableComponents) {
let _private = this[__.private];
if (!_private.renderLayout) {
let that = this;
_private.renderLayout = {
getCenter: function () {
return that.orientedBox.center;
},
selfToWorld: function () {
return that.selfToWorld.apply(that, arguments);
},
worldToScreen: function () {
let camera = that.app.camera;
return camera.worldToScreen.apply(camera, arguments);
}
};
}
for (let i = 0, l = renderableComponents.length; i < l; i++) {
let component = renderableComponents[i];
if (component.active === false) {
continue;
}
component.onRender(_private.renderLayout);
}
}
}
_traverseInheritance(callback) {
this.traverseInheritance(
(object) => {
let style = object.body.style;
if (!style) {
return;
}
callback(style);
},
_getStyleInheritance
);
}
_createStyleAttributes(data, attributeKey) {
let _private = this[__.private];
let attributes = new ObjectAttributes({
data,
onChange: (ev) => {
let key = ev.key;
let propName = ev.propName;
let value = Utils.parseValue(ev.value, null);
this._traverseInheritance((bodyStyle) => {
let attribute = bodyStyle[attributeKey];
if (propName) {
attribute[propName][key] = value;
}
else {
attribute[key] = value;
}
});
}
});
_private.styleObjectAttributes.push(attributes);
return attributes.values;
}
_createStyle() {
let objectBodyStyle = this.body.style;
if (!objectBodyStyle) {
return null;
}
let style = {};
// #region Hook functions
style.begin = () => {
this._traverseInheritance((bodyStyle) => {
bodyStyle.begin();
});
};
style.end = () => {
this._traverseInheritance((bodyStyle) => {
bodyStyle.end();
});
};
style.beginDefaultValues = () => {
this._traverseInheritance((bodyStyle) => {
bodyStyle.beginDefaultValues();
});
};
style.endDefaultValues = () => {
this._traverseInheritance((bodyStyle) => {
bodyStyle.endDefaultValues();
});
};
style.setAttribute = (type, value) => {
this._traverseInheritance((bodyStyle) => {
bodyStyle.setAttribute(type, value);
});
};
style.getAttribute = (type) => {
return this.body.style.getAttribute(type);
};
style.setUniform = (name, value) => {
this._traverseInheritance((bodyStyle) => {
bodyStyle.setUniform(name, value);
});
};
style.getUniform = (name) => {
return this.body.style.getUniform(name);
};
style.setMacro = (name, value) => {
this._traverseInheritance((bodyStyle) => {
bodyStyle.setMacro(name, value);
});
};
style.getMacro = (name) => {
return this.body.style.getMacro(name);
};
style.getUV = (type) => {
return this.body.style.getUV(type);
}
style.getDiffValues = () => {
return this.body.style.getDiffValues();
}
style.setUV = (type, key, value) => {
this._traverseInheritance((bodyStyle) => {
bodyStyle.setUV(type, key, value);
});
}
style.copy = (from) => {
if (from.isObjectStyle) {
from = from.object.body.style;
}
this._traverseInheritance((bodyStyle) => {
bodyStyle.copy(from);
});
}
style.copyFromProperties = (values) => {
this._traverseInheritance((bodyStyle) => {
bodyStyle.copyFromProperties(values);
});
}
style.getProperties = () => {
return this.body.style.getProperties();
}
// #endregion
// #region Hook attributes
let edge, effect, attributes, uniforms, macros, uv;
// Hook get accessor
Object.defineProperties(style, {
'isInstancedDrawing': {
get: () => {
return this.body.style.isInstancedDrawing;
}
},
'object': {
get: () => {
return this;
}
},
'isObjectStyle': {
get: () => {
return true;
}
},
'edge': {
get: () => {
edge = edge || this._createStyleAttributes(objectBodyStyle.getData('edge'), 'edge');
return edge;
},
set: (value) => {
this._traverseInheritance((bodyStyle) => {
bodyStyle.edge = value;
});
}
},
'effect': {
get: () => {
effect = effect || this._createStyleAttributes(objectBodyStyle.getData('effect'), 'effect');
return effect;
},
set: (value) => {
this._traverseInheritance((bodyStyle) => {
bodyStyle.effect = value;
});
}
},
'attributes': {
get: () => {
attributes = attributes || this._createStyleAttributes(objectBodyStyle.getData('attributes'), 'attributes');
return attributes;
},
set: (value) => {
this._traverseInheritance((bodyStyle) => {
bodyStyle.attributes = value;
});
}
},
'uniforms': {
get: () => {
uniforms = uniforms || this._createStyleAttributes(objectBodyStyle.getData('uniforms'), 'uniforms');
return uniforms;
},
set: (value) => {
this._traverseInheritance((bodyStyle) => {
bodyStyle.uniforms = value;
});
}
},
'macros': {
get: () => {
macros = macros || this._createStyleAttributes(objectBodyStyle.getData('macros'), 'macros');
return macros;
},
set: (value) => {
this._traverseInheritance((bodyStyle) => {
bodyStyle.macros = value;
});
}
},
'uv': {
get: () => {
uv = uv || this._createStyleAttributes(objectBodyStyle.getData('uv').map, 'uv');
return uv;
},
set: (value) => {
this._traverseInheritance((bodyStyle) => {
bodyStyle.uv = value;
});
}
}
});
// Collect common accessors keys
let keys = Object.keys(Style.cDefaultValues);
keys.push('imageSlotType');
keys.push('image');
keys.push('map');
keys.push('envMap');
keys.push('alphaMap');
keys.push('emissiveMap');
keys.push('normalMap');
keys.push('colorMapping');
keys.push('aoMap');
// Hook common accessors
keys.forEach(key => {
Object.defineProperty(style, key, {
get: () => {
let value = this.body.style[key];
if (Utils.isArray(value)) {
return value.slice(0);
}
else {
return value;
}
},
set: (value) => {
this._traverseInheritance((bodyStyle) => {
bodyStyle[key] = value;
});
}
});
});
// #endregion
// #region JSON
style.toJSON = () => {
return this.body.style.toJSON();
};
// #endregion
return style;
}
_getStyleValues(style) {
if (style) {
if (style.isObjectStyle || style.isStyle) {
return JSON.parse(JSON.stringify(style));
}
}
return style;
}
// #endregion
// #region Overrides
onSetupParent(param) {
this.onSetupComponent(param);
this.onSetupBody(param);
super.onSetupParent(param);
}
onSetupComponent(param) {
this.addComponent(HelperComponent, 'helper', registerComponentParam);
this.addComponent(LerpComponent, 'lerp', registerComponentParam);
this.addComponent(BoundingComponent, 'bounding', registerComponentParam);
this.addComponent(LevelComponent, 'level', registerComponentParam);
this.addComponent(ActionGroupComponent, 'actionGroup', registerComponentParam);
this.addComponent(BlueprintComponent, 'blueprint', registerComponentParam);
this.addComponent(ColliderComponent, 'collider', registerComponentParam);
this.addComponent(TransformComponent, 'transform', registerComponentParam);
this.addComponent(RenderComponent, 'render', registerComponentParam);
this.addComponent(RelationshipComponent, 'relationship', registerComponentParam);
this.addComponent(TaskExecutorComponent, 'taskExecutor', registerComponentParam);
this.addComponent(MonitorDataComponent, 'monitorData', registerComponentParam);
}
onSetupBody(param) {
this._body = new BodyObject();
this._body.init(this, param);
// Sync body node name
let name = param['name'] || this.name;
this.node.setName(name);
// If we provide root or renderable node then indicates resource is ready
let rootNode = param['rootNode'];
let renderableNode = param['renderableNode'];
if (rootNode || renderableNode) {
let dynamic = Utils.parseValue(param['dynamic'], false);
if (!dynamic) {
this._resourceState = Object3D.ResourceState.Loaded;
}
}
}
onSetupAttributes(param) {
let _private = this[__.private];
// Setup tags
let defaultTags = this.onGetDefaultTags();
_private.tags = Utils.mergeSet(param['tags'], defaultTags);
// Setup extras
let external = param['extras'] || param['external'];
if (external) {
_private.external = Object.assign({}, external);
}
}
onGetDefaultTags() {
return this.constructor.defaultTagArray;
}
onSetupState(options) {
// We are loading resource now
let dynamic = Utils.parseValue(options['dynamic'], false);
if (!dynamic && this._resourceState != Object3D.ResourceState.Loaded) {
this._resourceState = Object3D.ResourceState.Loading;
}
// Setup pickable
let pickable = Utils.parseValue(options['pickable'], true);
if (!pickable) {
this.pickable = false;
}
// Setup visibility
let visible = Utils.parseValue(options['visible'], true);
if (!visible) {
this.visible = false;
}
// Setup active
let active = Utils.parseValue(options['active'], true);
if (!active) {
this.active = false;
}
// Setup render attributes
if (Utils.isNumber(options['renderOrder'])) {
this.renderOrder = options['renderOrder'];
}
if (Utils.isBoolean(options['castShadow'])) {
this.castShadow = options['castShadow'];
}
if (Utils.isBoolean(options['receiveShadow'])) {
this.receiveShadow = options['receiveShadow'];
}
if (Utils.isBoolean(options['alwaysOnTop'])) {
this.alwaysOnTop = options['alwaysOnTop'];
}
if (Utils.isNumber(options['layerMask'])) {
this.layerMask = options['layerMask'];
}
}
onSetupPosition(param) {
if (param['localPosition']) {
this.localPosition = param['localPosition'];
}
else if (param['position']) {
this.position = param['position'];
}
}
onSetupAngles(param) {
if (param['localAngles']) {
this.localAngles = param['localAngles'];
}
if (param['localRotation']) {
this.localRotation = param['localRotation'];
}
else if (param['localQuaternion']) {
this.localQuaternion = param['localQuaternion'];
}
else if (param['angles']) {
this.angles = param['angles'];
}
else if (param['rotation']) {
this.rotation = param['rotation'];
}
}
onSetupScale(param) {
if (param['localScale']) {
this.localScale = param['localScale'];
}
else if (param['scale']) {
this.scale = param['scale'];
}
}
onSetupMatrix(param) {
if (param['matrix']) {
this.matrix = param['matrix'];
}
}
onSetupTransform(param) {
this.onSetupPosition(param);
this.onSetupAngles(param);
this.onSetupScale(param);
this.onSetupMatrix(param);
// Setup keep size
if (Utils.isValid(param['keepSizeDistance'])) {
this.keepSizeDistance = param['keepSizeDistance'];
}
if (param['keepSize']) {
this.keepSize = param['keepSize'];
}
}
onSetupStyle(param) {
let styleValues = this._getStyleValues(param['style']);
if (!styleValues) {
return;
}
let style = this.body.style;
style.begin();
this._copyStyleValues(styleValues);
const defaultValues = styleValues['default'];
if (defaultValues) {
style.beginDefaultValues();
this._copyStyleValues(defaultValues);
style.endDefaultValues();
}
style.end();
}
onSetupURL(param) {
let _private = this[__.private];
// Load resource from URL if needed
_private.resource.url = Utils.parseValue(param.url, '');
if (param.nodeName) {
_private.resource.nodeName = param.nodeName;
}
if (param.inverseRotationMode) {
_private.resource.inverseRotationMode = param.inverseRotationMode;
}
if (param.excludeNodeNames) {
_private.resource.excludeNodeNames = param.excludeNodeNames;
}
if (param.instanceId) {
_private.resource.instanceId = param.instanceId;
}
if (param.instanceCount) {
_private.resource.instanceCount = param.instanceCount;
}
if (param.instanceStyle) {
_private.resource.instanceStyle = param.instanceStyle;
}
}
onSetupResource(param) {
this.onSetupURL(param);
// Setup components
const components = param['components'];
const toolkit = param['toolkit'];
if (components && toolkit) {
this.onImportComponents(components, toolkit, param);
}
// Start to load resource
this._onLoadResource(param,
// Resolve
() => {
let dynamic = Utils.parseValue(param['dynamic'], false);
if (!dynamic) {
this.onLoadComplete();
}
},
// Reject
(ev) => {
let _private = this[__.private];
this._resourceState = Object3D.ResourceState.Error;
if (this._loadPromise) {
this._loadPromise.reject(ev);
}
// Notify error
if (_private.onError) {
_private.onError(ev);
}
}
);
}
onSetupComplete(param) {
// Set pivot
let pivot = param['pivot'];
if (pivot) {
this.pivot = pivot;
}
// Initialize done
this._flags.enable(Flag.Initializing, false);
}
/**
* When create.
* @param {Object} options The options.
* @private
*/
onCreate(param) {
// We have loaded resource
if (this.loaded) {
this.onNotifyComplete();
}
else {
// If we do not need any dynamic then notify complete directly
let dynamic = Utils.parseValue(param['dynamic'], false);
if (dynamic) {
this.onNotifyComplete();
}
}
// Notify create event in batch mode
Utils.setBatchTimeout(() => {
if (this.destroyed) {
return;
}
this.trigger(EventType.Create);
});
}
clearInitialLocalBoundingBox() {
// Clear initial bounding box
if (this.hasComponent('bounding')) {
this.initialLocalBoundingBox = null;
}
}
onLoadComplete() {
if (this.destroyed) {
return;
}
switch (this._resourceState) {
// Load complete (loading->loaded)
case Object3D.ResourceState.Loading:
case Object3D.ResourceState.Loaded:
let _private = this[__.private];
// Update loaded flag
this._resourceState = Object3D.ResourceState.Loaded;
// Sync attributes
this._syncAttributes();
this.clearInitialLocalBoundingBox();
// Load completed
if (this._loadPromise) {
this._loadPromise.resolve({ object: this });
}
// Notify load resource event in batch and delay mode to make sure event order is: create -> load
Utils.setBatchTimeout(() => {
Utils.setTimeout(() => {
this.trigger(EventType.Load);
});
});
// Notify outside if we have completed
this.onNotifyComplete();
break;
// If it's not in loaded/loading state then indicates we have unload in during loading state
case Object3D.ResourceState.Ready:
// Unload resource what just loaded ... what a pity
this._unloadResource(false);
break;
default:
break;
}
}
/**
* When notify sync-complete.
* @private
*/
onSyncComplete() {
let _private = this[__.private];
let onSyncComplete = _private.onSyncComplete;
if (onSyncComplete) {
onSyncComplete({ object: this });
_private.onSyncComplete = null;
}
}
onDelayComplete() {
let _private = this[__.private];
let onComplete = _private.onComplete;
if (onComplete) {
Utils.setTimeout(() => {
if (this.destroyed) {
return;
}
onComplete({ object: this });
});
_private.onComplete = null;
}
}
onProcessSyncCompleteCallbacks() {
let _private = this[__.private];
let syncCompleteCallbacks = _private.syncCompleteCallbacks;
if (syncCompleteCallbacks.length) {
_private.syncCompleteCallbacks.forEach(callback => {
callback();
});
_private.syncCompleteCallbacks.length = 0;
}
}
/**
* When notify complete.
* @private
*/
onNotifyComplete() {
this.onSyncComplete();
this.onDelayComplete();
this.onProcessSyncCompleteCallbacks();
}
/**
* 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._flags.has(Flag.Initializing)) {
this.onRefresh();
}
// Prevent call this function from parent class, so we delay it
Utils.setTimeout(() => {
if (this.destroyed) {
return;
}
let dynamic = Utils.parseValue(options['dynamic'], false);
if (!dynamic) {
// Notify all loadable components
let loadableComponents = this.loadableComponents;
if (loadableComponents) {
loadableComponents.forEach(component => {
const promise = component.onLoadResource();
promise && this.taskExecutor.add(promise);
});
}
if (this.hasResource()) {
this.onLoadRenderableResource(options, resolve, reject);
}
// Here we do not proivde any URL, so let it to be loaded state
else {
resolve();
}
}
else {
resolve();
}
});
}
/**
* When unload resource.
* @private
*/
onUnloadResource() {
}
onAfterDestroy() {
let _private = this[__.private];
// Destroy body object
if (this._body) {
this._body.dispose();
this._body = null;
}
_private.style = undefined;
_private.styleObjectAttributes.forEach(objectAttribute => {
objectAttribute.dispose();
});
_private.styleObjectAttributes.length = 0;
_private.renderLayout = null;
_private.onError = null;
_private.onComplete = null;
_private.onSyncComplete = null;
_private.options = {};
super.onAfterDestroy();
}
onAddToParentNode(node, parentNode, options) {
// If node has not parent then we can not use attach mode, due to world position is always [0, 0, 0]
let enableAttachMode = node.getParent() ? true : false;
if (enableAttachMode) {
let attachMode = Utils.parseValue(options['attachMode'], true);
if (attachMode) {
parentNode.attach(node);
}
else {
parentNode.add(node);
}
}
else {
parentNode.add(node);
}
}
onCopy(object) {
}
onCreateBodyNode(type) {
return Utils.createObject(type);
}
onGetPivot() {
let tempData = this._getTempData();
let pivot = this.transform.getPivot();
tempData['pivot'] = pivot;
return pivot;
}
onSetPivot(value) {
if (this.loaded) {
this.transform.setPivot(value, this._getTempData()['basePivot']);
}
else {
this.addSyncCompleteCallback(() => {
let tempData = this._getTempData();
let pivot = tempData['pivot'];
if (pivot) {
let position = this.position;
this.transform.setPivot(value, this._getTempData()['basePivot']);
this.position = position;
}
});
}
}
onUpdate(deltaTime) {
super.onUpdate(deltaTime);
this._renderComponents();
}
onLoadRenderableResource(options, resolve, reject) {
this.render.load(
options,
resolve,
() => {
if (this.options.error) {
this.options.error();
}
reject();
}
);
}
onBeforeSetParent(parent) {
// If the final parent is set to Object3D, you need to remove yourself from the parent.
this._needRemove = false;
}
onAddChild(object, options) {
this._body.createRootNode();
object.onSetParent(this, (parentNode) => {
this.onAddToParentNode(object.node, parentNode, options);
let attachMode = Utils.parseBoolean(options['attachMode'], true);
if (attachMode) {
return false;
}
let localPosition = options['localPosition'];
let ignoreScale = Utils.parseValue(options['ignoreScale'], true);
// Update position by sub node
let subNodeName = options['subNodeName'];
if (subNodeName) {
let node = this._body.getNodeByName(subNodeName);
if (!node) {
Utils.error(`Sub node '${subNodeName}' is not existing when add object`, this);
return false;
}
if (localPosition) {
let subNodeLocalPosition = this.worldToSelf(node.position);
let offset = MathUtils.addVector(subNodeLocalPosition, localPosition);
object.position = this.selfToWorld(offset, ignoreScale);
}
else {
object.position = node.position;
}
}
// Update position without sub node
else {
if (localPosition) {
object.position = this.selfToWorld(localPosition, ignoreScale);
}
}
});
}
// #endregion
// #region Common
/**
* Get/Set name.
* @type {String}
* @example
* object.name = 'MyObject';
* @public
*/
get name() {
return this._name;
}
set name(value) {
super.name = value;
this.node.setName(value);
}
/**
* Get/Set tags.
* @type {Set<String>}
* @example
* // Get tags
* let tags = object.tags;
* console.log(tags);
*
* // Set tags by array
* object.tags = ['one','two','three'];
*
* // Set tags by set
* object.tags = new Set(['one','two','three']);
* @public
*/
get tags() {
let _private = this[__.private];
_private.tags = _private.tags || new Set();
return _private.tags;
}
set tags(value) {
if (Utils.isArray(value) || Utils.isSet(value)) {
const tags = this.tags;
tags.clear();
value.forEach(element => {
tags.add(element);
});
}
}
/**
* Get/Set the external info.
* @type {Object}
* @private
*/
get external() {
let _private = this[__.private];
return _private.external;
}
set external(value) {
let _private = this[__.private];
_private.external = value;
}
// #region Render
/**
* Get/Set layer mask, it would use AND operator to test with camera's layer mask.
* If it's equals to 0 then indicates it can not render in this camera.
* The default value is 1.
* It CAN NOT work with instanced drawing mode for now.
* @type {Number}
* @example
* // Hide object by changing layer mask
* object.layerMask = 0;
* @public
*/
get layerMask() {
let bodyNode = this.bodyNode;
return bodyNode.getLayerMask();
}
set layerMask(value) {
let bodyNode = this.bodyNode;
let style = this.body.style;
if (style && style.isInstancedDrawing) {
Utils.warn(`The layer mask can not work in instanced drawing mode`);
}
bodyNode.setLayerMask(value);
}
// #endregion
// #endregion
// #region State
/**
* The function to call when start to process some action with object(s).
* @callback ProcessObjectCallback
* @param {Object3D} object The object.
* @returns {Boolean} False indicates reject to set attribute, otherwise continue to set attribute.
*/
/**
* Get visible state.
* @returns {Boolean}
* @example
* let visible = object.getVisible();
* if (visible) {
* console.log('object is showing');
* }
* @public
*/
getVisible() {
return this.bodyNode.getVisible();
}
/**
* Set visible state.
* @param {Boolean} value True indicates show it, otherwise hide it.
* @param {Boolean|ProcessObjectCallback} [recursive=false] If it's boolean value type then means whether process it with all children.
* @example
* // Hide object self only, exclude all children
* object.setVisible(false, false);
*
* // Hide object(s) but exclude children what name equals to 'stone'
* object.setVisible(false, (obj) => {
* if (obj.name == 'stone') {
* return false;
* }
* });
* @public
*/
setVisible(value, recursive = false) {
if (this._beforeSetAttribute(recursive)) {
// Update body visible state
let bodyNode = this.bodyNode;
if (bodyNode.getVisible() != value) {
bodyNode.setVisible(value);
this.trigger(EventType.VisibleChange, { value });
}
// If resource is not loaded then we would notify visible changed forcely
// In order to let dynamic load component work
else if (!this.loaded) {
this.trigger(EventType.VisibleChange, { value });
}
// Notify all components
let visibleComponents = this.visibleComponents;
if (visibleComponents) {
visibleComponents.forEach(component => {
if (component.onVisibleChange) {
component.onVisibleChange(value);
}
});
}
}
// Change all children if needed
// We make recursive as true value even though recursive is callback function
if (recursive && this.hasChildren()) {
this._setChildrenAttributeState('visible', 'Visible', value, recursive);
}
}
/**
* Get pickable state.
* @returns {Boolean}
* @private
*/
getPickable() {
return this._flags.has(Flag.Pickable);
}
/**
* Set pickable state.
* @param {Boolean} value The pickable state.
* @param {Boolean} [recursive=false] True indicates process it with all children.
* @private
*/
setPickable(value, recursive = false) {
this._flags.enable(Flag.Pickable, value);
this._setAttributeState('pickable', 'Pickable', value, recursive);
}
/**
* Get renderable state.
* @returns {Boolean}
* @private
*/
getRenderable() {
console.warn('Please get visible with getVisible instead of getRenderable');
return this.getVisible();
}
/**
* Set renderable state.
* @param {Boolean} value True indicates render it, otherwise do not render it.
* @param {Boolean} [recursive=false] True indicates process it with all children.
* @private
*/
setRenderable(value, recursive = false) {
console.warn('Please set visible with setVisible instead of setRenderable');
this.setVisible(value, recursive);
}
/**
* Get active state.
* @return {Boolean}
* @private
*/
getActive() {
return this._flags.has(Flag.Active);
}
/**
* Set active state.
* @param {Boolean} value True indicates active it, otherwise do not active it.
* @param {Boolean} [recursive=false] True indicates process it with all children.
* @private
*/
setActive(value, recursive = false) {
// Create root node to make sure active is different with visible
this._body.createRootNode();
// Update active state
if (this._flags.enable(Flag.Active, value)) {
// Update visible state of root node
this._body.getRootNode().setVisible(value);
// Update root node active state
this.node.getUserData()['active'] = value;
// Update all components
this.getAllComponents().forEach(component => {
component.active = value;
});
// Notify active changed
this.trigger(EventType.ActiveChange, { value });
}
// Let the dynamic load component to work
else if (!this.loaded) {
this.trigger(EventType.ActiveChange, { value });
}
// Change all children if needed
if (recursive) {
this.children.forEach(child => {
child.setActive(value, true);
});
}
}
hasInherit() {
let _private = this[__.private];
return !!_private.inherit;
}
/**
* @typedef {Object} InheritData
* @property {InheritType} style? The style attributes inherit type (default value is InheritType.Normal).
* @property {InheritType} visible? The visible attributes inherit type (default value is InheritType.Normal).
* @property {InheritType} pickable? The pickable attributes inherit type (default value is InheritType.Normal).
* @public
*/
/**
* Get the inherit.
* @type {InheritData}
* @example
* object.inherit.style = THING.InheritType.Jump;
* object.inherit.visible = THING.InheritType.Break;
* object.inherit.pickable = THING.InheritType.Stop;
* @public
*/
get inherit() {
let _private = this[__.private];
if (!_private.inherit) {
_private.inherit = new TypedObject({
onConvertValue: (key, value) => {
if (Utils.isBoolean(value)) {
return value ? InheritType.Normal : InheritType.Stop;
}
return value;
},
values: {
visible: InheritType.Normal,
pickable: InheritType.Normal,
style: InheritType.Normal
}
});
}
return _private.inherit;
}
/**
* Get/Set visible state.
* @type {Boolean}
* @example
* let object = new THING.Box();
* // @expect(object.visible == true)
* object.visible = false;
* // @expect(object.visible == false)
* @public
*/
get visible() {
return this.getVisible();
}
set visible(value) {
this.setVisible(value, true);
}
/**
* Check whether it's invisible in scene.
* @type {Boolean}
* @private
*/
get invisible() {
return !this._body.getRenderableNode().getSceneVisible();
}
/**
* Get/Set the pickable state.
* @type {Boolean}
* @example
* let object = new THING.Box();
* object.pickable = false;
* @public
*/
get pickable() {
return this.getPickable();
}
set pickable(value) {
this.setPickable(value, true);
}
/**
* Get/Set renderable state, would not change any children active.
* @type {Boolean}
* @private
*/
get renderable() {
return this.getRenderable();
}
set renderable(value) {
this.setRenderable(value, false);
}
/**
* Get/Set active state, would not change any children active.
* @type {Boolean}
* @example
* let box = new THING.Box();
* let box2 = new THING.Box({parent: box});
* box.active = false;
* // @expect(box2.active == true);
* // @expect(box.active == false);
* @public
*/
get active() {
return this.getActive();
}
set active(value) {
this.setActive(value, false);
}
// #endregion
// #region Transform
/**
* Get the distance to world position.
* @param {THING.Object3D|Array<Number>} target The target object or world position.
* @returns {Number}
* @example
* let distance = object.distanceTo([0, 10, 0]);
* if (distance > 5000) {
* console.log('object is so far from specified position');
* }
* @public
*/
distanceTo(target) {
if (target.isObject3D) {
return MathUtils.getDistance(this.position, target.position);
}
else {
return MathUtils.getDistance(this.position, target);
}
}
/**
* Keep object's size in screen(auto adjust object's scale).
* @type {Boolean}
* @example
* let keepSize = object.keepSize;
* if (keepSize) {
* console.log('object is keep size to render');
* }
* @public
*/
get keepSize() {
return this._flags.has(Flag.KeepSize);
}
set keepSize(value) {
this._flags.enable(Flag.KeepSize, value);
this.transform.keepSize(value, this.app.camera);
}
/**
* Get/Set pivot in self oriented box from [left, bottom, back].
* @type {Array<Number>}
* @example
* // Make object origin to [right, top, front] position
* object.pivot = [1, 1, 1];
* @public
*/
get pivot() {
return this.getPivot();
}
set pivot(value) {
value = value || [0.5, 0.5, 0.5];
if (MathUtils.equalsVector3(this.pivot, value)) {
return;
}
this.setPivot(value);
}
// #endregion
// #region Traverse
traverseInheritance(callback, onGetInheritance, jumpSelf) {
if (!jumpSelf) {
callback(this);
}
if (this.hasChildren()) {
let children = this.children;
for (let i = 0, l = children.length; i < l; i++) {
let child = children[i];
let inheritance = onGetInheritance(child);
switch (inheritance) {
case InheritType.Normal:
child.traverseInheritance(callback, onGetInheritance);
break;
case InheritType.Break:
callback(child);
break;
case InheritType.Jump:
child.traverseInheritance(callback, onGetInheritance, true);
break;
case InheritType.Stop:
break;
default:
break;
}
}
}
}
// #endregion
// #region Nodes
/**
* Set the parent.
* @param {THING.BaseObject} parent The parent.
* @param {Function} onAddNode The add node into parent callback function.
* @private
*/
onSetParent(parent, onAddNode, options = {}) {
// Attach to new parent
if (parent) {
if (onAddNode) {
onAddNode(parent.node);
}
}
// Just remove from current parent in render tree
else if (this.parent && this.parent.node) {
if (this._needRemove) {
this.parent.node.remove(this.node);
}
else {
// When modifying the parent, ensure that the parent is not empty
// Otherwise, the "attach" will fail
let attachMode = Utils.parseBoolean(options.attachMode, true);
if (attachMode) {
this.app.root.node.attach(this.node);
}
else {
this.app.root.node.add(this.node);
}
}
}
this._needRemove = true;
// Set the parent
super.onSetParent(parent);
// Notify all components
let parentChangeComponents = this.parentChangeComponents;
if (parentChangeComponents) {
parentChangeComponents.forEach(component => {
if (component.onParentChange) {
component.onParentChange(parent);
}
});
}
}
/**
* Add object as child.
* @param {THING.BaseObject} object The object what you want to add.
* @param {Object} options The options.
* @param {String} options.subNodeName The sub node name.
* @param {Array<Number>} options.localPosition The local position of parent or sub node.
* @param {Boolean} [options.attachMode=true] True indicates to keep world transform.
* @param {Boolean} [options.ignoreScale=false] True indicates ignore scale when add it as child.
* @returns {Boolean}
* @example
* // Keep local transform of box to be added to object
* object.add(new THING.Box({ localPosition: [0, 10, 0]}), { attachMode: false });
* @public
*/
add(object, options = {}) {
super.add(object, options);
}
/**
* Promote node as child object.
* @param {String} name The node name.
* @param {THING.Object3D} parent The parent object, if it's null then indicates use current object as parent.
* @returns {THING.Object3D}
* @example
* // Promote all sub nodes
* var nodeNames = entity.body.nodeNames;
* nodeNames.forEach(name => {
* entity.promoteNode(name);
* });
* @public
*/
promoteNode(name, parent) {
if (!name) {
return null;
}
// We can not promote node when it's in instanced drawing mode
if (this.isInstancedDrawing) {
this.makeInstancedDrawing(false);
}
// Create root node to let sub node attach it
this._body.createRootNode();
// Get the parent
parent = parent || this;
// Remove some components, due to we can not work it together
this.removeComponent('animation');
// Promote node in body object
let node = this._body.promoteNode(name, parent.node);
// Create object
let object = new Object3D({
name,
renderableNode: node,
parent,
});
return object;
}
/**
* Get pivot in self oriented box.
* @type {Array<Number>}
* @private
*/
getPivot() {
let tempData = this._getTempData();
let data = tempData['pivot'];
if (data) {
return data;
}
return this.onGetPivot();
}
/**
* Set pivot in self oriented box.
* @param {Array<Number>} value The pivot value.
* @private
*/
setPivot(value) {
if (value.length < 3) {
value[0] = Utils.parseValue(value[0], 0.5);
value[1] = Utils.parseValue(value[1], 0.5);
value[2] = Utils.parseValue(value[2], 0.5);
}
let tempData = this._getTempData();
this.onSetPivot(value);
tempData['pivot'] = value;
}
/**
* Clear pivot node.
* @private
*/
clearPivot() {
this.transform.clearPivot();
let tempData = this._getTempData();
delete tempData['pivot'];
}
/**
* Get the attached points.
* @type {THING.Selector}
* @private
*/
get attachedPoints() {
return this.query('.AttachedPoint', { recursive: false });
}
// #endregion
// #region Picker
/**
* @typedef {Object} RaycastInfo
* @property {Array<Number>} origin The start position.
* @property {Array<Number>} direction The direction in world space.
*/
/**
* @typedef {Object} RaycastResult
* @property {THING.Object3D} object The picked object.
* @property {Array<Number>} position The picked position.
* @property {Number} distance The distance to ray origin.
*/
/**
* Raycast test.
* @param {RaycastInfo} ray The ray info.
* @param {Boolean} [recursive=true] True indicates process it with all children.
* @returns {Array<RaycastResult>} The test result(sort by distance from near to far).
* @private
*/
raycast(ray, recursive = true) {
let node = recursive ? this.node : this.bodyNode;
// Only works on renderable object
if (node.isRenderableNode) {
let targets = [];
node.raycast(ray, targets);
return targets.map(target => {
return {
node: target.node,
distance: target.distance,
position: target.position
};
});
}
// We can not ray cast on none-renderable object
else {
return {
node: null,
distance: 0,
position: null
};
}
}
/**
* Get/Set pick mode.
* @type {PickMode}
* @private
*/
get pickMode() {
return this.bodyNode.getAttribute('PickMode');
}
set pickMode(value) {
this.bodyNode.setAttribute('PickMode', value);
}
// #endregion
// #region Style
hasStyle() {
return this.body.hasStyle();
}
getStyle() {
let _private = this[__.private];
if (_private.style === undefined) {
_private.style = this._createStyle();
}
if (_private.style) {
return _private.style;
}
else {
return null;
}
}
/**
* Set style values.
* @param {*} value The style values.
* @param {Boolean} [recursive=false] True indicates to set all children style values.
* @example
* let box1 = new THING.Box();
* let box2 = new THING.Box({parent: box1});
* box1.setStyle({color: 'red'});
* // @expect(box1.style.color[0] == 1);
* // @expect(box1.style.color[1] == 0);
* // @expect(box1.style.color[2] == 0);
* // @expect(box2.style.color == null);
* @public
*/
setStyle(value, recursive = false) {
if (recursive) {
this.traverse((object) => {
object.body.setStyle(value);
});
}
else {
this.body.setStyle(value);
}
}
/**
* Get/Set style.
* @type {THING.Style}
* @example
* let style = object.style;
* style.color = 'red';
* style.opacity = 0.1;
* @public
*/
get style() {
return this.getStyle();
}
set style(value) {
this.setStyle(value);
}
// #endregion
// #region Resources
onBeforeDestroy() {
if (this.destroyed) {
return false;
}
this.onSyncBeforeDestroy();
// Disable instanced drawing first
if (this.isInstancedDrawing) {
this.makeInstancedDrawing(false);
}
// Skip matrix update to speed up render
this.node.setAttribute('SkipMatrixUpdate', true);
return super.onBeforeDestroy();
}
/**
* When notify sync-before-destroy.
* @private
*/
onSyncBeforeDestroy() {
let _private = this[__.private];
let onSyncBeforeDestroy = _private.onSyncBeforeDestroy;
if (onSyncBeforeDestroy) {
onSyncBeforeDestroy({ object: this });
_private.onSyncBeforeDestroy = null;
}
}
/**
* Clone the object, if the clone target has been loaded, clone the node directly, otherwise use url clone.
* @param {Boolean} recursive True indicates to load all children resources.
* @param {THING.BaseObject} parent The parent object, default is app root object.
* @returns {THING.Object3D}
* @example
* // Clone the object and move up
* let otherObject = object.clone();
* otherObject.translateY(10);
* @public
*/
clone(recursive, parent, options = {}) {
let object = null;
let attachMode = Utils.parseValue(options['attachMode'], true);
let objParent = parent !== undefined ? parent : this.app.root;
object = Utils.applyNew(this.constructor, {
dynamic: true,
parent: objParent
});
object.options = Object.assign({}, options);
object.copy(this);
// Update the localPosition whit attachMode
if (!attachMode && objParent != this.parent) {
object.localPosition = this.localPosition;
}
recursive = Utils.parseBoolean(recursive, true);
if (recursive) {
this.children.forEach(child => {
child.clone(true, object, { attachMode: false });
});
}
return object;
}
/**
* Clone in async mode.
* @param {Boolean} recursive True indicates to load all children resources.
* @param {THING.BaseObject} parent The parent object, default is app root object.
* @returns {Promise<any>}
* @private
*/
cloneAsync(recursive, parent, options) {
return new Promise((resolve, reject) => {
let object = this.clone(recursive, parent, options);
if (object) {
object.copyPromise.then(() => {
resolve(object);
});
}
else {
reject();
}
});
}
/**
* Wait for object load completed.
* @returns {Promise<any>}
* @example
* // Wait for object load completed then watch it
* await object.waitForComplete();
* app.camera.fit(object);
* @public
*/
waitForComplete() {
if (this.loaded) {
return Promise.resolve({ object: this });
}
else if (this._resourceState == Object3D.ResourceState.Error) {
return Promise.reject({ object: this, desc: 'Object load failed' });
}
if (!this._loadPromise) {
this._loadPromise = new ResolvablePromise();
}
return this._loadPromise;
}
/**
* @typedef {Object} ResourceResult
* @property {String} url The url.
* @property {Array<Number>} localPosition? The local position.
* @property {Array<Number>} localAngles? The local angles.
* @property {Array<Number>} localScale? The local scale.
* @property {Array<ResourceResult>} children? The children.
*/
/**
* Set resource only, do not load resource.
* @param {ResourceResult} value The resource.
* @private
*/
setResource(value) {
let _private = this[__.private];
if (Utils.isString(value)) {
value = { url: value };
}
_private.resource = value;
}
/**
* Get resource.
* @returns {ResourceResult}
* @private
*/
getResource() {
let _private = this[__.private];
return _private.resource;
}
/**
* Check whether has resource.
* @returns {Boolean}
* @private
*/
hasResource() {
let _private = this[__.private];
let resource = _private.resource;
if (resource.url) {
return true;
}
let children = resource.children;
if (children && children.length) {
return true;
}
return false;
}
/**
* Reload resource.
* @param {Boolean} [recursive=true] True indicates to load all children resources.
* @param {Object} [options={}] The load options.
* @returns {Promise<any>}
* @private
*/
reloadResource(recursive = true, options = {}) {
return this.unloadResource(recursive).then(() => {
this.loadResource(recursive, options);
});
}
/**
* Load resource.
* @param {Boolean} [recursive=true] True indicates to load all children resources.
* @param {Object} [options={}] The load options.
* @returns {Promise<any>}
* @example
* // Wait the object to load resource completed
* await object.loadResource();
* @public
*/
loadResource(recursive = true, options = {}) {
return new Promise((resolve, reject) => {
// Load child resources
if (recursive) {
let promises = [];
if (this.hasChildren()) {
this.children.forEach(child => {
let promise = child.loadResource(recursive, options);
if (!promise) {
Utils.error(`Load resource failed, due to promise is invalid`, child);
return;
}
promises.push(promise);
});
}
if (promises.length) {
Promise.all(promises).then(() => {
this._loadSelfResource(options, resolve, reject);
});
}
else {
this._loadSelfResource(options, resolve, reject);
}
}
else {
this._loadSelfResource(options, resolve, reject);
}
}).then(
// OK
() => {
this.onLoadComplete();
},
// Error
() => {
}
);
}
/**
* Unload resource.
* @param {Boolean} [recursive=true] True indicates to unload all children resources.
* @returns {Promise<any>}
* @example
* // Wait the object to unload resource completed
* await object.unloadResource();
* @public
*/
unloadResource(recursive = true) {
// We can not unload resource when it's in instanced drawing mode
this.makeInstancedDrawing(false);
return new Promise((resolve, reject) => {
// Loaded -> Unload
if (this.loaded) {
// Start to unload resource
this._unloadResource(recursive);
// We are in ready to load resource state now
this._resourceState = Object3D.ResourceState.Ready;
// Clear some promises
this._loadPromise = null;
resolve();
}
// Loading -> Wait for complete -> Unload
else if (this.loading) {
// Wait for complete
this.waitForComplete().then(() => {
this.unloadResource();
resolve();
});
}
else {
resolve();
}
});
}
setBodyNode(node) {
this.body.setNode(node);
}
getBodyNode() {
return this.bodyNode;
}
/**
* Get/Set the options.
* @type {Object}
* @private
*/
get options() {
let _private = this[__.private];
return _private.options;
}
set options(value) {
let _private = this[__.private];
if (value) {
_private.options = Object.assign({}, value);
}
else {
_private.options = {};
}
}
get copyPromise() {
return this[__.private].copyPromise;
}
/**
* Get/Set the loaded flag.
* @type {Boolean}
* @private
*/
get loaded() {
return this._resourceState == Object3D.ResourceState.Loaded;
}
set loaded(value) {
if (value) {
this._resourceState = Object3D.ResourceState.Loaded;
}
else {
this._resourceState = Object3D.ResourceState.Ready;
}
}
/**
* Get the loading flag.
* @type {Boolean}
* @private
*/
get loading() {
return this._resourceState == Object3D.ResourceState.Loading;
}
/**
* Get the body object.
* @type {THING.BodyObject}
* @public
*/
get body() {
return this._body;
}
/**
* Get the root node.
* @type {Object}
* @private
*/
get node() {
return this._body.getNode();
}
/**
* Get the pivot node.
* @type {Object}
* @private
*/
get pivotNode() {
return this._body.getPivotNode();
}
/**
* Get the body node.
* @type {Object}
* @private
*/
get bodyNode() {
return this._body.getRenderableNode();
}
/**
* Get/Set resource.
* @type {ResourceResult}
* @private
*/
get resource() {
return this[__.private].resource;
}
set resource(value) {
if (!value) {
return;
}
// Skip for the same resource
if (this._isSameResource(value)) {
return;
}
this._updateResource(value);
}
// #endregion
// #region copy
/**
* Copy from object (except UUID attribute and parent).
* It would reload resource.
* @param {THING.BaseObject} object The source object.
* @returns {Promise<any>}
* @example
* // Copy the object from query result
* let sourceObject = app.query('#master')[0];
* if (sourceObject) {
* object.copy(sourceObject);
* }
* @public
*/
copy(object, options = {}) {
let _private = this[__.private];
// Start to copy values
this.onBeforeCopy(object, options);
// Start to copy style
this.onCopyStyle(object, options);
// Create copy promise
_private.copyPromise = new ResolvablePromise();
// Wait resource promise to finish
let promises = [];
promises.push(this._createResourcePromise());
promises.push(object.waitForComplete());
return Promise.all(promises).then(() => {
if (object.isInstancedDrawing === false && object.style.isInstancedDrawing === true) {
let ustyle = object.bodyNode.getStyle();
let curInstanceCount = ustyle.getInstancedCount();
let maxInstanceCOunt = ustyle.getInstancedMaxCount();
if (curInstanceCount >= maxInstanceCOunt) {
ustyle.setInstancedMaxCount(maxInstanceCOunt * 2);
}
}
// Create copy promise
const cloneNode = object.bodyNode.clone();
this.body.setNode(cloneNode);
// Copy component
this.onCopyComponents(object);
// Continue to copy object on inherited class
this.onCopy(object);
// Make instanced drawing mode when it's needed
if (object.isInstancedDrawing) {
this.makeInstancedDrawing();
}
this._resourceState = Object3D.ResourceState.Loaded;
this.onLoadComplete();
// Copy finished
_private.copyPromise.resolve();
});
}
onCopyComponents(object) {
// Copy components
let accessorInfo = object.getAccessorInfo();
for (let name in accessorInfo) {
let { isResident, hasInstance, classType } = accessorInfo[name];
// Check whether need to copy it
if (!this._needCopyComponent(isResident, hasInstance, classType)) {
continue;
}
let accessor = accessorInfo[name].accessor;
let sourceComponent = accessor.get();
let targetComponent = this[name];
if (!targetComponent) {
this.addComponent(classType, name, accessorInfo[name].args);
targetComponent = this[name];
}
if (targetComponent.onCopy) {
targetComponent.onCopy(sourceComponent);
}
else if (targetComponent.onImport) {
if (sourceComponent && sourceComponent.onExport) {
let data = sourceComponent.onExport();
if (data) {
targetComponent.onImport(Utils.cloneObject(data, false));
}
}
}
}
}
onCopyResource(object) {
// Update resource
this._updateResource(object.resource).then(() => {
this.onCopyComponents(object);
});
}
onBeforeCopy(object, options = {}) {
// Common
this.onCopyAttribute(object, options);
// Update transform
this.onCopyTransform(object, options);
}
onAfterCopy(object) {
this.onCopyResource(object);
// Continue to copy object on inherited class
this.onCopy(object);
// Make instanced drawing mode when it's needed
if (object.isInstancedDrawing) {
this.makeInstancedDrawing();
}
}
copyCompnents(object) {
this.onCopyComponents(object);
}
onCopyStyle(object, options = {}) {
// Update style
let style = object.style;
if (style) {
let styleData = options.onUpdateStyle ? options.onUpdateStyle(style.getProperties()) : style.getProperties();
this.style.copyFromProperties(styleData);
}
}
onCopyAttribute(object, options = {}) {
let _private = this[__.private];
// Common
this.name = options.onUpdateValue ? options.onUpdateValue(object, 'name') : object.name;
this.id = options.onUpdateValue ? options.onUpdateValue(object, 'id') : object.id;
this.queryable = options.onUpdateValue ? options.onUpdateValue(object, 'queryable') : object.queryable;
this.destroyable = options.onUpdateValue ? options.onUpdateValue(object, 'destroyable') : object.destroyable;
this.active = options.onUpdateValue ? options.onUpdateValue(object, 'active') : object.active;
this.visible = options.onUpdateValue ? options.onUpdateValue(object, 'visible') : object.visible;
this.userData = options.onUpdateValue ? options.onUpdateValue(object, 'userData') : object.userData;
this.alwaysOnTop = options.onUpdateValue ? options.onUpdateValue(object, 'alwaysOnTop') : object.alwaysOnTop;
// Update private info
_private.tags = new Set(options.onUpdateValue ? options.onUpdateValue(object, 'tags') : object.tags);
if (object.external) {
_private.external = Object.assign({}, options.onUpdateValue ? options.onUpdateValue(object, 'external') : object.external);
}
}
onCopyTransform(object, options = {}) {
// We must keep root and body transform both, due to body and root would be 2 different nodes
const position = options.onUpdateValue ? options.onUpdateValue(object, 'position') : object.position;
const quaternion = options.onUpdateValue ? options.onUpdateValue(object, 'quaternion') : object.quaternion;
const scale = options.onUpdateValue ? options.onUpdateValue(object, 'scale') : object.scale;
const objectMatrixWorld = MathUtils.composeToMat4(position, quaternion, scale);
const bodyMatrixWorld = MathUtils.composeToMat4(object.body.position, object.body.quaternion, object.body.scale);
this.matrixWorld = objectMatrixWorld;
this.body.matrixWorld = bodyMatrixWorld;
}
// #endregion
// #region Notifier
addSyncCompleteCallback(callback) {
let _private = this[__.private];
_private.syncCompleteCallbacks.push(callback);
}
// #endregion
// #region Import/Export
/**
* When check whether need to collect model.
* @returns {Boolean}
* @private
*/
onCollectMesh() {
// Skip to collect mesh for prefab
if (this.isSubObject) {
return false;
}
return true;
}
/**
* When import external data.
* @param {Object} external The external data.
* @param {Object} options The options
* @private
*/
onImportExternalData(external, options) {
let _private = this[__.private];
if (external) {
_private.external = external;
}
}
/**
* When export external data.
* @param {Object} options The options.
* @returns {Object}
* @private
*/
onExportExternalData(options) {
if (this.external) {
return this.external;
}
return null;
}
// #endregion
// #region Internal components attributes and functions
// #region Component - render
/**
* Check whether it's in instanced drawing mode.
* @type {Boolean}
* @private
*/
get isInstancedDrawing() { return this.render.isInstancedDrawing; }
/**
* Get/Set the instance group name.
* @type {String}
* @private
*/
get instanceGroupName() { return this.render.instanceGroupName; }
set instanceGroupName(value) { this.render.instanceGroupName = value; }
/**
* Get/Set the render order, drawing order is from low to high.
* @type {Number}
* @private
*/
get renderOrder() { return this.render.renderOrder; }
set renderOrder(value) { this.render.renderOrder = value; }
/**
* Get/Set the cast shadow.
* @type {Boolean}
* @private
*/
get castShadow() { return this.render.castShadow; }
set castShadow(value) { this.render.castShadow = value; }
/**
* Get/Set the receive shadow.
* @type {Boolean}
* @private
*/
get receiveShadow() { return this.render.receiveShadow; }
set receiveShadow(value) { this.render.receiveShadow = value; }
/**
* Enable/Disable always on top.
* @type {Boolean}
* @example
* // Keep object render as the top of render layer
* object.alwaysOnTop = true;
* @public
*/
get alwaysOnTop() { return this.render.alwaysOnTop; }
set alwaysOnTop(value) { this.render.alwaysOnTop = value; }
/**
* Get render order.
* @returns {Number}
* @private
*/
getRenderOrder() { return this.render.getRenderOrder.apply(this.render, arguments); }
/**
* Set render order.
* @param {Number} value The value.
* @param {Boolean} recursive True indicates process it with all children.
* @private
*/
setRenderOrder() { return this.render.setRenderOrder.apply(this.render, arguments); }
/**
* Get cast shadow.
* @returns {Boolean}
* @private
*/
getCastShadow() { return this.render.getCastShadow.apply(this.render, arguments); }
/**
* Set cast shadow.
* @param {Boolean} value The value.
* @param {Boolean} recursive True indicates process it with all children.
* @private
*/
setCastShadow() { return this.render.setCastShadow.apply(this.render, arguments); }
/**
* Get receive shadow.
* @returns {Boolean}
* @private
*/
getReceiveShadow() { return this.render.getReceiveShadow.apply(this.render, arguments); }
/**
* Set receive shadow.
* @param {Boolean} value The value.
* @param {Boolean} recursive True indicates process it with all children.
* @private
*/
setReceiveShadow() { return this.render.setReceiveShadow.apply(this.render, arguments); }
/**
* Make object in instanced drawing mode.
* @param {Boolean} value True indicates enable instanced drawing mode.
* @param {Object} options The options.
* @param {String} [options.renderMode='InstancedRendering'] The render mode(could be 'SharedRendering/InstancedRendering').
* @returns {Boolean}
* @example
* // Make object render in instanced drawing mode
* if (object.makeInstancedDrawing()) {
* console.log('Enable instanced drawing');
* }
* @public
*/
makeInstancedDrawing() { return this.render.makeInstancedDrawing.apply(this.render, arguments); }
/**
* @typedef {Object} InstancingNodeOptions
* @property {Number} maxNumber The max number.
* @property {Number} number The current number.
* @property {Array<Array<Number>>} matrices The matrices.
* @property {Array<Number>} pickedIds The picked Ids.
* @private
*/
/**
* Enable instancing node.
* @remark It will make body node invisible after enable it.
* @param {InstancingNodeOptions} options The options.
* @returns {Boolean}
* @private
*/
enableInstancing() { return this.render.enableInstancing.apply(this.render, arguments); }
/**
* Disable instancing node.
* @private
*/
disableInstancing() { return this.render.disableInstancing.apply(this.render, arguments); }
/**
* Set/Update instancing node options.
* @param {InstancingNodeOptions} options The options.
* @returns {Boolean}
* @private
*/
setInstancing() { return this.render.setInstancing.apply(this.render, arguments); }
/**
* Get the instancing node info.
* @returns {InstancingNodeOptions}
* @private
*/
getInstancing() { return this.render.getInstancing.apply(this.render, arguments); }
/**
* @typedef {Object} RenderableNodeGeometryInfo
* @property {Number} vertices The number of vertices.
* @property {Number} triangles The number of triangles.
* @property {Number} materials The number of materials.
* @property {Number} textures The number of textures.
*/
/**
* Get the geometry info.
* @returns {RenderableNodeGeometryInfo}
* @private
*/
getGeometryInfo() { return this.render.getGeometryInfo.apply(this.render, arguments); }
// #endregion
// #region Component - level
/**
* Check whether it's current level.
* @type {Boolean}
* @deprecated Deprecated, call it with LevelComponent.
* @private
*/
get isCurrentLevel() { return this.level.isCurrentLevel; }
// #endregion
// #region Component - bounding
/**
* Get bounding box recursively.
* @type {THING.Box3}
* @example
* let boundingBox = object.boundingBox;
* if (boundingBox.halfSize[1] > 100) {
* console.log('The object is so tall');
* }
* @public
*/
get boundingBox() { return this.bounding.boundingBox; }
/**
* Get oriented box recursively.
* @type {OrientedBoxResult}
* @example
* let orientedBox = object.orientedBox;
* if (orientedBox.angles[1] > 0) {
* console.log('The object has rotated by Y-axis');
* }
* @public
*/
get orientedBox() { return this.bounding.orientedBox; }
/**
* Get/Set the initial local bounding box.
* @type {BoundingBoxResult}
* @private
*/
get initialLocalBoundingBox() { return this.bounding.initialLocalBoundingBox; }
set initialLocalBoundingBox(value) { this.bounding.initialLocalBoundingBox = value; }
/**
* Get the axis-aligned bounding box (AABB).
* @param {Boolean} [recursive=true] True indicates get bounding box including all children.
* @param {Boolean} [updateMatrix=true] True indicates update matrix for self and all children.
* @param {Boolean} [local=false] True indicates get the local transform from parent.
* @returns {THING.Box3}
* @private
*/
getAABB() { return this.bounding.getAABB.apply(this.bounding, arguments); }
/**
* @typedef {Object} OrientedBoxResult
* @property {Array<Number>} angles The angles base on center.
* @property {Array<Number>} center The center world position.
* @property {Array<Number>} size The bounding box size.
* @property {Array<Number>} halfSize The bounding box half size.
* @property {Number} radius The bounding box radius.
*/
/**
* Get the oriented bounding box (OBB).
* @param {Boolean} [recursive=true] True indicates get oriented box including all children.
* @param {Boolean} [updateMatrix=true] True indicates update matrix for self and all children.
* @param {Boolean} [local=false] True indicates get the local transform from parent.
* @returns {OrientedBoxResult}
* @private
*/
getOBB() { return this.bounding.getOBB.apply(this.bounding, arguments); }
// #endregion
// #region Component - lerp
/**
* @typedef LerpArgs
* @property {Object} from The source attributes.
* @property {Object} to The target attributes.
* @property {Number} [loop=-1] The loop times, -1 indicates unlimited, and set the loop type to repeat.
* @property {Number} [times=-1] The loop times, -1 indicates unlimited.
* @property {Number} [duration=1000] The time in milliseconds.
* @property {Number} [delayTime=0] The delay time in milliseconds.
* @property {LerpType} lerpType? The lerp type.
* @property {LoopType} loopType? The loop type.
* @property {Boolean} orientToPath? Whether to face the path when moving.
* @property {Function} onRepeat The callback function to trigger repeat action.
* @property {Function} onStart The callback function to trigger start action.
* @property {Function} onStop The callback function to trigger stop action.
* @property {Function} onUpdate The callback function to trigger update action.
* @property {Function} onComplete The callback function to trigger complete action.
*/
/**
* The position/rotate/scale lerp args.
* @typedef {LerpArgs} LerpWithSpaceTypeArgs
* @param {THING.SpaceType} [spaceType=THING.SpaceType.World] The space type.
*/
/**
* Get the flying state.
* @type {Boolean}
* @public
*/
get isFlying() { return this.lerp.isFlying; }
/**
* Lerp points.
* @private
*/
lerpPoints() { return this.lerp.lerpPoints.apply(this.lerp, arguments); }
/**
* Lerp points in async mode.
* @private
*/
lerpPointsAsync() { return this.lerp.lerpPointsAsync.apply(this.lerp, arguments); }
/**
* Stop moving.
* @example
* object.stopMoving();
* @public
*/
stopMoving() { return this.lerp.stopMoving.apply(this.lerp, arguments); }
/**
* Pause moving.
* @example
* object.pauseMoving();
* @public
*/
pauseMoving() { return this.lerp.pauseMoving.apply(this.lerp, arguments); }
/**
* Resume moving.
* @example
* object.resumeMoving();
* @public
*/
resumeMoving() { return this.lerp.resumeMoving.apply(this.lerp, arguments); }
/**
* @typedef {LerpWithSpaceTypeArgs} MovePathLerpArgs
* @param {Array<Number>} up The up direction.
* @param {Boolean} [closure=false] True indicates it's closure path.
*/
/**
* Move object in duration.
* @param {Array<Number>} value The position.
* @param {MovePathLerpArgs} param The parameters.
* @example
* object.moveTo(object.selfToWorld(THING.Math.randomVector([-200, -5, -200], [200, 5, 200])), {
* loopType: THING.LoopType.PingPong,
* duration: THING.Math.randomInt(1000, 5000)
* });
* @public
*/
moveTo() { return this.lerp.moveTo.apply(this.lerp, arguments); }
/**
* Move object in duration (async).
* @param {Array<Number>} value The position.
* @param {MovePathLerpArgs} param The parameters.
* @returns {Promise<any>}
* @example
* await object.moveToAsync(object.selfToWorld(THING.Math.randomVector([-200, -5, -200], [200, 5, 200])), {
* duration: THING.Math.randomInt(1000, 5000)
* });
* @private
*/
moveToAsync() { return this.lerp.moveToAsync.apply(this.lerp, arguments); }
/**
* Move object with path in duration.
* @param {Array<Array<Number>>} value The path of position.
* @param {MovePathLerpArgs} param The parameters.
* @example
* let path = [
* [100, 0, 0],
* [100, 0, 100],
* [0, 0, 100],
* [0, 0, 0],
* ];
*
* object.movePath(path.map(point => object.selfToWorld(point)), {
* duration: 5 * 1000,
* loopType: THING.LoopType.Repeat,
* });
* @public
*/
movePath() { return this.lerp.movePath.apply(this.lerp, arguments); }
/**
* Move object with path in duration (async).
* @param {Array<Array<Number>>} value The path of position.
* @param {MovePathLerpArgs} param The parameters.
* @returns {Promise<any>}
* @example
* let path = [
* [100, 0, 0],
* [100, 0, 100],
* [0, 0, 100],
* [0, 0, 0],
* ];
*
* await object.movePathAsync(path.map(point => object.selfToWorld(point)), {
* duration: 5 * 1000
* });
* @private
*/
movePathAsync() { return this.lerp.movePathAsync.apply(this.lerp, arguments); }
/**
* Stop scaling.
* @example
* object.stopScaling();
* @public
*/
stopScaling() { return this.lerp.stopScaling.apply(this.lerp, arguments); }
/**
* Pause scaling.
* @example
* object.pauseScaling();
* @public
*/
pauseScaling() { return this.lerp.pauseScaling.apply(this.lerp, arguments); }
/**
* Resume scaling.
* @example
* object.resumeScaling();
* @public
*/
resumeScaling() { return this.lerp.resumeScaling.apply(this.lerp, arguments); }
/**
* Scale object in duration.
* @param {Array<Number>} value The scale.
* @param {LerpWithSpaceTypeArgs} param The parameters.
* @example
* object.scaleTo(THING.Math.randomVector([1, 1, 1], [3, 3, 3]), {
* loopType: THING.LoopType.PingPong,
* duration: THING.Math.randomInt(1000, 5000)
* });
* @public
*/
scaleTo() { return this.lerp.scaleTo.apply(this.lerp, arguments); }
/**
* Scale object in duration (async).
* @example
* await object.scaleToAsync(THING.Math.randomVector([1, 1, 1], [3, 3, 3]), {
* duration: THING.Math.randomInt(1000, 5000)
* });
* @private
*/
scaleToAsync() { return this.lerp.scaleToAsync.apply(this.lerp, arguments); }
/**
* Stop rotating.
* @example
* object.stopRotating();
* @public
*/
stopRotating() { return this.lerp.stopRotating.apply(this.lerp, arguments); }
/**
* Pause rotating.
* @example
* object.pauseRotating();
* @public
*/
pauseRotating() { return this.lerp.pauseRotating.apply(this.lerp, arguments); }
/**
* Resume rotating.
* @example
* object.resumeRotating();
* @public
*/
resumeRotating() { return this.lerp.resumeRotating.apply(this.lerp, arguments); }
/**
* Rotate object in duration.
* @param {Array<Number>} value The angles.
* @param {LerpWithSpaceTypeArgs} param The parameters.
* @example
* object.rotateTo([0, 360, 0], {
* loopType: THING.LoopType.Repeat,
* duration: 10 * 1000
* });
* @public
*/
rotateTo() { return this.lerp.rotateTo.apply(this.lerp, arguments); }
/**
* Rotate object in duration (async).
* @example
* await object.rotateToAsync([0, 360, 0], {
* duration: 10 * 1000
* });
* @private
*/
rotateToAsync() { return this.lerp.rotateToAsync.apply(this.lerp, arguments); }
/**
* Stop fading.
* @private
*/
stopFading() { return this.lerp.stopFading.apply(this.lerp, arguments); }
/**
* Pause fading.
* @private
*/
pauseFading() { return this.lerp.pauseFading.apply(this.lerp, arguments); }
/**
* Resume fading.
* @private
*/
resumeFading() { return this.lerp.resumeFading.apply(this.lerp, arguments); }
/**
* Fade object in.
* @param {LerpArgs} param The parameters.
* @public
*/
fadeIn() { return this.lerp.fadeIn.apply(this.lerp, arguments); }
/**
* Fade object out.
* @param {LerpArgs} param The parameters.
* @public
*/
fadeOut() { return this.lerp.fadeOut.apply(this.lerp, arguments); }
/**
* Fade object in (async).
* @private
*/
fadeInAsync() { return this.lerp.fadeInAsync.apply(this.lerp, arguments); }
/**
* Fade object out (async).
* @private
*/
fadeOutAsync() { return this.lerp.fadeOutAsync.apply(this.lerp, arguments); }
/**
* Stop flying.
* @example
* object.stopFlying();
* @public
*/
stopFlying() { return this.lerp.stopFlying.apply(this.lerp, arguments); }
/**
* Pause flying.
* @example
* object.pauseFlying();
* @public
*/
pauseFlying() { return this.lerp.pauseFlying.apply(this.lerp, arguments); }
/**
* Resume flying.
* @example
* object.resumeFlying();
* @public
*/
resumeFlying() { return this.lerp.resumeFlying.apply(this.lerp, arguments); }
/**
* @typedef {Object} LerpFlyToArgs
* @property {Array<Number>} position The position where to fly.
* @property {Array<Number>|THING.BaseObject} target The target where to fly, if it's object then auto select the best position from its bounding box.
* @property {Number} duration The action time in milliseconds.
* @property {Number} delayTime The delay time in milliseconds.
* @property {Number} distance The distance(only works for object target mode).
* @property {Number} horzAngle The horz angle(only works for object target mode).
* @property {Number} vertAngle The vert angle(only works for object target mode).
* @property {LerpType} lerpType The lerp type.
* @property {LerpType} positionLerpType The position lerp type.
* @property {LerpType} targetLerpType The target lerp type.
* @property {LerpType} upLerpType The lerp type.
* @property {Function} onStart The start callback function.
* @property {Function} onStop The stop callback function.
* @property {Function} onUpdate The update callback function.
* @property {Function} onComplete The complete callback function.
*/
/**
* Fly to specified position in duration.
* @param {THING.BaseObject|LerpFlyToArgs} param The object or parameters.
* @example
* object.flyTo({
* target: otherTarget,
* horzAngle: 0,
* vertAngle: 45
* });
* @public
*/
flyTo() { return this.lerp.flyTo.apply(this.lerp, arguments); }
/**
* Fly to specified position in duration (async).
* @param {THING.BaseObject|LerpFlyToArgs} param The object or parameters.
* @returns {Promise<any>}
* @example
* await object.flyToAsync({
* target: otherTarget,
* horzAngle: 0,
* vertAngle: 45
* });
* @private
*/
flyToAsync() { return this.lerp.flyToAsync.apply(this.lerp, arguments); }
/**
* Auto set it to best position of object.
* @param {Object} param The parameters.
* @param {THING.BaseObject} param.target The target object.
* @example
* THING.App.current.camera.fit(otherTarget);
* @public
*/
fit() { return this.lerp.fit.apply(this.lerp, arguments); }
/**
* Stop UV transform.
* @param {ImageSlotType} slotType The slot type of style.
* @example
* object.stopUVTransform(THING.ImageSlotType.Map);
* @public
*/
stopUVTransform() { return this.lerp.stopUVTransform.apply(this.lerp, arguments); }
/**
* Pause UV transform.
* @param {ImageSlotType} slotType The slot type of style.
* @example
* object.pauseUVTransform(THING.ImageSlotType.Map);
* @public
*/
pauseUVTransform() { return this.lerp.pauseUVTransform.apply(this.lerp, arguments); }
/**
* Resume UV transform.
* @param {ImageSlotType} slotType The slot type of style.
* @example
* object.resumeUVTransform(THING.ImageSlotType.Map);
* @public
*/
resumeUVTransform() { return this.lerp.resumeUVTransform.apply(this.lerp, arguments); }
/**
* Start UV transform in duration.
* @param {ImageSlotType} slotType The slot type of style.
* @param {StyleUVMatrixResult|LerpArgs} value The UV transform info.
* @param {LerpArgs} param? The parameters.
* @example
* // Lerp UV offset from [0, 0] to [-1, 0] in 2 seconds by repeat mode
* object.uvTransformTo(THING.ImageSlotType.Map, {
* from: { offset: [0, 0] },
* to: { offset: [-1, 0] },
* duration: 2000,
* loopType: THING.LoopType.Repeat,
* times: 3
* });
* @public
*/
uvTransformTo() { return this.lerp.uvTransformTo.apply(this.lerp, arguments); }
/**
* Start UV transform in duration (async).
* @param {ImageSlotType} slotType The slot type of style.
* @param {StyleUVMatrixResult|LerpArgs} value The UV transform info.
* @param {LerpArgs} param? The parameters.
* @returns {Promise<any>}
* @example
* // Lerp UV offset from [0, 0] to [-1, 0] in 2 seconds by repeat mode and wait for complete
* await object.uvTransformToAsync(THING.ImageSlotType.Map, {
* from: { offset: [0, 0] },
* to: { offset: [-1, 0] },
* duration: 2000,
* loopType: THING.LoopType.Repeat,
* times: 3
* });
* @private
*/
uvTransformToAsync() { return this.lerp.uvTransformToAsync.apply(this.lerp, arguments); }
// #endregion
// #region Component - transform
/**
* Get/Set local(offset) position of the parent space.
* @type {Array<Number>}
* @public
*/
get localPosition() { return this.transform.getLocalPosition(); }
set localPosition(value) { this.transform.setLocalPosition(value); }
/**
* Get/Set angles of the inertial space.
* @type {Array<Number>}
* @public
*/
get localAngles() { return this.transform.getLocalAngles(); }
set localAngles(value) { this.transform.setLocalAngles(value); }
/**
* Get/Set angles of the inertial space.
* @type {Array<Number>}
* @public
*/
get localRotation() { return this.transform.getLocalAngles(); }
set localRotation(value) { this.transform.setLocalAngles(value); }
/**
* Get/Set quaternion of the inertial space.
* @type {Array<Number>}
* @public
*/
get localQuaternion() { return this.transform.getLocalQuaternion(); }
set localQuaternion(value) { this.transform.setLocalQuaternion(value); }
/**
* Get/Set scale of the self coordinate system.
* @type {Array<Number>}
* @public
*/
get localScale() { return this.transform.getLocalScale(); }
set localScale(value) { this.transform.setLocalScale(value); }
/**
* Get/Set world position of the world space.
* @type {Array<Number>}
* @public
*/
get position() { return this.transform.getWorldPosition(); }
set position(value) { this.transform.setWorldPosition(value); }
/**
* Get/Set angles of the world space.
* @type {Array<Number>}
* @public
*/
get angles() { return this.transform.getWorldAngles(); }
set angles(value) { this.transform.setWorldAngles(value); }
/**
* Get/Set angles of the world space.
* @type {Array<Number>}
* @public
*/
get rotation() { return this.transform.getWorldAngles(); }
set rotation(value) { this.transform.setWorldAngles(value); }
/**
* Get/Set quaternion of the world space.
* @type {Array<Number>}
* @public
*/
get quaternion() { return this.transform.getWorldQuaternion(); }
set quaternion(value) { this.transform.setWorldQuaternion(value); }
/**
* Get/Set scale of the world coordinate system.
* @type {Array<Number>}
* @public
*/
get scale() { return this.transform.getWorldScale(); }
set scale(value) { this.transform.setWorldScale(value); }
/**
* Get/Set the matrix.
* @type {Array<Number>}
* @private
*/
get matrix() { return this.transform.getMatrix(); }
set matrix(value) { this.transform.setMatrix(value); }
/**
* Get/Set the matrix world.
* @type {Array<Number>}
* @private
*/
get matrixWorld() { return this.transform.getMatrixWorld([], true); }
set matrixWorld(value) { this.transform.setMatrixWorld(value); }
/**
* Get the inversed matrix.
* @type {Array<Number>}
* @private
*/
get inversedMatrix() { return this.transform.inversedMatrix; }
/**
* Get the inversed matrix world.
* @type {Array<Number>}
* @private
*/
get inversedMatrixWorld() { return this.transform.inversedMatrixWorld; }
/**
* Get the up direction of world space.
* @type {Array<Number>}
* @private
*/
get up() { return this.transform.up; }
/**
* Get the forward direction in world space.
* @type {Array<Number>}
* @private
*/
get forward() { return this.transform.forward; }
/**
* Get the cross direction in world space.
* @type {Array<Number>}
* @private
*/
get cross() { return this.transform.cross; }
/**
* Get/Set keep size distance, null indicates use the current camera position to calculate distance.
* @type {Number}
* @private
*/
get keepSizeDistance() { return this.transform.keepSizeDistance; }
set keepSizeDistance(value) { this.transform.keepSizeDistance = value; }
/**
* Get local position.
* @param {Array<Number>} target? The target to save result.
* @returns {Array<Number>} The reference of target.
* @private
*/
getLocalPosition() { return this.transform.getLocalPosition.apply(this.transform, arguments); }
/**
* Get quaternion of the inertial space.
* @param {Array<Number>} target? The target to save result.
* @returns {Array<Number>} The reference of target.
* @private
*/
getLocalQuaternion() { return this.transform.getLocalQuaternion.apply(this.transform, arguments); }
/**
* Get scale of the self coordinate system.
* @param {Array<Number>} target? The target to save result.
* @returns {Array<Number>} The reference of target.
* @private
*/
getLocalScale() { return this.transform.getLocalScale.apply(this.transform, arguments); }
/**
* Get world position.
* @param {Array<Number>} target? The target to save result.
* @returns {Array<Number>} The reference of target.
* @private
*/
getWorldPosition() { return this.transform.getWorldPosition.apply(this.transform, arguments); }
/**
* Get quaternion of the world space.
* @param {Array<Number>} target? The target to save result.
* @returns {Array<Number>} The reference of target.
* @private
*/
getWorldQuaternion() { return this.transform.getWorldQuaternion.apply(this.transform, arguments); }
/**
* Get scale of the world coordinate system.
* @param {Array<Number>} target? The target to save result.
* @returns {Array<Number>} The reference of target.
* @private
*/
getWorldScale() { return this.transform.getWorldScale.apply(this.transform, arguments); }
/**
* Get the forward direction in world space.
* @param {Array<Number>} target? The target to save result.
* @returns {Array<Number>} The reference of target.
* @private
*/
getForward() { return this.transform.getForward.apply(this.transform, arguments); }
/**
* Rotate on axis by angle in self space.
* @param {Array<Number>} axis The axis in self space.
* @param {Number} angle The degree.
* @example
* let box = new THING.Box();
* box.rotate(THING.Utils.xAxis, 45);
* @expect(THING.Math.equalsVector3([45, 0, 0], box.angles) == true);
* @public
*/
rotate() { return this.transform.rotateOnAxis.apply(this.transform, arguments); }
/**
* Rotate on axis by angle in self space.
* @param {Array<Number>} axis The axis in self space.
* @param {Number} angle The degree.
* @example
* let box = new THING.Box();
* box.rotateOnAxis(THING.Utils.xAxis, 45);
* @expect(THING.Math.equalsVector3([45, 0, 0], box.angles) == true);
* @public
*/
rotateOnAxis() { return this.transform.rotateOnAxis.apply(this.transform, arguments); }
/**
* Rotate on x-axis in self coordinates.
* @param {Number} angle The angle.
* @public
*/
rotateX() { return this.transform.rotateX.apply(this.transform, arguments); }
/**
* Rotate on y-axis in self coordinates.
* @param {Number} angle The angle.
* @public
*/
rotateY() { return this.transform.rotateY.apply(this.transform, arguments); }
/**
* Rotate on z-axis in self coordinates.
* @param {Number} angle The angle.
* @public
*/
rotateZ() { return this.transform.rotateZ.apply(this.transform, arguments); }
/**
* Translate on axis by distance in self space.
* @param {Array<Number>} axis The axis in self space.
* @param {Number} distance The distance.
* @private
*/
translateOnAxis() { return this.transform.translateOnAxis.apply(this.transform, arguments); }
/**
* Translate on x-axis in self coordinates.
* @param {Number} distance The distance.
* @public
*/
translateX() { return this.transform.translateX.apply(this.transform, arguments); }
/**
* Translate on y-axis in self coordinates.
* @param {Number} distance The distance.
* @public
*/
translateY() { return this.transform.translateY.apply(this.transform, arguments); }
/**
* Translate on z-axis in self coordinates.
* @param {Number} distance The distance.
* @public
*/
translateZ() { return this.transform.translateZ.apply(this.transform, arguments); }
/**
* Translate in self coordinates.
* @param {Array<Number>} offset The offset.
* @public
*/
translate() { return this.transform.translate.apply(this.transform, arguments); }
/**
* @typedef {Object} LookAtArgs
* @property {Array<Number>} up The up direction.
* @property {AxisType} [lockAxis] The lock axis type.
* @property {Boolean} [always=false] True indicates look at target always
* @property {Boolean} [lookOnPlane=false] True indicates look on target's plane by its forward.
*/
/**
* Look at object or position.
* @param {Array<Number>|THING.Object3D} target The target what to look at.
* @param {LookAtArgs} param The parameters.
* @public
*/
lookAt() { return this.transform.lookAt.apply(this.transform, arguments); }
/**
* Convert local position to self position.
* @param {Array<Number>} position The local position.
* @param {Boolean} [ignoreScale=false] True indicates ignore scale factor.
* @returns {Array<Number>}
* @private
*/
localToSelf() { return this.transform.localToSelf.apply(this.transform, arguments); }
/**
* Convert self position to local position.
* @param {Array<Number>} position The self position.
* @param {Boolean} [ignoreScale=false] True indicates ignore scale factor.
* @returns {Array<Number>}
* @private
*/
selfToLocal() { return this.transform.selfToLocal.apply(this.transform, arguments); }
/**
* Convert world position to self position.
* @param {Array<Number>} position The world position.
* @param {Boolean} [ignoreScale=false] True indicates ignore scale factor.
* @returns {Array<Number>}
* @example
* let worldPos = [5, 10, 0];
* let obj = new THING.Object3D({position: worldPos});
* let selfPos = obj.worldToSelf(worldPos);
* // print [0, 0, 0]
* console.log(selfPos);
* @public
*/
worldToSelf() { return this.transform.worldToSelf.apply(this.transform, arguments); }
/**
* Convert self position to world position.
* @param {Array<Number>} position The self position.
* @param {Boolean} [ignoreScale=false] True indicates ignore scale factor.
* @returns {Array<Number>}
* @example
* let obj = new THING.Object3D({ position: [0, 15, 0] });
* let worldPos = obj.selfToWorld([0, -15, 0]);
* // print [0, 0, 0]
* console.log(worldPos);
* @public
*/
selfToWorld() { return this.transform.selfToWorld.apply(this.transform, arguments); }
/**
* Convert local position to world position. same as call parent selfToWorld.
* @param {Array<Number>} position The local position. (The local position is relative to the parent)
* @param {Boolean} [ignoreScale=false] True indicates ignore scale factor.
* @returns {Array<Number>}
* @example
* let parentObj = new THING.Object3D({
* position: [0, 5, 0]
* });
*
* let childObj = new THING.Object3D({
* localPosition: [0, 10, 0],
* parent: parentObj
* });
*
* let worldPos = childObj.localToWorld([0, -5, 0]);
*
* // print [0, 0, 0]
* console.log(worldPos);
* @public
*/
localToWorld() { return this.transform.localToWorld.apply(this.transform, arguments); }
/**
* Convert world position to local position. same as call parent worldToSelf.
* @param {Array<Number>} position The world position.
* @param {Boolean} [ignoreScale=false] True indicates ignore scale factor.
* @returns {Array<Number>} Returns local position relative to the parent
* @example
* let parentObj = new THING.Object3D({
* position: [0, 5, 0]
* });
*
* let childObj = new THING.Object3D({
* localPosition: [0, 10, 0],
* parent: parentObj
* });
*
* let localPosition = childObj.worldToLocal([0, 5, 0]);
*
* // print [0, 0, 0]
* console.log(localPosition);
* @public
*/
worldToLocal() { return this.transform.worldToLocal.apply(this.transform, arguments); }
/**
* Get self quaternion from target position.
* @param {Array<Number>} target The target position.
* @returns {Array<Number>}
* @private
*/
getSelfQuaternionFromTarget() { return this.transform.getSelfQuaternionFromTarget.apply(this.transform, arguments); }
/**
* Get self angles from target position.
* @param {Array<Number>} target The target position.
* @returns {Array<Number>}
* @private
*/
getSelfAnglesFromTarget() { return this.transform.getSelfAnglesFromTarget.apply(this.transform, arguments); }
/**
* Get world quaternion from target position.
* @param {Array<Number>} target The target position.
* @returns {Array<Number>}
* @private
*/
getWorldQuaternionFromTarget() { return this.transform.getWorldQuaternionFromTarget.apply(this.transform, arguments); }
/**
* Get world angles from target position.
* @param {Array<Number>} target The target position.
* @returns {Array<Number>}
* @private
*/
getWorldAnglesFromTarget() { return this.transform.getWorldAnglesFromTarget.apply(this.transform, arguments); }
/**
* Get the world position from angles in self space.
* @param {Array<Number>} value The angles in self space.
* @param {Number} distance The distance in meter(s).
* @returns {Array<Number>}
* @private
*/
getWorldPositionFromSelfAngles() { return this.transform.getWorldPositionFromSelfAngles.apply(this.transform, arguments); }
/**
* Get matrix world.
* @returns {Array<Number>}
* @private
*/
getMatrixWorld() { return this.transform.getMatrixWorld.apply(this.transform, arguments); }
/**
* Set matrix world.
* @param {Array<Number>} value The matrix value.
* @param {Boolean} [updateMatrix=true] True indicates update all parents matrix world.
* @private
*/
setMatrixWorld() { return this.transform.setMatrixWorld.apply(this.transform, arguments); }
/**
* Get the local matrix of target object, it would return the relative matrix of target space world.
* @param {THING.Object3D} target The target object.
* @param {Boolean} [updateMatrix=false] True indicates to update matrix world forcely.
* @returns {Array<Number>}
* @example
* let box1 = new THING.Box(10, 5, 2.5);
* let box2 = new THING.Box(10, 5, 2.5, { position: [0, 10, 0] });
* THING.Math.mat4.equals(box1.matrixWorld, THING.Math.mat4.mul([], box2.matrixWorld, box1.getLocalMatrix(box2)));
* @private
*/
getLocalMatrix() { return this.transform.getLocalMatrix.apply(this.transform, arguments); }
/**
* Update self and all parents and children matrix world.
* @param {Boolean} updateParents True indicates update all parents.
* @param {Boolean} updateChildren True indicates update all children.
* @private
*/
updateWorldMatrix() { return this.transform.updateWorldMatrix.apply(this.transform, arguments); }
/**
* Update self and all children matrix world.
* @private
*/
updateMatrixWorld() { return this.transform.updateMatrixWorld.apply(this.transform, arguments); }
/**
* Bind sub node, it will auto update world position by sub node.
* The position, angles and scale will lock after binding sub node.
* @param {THING.Object3D} target The target.
* @param {String} subNodeName The sub node name.
* @returns {Boolean}
* @example
* let box = new THING.Box();
* box.bindSubNode(car, 'chair');
* @private
*/
bindSubNode() { return this.transform.bindSubNode.apply(this.transform, arguments); }
/**
* Unbind sub node.
* @example
* let box = new THING.Box();
* box.bindSubNode(car, 'chair');
* box.unbindSubNode();
* @private
*/
unbindSubNode() { return this.transform.unbindSubNode.apply(this.transform, arguments); }
// #endregion
// #endregion
get isBaseObject3D() {
console.warn('Plase use isObject3D instead of isBaseObject3D');
return true;
}
}
export { Object3D }