import { Version } from '@uino/base-thing';
import { Utils } from '../common/Utils';
import { MathUtils } from '../math/MathUtils';
import { CubeTexture } from '../resources/CubeTexture';
const __ = {
private: Symbol('private'),
}
const cStyleTextureKeys = [
'map',
'envMap'
];
const _defaultOptions = {};
// #region Private Functions
// Get resource objects number.
function _getResourceObjectsNumber(objects) {
let number = objects.length;
for (let i = 0, l = objects.length; i < l; i++) {
let object = objects[i];
if (object.externalComponents) {
number++;
}
if (object.children) {
number += _getResourceObjectsNumber(object.children);
}
}
return number;
}
function _fixRootPath(rootPath) {
if (!rootPath) {
return;
}
for (let key in rootPath) {
if (!rootPath[key]._endsWith('/')) {
rootPath[key] += '/';
}
}
}
// #endregion
/**
* @class SceneObjectParser
* The scene object parser.
* @memberof THING
*/
class SceneObjectParser {
/**
* The parser of objects scene in tree mode.
*/
constructor() {
this[__.private] = {};
let _private = this[__.private];
_private.models = [];
_private.tags = [];
_private.images = [];
_private.cubeTextures = [];
_private.resolvers = {};
_private.info = {
number: 0,
totalNumber: 0,
};
_private.version = new Version('1.0.0');
_private.resources = {};
_private.options = {};
_private.modelRootPath = '';
_private.textureRootPath = '';
_private.objects = [];
_private.toolkit = {
getRootPath: function () {
return _private.options['rootPath'] || _private.resources['rootPath'];
},
getTextureRootPath: function () {
if (!_private.textureRootPath) {
let rootPath = this.getRootPath();
_private.textureRootPath = rootPath['texture'];
}
return _private.textureRootPath;
},
getModelRootPath: function () {
if (!_private.modelRootPath) {
let rootPath = this.getRootPath();
_private.modelRootPath = rootPath['model'];
}
return _private.modelRootPath;
},
resolveURL: function (url) {
if (url._startsWith('http://') || url._startsWith('https://')) {
return url;
}
else {
let basePath = _private.options['basePath'];
if (basePath) {
return basePath._appendURL(url);
}
else {
return url;
}
}
},
resolveTextureURL: function (url) {
if (url._startsWith('http://') || url._startsWith('https://')) {
return url;
}
else if (url._startsWith('.')) {
return this.resolveURL(url);
}
else {
let rootPath = this.resolveURL(this.getTextureRootPath());
return rootPath._appendPath(url);
}
},
resolveModelURL: function (url, version) {
if (url._startsWith('http://') || url._startsWith('https://') || url._startsWith('file://') || url._startsWith('blob:')) {
return url;
}
else if (url._startsWith('.')) {
return this.resolveURL(url);
}
else {
let rootPath = this.getModelRootPath();
let basePath = rootPath + url;
let modelPath = '';
let modelVersion = _private.options['useLatestModel'] ? 0 : version;
if (modelVersion !== undefined) {
modelPath = basePath + `/${modelVersion}/gltf`;
}
else {
modelPath = basePath;
}
return this.resolveURL(modelPath);
}
},
getResolver: function (type) {
return _private.resolvers[type];
},
getModelDataByIndex: function (index) {
let modelData = _private.models[index];
return modelData;
},
getCubeTextureByIndex: function (index) {
let cubeTexture = _private.cubeTextures[index];
return cubeTexture;
}
}
}
// #region Private
_getObjectClassType(res) {
let _private = this[__.private];
// Get the class type
let classType = Utils.getRegisteredClasses()[res.type];
// Try to get class type from outside
let onGetClassType = _private.options['onGetClassType'];
if (onGetClassType) {
classType = onGetClassType(res) || classType;
}
return classType;
}
_getComponentClassType(res) {
let _private = this[__.private];
// Get the class type
let classType = Utils.getRegisteredClasses()[res.type];
// Try to get class type from outside
let onGetComponentClassType = _private.options['onGetComponentClassType'];
if (onGetComponentClassType) {
classType = onGetComponentClassType(res) || classType;
}
return classType;
}
_loadModels(models, error) {
let _private = this[__.private];
for (let i = 0, l = models.length; i < l; i++) {
let model = models[i];
let version = Utils.parseValue(model['version'], 0);
let url = model['url'];
let size = model['size'];
// Check URL resource path
if (!url) {
if (error) {
error(`load model resource failed, due to url is empty`, model);
}
continue;
}
// Check model bounding size
if (!size) {
if (error) {
error(`load model resource failed, due to size is invalid`, model);
}
continue;
}
// Build the URL resource path
let resURL = _private.toolkit.resolveModelURL(url, version);
// Fix size to prevent 0 value
MathUtils.fixScaleFactor(size);
// Update models
_private.models.push({
url: resURL,
size,
initialLocalBoundingBox: {
// object center are at the foot,however the boundingbox center in the center of box
// so we up the bounding half of y,make the two center coinside
center: [0, size[1] / 2, 0],
halfSize: MathUtils.divideVector(size, 2)
}
});
}
}
_loadTags(tags) {
let _private = this[__.private];
_private.tags = Array.from(tags);
}
_loadImages(images) {
let _private = this[__.private];
let app = Utils.getCurrentApp();
let textureManager = app.resourceManager.getTextureManager();
for (let i = 0, l = images.length; i < l; i++) {
let image = images[i];
let url = _private.toolkit.resolveTextureURL(image.url);
let imageTexture = textureManager.load(url, undefined, { flipY: Utils.isValid(image.flipY) ? image.flipY : false });
_private.images.push(imageTexture);
}
}
_loadCubeTextures(cubeTextures) {
let _private = this[__.private];
for (let i = 0; i < cubeTextures.length; i++) {
let cubeTexture = new CubeTexture({ url: cubeTextures[i] });
_private.cubeTextures.push(cubeTexture);
}
}
_loadResolvers(resolvers) {
let _private = this[__.private];
if (resolvers) {
resolvers.forEach(resolver => {
const type = resolver.type;
const cls = Utils.getRegisteredClass(type);
if (cls) {
_private.resolvers[type] = new cls({ data: resolver.data });
}
});
}
}
_buildBodyResource(res) {
let _private = this[__.private];
let resource = {};
let id = res.id;
if (Utils.isValid(id)) {
let model = _private.models[id];
if (model) {
resource['url'] = model.url;
}
}
let position = res.position;
if (position) {
resource['localPosition'] = position;
}
let quaternion = res.quaternion;
if (quaternion) {
resource['localAngles'] = MathUtils.getAnglesFromQuat(quaternion);
}
let scale = res.scale;
if (scale) {
resource['localScale'] = MathUtils.fixScaleFactor(scale);
}
let children = res.children;
if (children) {
resource['children'] = [];
for (let i = 0, l = children.length; i < l; i++) {
let child = children[i];
resource['children'].push(this._buildBodyResource(child));
}
}
return resource;
}
_buildStyle(style) {
let _private = this[__.private];
let ret = {};
for (let key in style) {
let value = style[key];
ret[key] = value;
if (cStyleTextureKeys.includes(key)) {
switch (key) {
case 'map':
let image = _private.images[value];
if (!image) {
continue;
}
ret[key] = image;
break;
case 'envMap':
let envMap = _private.cubeTextures[value];
if (!envMap) {
continue;
}
ret[key] = envMap;
break;
}
}
}
return ret;
}
_loadComponents(object, components, error, args) {
let _private = this[__.private];
object.onImportComponents(components, {
args,
onGetObjectClassType: (component) => {
return this._getComponentClassType(component);
},
onGetComponentExternalData: _private.options['onGetComponentExternalData'],
onError: error
});
}
_loadTemporaryComponents(object, components, isEditor, load, progress, error, resolve) {
this._loadComponents(object, components, error, {
isTemporary: true, // We just use these options as temporary usage, do not store in cache
isEditor
});
// We finish to load object with components
this._updateProgress(load, progress, resolve);
}
_mapTags(tags) {
let _private = this[__.private];
return tags.map(index => {
return _private.tags[index];
});
}
_onUpdateCreateOptions(classType, createOptions, options) {
if (classType.onParseCreateOptions) {
classType.onParseCreateOptions(createOptions, options);
}
let onUpdateCreateOptions = options['onUpdateCreateOptions'];
if (onUpdateCreateOptions) {
let _private = this[__.private];
onUpdateCreateOptions(_private.options['owner'], classType, createOptions);
}
}
_onCreateObject(object, createOptions, options) {
let onCreateObject = options['onCreateObject'];
if (onCreateObject) {
let _private = this[__.private];
onCreateObject(_private.options['owner'], object, createOptions);
}
}
_updateProgress(load, progress, resolve) {
let _private = this[__.private];
let info = _private.info;
// Update progress
info.number++;
// Notify loading progress
if (progress) {
progress({ progress: info.number / info.totalNumber });
}
// Check whether load completed
if (info.number == info.totalNumber) {
if (load) {
load({ objects: Utils.cloneObject(_private.objects) });
}
resolve();
_private.objects.length = 0;
}
}
_loadObjects(objects, parent, parentExtrasData, load, progress, error, resolve, reject) {
let _private = this[__.private];
// Get resources
let resources = _private.resources;
let uuids = resources['uuids'];
// Get options
let dynamic = Utils.parseValue(_private.options['dynamic'], false);
let hidden = Utils.parseValue(_private.options['hidden'], false);
let isEditor = Utils.parseValue(_private.options['isEditor'], false);
// Start to create objects
for (let i = 0, l = objects.length; i < l; i++) {
let objRes = objects[i];
// Get class type
let classType = this._getObjectClassType(objRes);
if (!classType) {
if (error) {
error({ err: `Create object failed in scene builder, due to 'THING[${objRes.type}]' is not existing` });
}
continue;
}
// Get UUID
let uuid = uuids ? uuids[objRes.uuid] : null;
// Get object info
let id = objRes.id;
let name = objRes.name;
let visible = objRes.visible;
let localPosition = objRes.position;
let localQuaternion = objRes.quaternion;
let localScale = objRes.scale;
let userData = objRes.userData;
let tags = objRes.tags;
let body = objRes.body;
let style = objRes.style;
let components = objRes.components;
let externalComponents = objRes.externalComponents;
let layerMask = objRes.layerMask;
let queryable = objRes.queryable;
let renderOrder = objRes.renderOrder;
let castShadow = objRes.castShadow;
// Compatible external
objRes.extras = objRes.extras || objRes.external;
// Parse scale
if (localScale) {
MathUtils.fixScaleFactor(localScale);
}
// Parse tags
if (tags) {
tags = this._mapTags(tags);
}
// Update extras data by class
let extras;
let parseExtrasData = classType.parseExtrasData || classType.parseExternalData;
if (parseExtrasData) {
extras = parseExtrasData({
parentData: parentExtrasData,
data: objRes,
children: objRes.children || null,
resources: _private.resources,
toolkit: _private.toolkit,
extras: _private.options['extras'] || _private.options['external'] || _defaultOptions
});
}
else {
extras = objRes.extras || {};
}
// Build create options
let createOptions = {
id,
name,
uuid,
visible,
layerMask,
queryable,
renderOrder,
localPosition,
localQuaternion,
localScale,
castShadow,
userData,
tags,
parent,
dynamic,
extras,
components,
toolkit: _private.toolkit,
error: () => {
// Need to update progress even it's error ...
this._updateProgress(load, progress, resolve);
},
syncComplete: () => {
this._updateProgress(load, progress, resolve);
},
syncBeforeDestroy: (ev) => {
if (!ev.object.loaded) {
this._updateProgress(load, progress, resolve);
}
}
};
if (objRes.bundleInfo) {
createOptions.bundleInfo = objRes.bundleInfo
}
// Try to get body URL from models
let model;
if (body) {
model = this._parseBodyResource(body, error);
createOptions['url'] = model.url;
createOptions['nodeName'] = body.node;
createOptions['inverseRotationMode'] = body.inverseRotationMode;
createOptions['excludeNodeNames'] = body.excludeNodeNames;
}
// Build style
if (style) {
createOptions.style = this._buildStyle(style);
}
// instance data
if (extras) {
if (extras.instanceId) {
createOptions['instanceId'] = extras.instanceId;
}
if (extras.instanceCount) {
createOptions['instanceCount'] = extras.instanceCount;
}
if (extras.instanceStyle) {
if (Utils.isValid(extras.instanceStyle.map)) {
const imageTexture = _private.images[extras.instanceStyle.map];
extras.instanceStyle.map = {
url: imageTexture.url,
flipY: imageTexture.flipY,
};
}
if (Utils.isValid(extras.instanceStyle.envMap)) {
extras.instanceStyle.envMap = _private.cubeTextures[extras.instanceStyle.envMap];
}
createOptions['instanceStyle'] = extras.instanceStyle;
}
}
// Update create options
this._onUpdateCreateOptions(classType, createOptions, _private.options);
// Create object
let object = new classType(createOptions);
// set visible
if (hidden) {
object.visible = false;
}
// Update created objects
_private.objects.push(object);
// Set the initial bound size to calculate bounding box
// If object had been loaded then skip to set initial local bounding box
if (model && !object.loaded) {
object.initialLocalBoundingBox = model.initialLocalBoundingBox;
}
// Load body children resources
else if (body) {
let children = body.children;
if (children && children.length) {
object.setResource(this._buildBodyResource(body));
if (!dynamic) {
object.loadResource(false);
}
}
}
// Load external components
if (externalComponents) {
object.onImportExternalComponents(externalComponents).then(() => {
// Continue to load children
if (objRes.children) {
this._loadObjects(objRes.children, object, createOptions.extras, load, progress, error, resolve, reject);
}
// Notify outside we have created object
this._onCreateObject(object, createOptions, _private.options);
// We finish to load object with external component
this._updateProgress(load, progress, resolve);
});
}
else {
// Continue to load children
if (objRes.children) {
this._loadObjects(objRes.children, object, createOptions.extras, load, progress, error, resolve, reject);
}
// Notify outside we have created object
this._onCreateObject(object, createOptions, _private.options);
}
}
}
_parseResources(resources, error) {
let _private = this[__.private];
const onGetRootPath = _private.options['onGetRootPath'];
if (onGetRootPath) {
let rootPath = resources.rootPath || { model: '', texture: '' };
rootPath = onGetRootPath(rootPath);
if (rootPath) {
resources.rootPath = rootPath;
}
}
_private.resources = resources;
_fixRootPath(_private.resources['rootPath']);
// Make sure uuids is exist
_private.resources['uuid'] = _private.resources['uuid'] || [];
// Load models
if (resources['models']) {
this._loadModels(resources['models'], error);
}
// Load tags
if (resources['tags']) {
this._loadTags(resources['tags']);
}
// Load images
if (resources['images']) {
this._loadImages(resources['images']);
}
// Load cubeTextures
if (resources['cubeTextures']) {
this._loadCubeTextures(resources['cubeTextures']);
}
// Load resolvers
if (resources['resolvers']) {
this._loadResolvers(resources['resolvers']);
}
}
_parseObjects(objects, parent, parentExtrasData, load, progress, error, resolve, reject) {
let _private = this[__.private];
let info = _private.info;
info.totalNumber += _getResourceObjectsNumber(objects);
_private.objects.length = 0;
if (info.totalNumber) {
this._loadObjects(objects, parent, parentExtrasData, load, progress, error, resolve, reject);
}
// If there are no any objects need to load then notify complete now
else {
// Notify loading progress
if (progress) {
progress({ progress: 1 });
}
// Notify load completed
if (load) {
load({ objects: _private.objects });
}
resolve();
}
}
_copyOptions(options, onError) {
let _private = this[__.private];
_private.options = Object.assign({}, options);
_fixRootPath(_private.options['rootPath']);
_private.options['useLatestModel'] = Utils.parseValue(_private.options['useLatestModel'], true);
// toolkit
let isEditor = Utils.parseValue(_private.options['isEditor'], false);
_private.toolkit.args = {
isTemporary: true, // We just use these options as temporary usage, do not store in cache
isEditor
};
_private.toolkit.onGetObjectClassType = (component) => {
return this._getComponentClassType(component);
};
_private.toolkit.onGetComponentExternalData = options.onGetComponentExternalData;
_private.toolkit.onError = onError;
}
_parseBodyResource(body, error) {
let _private = this[__.private];
let model = _private.models[body.id];
if (!model) {
if (error) {
error(`load model resource failed, due to body id('${body.id}') is invalid`);
}
return null;
}
return model;
}
_parseOwner(owner, data, load, progress, error, resolve, reject) {
let _private = this[__.private];
if (owner.isRootObject) {
return;
}
// Parse tags
let tags = data['tags'];
if (tags) {
owner.tags = Utils.mergeSet(owner.tags, this._mapTags(tags));
}
// Start to load components of owner
let components = data['components'];
if (components) {
let isEditor = Utils.parseValue(_private.options['isEditor'], false);
this._loadComponents(owner, components, error, {
isTemporary: true, // We just use these options as temporary usage, do not store in cache
isEditor
});
}
// Build style
let style = data['style'];
if (style) {
const bodyStyle = owner.body.style;
let styleData = this._buildStyle(style);
// Merge create options
let options = owner.options;
if (options && options.style) {
Utils.mergeObject(styleData, options.style, true);
}
// Set style
for (const key in styleData) {
if (Object.hasOwnProperty.call(styleData, key)) {
const element = styleData[key];
bodyStyle[key] = element;
}
}
}
// Load body resource
let body = data['body'];
if (body) {
let model = this._parseBodyResource(body, error);
if (model) {
let info = _private.info;
info.totalNumber++;
let resourceManager = Utils.getCurrentApp().resourceManager;
resourceManager.loadModelAsync(model.url).then((ev) => {
let node = ev.node;
owner.setBodyNode(node);
let position = body.position;
if (position) {
owner.body.localPosition = position;
}
let quaternion = body.quaternion;
if (quaternion) {
owner.body.localQuaternion = quaternion;
}
let scale = body.scale;
if (scale) {
owner.body.localScale = scale;
}
// We finish to load object with components
this._updateProgress(load, progress, resolve);
}).catch((ev) => {
// Error ...
this.clear();
if (error) {
error(ev);
}
reject(ev);
});
}
}
}
_parseViewpoint(owner, data, options) {
const useDefaultViewpoint = Utils.parseValue(options["useDefaultViewpoint"], true);
let viewpoint = data['viewpoint'];
if (useDefaultViewpoint && viewpoint) {
let app = Utils.getCurrentApp();
if (owner) {
app.camera.position = owner.selfToWorld(viewpoint.position);
app.camera.target = owner.selfToWorld(viewpoint.target);
}
else {
app.camera.position = viewpoint.position;
app.camera.target = viewpoint.target;
}
app.camera.processAdjustNear();
}
}
// #endregion
clear() {
let _private = this[__.private];
_private.models.length = 0;
_private.tags.length = 0;
_private.images.forEach(image => {
image.release();
});
_private.images.length = 0;
let info = _private.info;
info.number = 0;
info.totalNumber = 0;
_private.modelRootPath = '';
_private.textureRootPath = '';
_private.resources = {};
_private.options = {};
_private.resolvers = {};
}
parse(data, load, progress, error, options) {
let _private = this[__.private];
return new Promise((resolve, reject) => {
// Prepare for loading
this.clear();
// Copy options
this._copyOptions(options, error);
// Get the data info
let resources = data['resources'];
let objects = data['children'] || data['objects'] || [];
// Start to load resources
this._parseResources(resources, error);
// Start to load objects
let owner = _private.options['owner'];
// Parse viewpoint
this._parseViewpoint(owner, data, options);
// Make owner
if (owner) {
if (Utils.isNull(owner.isOwner)) {
Object.defineProperty(owner, 'isOwner', {
get: function () {
return true;
}
});
}
// Parse owner
this._parseOwner(owner, data, load, progress, error, resolve, reject);
}
// Parse all children of owner
this._parseObjects(objects, owner, null, load, progress, error, resolve, reject);
}).then(() => {
// Finished
this.clear();
}).catch((ev) => {
// Error ...
this.clear();
if (error) {
error(ev);
}
});
}
getVersion() {
let _private = this[__.private];
return _private.version;
}
}
export { SceneObjectParser }