playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
236 lines (235 loc) • 8.87 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { Debug } from "../../core/debug.js";
import { path } from "../../core/path.js";
import { http, Http } from "../../platform/net/http.js";
import { Asset } from "../asset/asset.js";
class ResourceHandler {
/**
* @param {AppBase} app - The running {@link AppBase}.
* @param {string} handlerType - The type of the resource the handler handles.
*/
constructor(app, handlerType) {
/**
* Type of the resource the handler handles.
*/
__publicField(this, "handlerType", "");
/**
* The running app instance.
*
* @type {AppBase}
* @protected
*/
__publicField(this, "_app");
/** @private */
__publicField(this, "_maxRetries", 0);
/**
* The registered parsers, consulted newest-first during selection.
*
* @type {ResourceParser[]}
* @ignore
*/
__publicField(this, "_parsers", []);
this._app = app;
this.handlerType = handlerType;
}
/**
* Gets the running {@link AppBase} instance.
*
* @type {AppBase}
*/
get app() {
return this._app;
}
/**
* Sets the number of times to retry a failed request for the resource.
*
* @type {number}
*/
set maxRetries(value) {
this._maxRetries = value;
}
/**
* Gets the number of times to retry a failed request for the resource.
*
* @type {number}
*/
get maxRetries() {
return this._maxRetries;
}
/**
* Registers a {@link ResourceParser} for this handler. Parsers are consulted newest-first: the most
* recently added parser whose {@link ResourceParser#canParse} returns true is selected. This lets a
* later registration override a built-in parser for the same format.
*
* Register parsers before starting loads for this handler's type - selection runs for both the
* load and open phases, so changing the registry while loads are in flight can route them
* inconsistently. Note that handlers that implement their own loading without consulting
* registered parsers (for example cubemap or font) ignore registered parsers.
*
* @param {ResourceParser} parser - The parser to register. Must implement `canParse(context)`.
* @param {*} [decider] - Removed. Previously a `(url, data) => boolean` selector; implement
* `canParse(context)` on the parser instead. If passed, it is ignored and logs a warning.
* @example
* app.loader.getHandler('model').addParser(new ObjModelParser(app.graphicsDevice));
*/
addParser(parser, decider) {
Debug.assert(
parser && typeof parser.canParse === "function",
"ResourceHandler#addParser: the parser must implement canParse(context). The (parser, decider) form was removed - move the decider logic into the parser's canParse(context) method."
);
if (decider !== void 0) {
Debug.removed('ResourceHandler#addParser(parser, decider): the "decider" argument was removed. Implement canParse(context) on the parser instead.');
}
if (!parser || typeof parser.canParse !== "function") {
return;
}
parser.handler = this;
this._parsers.push(parser);
}
/**
* Removes a previously registered {@link ResourceParser}.
*
* @param {ResourceParser} parser - The parser to remove.
*/
removeParser(parser) {
const index = this._parsers.indexOf(parser);
if (index !== -1) {
this._parsers.splice(index, 1);
parser.handler = null;
}
}
/**
* Gets a read-only copy of the registered parsers.
*
* @type {ResourceParser[]}
*/
get parsers() {
return this._parsers.slice();
}
/**
* Fetches a resource's raw data using this handler's retry settings, reusing pre-fetched
* `asset.file.contents` when available. A convenience for a {@link ResourceParser}'s `load` method,
* so parsers don't reimplement the fetch boilerplate.
*
* @param {string | {load: string, original: string}} url - The resource URL, or a load/original
* structure.
* @param {string} responseType - The {@link Http.ResponseType} to fetch as (for example
* `Http.ResponseType.ARRAY_BUFFER` for a binary format, or `Http.ResponseType.TEXT`).
* @param {ResourceHandlerCallback} callback - Called with `(err, data)` when the fetch completes.
* @param {Asset} [asset] - The asset being loaded, used to reuse already-fetched contents.
*/
fetch(url, responseType, callback, asset) {
if (typeof url === "string") {
url = { load: url, original: url };
}
if (responseType === Http.ResponseType.ARRAY_BUFFER) {
Asset.fetchArrayBuffer(url.load, callback, asset, this.maxRetries);
return;
}
http.get(url.load, {
responseType,
retry: this.maxRetries > 0,
maxRetries: this.maxRetries
}, callback);
}
/**
* Builds the {@link ParserContext} used for parser selection.
*
* @param {string | {load: string, original: string} | null} url - The URL, a load/original
* structure, or null.
* @param {Asset} [asset] - The asset being loaded, if any.
* @returns {ParserContext} The parser context.
* @ignore
*/
_makeContext(url, asset) {
const original = url && typeof url === "object" ? url.original : url;
const clean = original ? original.split("?")[0] : "";
return {
url: original ?? null,
ext: original ? path.getExtension(clean).toLowerCase().replace(".", "") : "",
basename: original ? path.getBasename(clean).toLowerCase() : "",
asset,
app: this._app
};
}
/**
* Selects a parser for the given context, consulting registered parsers newest-first.
*
* @param {ParserContext} context - The context built by {@link ResourceHandler#_makeContext}.
* @returns {ResourceParser|null} The first parser whose `canParse` returns true, or null.
* @ignore
*/
_selectParser(context) {
for (let i = this._parsers.length - 1; i >= 0; i--) {
if (this._parsers[i].canParse(context)) {
return this._parsers[i];
}
}
return null;
}
/**
* Load a resource from a remote URL. When parsers are registered, the matching parser's `load` is
* used; otherwise the base implementation does nothing (subclasses may override).
*
* @param {string | {load: string, original: string}} url - Either the URL of the resource to
* load or a structure containing the load URL (used for loading the resource) and the original
* URL (used for identifying the resource format; necessary when loading, for example, from
* a blob URL).
* @param {ResourceHandlerCallback} callback - The callback used when the resource is loaded or
* an error occurs.
* @param {Asset} [asset] - Optional asset that is passed by ResourceLoader.
*/
load(url, callback, asset) {
if (this._parsers.length === 0) {
return;
}
if (typeof url === "string") {
url = {
load: url,
original: url
};
}
const context = this._makeContext(url, asset);
const parser = this._selectParser(context);
if (!parser) {
callback(`No parser found for resource: ${context.url}`);
return;
}
Debug.assert(
typeof parser.load === "function",
`ResourceHandler('${this.handlerType}'): the selected parser has no load(url, callback, asset) method. Pre-fetch parsers that implement parse(data, ...) must be used by a handler that overrides load().`
);
parser.load(url, callback, asset);
}
/**
* The open function is passed the raw resource data. The handler can then process the data
* into a format that can be used at runtime. When parsers are registered, the matching parser's
* `open` is used (if it implements one); otherwise the base implementation simply returns the data.
*
* @param {string} url - The URL of the resource to open.
* @param {*} data - The raw resource data passed by callback from {@link load}.
* @param {Asset} [asset] - Optional asset that is passed by ResourceLoader.
* @returns {*} The parsed resource data.
*/
open(url, data, asset) {
if (this._parsers.length === 0) {
return data;
}
const parser = this._selectParser(this._makeContext(url, asset));
return parser?.open ? parser.open(url, data, asset) : data;
}
/**
* The patch function performs any operations on a resource that requires a dependency on its
* asset data or any other asset data. The base implementation does nothing.
*
* @param {Asset} asset - The asset to patch.
* @param {AssetRegistry} assets - The asset registry.
*/
patch(asset, assets) {
}
}
export {
ResourceHandler
};