From 3f7ea0d10cee8f1a2008e670719d5cb9ea4f8fd3 Mon Sep 17 00:00:00 2001 From: Elliott Thompson Date: Tue, 22 Mar 2022 17:48:46 +0000 Subject: [PATCH 01/13] public AssetListLoader --- src/asset/asset-list-loader.js | 214 +++++++++++++++++++++ src/index.js | 1 + test/asset/asset-list-loader.test.mjs | 255 ++++++++++++++++++++++++++ 3 files changed, 470 insertions(+) create mode 100644 src/asset/asset-list-loader.js create mode 100644 test/asset/asset-list-loader.test.mjs diff --git a/src/asset/asset-list-loader.js b/src/asset/asset-list-loader.js new file mode 100644 index 00000000000..296b42e8e84 --- /dev/null +++ b/src/asset/asset-list-loader.js @@ -0,0 +1,214 @@ +import { EventHandler } from '../core/event-handler.js'; + +import { Asset } from './asset.js'; + +/** + * @class + * @name AssetListLoader + * @augments EventHandler + * @classdesc Used to load a group of assets and fires a callback when all assets are loaded. + * @param {Asset[]|number[]} assetList - An array of {@link Asset} objects to load or an array of Asset IDs to load. + * @param {AssetRegistry} assetRegistry - The application's asset registry. + */ +class AssetListLoader extends EventHandler { + constructor(assetList, assetRegistry) { + super(); + this._assets = []; + this._loadingAssets = new Set(); + this._registry = assetRegistry; + this._loaded = false; + this._count = 0; // running count of successfully loaded assets + this._failed = []; // list of assets that failed to load + + this._waitingAssets = []; + + if (assetList.length && assetList[0] instanceof Asset) { + assetList.forEach((asset, i, array) => { + // filter out duplicates + if (array.indexOf(asset) !== i) { + return; + } + if (!asset.registry) { + asset.registry = assetRegistry; + } + this._assets.push(asset); + }); + } else { + // list of Asset IDs + assetList.forEach((assetId, i, array) => { + // filter out duplicates + if (array.indexOf(assetId) !== i) { + return; + } + var asset = assetRegistry.get(assetList[i]); + if (asset) { + this._assets.push(asset); + } else { + this._waitForAsset(assetList[i]); + } + }); + } + } + + destroy() { + // remove any outstanding listeners + var self = this; + + this._registry.off("load", this._onLoad); + this._registry.off("error", this._onError); + + this._waitingAssets.forEach(function (id) { + self._registry.off("add:" + id, this._onAddAsset); + }); + + this.off("progress"); + this.off("load"); + } + + /** + * @function + * @name AssetListLoader#load + * @description Start loading asset list, call done() when all assets have loaded or failed to load. + * @param {Function} done - Callback called when all assets in the list are loaded. Passed (err, failed) where err is the undefined if no errors are encountered and failed contains a list of assets that failed to load. + * @param {object} [scope] - Scope to use when calling callback. + * + */ + load(done, scope) { + var i = 0; + var l = this._assets.length; + var asset; + + this._count = 0; + this._failed = []; + this._callback = done; + this._scope = scope; + this._loaded = false; + + this._registry.on("load", this._onLoad, this); + this._registry.on("error", this._onError, this); + + let loadingAssets = false; + for (i = 0; i < l; i++) { + asset = this._assets[i]; + + // Track assets that are not loaded or are currently loading + // as some assets may be loading by this call + if (!asset.loaded) { + loadingAssets = true; + this._loadingAssets.add(asset); + this._registry.add(asset); + } + } + this._loadingAssets.forEach((asset) => { + this._registry.load(asset); + }); + if (!loadingAssets && this._waitingAssets.length === 0) { + this.fire("load", this._assets); + } + } + + /** + * @function + * @name AssetListLoader#ready + * @param {Function} done - Callback called when all assets in the list are loaded. + * @param {object} [scope] - Scope to use when calling callback. + */ + ready(done, scope) { + scope = scope || this; + + if (this._loaded) { + done.call(scope, this._assets); + } else { + this.once("load", function (assets) { + done.call(scope, assets); + }); + } + } + + // called when all assets are loaded + _loadingComplete() { + if (this._loaded) return; + this._loaded = true; + this._registry.off("load", this._onLoad, this); + this._registry.off("error", this._onError, this); + + if (this._failed && this._failed.length) { + if (this._callback) { + this._callback.call(this._scope, "Failed to load some assets", this._failed); + } + this.fire("error", this._failed); + } else { + if (this._callback) { + this._callback.call(this._scope); + } + this.fire("load", this._assets); + } + } + + // called when an (any) asset is loaded + _onLoad(asset) { + var self = this; + + // check this is an asset we care about + if (this._loadingAssets.has(asset)) { + this._count++; + this.fire("progress", asset); + } + + if (this._count === this._loadingAssets.size) { + // call next tick because we want + // this to be fired after any other + // asset load events + setTimeout(function () { + self._loadingComplete(self._failed); + }, 0); + } + } + + // called when an asset fails to load + _onError(err, asset) { + var self = this; + + // check this is an asset we care about + if (this._loadingAssets.has(asset)) { + this._count++; + this._failed.push(asset); + } + + if (this._count === this._loadingAssets.size) { + // call next tick because we want + // this to be fired after any other + // asset load events + setTimeout(function () { + self._loadingComplete(self._failed); + }, 0); + } + } + + // called when a expected asset is added to the asset registry + _onAddAsset(asset) { + // remove from waiting list + var index = this._waitingAssets.indexOf(asset); + if (index >= 0) { + this._waitingAssets.splice(index, 1); + } + + this._assets.push(asset); + var i; + var l = this._assets.length; + for (i = 0; i < l; i++) { + asset = this._assets[i]; + if (!asset.loaded) { + this._loadingAssets.add(asset); + this._registry.load(asset); + } + } + } + + _waitForAsset(assetId) { + this._waitingAssets.push(assetId); + this._registry.once('add:' + assetId, this._onAddAsset, this); + } +} + +export { AssetListLoader }; diff --git a/src/index.js b/src/index.js index ad30ab2b61b..cbddcddd0ce 100644 --- a/src/index.js +++ b/src/index.js @@ -182,6 +182,7 @@ export { TextureAtlasHandler } from './resources/texture-atlas.js'; // ASSETS export * from './asset/constants.js'; export { Asset } from './asset/asset.js'; +export { AssetListLoader } from './asset/asset-list-loader.js'; export { AssetReference } from './asset/asset-reference.js'; export { AssetRegistry } from './asset/asset-registry.js'; export { LocalizedAsset } from './asset/asset-localized.js'; diff --git a/test/asset/asset-list-loader.test.mjs b/test/asset/asset-list-loader.test.mjs new file mode 100644 index 00000000000..f518b8d2832 --- /dev/null +++ b/test/asset/asset-list-loader.test.mjs @@ -0,0 +1,255 @@ +import { Application } from '../../src/framework/app-base.js'; +import { AssetListLoader } from '../../src/asset/asset-list-loader.js'; +import { Asset } from '../../src/asset/asset.js'; + +import { HTMLCanvasElement } from '@playcanvas/canvas-mock'; + +import { expect } from 'chai'; + +describe('AssetListLoader', function () { + + let app; + const assetPath = 'http://localhost:3000/test/test-assets/'; + + beforeEach(function () { + const canvas = new HTMLCanvasElement(500, 500); + app = new Application(canvas); + }); + + afterEach(function () { + app.destroy(); + }); + + describe('#constructor', function () { + it('instantiates correctly', function () { + const assetListLoader = new AssetListLoader([], app.assets); + expect(assetListLoader).to.be.ok; + }); + it('stores a single asset', function () { + const assets = [ + new Asset('model', 'container', { url: `${assetPath}test.glb` }) + ]; + const assetListLoader = new AssetListLoader(Object.values(assets), app.assets); + expect(assetListLoader._assets[0].name).to.be.equal('model'); + }); + it('stores multiple assets', function () { + const assets = [ + new Asset('model', 'container', { url: `${assetPath}test.glb` }), + new Asset('styling', 'css', { url: `${assetPath}test.css` }) + ]; + const assetListLoader = new AssetListLoader(assets, app.assets); + expect(assetListLoader._assets[0].name).to.be.equal('model'); + expect(assetListLoader._assets[1].name).to.be.equal('styling'); + }); + it('stores single copies of duplicated assets', function () { + const assets = [ + new Asset('model', 'container', { url: `${assetPath}test.glb` }) + ]; + const assetListLoader = new AssetListLoader([assets[0], assets[0]], app.assets); + expect(assetListLoader._assets.length).to.be.equal(1); + }); + it('adds the supplied registry to any assets that do not have one', function () { + const assets = [ + new Asset('model', 'container', { url: `${assetPath}test.glb` }) + ]; + expect(assets[0].registry).to.be.equal(null); + const assetListLoader = new AssetListLoader([assets[0], assets[0]], app.assets); + expect(assetListLoader._assets[0].registry).to.be.equal(app.assets); + }); + }); + + describe('#ready', function () { + it('can return a single loaded asset', function (done) { + const asset = new Asset('model', 'container', { url: `${assetPath}test.glb` }); + const assetListLoader = new AssetListLoader([asset], app.assets); + assetListLoader.ready((assets) => { + expect(assets.length).to.equal(1); + expect(assets[0].name).to.equal('model'); + expect(assets[0].loaded).to.equal(true); + done(); + }); + assetListLoader.load(); + }); + it('can return multiple loaded assets', function (done) { + const assets = [ + new Asset('model', 'container', { url: `${assetPath}test.glb` }), + new Asset('styling', 'css', { url: `${assetPath}test.css` }) + ]; + const assetListLoader = new AssetListLoader(assets, app.assets); + assetListLoader.ready((assets) => { + expect(assets.length).to.equal(2); + expect(assets[0].name).to.equal('model'); + expect(assets[0].loaded).to.equal(true); + expect(assets[1].name).to.equal('styling'); + expect(assets[1].loaded).to.equal(true); + done(); + }); + assetListLoader.load(); + }); + it('can return a single duplicated loaded asset', function (done) { + const asset = new Asset('model', 'container', { url: `${assetPath}test.glb` }); + const assetListLoader = new AssetListLoader([asset, asset], app.assets); + assetListLoader.ready((assets) => { + expect(assets.length).to.equal(1); + expect(assets[0].name).to.equal('model'); + expect(assets[0].loaded).to.equal(true); + done(); + }); + assetListLoader.load(); + }); + }); + + describe('#load', function () { + it('can succeed if an asset is already loaded', function (done) { + const asset = new Asset('model', 'container', { url: `${assetPath}test.glb` }); + const assetListLoader = new AssetListLoader([asset], app.assets); + asset.on('load', (asset) => { + expect(asset.loaded).to.equal(true); + assetListLoader.ready((assets) => { + expect(assets.length).to.equal(1); + expect(assets[0].name).to.equal('model'); + expect(assets[0].loaded).to.equal(true); + done(); + }); + assetListLoader.load(); + }); + app.assets.add(asset); + app.assets.load(asset); + }); + it('can succeed if one asset is already loaded and another is not', function (done) { + const assets = [ + new Asset('model', 'container', { url: `${assetPath}test.glb` }), + new Asset('styling', 'css', { url: `${assetPath}test.css` }) + ]; + const assetListLoader = new AssetListLoader(assets, app.assets); + assets[0].on('load', (asset) => { + expect(asset.name).to.equal('model'); + expect(asset.loaded).to.equal(true); + assetListLoader.ready((assets) => { + expect(assets.length).to.equal(2); + expect(assets[0].name).to.equal('model'); + expect(assets[0].loaded).to.equal(true); + expect(assets[1].name).to.equal('styling'); + expect(assets[1].loaded).to.equal(true); + done(); + }); + assetListLoader.load(); + }); + app.assets.add(assets[0]); + app.assets.load(assets[0]); + }); + it('can succeed if an asset is already loading', function (done) { + const asset = new Asset('model', 'container', { url: `${assetPath}test.glb` }); + const assetListLoader = new AssetListLoader([asset], app.assets); + app.assets.add(asset); + app.assets.load(asset); + expect(asset.loading).to.equal(true); + assetListLoader.ready((assets) => { + expect(assets.length).to.equal(1); + expect(assets[0].name).to.equal('model'); + expect(assets[0].loaded).to.equal(true); + done(); + }); + assetListLoader.load(); + }); + it('can succeed if one asset is already loading and another is not', function (done) { + const assets = [ + new Asset('model', 'container', { url: `${assetPath}test.glb` }), + new Asset('styling', 'css', { url: `${assetPath}test.css` }) + ]; + const assetListLoader = new AssetListLoader(assets, app.assets); + app.assets.add(assets[0]); + app.assets.load(assets[0]); + expect(assets[0].loading).to.equal(true); + assetListLoader.ready((assets) => { + expect(assets.length).to.equal(2); + expect(assets[0].name).to.equal('model'); + expect(assets[0].loaded).to.equal(true); + expect(assets[1].name).to.equal('styling'); + expect(assets[1].loaded).to.equal(true); + done(); + }); + assetListLoader.load(); + }); + it('can succeed if one asset is already loaded, another is loading and one is not loaded', function (done) { + const assets = [ + new Asset('model', 'container', { url: `${assetPath}test.glb` }), + new Asset('styling', 'css', { url: `${assetPath}test.css` }), + new Asset('binfile', 'binary', { url: `${assetPath}test.bin` }) + ]; + const assetListLoader = new AssetListLoader(assets, app.assets); + assets[0].on('load', (asset) => { + expect(asset.name).to.equal('model'); + expect(asset.loaded).to.equal(true); + app.assets.add(assets[1]); + app.assets.load(assets[1]); + expect(assets[1].loading).to.equal(true); + assetListLoader.ready((assets) => { + expect(assets.length).to.equal(3); + expect(assets[0].name).to.equal('model'); + expect(assets[0].loaded).to.equal(true); + expect(assets[1].name).to.equal('styling'); + expect(assets[1].loaded).to.equal(true); + expect(assets[2].name).to.equal('binfile'); + expect(assets[2].loaded).to.equal(true); + done(); + }); + assetListLoader.load(); + }); + app.assets.add(assets[0]); + app.assets.load(assets[0]); + }); + it('can succeed if multiple assets load the same url', function (done) { + const assets = [ + new Asset('model1', 'container', { url: `${assetPath}test.glb` }), + new Asset('model2', 'container', { url: `${assetPath}test.glb` }) + ]; + const assetListLoader = new AssetListLoader(assets, app.assets); + assetListLoader.ready((assets) => { + expect(assets.length).to.equal(2); + expect(assets[0].name).to.equal('model1'); + expect(assets[0].loaded).to.equal(true); + expect(assets[1].name).to.equal('model2'); + expect(assets[1].loaded).to.equal(true); + done(); + }); + assetListLoader.load(); + }); + it('can succeed if an empty list is passed in', function (done) { + const assetListLoader = new AssetListLoader([], app.assets); + assetListLoader.ready((assets) => { + expect(assets.length).to.equal(0); + done(); + }); + assetListLoader.load(); + }); + it('can successfully load assets from ids that are in the registry', function (done) { + const assets = [ + new Asset('model', 'container', { url: `${assetPath}test.glb` }), + new Asset('styling', 'css', { url: `${assetPath}test.css` }) + ]; + app.assets.add(assets[0]); + app.assets.add(assets[1]); + const assetListLoader = new AssetListLoader([assets[0].id, assets[1].id], app.assets); + assetListLoader.ready((assets) => { + expect(assets.length).to.equal(2); + done(); + }); + assetListLoader.load(); + }); + it('can successfully load assets from ids that are not yet in the registry', function (done) { + const assets = [ + new Asset('model', 'container', { url: `${assetPath}test.glb` }), + new Asset('styling', 'css', { url: `${assetPath}test.css` }) + ]; + const assetListLoader = new AssetListLoader([assets[0].id, assets[1].id], app.assets); + assetListLoader.ready((assets) => { + expect(assets.length).to.equal(2); + done(); + }); + assetListLoader.load(); + app.assets.add(assets[0]); + app.assets.add(assets[1]); + }); + }); +}); From 4a97ae34046cdbf6f01a3d4e0dc3cd4a015318df Mon Sep 17 00:00:00 2001 From: Elliott Thompson Date: Tue, 22 Mar 2022 17:50:14 +0000 Subject: [PATCH 02/13] Update examples browser to support an in example application --- examples/scripts/example-data.mjs | 3 + examples/scripts/iframe/index.mjs | 21 - examples/scripts/iframe/index.mustache | 126 +- examples/src/app/code-editor.tsx | 6 + examples/src/app/example.tsx | 1 + examples/src/app/helpers/example-data.mjs | 27 +- examples/src/app/helpers/formatters.mjs | 25 +- examples/tsconfig.json | 3 +- package-lock.json | 1443 +++++++++------------ package.json | 2 +- src/asset/asset.js | 2 +- 11 files changed, 707 insertions(+), 952 deletions(-) diff --git a/examples/scripts/example-data.mjs b/examples/scripts/example-data.mjs index 4bc3bf66272..afc16c64f31 100644 --- a/examples/scripts/example-data.mjs +++ b/examples/scripts/example-data.mjs @@ -33,6 +33,9 @@ fs.readdirSync(`${MAIN_DIR}/src/examples/`).forEach(function (category) { exampleData[category][example].javaScriptFunction = Prettier.format(Babel.transform(exampleData[category][example].typeScriptFunction, { retainLines: true, filename: `transformedScript.tsx`, presets: ["typescript"] }).code, { parser: BabelParser.parse, tabWidth: 4 }); exampleData[category][example].nameSlug = example; exampleData[category][example].categorySlug = category; + const files = formatters.retrieveStaticObject(exampleFileText, 'FILES'); + // eslint-disable-next-line no-eval + if (files) exampleData[category][example].files = eval('(' + files + ')'); }); }); diff --git a/examples/scripts/iframe/index.mjs b/examples/scripts/iframe/index.mjs index 4f9040faa68..171917ec9b2 100644 --- a/examples/scripts/iframe/index.mjs +++ b/examples/scripts/iframe/index.mjs @@ -20,22 +20,6 @@ fs.copyFileSync(`${MAIN_DIR}/./node_modules/promise-polyfill/dist/polyfill.min.j fs.copyFileSync(`${MAIN_DIR}/./node_modules/whatwg-fetch/dist/fetch.umd.js`, `${MAIN_DIR}/dist/build/fetchPolyfill.js`); fs.copyFileSync(`${MAIN_DIR}/./node_modules/regenerator-runtime/runtime.js`, `${MAIN_DIR}/dist/build/regeneratorRuntimePolyfill.js`); -const EXAMPLE_CONSTS = [ - "vshader", - "fshader", - "fshaderFeedback", - "fshaderCloud", - "vshaderFeedback", - "vshaderCloud" -]; - -function retrieveConstString(data, name) { - const start = data.indexOf(`const ${name} = `); - if (start < 0) return; - const end = data.indexOf("`;", start); - return data.substring(start + name.length + 10, end); -} - function loadHtmlTemplate(data) { const html = fs.readFileSync(`${MAIN_DIR}/scripts/iframe/index.mustache`, "utf8"); const template = Handlebars.compile(html); @@ -48,10 +32,6 @@ function buildExample(category, filename) { "utf8" ); - const exampleConstValues = EXAMPLE_CONSTS - .map(k => ({ k, v: retrieveConstString(exampleString, k) })) - .filter(c => c.v); - const exampleClass = formatters.getExampleClassFromTextFile(Babel, exampleString); if (!fs.existsSync(`${MAIN_DIR}/dist/iframe/${category}/`)) { fs.mkdirSync(`${MAIN_DIR}/dist/iframe/${category}/`); @@ -70,7 +50,6 @@ function buildExample(category, filename) { } fs.writeFileSync(`${MAIN_DIR}/dist/iframe/${category}/${filename.replace(".tsx", "")}.html`, loadHtmlTemplate({ exampleClass: exampleClass, - exampleConstValues: JSON.stringify(exampleConstValues), enginePath: process.env.ENGINE_PATH || enginePath })); } diff --git a/examples/scripts/iframe/index.mustache b/examples/scripts/iframe/index.mustache index 5eac61563bc..19348d4c599 100644 --- a/examples/scripts/iframe/index.mustache +++ b/examples/scripts/iframe/index.mustache @@ -36,12 +36,6 @@ }); } - // include any constants necessary for the example - var constValues = {{{ exampleConstValues }}}; - for (var i = 0; i < constValues.length; i++) { - window[constValues[i].k] = constValues[i].v; - } - // include the example class which contains the example function to execute and any assets to load {{{ exampleClass }}} var example = new Example(); @@ -58,9 +52,10 @@ presets: ["react", "typescript", "env"] }).code; } - window.exampleFunction = new Function('app', 'canvas', 'assets', 'data', exampleFunction); + window.exampleFunction = new Function('canvas', 'data', exampleFunction); } window.loadFunction = example.load; + window.files = window.top.editedFiles || example.constructor.FILES; // get url parameters var queryString = window.location.search; @@ -68,7 +63,7 @@ +