diff --git a/src/eterna/FoldingAPIApp.ts b/src/eterna/FoldingAPIApp.ts new file mode 100644 index 00000000..2d77e639 --- /dev/null +++ b/src/eterna/FoldingAPIApp.ts @@ -0,0 +1,133 @@ +import * as log from 'loglevel'; +import ExternalInterface, {ExternalInterfaceCtx} from './util/ExternalInterface'; +import Vienna from './folding/Vienna'; +import Vienna2 from './folding/Vienna2'; +import NuPACK from './folding/NuPACK'; +import Contrafold from './folding/Contrafold'; +import EternaFold from './folding/Eternafold'; +import EternaFoldThreshknot from './folding/EternafoldThreshknot'; +import RNAFoldBasic from './folding/RNAFoldBasic'; +import FolderManager from './folding/FolderManager'; +import LinearFoldC from './folding/LinearFoldC'; +import LinearFoldE from './folding/LinearFoldE'; +import LinearFoldV from './folding/LinearFoldV'; +import Folder from './folding/Folder'; +import FoldingAPI from './eternaScript/FoldingAPI'; +import addSelectFolderAPIToInterface from './eternaScript/SelectFolderAPI'; + +interface FoldingAppParams { + containerID?: string; + folderName?: string; +} + +interface ProcessedFoldingAppParams { + containerID: string; + folderName: string; +} + +export class WasmNotSupportedError extends Error {} + +export class ContainerElementNotFound extends Error {} + +/** + * Entry point for the folding API provider. + * + * This is an alternate version of EternaJS, only exposing the API needed for scripts to work + * (e.g. `Lib.fold` via `document.getElementById("maingame").fold`). + * */ +export default class FoldingAPIApp { + constructor(params: FoldingAppParams) { + // Default param values + params.containerID = params.containerID || 'maingame'; + params.folderName = 'vienna'; + + this._params = {containerID: params.containerID, folderName: params.folderName}; + + const appContainer: HTMLElement | null = document.getElementById(params.containerID); + if (!appContainer) { + throw new ContainerElementNotFound(`Could not find HTML element with ID ${params.containerID}`); + } + this._appContainer = appContainer; + + ExternalInterface.init(appContainer); + } + + private static isWebAssemblySupported() { + return typeof WebAssembly === 'object'; + } + + public async run(): Promise { + if (!FoldingAPIApp.isWebAssemblySupported()) { + throw new WasmNotSupportedError( + "Can't initialize the folding API app, since the browser doesn't support WASM" + ); + } + + await this.initFoldingEngines(); + this.initScriptInterface(); + } + + public disposeNow(): void { + this._appContainer.innerHTML = ''; + + FolderManager.dispose(); + ExternalInterface.dispose(); + } + + private async initFoldingEngines(): Promise { + log.info('Initializing folding engines...'); + const folders: (Folder | null)[] = await Promise.all([ + Vienna.create(), + Vienna2.create(), + NuPACK.create(), + LinearFoldC.create(), + LinearFoldE.create(), + LinearFoldV.create(), + Contrafold.create(), + EternaFold.create(), + EternaFoldThreshknot.create(), + RNAFoldBasic.create()]); + + log.info('Folding engines intialized'); + for (const folder of folders) { + if (folder !== null) { + FolderManager.instance.addFolder(folder); + } + } + + const folder = FolderManager.instance.getFolder(this._params.folderName); + if (folder === null) { + log.warn(`No such folder '${this._params.folderName}'`); + } else { + this._folder = folder; + } + } + + private trySelectFolder(folderName: string): boolean { + const folder = FolderManager.instance.getFolder(folderName); + if (folder === null) { + log.warn(`No such folder '${this._params.folderName}'`); + return false; + } else { + return true; + } + } + + private initScriptInterface(): void { + new FoldingAPI({ + getFolder: () => this._folder, + getIsPseudoknot: () => false + }).registerToScriptInterface(this._scriptInterface); + addSelectFolderAPIToInterface({ + selectFolder: (folderName) => this.trySelectFolder(folderName), + scriptInterface: this._scriptInterface + }); + + ExternalInterface.pushContext(this._scriptInterface); + } + + private readonly _params: ProcessedFoldingAppParams; + private readonly _scriptInterface: ExternalInterfaceCtx = new ExternalInterfaceCtx(); + private readonly _appContainer: HTMLElement; + private _folder: Folder; +} diff --git a/src/eterna/index.ts b/src/eterna/index.ts index 97ffe947..2f8853a8 100644 --- a/src/eterna/index.ts +++ b/src/eterna/index.ts @@ -1,5 +1,6 @@ import * as log from 'loglevel'; import EternaApp from 'eterna/EternaApp'; +import FoldingAPIApp from 'eterna/FoldingAPIApp'; import * as PIXI from 'pixi.js'; const isProduction = process.env.NODE_ENV === 'production'; @@ -8,9 +9,11 @@ log.setLevel(isProduction ? 'info' : 'trace'); declare global { interface Window { EternaApp: typeof EternaApp; + FoldingAPIApp: typeof FoldingAPIApp; app: EternaApp; // this syntax is used in index.html.tmpl, at least... __PIXI_APP__?: PIXI.Application; } } window.EternaApp = EternaApp; +window.FoldingAPIApp = FoldingAPIApp; diff --git a/src/folding-api.html.tmpl b/src/folding-api.html.tmpl new file mode 100644 index 00000000..bc365a9b --- /dev/null +++ b/src/folding-api.html.tmpl @@ -0,0 +1,73 @@ + + + + + + EternaJS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +<%= htmlWebpackPlugin.tags.bodyTags %> + + + + diff --git a/src/index.html.tmpl b/src/index.html.tmpl index 877dc4d4..bb19fa1f 100644 --- a/src/index.html.tmpl +++ b/src/index.html.tmpl @@ -12,6 +12,8 @@ diff --git a/webpack.common.js b/webpack.common.js index f1953475..640d83f1 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -38,7 +38,7 @@ function getEngineLocation() { module.exports = { devtool: "inline-source-map", - + entry: { main: ['core-js/stable', 'regenerator-runtime/runtime', "./src/eterna/index.ts"], vendor: vendorDependencies @@ -73,7 +73,7 @@ module.exports = { path: false } }, - + module: { rules: [ { @@ -95,7 +95,7 @@ module.exports = { } ], }, - + // When importing a module whose path matches one of the following, just // assume a corresponding global variable exists and use that instead. // This is important because it allows us to avoid bundling all of our @@ -124,12 +124,12 @@ module.exports = { optimization: { splitChunks: { chunks: 'all', - }, + }, }, - - plugins: [ + + plugins: [ new webpack.EnvironmentPlugin(Object.keys(process.env)), - + // Generate an index.html that includes our webpack bundles new HtmlWebpackPlugin({ template: 'src/index.html.tmpl', @@ -142,6 +142,19 @@ module.exports = { } }), + // Generate folding=api.html, for the scripts page + new HtmlWebpackPlugin({ + template: 'src/folding-api.html.tmpl', + filename: "folding-api.html", + inject: false, + scriptLoading: 'blocking', + process: { + env: { + ...process.env + } + } + }), + // Generate a manifest.json file containing our entry point file names: // https://github.com/danethurber/webpack-manifest-plugin#hooks-options new WebpackManifestPlugin({