Object.assign(pc, function () {
// auto incrementing number for asset ids
var assetIdCounter = 0;
var ABSOLUTE_URL = new RegExp(
'^' + // beginning of the url
'\\s*' + // ignore leading spaces (some browsers trim the url automatically, but we can't assume that)
'(?:' + // beginning of protocol scheme (non-captured regex group)
'[a-z]+[a-z0-9\\-\\+\\.]*' + // protocol scheme must (RFC 3986) consist of "a letter and followed by any combination of letters, digits, plus ("+"), period ("."), or hyphen ("-")."
':' + // protocol scheme must end with colon character
')?' + // end of optional scheme group, the group is optional since the string may be a protocol-relative absolute URL
'//', // a absolute url must always begin with two forward slash characters (ignoring any leading spaces and protocol scheme)
'i' // non case-sensitive flag
);
var VARIANT_SUPPORT = {
pvr: 'extCompressedTexturePVRTC',
dxt: 'extCompressedTextureS3TC',
etc2: 'extCompressedTextureETC',
etc1: 'extCompressedTextureETC1'
};
var VARIANT_DEFAULT_PRIORITY = ['pvr', 'dxt', 'etc2', 'etc1'];
/**
* @constructor
* @name pc.Asset
* @classdesc An asset record of a file or data resource that can be loaded by the engine.
* The asset contains three important fields:<br/>
* <strong>file</strong>: contains the details of a file (filename, url) which contains the resource data, e.g. an image file for a texture asset<br/>
* <strong>data</strong>: contains a JSON blob which contains either the resource data for the asset (e.g. material data) or additional data for the file (e.g. material mappings for a model)<br/>
* <strong>resource</strong>: contains the final resource when it is loaded. (e.g. a {@link pc.StandardMaterial} or a {@link pc.Texture})<br/>
*
* See the {@link pc.AssetRegistry} for details on loading resources from assets.
* @description Create a new Asset record. Generally, Assets are created in the loading process and you won't need to create them by hand.
* @param {String} name A non-unique but human-readable name which can be later used to retrieve the asset.
* @param {String} type Type of asset. One of ["animation", "audio", "binary", "cubemap", "css", "font", "json", "html", "material", "model", "script", "shader", "text", "texture"]
* @param {Object} file Details about the file the asset is made from. At the least must contain the 'url' field. For assets that don't contain file data use null.
* @example
* var file = {
* filename: "filename.txt",
* url: "/example/filename.txt",
* }
* @param {Object} [data] JSON object with additional data about the asset (e.g. for texture and model assets) or contains the asset data itself (e.g. in the case of materials)
* @example
* var asset = new pc.Asset("a texture", "texture", {
* url: "http://example.com/my/assets/here/texture.png"
* });
* @property {String} name The name of the asset
* @property {Number} id The asset id
* @property {String} type The type of the asset. One of ["animation", "audio", "binary", "cubemap", "css", "font", "json", "html", "material", "model", "script", "shader", "text", "texture"]
* @property {pc.Tags} tags Interface for tagging. Allows to find assets by tags using {@link pc.AssetRegistry#findByTag} method.
* @property {Object} file The file details or null if no file
* @property {String} [file.url] The URL of the resource file that contains the asset data
* @property {String} [file.filename] The filename of the resource file
* @property {Number} [file.size] The size of the resource file
* @property {String} [file.hash] The MD5 hash of the resource file data and the Asset data field
* @property {Object} data JSON data that contains either the complete resource data (e.g. in the case of a material) or additional data (e.g. in the case of a model it contains mappings from mesh to material)
* @property {Object} resource A reference to the resource when the asset is loaded. e.g. a {@link pc.Texture} or a {@link pc.Model}
* @property {Array} resources A reference to the resources of the asset when it's loaded. An asset can hold more runtime resources than one e.g. cubemaps
* @property {Boolean} preload If true the asset will be loaded during the preload phase of application set up.
* @property {Boolean} loaded True if the resource is loaded. e.g. if asset.resource is not null
* @property {pc.AssetRegistry} registry The asset registry that this Asset belongs to
*/
var Asset = function (name, type, file, data) {
this._id = ++assetIdCounter;
this.name = name || '';
this.type = type;
this.tags = new pc.Tags(this);
this._preload = false;
this.variants = new pc.AssetVariants(this);
this._file = null;
this._data = data || { };
// This is where the loaded resource(s) will be
this._resources = [];
// a string-assetId dictionary that maps
// locale to asset id
this._i18n = {};
// Is resource loaded
this.loaded = false;
this.loading = false;
this.registry = null;
pc.events.attach(this);
if (file) this.file = file;
};
/**
* @event
* @name pc.Asset#load
* @description Fired when the asset has completed loading
* @param {pc.Asset} asset The asset that was loaded
*/
/**
* @event
* @name pc.Asset#remove
* @description Fired when the asset is removed from the asset registry
* @param {pc.Asset} asset The asset that was removed
*/
/**
* @event
* @name pc.Asset#error
* @description Fired if the asset encounters an error while loading
* @param {String} err The error message
* @param {pc.Asset} asset The asset that generated the error
*/
/**
* @event
* @name pc.Asset#change
* @description Fired when one of the asset properties `file`, `data`, `resource` or `resources` is changed
* @param {pc.Asset} asset The asset that was loaded
* @param {String} property The name of the property that changed
* @param {*} value The new property value
* @param {*} oldValue The old property value
*/
/**
* @event
* @name pc.Asset#add:localized
* @description Fired when we add a new localized asset id to the asset.
* @param {String} locale The locale
* @param {Number} assetId The asset id we added.
*/
/**
* @event
* @name pc.Asset#remove:localized
* @description Fired when we remove a localized asset id from the asset.
* @param {String} locale The locale
* @param {Number} assetId The asset id we removed.
*/
Object.assign(Asset.prototype, {
/**
* @name pc.Asset#getFileUrl
* @function
* @description Return the URL required to fetch the file for this asset.
* @returns {String} The URL
* @example
* var assets = app.assets.find("My Image", "texture");
* var img = "<img src='" + assets[0].getFileUrl() + "'>";
*/
getFileUrl: function () {
var file = this.getPreferredFile();
if (!file || !file.url)
return null;
var url = file.url;
if (this.registry && this.registry.prefix && !ABSOLUTE_URL.test(url))
url = this.registry.prefix + url;
// add file hash to avoid hard-caching problems
if (this.type !== 'script' && file.hash) {
var separator = url.indexOf('?') !== -1 ? '&' : '?';
url += separator + 't=' + file.hash;
}
return url;
},
getPreferredFile: function () {
if (!this.file)
return null;
if (this.type === 'texture' || this.type === 'textureatlas' || this.type === 'bundle') {
var app = this.registry._loader._app;
var device = app.graphicsDevice;
for (var i = 0, len = VARIANT_DEFAULT_PRIORITY.length; i < len; i++) {
var variant = VARIANT_DEFAULT_PRIORITY[i];
// if the device supports the variant
if (! device[VARIANT_SUPPORT[variant]]) continue;
// if the variant exists in the asset then just return it
if (this.file.variants[variant]) {
return this.file.variants[variant];
}
// if the variant does not exist but the asset is in a bundle
// and the bundle contain assets with this variant then return the default
// file for the asset
if (app.enableBundles) {
var bundles = app.bundles.listBundlesForAsset(this);
if (! bundles) continue;
for (var j = 0, len2 = bundles.length; j < len2; j++) {
if (bundles[j].file && bundles[j].file.variants && bundles[j].file.variants[variant]) {
return this.file;
}
}
}
}
}
return this.file;
},
/**
* @private
* @function
* @name pc.Asset#getLocalizedAssetId
* @param {String} locale The desired locale e.g. ar-AR.
* @description Returns the asset id of the asset that corresponds to the specified locale.
* @returns {Number} An asset id or null if there is no asset specified for the desired locale.
*/
getLocalizedAssetId: function (locale) {
// tries to find either the desired locale or a fallback locale
locale = pc.I18n.findAvailableLocale(locale, this._i18n);
return this._i18n[locale] || null;
},
/**
* @private
* @function
* @name pc.Asset#addLocalizedAssetId
* @param {String} locale The locale e.g. ar-AR.
* @param {Number} assetId The asset id
* @description Adds a replacement asset id for the specified locale. When the locale in {@link pc.Application#i18n} changes then
* references to this asset will be replaced with the specified asset id. (Currently only supported by the {@link pc.ElementComponent}).
*/
addLocalizedAssetId: function (locale, assetId) {
this._i18n[locale] = assetId;
this.fire('add:localized', locale, assetId);
},
/**
* @private
* @function
* @name pc.Asset#removeLocalizedAssetId
* @param {String} locale The locale e.g. ar-AR.
* @description Removes a localized asset
*/
removeLocalizedAssetId: function (locale) {
var assetId = this._i18n[locale];
if (assetId) {
delete this._i18n[locale];
this.fire('remove:localized', locale, assetId);
}
},
/**
* @function
* @name pc.Asset#ready
* @description Take a callback which is called as soon as the asset is loaded. If the asset is already loaded the callback is called straight away
* @param {Function} callback The function called when the asset is ready. Passed the (asset) arguments
* @param {Object} scope Scope object to use when calling the callback
* @example
* var asset = app.assets.find("My Asset");
* asset.ready(function (asset) {
* // asset loaded
* });
* app.assets.load(asset);
*/
ready: function (callback, scope) {
scope = scope || this;
if (this.resource) {
callback.call(scope, this);
} else {
this.once("load", function (asset) {
callback.call(scope, asset);
});
}
},
reload: function () {
// no need to be reloaded
if (!this.loaded)
return;
if (this.type === 'cubemap') {
this.registry._loader.patch(this, this.registry);
} else {
this.loaded = false;
this.registry.load(this);
}
},
/**
* @function
* @name pc.Asset#unload
* @description Destroys the associated resource and marks asset as unloaded.
* @example
* var asset = app.assets.find("My Asset");
* asset.unload();
* // asset.resource is null
*/
unload: function () {
if (!this.loaded && !this.resource)
return;
this.fire('unload', this);
this.registry.fire('unload:' + this.id, this);
if (this.resource && this.resource.destroy)
this.resource.destroy();
this.resource = null;
this.loaded = false;
if (this.file) {
// remove resource from loader cache
this.registry._loader.clearCache(this.getFileUrl(), this.type);
}
}
});
Object.defineProperty(Asset.prototype, 'id', {
get: function () {
return this._id;
},
set: function (value) {
this._id = value;
if (value > assetIdCounter)
assetIdCounter = value;
}
});
Object.defineProperty(Asset.prototype, 'file', {
get: function () {
return this._file;
},
set: function (value) {
// fire change event when the file changes
// so that we reload it if necessary
// set/unset file property of file hash been changed
var key;
var valueAsBool = !!value;
var fileAsBool = !!this._file;
if (valueAsBool !== fileAsBool || (value && this._file && value.hash !== this._file)) {
if (value) {
if (!this._file)
this._file = { };
this._file.url = value.url;
this._file.filename = value.filename;
this._file.hash = value.hash;
this._file.size = value.size;
this._file.variants = this.variants;
if (value.hasOwnProperty('variants')) {
this.variants.clear();
if (value.variants) {
for (key in value.variants) {
if (!value.variants[key])
continue;
this.variants[key] = value.variants[key];
}
}
}
this.fire('change', this, 'file', this._file, this._file);
this.reload();
} else {
this._file = null;
this.variants.clear();
}
} else if (value && this._file && value.hasOwnProperty('variants')) {
this.variants.clear();
if (value.variants) {
for (key in value.variants) {
if (!value.variants[key])
continue;
this.variants[key] = value.variants[key];
}
}
}
}
});
Object.defineProperty(Asset.prototype, 'data', {
get: function () {
return this._data;
},
set: function (value) {
// fire change event when data changes
// because the asset might need reloading if that happens
var old = this._data;
this._data = value;
if (value !== old) {
this.fire('change', this, 'data', value, old);
if (this.loaded)
this.registry._loader.patch(this, this.registry);
}
}
});
Object.defineProperty(Asset.prototype, 'resource', {
get: function () {
return this._resources[0];
},
set: function (value) {
var _old = this._resources[0];
this._resources[0] = value;
this.fire('change', this, 'resource', value, _old);
}
});
Object.defineProperty(Asset.prototype, 'resources', {
get: function () {
return this._resources;
},
set: function (value) {
var _old = this._resources;
this._resources = value;
this.fire('change', this, 'resources', value, _old);
}
});
Object.defineProperty(Asset.prototype, 'preload', {
get: function () {
return this._preload;
},
set: function (value) {
value = !!value;
if (this._preload === value)
return;
this._preload = value;
if (this._preload && !this.loaded && !this.loading && this.registry)
this.registry.load(this);
}
});
return {
Asset: Asset,
ASSET_ANIMATION: 'animation',
ASSET_AUDIO: 'audio',
ASSET_IMAGE: 'image',
ASSET_JSON: 'json',
ASSET_MODEL: 'model',
ASSET_MATERIAL: 'material',
ASSET_TEXT: 'text',
ASSET_TEXTURE: 'texture',
ASSET_CUBEMAP: 'cubemap',
ASSET_SHADER: 'shader',
ASSET_CSS: 'css',
ASSET_HTML: 'html',
ASSET_SCRIPT: 'script',
ABSOLUTE_URL: ABSOLUTE_URL
};
}());