import { ResolvablePromise, ResolvableCachedObject } from '@uino/base-thing';
import { Utils } from '../common/Utils';
import { MathUtils } from '../math/MathUtils';
import { SceneResourceType } from '../const';
import { Entity } from '../objects/Entity';
import { Object3D } from '../objects/Object3D';
import { SceneLoaderOld } from '../loaders/SceneLoaderOld'
const __ = {
private: Symbol('private'),
}
const cIdentityMat4 = MathUtils.createMat4();
const cBundleRootObjectKeyPrefix = '__prefab_root__';
// #region Private Functions
// Mark as prefab object.
function _markPrefabObject(object) {
if (object.isSubObject === undefined) {
Object.defineProperty(object, 'isSubObject', {
get: function () {
return true;
}
});
// Set the unique id
object.external = object.external || {};
object.external['uuid'] = object.external['uuid'] || MathUtils.generateUUID();
}
}
// Mark as prefab root object.
function _markPrefabRootObject(object, bundle) {
const key = `${cBundleRootObjectKeyPrefix}_${bundle.uuid}`;
if (object[key] === undefined) {
Object.defineProperty(object, key, {
get: function () {
return true;
}
});
}
}
// #endregion
/**
* Create 3D object.
* @callback OnPrefabBundleCreateObject
* @param {BaseObjectInitialOptions} options The options.
* @param {Function} load The load complete callback function.
* @returns {THING.Object3D}
*/
/**
* Create prefab object.
* @callback OnPrefabBundleCreatePrefabObject
* @param {BaseObjectInitialOptions} options The options.
* @param {Function} load The load complete callback function.
* @returns {THING.Entity}
*/
/**
* The prefab bundle.
* @typedef {Object} PrefabBundle
* @property {OnPrefabBundleCreateObject} createObject Create 3D object.
* @property {OnPrefabBundleCreatePrefabObject} createPrefabObject Create prefab object.
*/
/**
* @class PrefabBundleLoader
* The prefab bundle.
* @memberof THING
* @private
*/
class PrefabBundleLoader {
/**
* The prefab bundle resource loader, prefab can create multiple times.
*/
constructor() {
this[__.private] = {};
let _private = this[__.private];
_private.componentExternalDataMap = new WeakMap();
_private.caches = new ResolvableCachedObject();
_private.prefabFileCache = {};
_private.jsFileCache = {};
}
// #region Private
_markObjects(owner) {
// Mark all children with prefab flag
owner.children.traverse(child => {
_markPrefabObject(child);
});
}
_copyBody(to, from) {
// Copy body resource but keep some info
let userData = to.bodyNode.getUserData();
let matrixWorld = to.matrixWorld;
// Update body resource
to.bodyNode.copy(from.bodyNode);
// Copy style
to.body.style = from.body.style;
// Resume info
to.bodyNode.setUserData(userData);
to.matrixWorld = matrixWorld;
}
_copyOwner(to, from) {
this._copyBody(to, from);
// Make owner
Object.defineProperty(to, 'isOwner', {
get: function () {
return true;
}
});
// Copy some attributes
to.tags = from.tags;
// Clone components for owner
to.copyCompnents(from);
}
_buildCacheKey(bundle, options) {
return bundle.url;
}
_tryLoadFromCache(bundle, load, options = {}) {
let useSceneCache = Utils.parseValue(options['useSceneCache'], true);
if (!useSceneCache) {
return false;
}
let _private = this[__.private];
let key = this._buildCacheKey(bundle, options);
let cache = _private.caches.get(key);
if (!cache) {
return false;
}
// Wait for resource loaded
cache.pendingPromise.then(() => {
// Get options
let owner = options['owner'];
// Copy owner info
this._copyOwner(owner, cache.owner);
// Clone children
let promises = cache.owner.children.toArray().map(object => {
return object.cloneAsync(true, null, options);
});
// We must wait all object clone finished
Promise.all(promises).then((objs) => {
objs.forEach(obj => {
owner.add(obj, { attachMode: false });
});
}).then(() => {
this._markObjects(owner);
// Notify load from cache completed
if (load) {
load();
}
});
});
return true;
}
_hasCache(bundle, options = {}) {
let _private = this[__.private];
let key = this._buildCacheKey(bundle, options);
return _private.caches.has(key);
}
_createCache(bundle, options = {}) {
let _private = this[__.private];
let key = this._buildCacheKey(bundle, options);
return _private.caches.set(key, { owner: null });
}
_extractChildren(owner, cache) {
// Create owner as prefab object(cache always use editor mode)
cache.owner = new Entity({ parent: null, isEditor: true });
// Copy owner info
this._copyOwner(cache.owner, owner);
// Build clone options
let options = {
attachMode: false,
isEditor: true
};
// Make transform to origin for cache owner
let matrixWorld = owner.matrixWorld;
owner.matrixWorld = cIdentityMat4;
// Clone all children
let promises = owner.children.toArray().map(child => {
return child.cloneAsync(true, cache.owner, options);
});
// Resume transform
owner.matrixWorld = matrixWorld;
// We must wait all object clone finished
return Promise.all(promises);
}
_updateCache(owner, bundle, options = {}) {
return new Promise(async (resolve, reject) => {
let _private = this[__.private];
let key = this._buildCacheKey(bundle, options);
let cache = _private.caches.get(key);
// Extract and clone children
this._extractChildren(owner, cache).then(async () => {
// Make sure all children load finished
await owner.query('*').waitForComplete();
// Load prefab object completed
resolve();
cache.pendingPromise.resolve();
});
})
}
_loadObject(bundle, data, load, options = {}) {
let _private = this[__.private];
// Get options
let owner = options['owner'];
let isEditor = options['isEditor'];
let useSceneCache = false;
// Create or get the root object as parent object
_markPrefabRootObject(owner, bundle);
// Create cache
if (useSceneCache) {
// We are going to create cache first
if (!this._hasCache(bundle, options)) {
this._createCache(bundle, options);
}
}
// Load prefab resource into object
let sceneLoader = new SceneLoaderOld();
sceneLoader.parse(SceneResourceType.Object, data,
// Load
async () => {
if (useSceneCache) {
await this._updateCache(owner, bundle, options);
}
this._markObjects(owner);
// Notify all resources load completed
if (load) {
load();
}
},
// Progress
() => {
},
// Error
(ev) => {
Utils.error(ev);
},
// Options
{
isEditor,
basePath: bundle.url,
owner,
// Save external data of each components
onGetComponentExternalData: function (object, name, external) {
let info = _private.componentExternalDataMap.get(object);
if (info) {
info.components[name] = external;
}
else {
let components = {};
components[name] = external;
_private.componentExternalDataMap.set(object, {
components
});
}
},
// Get class type with bundle id
onGetClassType: function (res) {
let type = `${res.type}_${bundle.uuid}`;
return Utils.getRegisteredClasses()[type];
},
// Get component class type with bundle id
onGetComponentClassType: function (res) {
let result = null;
const componentMap = bundle.componentMap;
if (componentMap) {
result = componentMap[res.type];
}
if (!result) {
result = Utils.getRegisteredClasses()[`${res.type}_${bundle.uuid}`];
}
return result;
}
}
);
}
_createObject(bundle, sceneData, options = {}, load) {
let owner = options['owner'];
if (!owner) {
// Copy options without callbacks
let ownerOptions = Object.assign({}, options);
ownerOptions['complete'] = null;
// Create owner object with options(without callbacks)
owner = new Object3D(ownerOptions);
}
let loadOptions = {
owner,
isEditor: options['isEditor'],
useSceneCache: false
};
let onLoad = () => {
if (options['complete']) {
options['complete']({ object: owner });
}
if (load) {
load();
}
}
// Try to load from cache first
if (!this._tryLoadFromCache(bundle, onLoad, loadOptions)) {
this._loadObject(bundle, sceneData, onLoad, loadOptions);
}
return owner;
}
_linkPrefabObject(prefab, entrance) {
// Link export properties
if (Utils.isFunction(entrance.getExportProperties)) {
let properties = entrance.getExportProperties();
if (properties) {
Object.keys(properties).forEach(key => {
Object.defineProperty(prefab, key, {
enumerable: true,
configurable: true,
set(value) {
properties[key] = value;
},
get() {
return properties[key];
}
});
});
}
}
// Link export functions
if (Utils.isFunction(entrance.getExportFunctions)) {
let functions = entrance.getExportFunctions();
if (functions) {
Object.keys(functions).forEach(funcName => {
prefab[funcName] = function () {
return functions[funcName].apply(prefab, arguments);
}
});
}
}
}
_buildPrefabObject(data, bundle, sceneData) {
let entrance = data.entrance ? Utils.getRegisteredClass(data.entrance) : null;
return {
entrance,
// Prefab object would always create 'Entity' type
createObject: (options = {}, load) => {
return bundle.prefab.createPrefabObject(options, load);
},
// Create 'Entity' as root
createPrefabObject: (options = {}, load) => {
let resolvePromise = null;
let owner = options['owner'];
if (!owner) {
resolvePromise = new ResolvablePromise();
// Copy options without callbacks
let ownerOptions = Object.assign({}, options);
ownerOptions['complete'] = null;
ownerOptions['bundle'] = bundle;
ownerOptions['onLoadAsync'] = () => {
return resolvePromise;
}
// Create owner object with options(without callbacks)
owner = new Entity(ownerOptions);
}
let bundleOptions = Object.assign({}, options);
bundleOptions['owner'] = owner;
return this._createObject(bundle, sceneData, bundleOptions, () => {
// Link export properties and functions
if (entrance) {
this._linkPrefabObject(owner, entrance);
}
if (resolvePromise) {
resolvePromise.resolve();
}
if (load) {
load();
}
});
}
};
}
// Load files.
_loadFiles(bundle, files, options) {
let _private = this[__.private];
const autoCompile = Utils.parseBoolean(options['autoCompile'], false);
return new Promise(async (resolve, reject) => {
try {
let modules = []
for (let i = 0; i < files.length; i++) {
const file = files[i];
let fileUrl = bundle.url._appendPath(file);
let module = _private.jsFileCache[fileUrl];
if (!module) {
if (autoCompile) {
module = await bundle.loadCodeFromUrl(fileUrl, options);
}
else {
module = await Utils.importScript(fileUrl);
}
}
modules[i] = module;
}
resolve(modules);
}
catch (error) {
reject(error);
}
});
}
// #endregion
onDispose() {
let _private = this[__.private];
_private.componentExternalDataMap = new WeakMap();
_private.caches.clear(object => {
object.owner.destroy();
});
}
/**
* When load prefab bundle.
* @param {THING.Bundle} bundle The bundle object.
* @param {Object} options The load options.
* @param {Function} load When load callback function.
* @param {Function} error When error callback function.
* @private
*/
onLoad(bundle, options) {
let _private = this[__.private];
// , load, error
let prefabFileName = bundle.info.main;
if (prefabFileName) {
// Get the prefab file url
let prefabFileUrl = bundle.url._appendPath(prefabFileName);
(async () => {
try {
let data = _private.prefabFileCache[prefabFileUrl];
if (!data) {
data = _private.prefabFileCache[prefabFileUrl] = await Utils.loadJSONFile(prefabFileUrl);
}
// Load dependence files
let files = data['files'];
if (files) {
let componentMap = {};
const modules = await this._loadFiles(bundle, files, options);
modules.forEach(module => {
if (module) {
for (const key in module) {
const cls = module[key];
componentMap[key] = cls;
}
}
});
bundle.componentMap = componentMap;
}
// Load objects from root
let sceneData = data['root'];
if (sceneData) {
// Build prefab hookers
bundle.prefab = this._buildPrefabObject(data, bundle, sceneData);
}
options.onComplete();
}
catch (error) {
options.onError(error);
}
})();
}
else {
options.onComplete();
}
}
}
export { PrefabBundleLoader }