├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── img ├── wle-logo-horizontal-reversed-dark.png └── wle-logo-horizontal-reversed-light.png ├── package-lock.json ├── package.json └── packages ├── api ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── scripts │ └── pre-build.mjs ├── src │ ├── component.ts │ ├── decorators.ts │ ├── engine.ts │ ├── global.d.ts │ ├── index.ts │ ├── prefab.ts │ ├── property.ts │ ├── resources │ │ ├── material-manager.ts │ │ ├── mesh-manager.ts │ │ ├── resource.ts │ │ └── texture-manager.ts │ ├── scene-gltf.ts │ ├── scene.ts │ ├── types.ts │ ├── utils │ │ ├── bitset.ts │ │ ├── cbor.ts │ │ ├── event.ts │ │ ├── fetch.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ ├── misc.ts │ │ └── object.ts │ ├── wasm.ts │ └── wonderland.ts └── tsconfig.json └── test-e2e ├── .gitignore ├── README.md ├── animation.test.ts ├── cbor-test-vectors.ts ├── chai ├── almost.ts └── promise.ts ├── component.test.ts ├── deploy ├── engine.test.ts ├── event.test.ts ├── global.d.ts ├── i18n.test.ts ├── logger.test.ts ├── material.test.ts ├── memory.test.ts ├── mesh.test.ts ├── morphtargets.test.ts ├── object.test.ts ├── package.json ├── physx.test.ts ├── projects ├── advanced │ ├── Advanced.wlp │ ├── assets │ │ ├── SimpleSkin.glb │ │ └── spree_bank_32x16.hdr │ ├── image.png │ ├── js │ │ └── test-component-retarget.ts │ └── package.json ├── animations │ ├── Animations.wlp │ ├── animation.glb │ ├── animation2.glb │ ├── animation3.glb │ └── animation4.glb ├── components │ ├── TestJsComponentsMain.wlp │ ├── js │ │ └── test-component.ts │ └── package.json ├── language-switching │ ├── LanguageSwitching.wlp │ ├── js │ │ └── test-component-translate.ts │ ├── languages │ │ ├── en.json │ │ └── nl.json │ └── package.json ├── morph-targets │ ├── MorphTargets.wlp │ └── assets │ │ ├── MorphPrimitivesTest.glb │ │ └── MorphStressTest.glb ├── preferences.json ├── streaming │ ├── 4x4.png │ ├── TestStreaming.wlp │ └── image.png └── tiny │ └── Tiny.wlp ├── property.test.ts ├── resources ├── 2x2.png ├── Box.glb ├── BoxWithExtensions.glb ├── Broken.bin ├── Cube.glb ├── Rigged.glb ├── SimpleSkin.glb ├── TwoPlanesWithTextures.glb └── UVcube.glb ├── scene-gltf.test.ts ├── scene.test.ts ├── scripts ├── build-projects.mjs └── run-tests.mjs ├── setup.ts ├── skin.test.ts ├── texture.test.ts ├── types.ts ├── utils.test.ts ├── utils.ts └── web-test-runner.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # O.S 2 | 3 | .DS_Store 4 | 5 | # As a transition step, the wonderland files are produced at the root. 6 | # @todo: Remove at 1.0.0 7 | index.js 8 | index.d.ts 9 | wonderland.js 10 | wonderland.d.ts 11 | doc.json 12 | 13 | # Logs 14 | logs 15 | *.log 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | lerna-debug.log* 20 | 21 | # Diagnostic reports (https://nodejs.org/api/report.html) 22 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 23 | 24 | # Runtime data 25 | pids 26 | *.pid 27 | *.seed 28 | *.pid.lock 29 | 30 | # Directory for instrumented libs generated by jscoverage/JSCover 31 | lib-cov 32 | 33 | # Coverage directory used by tools like istanbul 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | .nyc_output 39 | 40 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 41 | .grunt 42 | 43 | # Bower dependency directory (https://bower.io/) 44 | bower_components 45 | 46 | # node-waf configuration 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | build/Release 51 | 52 | # Dependency directories 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # TypeScript v1 declaration files 57 | typings/ 58 | 59 | # TypeScript cache 60 | *.tsbuildinfo 61 | 62 | # Optional npm cache directory 63 | .npm 64 | 65 | # Optional eslint cache 66 | .eslintcache 67 | 68 | # Microbundle cache 69 | .rpt2_cache/ 70 | .rts2_cache_cjs/ 71 | .rts2_cache_es/ 72 | .rts2_cache_umd/ 73 | 74 | # Optional REPL history 75 | .node_repl_history 76 | 77 | # Output of 'npm pack' 78 | *.tgz 79 | 80 | # Yarn Integrity file 81 | .yarn-integrity 82 | 83 | # dotenv environment variables file 84 | .env 85 | .env.test 86 | 87 | # parcel-bundler cache (https://parceljs.org/) 88 | .cache 89 | 90 | # Next.js build output 91 | .next 92 | 93 | # Nuxt.js build / generate output 94 | .nuxt 95 | dist 96 | 97 | # Gatsby files 98 | .cache/ 99 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 100 | # https://nextjs.org/blog/next-9-1#public-directory-support 101 | # public 102 | 103 | # vuepress build output 104 | .vuepress/dist 105 | 106 | # Serverless directories 107 | .serverless/ 108 | 109 | # FuseBox cache 110 | .fusebox/ 111 | 112 | # DynamoDB Local files 113 | .dynamodb/ 114 | 115 | # TernJS port file 116 | .tern-port 117 | 118 | # Sublime Text project/workspace files 119 | *.sublime-project 120 | *.sublime-workspace 121 | 122 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | deploy/ 2 | test/ 3 | scripts/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Wonderland GmbH 4 | Copyright (c) 2021 Jonathan Hale 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Wonderland Engine Logo 6 | 7 | 8 | # Wonderland Engine API 9 | 10 | The bindings between Wonderland Engine's WebAssembly runtime and custom JavaScript 11 | or TypeScript components. 12 | 13 | Learn more about Wonderland Engine at [https://wonderlandengine.com](https://wonderlandengine.com). 14 | 15 | > 💡 The Wonderland Engine Runtime is compatible on all patch versions of the API, but the 16 | > major and minor versions are required to match. 17 | > 18 | > **Example:** You will be able to use Wonderland Editor 1.0.4 with API 19 | > version 1.0.0 or 1.0.9 for example, but not with API 1.1.0. 💡 20 | 21 | ## Usage 22 | 23 | Wonderland Engine projects usually come with this package pre-installed. 24 | Install via `npm` or `yarn`: 25 | 26 | ```sh 27 | npm i --save @wonderlandengine/api 28 | # or: 29 | yarn add @wonderlandengine/api 30 | ``` 31 | 32 | To update the API to the latest version use 33 | ``` 34 | npm i --save @wonderlandengine/api@latest 35 | ``` 36 | 37 | ### Writing a Custom Component 38 | 39 | **JavaScript** 40 | 41 | ```js 42 | import {Component, Property} from '@wonderlandengine/api'; 43 | 44 | class Forward extends Component { 45 | static TypeName = 'forward'; 46 | static Properties = { 47 | speed: Property.float(1.5) 48 | }; 49 | 50 | _forward = new Float32Array(3); 51 | 52 | update(dt) { 53 | this.object.getForward(this._forward); 54 | this._forward[0] *= this.speed*dt; 55 | this._forward[1] *= this.speed*dt; 56 | this._forward[2] *= this.speed*dt; 57 | this.object.translateLocal(this._forward); 58 | } 59 | } 60 | ``` 61 | 62 | **TypeScript** 63 | 64 | ```ts 65 | import {Component} from '@wonderlandengine/api'; 66 | import {Component} from '@wonderlandengine/api/decorators.js'; 67 | 68 | class Forward extends Component { 69 | static TypeName = 'forward'; 70 | 71 | @property.float(1.5) 72 | speed!: number; 73 | 74 | private _forward = new Float32Array(3); 75 | 76 | update(dt) { 77 | this.object.getForward(this._forward); 78 | this._forward[0] *= this.speed*dt; 79 | this._forward[1] *= this.speed*dt; 80 | this._forward[2] *= this.speed*dt; 81 | this.object.translateLocal(this._forward); 82 | } 83 | } 84 | ``` 85 | 86 | For more information, please refer to the [JavaScript Quick Start Guide](https://wonderlandengine.com/getting-started/quick-start-js). 87 | 88 | ### For Library Maintainers 89 | 90 | To ensure the user of your library can use a range of API versions with your library, 91 | use `"peerDependencies"` in your `package.json`: 92 | 93 | ```json 94 | "peerDependencies": { 95 | "@wonderlandengine/api": ">= 1.0.0 < 2" 96 | }, 97 | ``` 98 | 99 | Which signals that your package works with any API version `>= 1.0.0` 100 | (choose the lowest version that provides all features you need) until `2.0.0`. 101 | 102 | Also see the [Writing JavaScript Libraries Tutorial](https://wonderlandengine.com/tutorials/writing-js-library/). 103 | 104 | ## Contributing 105 | 106 | * [API code](./packages/api): The `@wonderlandengine/api` source code 107 | * [End2End tests](./packages/test-e2e): User-land testing for the `@wonderlandengine/api` package 108 | 109 | ### Installation 110 | 111 | Make sure to install dependencies first: 112 | 113 | ```sh 114 | npm i 115 | ``` 116 | 117 | ### Build 118 | 119 | To build the TypeScript code, use one of: 120 | 121 | ```sh 122 | cd api 123 | npm run build 124 | npm run build:watch 125 | ``` 126 | 127 | ### End-to-End Test 128 | 129 | For information about how to run the end-to-end tests, have a look at the 130 | `packages/test-e2e` [README.md](./test/README.md) 131 | 132 | ## License 133 | 134 | Wonderland Engine API TypeScript and JavaScript code is released under MIT license. 135 | The runtime and editor are licensed under the [Wonderland Engine EULA](https://wonderlandengine.com/eula). 136 | -------------------------------------------------------------------------------- /img/wle-logo-horizontal-reversed-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/img/wle-logo-horizontal-reversed-dark.png -------------------------------------------------------------------------------- /img/wle-logo-horizontal-reversed-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/img/wle-logo-horizontal-reversed-light.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wonderlandengine/api-workspace", 3 | "version": "1.0.0", 4 | "description": "Workspace for the @wonderland/api package", 5 | "author": "Wonderland GmbH", 6 | "license": "MIT", 7 | "private": true, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/WonderlandEngine/api.git" 11 | }, 12 | "keywords": [ 13 | "webxr", 14 | "wonderland", 15 | "components" 16 | ], 17 | "workspaces": [ 18 | "packages/api", 19 | "packages/test-e2e" 20 | ], 21 | "prettier": "@wonderlandengine/prettier-config", 22 | "scripts": { 23 | "build": "npm run build --workspace=packages/api", 24 | "test": "npm run test -ws", 25 | "test:e2e": "npm run build --workspace=packages/api && npm run test --workspace=packages/test-e2e --", 26 | "pretty": "prettier --write \"packages/**/src/**/*.ts\" \"packages/**/test*/**/*.ts\"", 27 | "pretty:check": "prettier --check \"packages/**/src/**/*.ts\" \"packages/**/test*/**/*.ts\"" 28 | }, 29 | "devDependencies": { 30 | "@wonderlandengine/prettier-config": "^1.0.0", 31 | "prettier": "^2.8.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/api/.gitignore: -------------------------------------------------------------------------------- 1 | # Auto-generated 2 | src/version.ts 3 | -------------------------------------------------------------------------------- /packages/api/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Wonderland GmbH 4 | Copyright (c) 2021 Jonathan Hale 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /packages/api/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Wonderland Engine Logo 6 | 7 | 8 | # Wonderland Engine API 9 | 10 | The bindings between Wonderland Engine's WebAssembly runtime and custom JavaScript 11 | or TypeScript components. 12 | 13 | Learn more about Wonderland Engine at [https://wonderlandengine.com](https://wonderlandengine.com). 14 | 15 | > 💡 The Wonderland Engine Runtime is compatible on all patch versions of the API, but the 16 | > major and minor versions are required to match. 17 | > 18 | > **Example:** You will be able to use Wonderland Editor 1.0.4 with API 19 | > version 1.0.0 or 1.0.9 for example, but not with API 1.1.0. 💡 20 | 21 | ## Usage 22 | 23 | Wonderland Engine projects usually come with this package pre-installed. 24 | Install via `npm` or `yarn`: 25 | 26 | ```sh 27 | npm i --save @wonderlandengine/api 28 | # or: 29 | yarn add @wonderlandengine/api 30 | ``` 31 | 32 | To update the API to the latest version use 33 | ``` 34 | npm i --save @wonderlandengine/api@latest 35 | ``` 36 | 37 | ### Writing a Custom Component 38 | 39 | **JavaScript** 40 | 41 | ```js 42 | import {Component, Property} from '@wonderlandengine/api'; 43 | 44 | class Forward extends Component { 45 | static TypeName = 'forward'; 46 | static Properties = { 47 | speed: Property.float(1.5) 48 | }; 49 | 50 | _forward = new Float32Array(3); 51 | 52 | update(dt) { 53 | this.object.getForward(this._forward); 54 | this._forward[0] *= this.speed*dt; 55 | this._forward[1] *= this.speed*dt; 56 | this._forward[2] *= this.speed*dt; 57 | this.object.translateLocal(this._forward); 58 | } 59 | } 60 | ``` 61 | 62 | **TypeScript** 63 | 64 | ```ts 65 | import {Component} from '@wonderlandengine/api'; 66 | import {Component} from '@wonderlandengine/api/decorators.js'; 67 | 68 | class Forward extends Component { 69 | static TypeName = 'forward'; 70 | 71 | @property.float(1.5) 72 | speed!: number; 73 | 74 | private _forward = new Float32Array(3); 75 | 76 | update(dt) { 77 | this.object.getForward(this._forward); 78 | this._forward[0] *= this.speed*dt; 79 | this._forward[1] *= this.speed*dt; 80 | this._forward[2] *= this.speed*dt; 81 | this.object.translateLocal(this._forward); 82 | } 83 | } 84 | ``` 85 | 86 | For more information, please refer to the [JavaScript Quick Start Guide](https://wonderlandengine.com/getting-started/quick-start-js). 87 | 88 | ### For Library Maintainers 89 | 90 | To ensure the user of your library can use a range of API versions with your library, 91 | use `"peerDependencies"` in your `package.json`: 92 | 93 | ```json 94 | "peerDependencies": { 95 | "@wonderlandengine/api": ">= 1.0.0 < 2" 96 | }, 97 | ``` 98 | 99 | Which signals that your package works with any API version `>= 1.0.0` 100 | (choose the lowest version that provides all features you need) until `2.0.0`. 101 | 102 | Also see the [Writing JavaScript Libraries Tutorial](https://wonderlandengine.com/tutorials/writing-js-library/). 103 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wonderlandengine/api", 3 | "version": "1.2.4", 4 | "description": "Wonderland Engine's JavaScript API.", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "type": "module", 9 | "exports": { 10 | ".": "./dist/index.js", 11 | "./version.js": "./dist/index.js", 12 | "./decorators.js": "./dist/index.js", 13 | "./utils/*": "./dist/index.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/WonderlandEngine/api.git" 18 | }, 19 | "keywords": [ 20 | "webxr", 21 | "wonderland", 22 | "components" 23 | ], 24 | "author": "Wonderland GmbH", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/WonderlandEngine/api/issues" 28 | }, 29 | "homepage": "https://github.com/WonderlandEngine/api#readme", 30 | "scripts": { 31 | "build": "tsc --emitDeclarationOnly && npm run build:bundle", 32 | "build:bundle": "esbuild src/index.ts --minify-whitespace --minify-syntax --bundle --format=esm --tsconfig=\"tsconfig.json\" --outfile=\"dist/index.js\" --sourcemap=linked", 33 | "build:watch": "tsc --watch", 34 | "prebuild": "node ./scripts/pre-build.mjs", 35 | "pretty": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 36 | "pretty:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"", 37 | "prepare": "npm run build", 38 | "doc": "typedoc --entryPoints ./src/index.ts --tsconfig tsconfig.json --json ./doc.json --treatWarningsAsErrors" 39 | }, 40 | "files": [ 41 | "dist/**/*.d.ts", 42 | "dist/**/*.js", 43 | "dist/**/*.js.map" 44 | ], 45 | "dependencies": { 46 | "@types/webxr": "^0.5.0", 47 | "wasm-feature-detect": "^1.3.0" 48 | }, 49 | "devDependencies": { 50 | "esbuild": "^0.21.5", 51 | "typedoc": "^0.23.21", 52 | "typescript": "^4.9.3" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/api/scripts/pre-build.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import {readFile, writeFile} from 'node:fs/promises'; 4 | import {dirname, join} from 'node:path'; 5 | import {fileURLToPath} from 'node:url'; 6 | 7 | /* Remove experimental warning */ 8 | const emit = process.emit; 9 | process.emit = function (name, data) { 10 | if (name === 'warning') { 11 | const msg = typeof data === 'string' ? data : data.name; 12 | if (msg?.includes('ExperimentalWarning')) { 13 | return false; 14 | } 15 | } 16 | return emit.apply(process, arguments); 17 | }; 18 | 19 | /** This script's absolute path. */ 20 | const scriptPath = dirname(fileURLToPath(import.meta.url)); 21 | 22 | async function write(path, content) { 23 | try { 24 | await writeFile(path, content); 25 | } catch (e) { 26 | console.error(`Failed to write ${path}, reason:`, e); 27 | return false; 28 | } 29 | console.log(`[BUILD]: '${path}' overwritten`); 30 | return true; 31 | } 32 | 33 | const promises = []; /* All script promises to wait for before terminating */ 34 | 35 | /* Write package version into a TypeScript file. 36 | * The API can them use the version information at runtime. */ 37 | 38 | /* Matches regexp of the form: major.minor.patch[-rc.tag] */ 39 | const pkg = JSON.parse(await readFile(join(scriptPath, '../package.json'))); 40 | 41 | const version = pkg.version; 42 | const matches = version.match(/([0-9]+).([0-9]+).([0-9]+)(?:-rc.([0-9]+))?/); 43 | if (!matches || matches.length < 4) { 44 | console.error(`Invalid version '${input}'. Expected: major.minor.patch[-rc.x]`); 45 | process.exit(1); 46 | } 47 | 48 | const result = { 49 | major: Number.parseInt(matches[1]), 50 | minor: Number.parseInt(matches[2]), 51 | patch: Number.parseInt(matches[3]), 52 | rc: matches[4] !== undefined ? Number.parseInt(matches[4]) : 0 53 | }; 54 | 55 | promises.push(write(join(scriptPath, '../src/version.ts'),`/** 56 | * This file is automatically generated. 57 | * 58 | * **Do not** modify this file directly, but instead update 59 | * the 'write-version.mjs' script. 60 | */ 61 | 62 | /** 63 | * Version type following a subset of the Semantic Versioning specification. 64 | */ 65 | export type Version = {major: number, minor: number, patch: number, rc: number}; 66 | 67 | /** Version of this API. */ 68 | export const APIVersion: Version = { 69 | major: ${result.major}, 70 | minor: ${result.minor}, 71 | patch: ${result.patch}, 72 | rc: ${result.rc} 73 | }; 74 | `)); 75 | 76 | const failed = (await Promise.all(promises)).includes(false); 77 | process.exit(failed ? 1 : 0); 78 | -------------------------------------------------------------------------------- /packages/api/src/component.ts: -------------------------------------------------------------------------------- 1 | import {LogTag} from './index.js'; 2 | import {Prefab} from './prefab.js'; 3 | import { 4 | AnimationComponent, 5 | BrokenComponent, 6 | CollisionComponent, 7 | Component, 8 | ComponentConstructor, 9 | DestroyedComponentInstance, 10 | InputComponent, 11 | LightComponent, 12 | MeshComponent, 13 | PhysXComponent, 14 | TextComponent, 15 | ViewComponent, 16 | } from './wonderland.js'; 17 | 18 | /** 19 | * Manage all component managers in a scene. 20 | * 21 | * @hidden 22 | */ 23 | export class ComponentManagers { 24 | /** Animation manager index. */ 25 | readonly animation: number = -1; 26 | /** Collision manager index. */ 27 | readonly collision: number = -1; 28 | /** JavaScript manager index. */ 29 | readonly js: number = -1; 30 | /** Physx manager index. */ 31 | readonly physx: number = -1; 32 | /** View manager index. */ 33 | readonly view: number = -1; 34 | 35 | /** 36 | * Component class instances per type to avoid GC. 37 | * 38 | * @note Maps the manager index to the list of components. 39 | * 40 | * @todo: Refactor ResourceManager and re-use for components. 41 | */ 42 | private readonly _cache: (Component | null)[][] = []; 43 | /** Manager index to component class. */ 44 | private readonly _constructors: ComponentConstructor[]; 45 | /* Manager name to the manager index. */ 46 | private readonly _nativeManagers: Map = new Map(); 47 | 48 | /** Host instance. */ 49 | private readonly _scene: Prefab; 50 | 51 | constructor(scene: Prefab) { 52 | this._scene = scene; 53 | const wasm = this._scene.engine.wasm; 54 | 55 | const native = [ 56 | AnimationComponent, 57 | CollisionComponent, 58 | InputComponent, 59 | LightComponent, 60 | MeshComponent, 61 | PhysXComponent, 62 | TextComponent, 63 | ViewComponent, 64 | ]; 65 | this._cache = new Array(native.length); 66 | this._constructors = new Array(native.length); 67 | 68 | for (const Class of native) { 69 | const ptr = wasm.tempUTF8(Class.TypeName); 70 | const manager = wasm._wl_scene_get_component_manager_index(scene._index, ptr); 71 | this._constructors; 72 | this._constructors[manager] = Class; 73 | this._cache[manager] = [] as Component[]; 74 | this._nativeManagers.set(Class.TypeName, manager); 75 | } 76 | 77 | this.animation = this._nativeManagers.get(AnimationComponent.TypeName)!; 78 | this.collision = this._nativeManagers.get(CollisionComponent.TypeName)!; 79 | this.physx = this._nativeManagers.get(PhysXComponent.TypeName)!; 80 | this.view = this._nativeManagers.get(ViewComponent.TypeName)!; 81 | 82 | const ptr = wasm.tempUTF8('js'); 83 | this.js = wasm._wl_scene_get_component_manager_index(scene._index, ptr); 84 | this._cache[this.js] = [] as Component[]; 85 | } 86 | 87 | createJs(index: number, id: number, type: number, object: number) { 88 | const wasm = this._scene.engine.wasm; 89 | const ctor = wasm._componentTypes[type]; 90 | if (!ctor) { 91 | throw new Error(`Type index ${type} isn't registered`); 92 | } 93 | 94 | const log = this._scene.engine.log; 95 | 96 | let component = null; 97 | try { 98 | component = new ctor(this._scene, this.js, id); 99 | } catch (e) { 100 | log.error( 101 | LogTag.Component, 102 | `Exception during instantiation of component ${ctor.TypeName}` 103 | ); 104 | log.error(LogTag.Component, e); 105 | component = new BrokenComponent(this._scene); 106 | } 107 | component._object = this._scene.wrap(object); 108 | 109 | try { 110 | component.resetProperties(); 111 | } catch (e) { 112 | log.error( 113 | LogTag.Component, 114 | `Exception during ${component.type} resetProperties() on object ${component.object.name}` 115 | ); 116 | log.error(LogTag.Component, e); 117 | } 118 | 119 | this._scene._jsComponents[index] = component; 120 | 121 | /* Add to cache. This is required because destruction is 122 | * ID-based and not index-based. */ 123 | this._cache[this.js][id] = component; 124 | 125 | return component; 126 | } 127 | 128 | /** 129 | * Retrieve a cached component. 130 | * 131 | * @param manager The manager index. 132 | * @param id The component id. 133 | * @returns The component if cached, `null` otherwise. 134 | */ 135 | get(manager: number, id: number) { 136 | return this._cache[manager][id] ?? null; 137 | } 138 | 139 | /** 140 | * Wrap the animation. 141 | * 142 | * @param id Id to wrap. 143 | * @returns The previous instance if it was cached, or a new one. 144 | */ 145 | wrapAnimation(id: number): AnimationComponent { 146 | return this.wrapNative(this.animation, id) as AnimationComponent; 147 | } 148 | 149 | /** 150 | * Wrap the collision. 151 | * 152 | * @param id Id to wrap. 153 | * @returns The previous instance if it was cached, or a new one. 154 | */ 155 | wrapCollision(id: number): CollisionComponent { 156 | return this.wrapNative(this.collision, id) as CollisionComponent; 157 | } 158 | 159 | /** 160 | * Wrap the view. 161 | * 162 | * @param id Id to wrap. 163 | * @returns The previous instance if it was cached, or a new one. 164 | */ 165 | wrapView(id: number): ViewComponent { 166 | return this.wrapNative(this.view, id) as ViewComponent; 167 | } 168 | 169 | /** 170 | * Wrap the physx. 171 | * 172 | * @param id Id to wrap. 173 | * @returns The previous instance if it was cached, or a new one. 174 | */ 175 | wrapPhysx(id: number): PhysXComponent { 176 | return this.wrapNative(this.physx, id) as PhysXComponent; 177 | } 178 | 179 | /** 180 | * Retrieves a component instance if it exists, or create and cache 181 | * a new one. 182 | * 183 | * @note This api is meant to be used internally. Please have a look at 184 | * {@link Object3D.addComponent} instead. 185 | * 186 | * @param componentType Component manager index 187 | * @param componentId Component id in the manager 188 | * 189 | * @returns JavaScript instance wrapping the native component 190 | */ 191 | wrapNative(manager: number, id: number) { 192 | if (id < 0) return null; 193 | 194 | const cache = this._cache[manager]; 195 | if (cache[id]) return cache[id]; 196 | 197 | const scene = this._scene; 198 | const Class = this._constructors[manager]; 199 | const component = new Class(scene, manager, id); 200 | cache[id] = component; 201 | return component; 202 | } 203 | 204 | /** 205 | * Wrap a native or js component. 206 | * 207 | * @throws For JavaScript components that weren't previously cached, 208 | * since that would be a bug in the runtime / api. 209 | * 210 | * @param manager The manager index. 211 | * @param id The id to wrap. 212 | * @returns The previous instance if it was cached, or a new one. 213 | */ 214 | wrapAny(manager: number, id: number) { 215 | if (id < 0) return null; 216 | 217 | if (manager === this.js) { 218 | const found = this._cache[this.js][id]; 219 | if (!found) { 220 | throw new Error('JS components must always be cached'); 221 | } 222 | return found.constructor !== BrokenComponent ? found : null; 223 | } 224 | 225 | return this.wrapNative(manager, id); 226 | } 227 | 228 | getNativeManager(name: string): number | null { 229 | const manager = this._nativeManagers.get(name); 230 | return manager !== undefined ? manager : null; 231 | } 232 | 233 | /** 234 | * Perform cleanup upon component destruction. 235 | * 236 | * @param instance The instance to destroy. 237 | * 238 | * @hidden 239 | */ 240 | destroy(instance: Component) { 241 | const localId = instance._localId; 242 | const manager = instance._manager; 243 | (instance._id as number) = -1; 244 | (instance._localId as number) = -1; 245 | (instance._manager as number) = -1; 246 | 247 | const erasePrototypeOnDestroy = this._scene.engine.erasePrototypeOnDestroy; 248 | /* Destroy the prototype of this instance to avoid using a dangling component */ 249 | if (erasePrototypeOnDestroy && instance) { 250 | Object.setPrototypeOf(instance, DestroyedComponentInstance); 251 | } 252 | 253 | /* Remove from the cache to avoid side-effects when 254 | * re-creating a component with the same id. */ 255 | this._cache[manager][localId] = null; 256 | } 257 | 258 | /** Number of managers, including the JavaScript manager. */ 259 | get managersCount() { 260 | /* +1 to account for the JavaScript manager */ 261 | return this._nativeManagers.size + 1; 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /packages/api/src/decorators.ts: -------------------------------------------------------------------------------- 1 | import {Component, ComponentConstructor} from './wonderland.js'; 2 | import {Property, PropertyArgs, PropertyKeys, ComponentProperty, Type} from './property.js'; 3 | 4 | /** 5 | * Decorator for JS component properties. 6 | * 7 | * @param data The property description as an object literal 8 | * @returns A decorator function modifying the `Properties` static 9 | * attribute 10 | */ 11 | function propertyDecorator(data: ComponentProperty) { 12 | return function (target: Component, propertyKey: string): void { 13 | const ctor = target.constructor as ComponentConstructor; 14 | ctor.Properties = ctor.hasOwnProperty('Properties') ? ctor.Properties : {}; 15 | ctor.Properties[propertyKey] = data; 16 | }; 17 | } 18 | 19 | /** 20 | * Decorator for making a getter enumerable. 21 | * 22 | * Usage: 23 | * 24 | * ```ts 25 | * class MyClass { 26 | * @enumerable() 27 | * get projectionMatrix(): Float32Array { ... } 28 | * } 29 | * ``` 30 | */ 31 | export function enumerable() { 32 | return function (_: any, __: string, descriptor: PropertyDescriptor) { 33 | descriptor.enumerable = true; 34 | }; 35 | } 36 | 37 | /** 38 | * Decorator for native properties. 39 | * 40 | * Usage: 41 | * 42 | * ```ts 43 | * class MyClass { 44 | * @nativeProperty() 45 | * get projectionMatrix(): Float32Array { ... } 46 | * } 47 | * ``` 48 | */ 49 | export function nativeProperty() { 50 | return function ( 51 | target: Component, 52 | propertyKey: string, 53 | descriptor: PropertyDescriptor 54 | ) { 55 | enumerable()(target, propertyKey, descriptor); 56 | propertyDecorator({type: Type.Native})(target, propertyKey); 57 | }; 58 | } 59 | 60 | /** 61 | * Property decorators namespace. 62 | * 63 | * You can use the decorators to mark a class attribute as 64 | * a Wonderland Engine property. 65 | * 66 | * Usage: 67 | * 68 | * ```ts 69 | * import {Mesh} from '@wonderlandengine/api'; 70 | * import {property} from '@wonderlandengine/api/decorators.js'; 71 | * 72 | * class MyComponent extends Component { 73 | * @property.bool(true) 74 | * myBool!: boolean; 75 | * 76 | * @property.int(42) 77 | * myInt!: number; 78 | * 79 | * @property.string('Hello World!') 80 | * myString!: string; 81 | * 82 | * @property.mesh() 83 | * myMesh!: Mesh; 84 | * } 85 | * ``` 86 | * 87 | * For JavaScript users, please declare the properties statically. 88 | */ 89 | export const property = {} as { 90 | [key in PropertyKeys]: ( 91 | ...args: PropertyArgs 92 | ) => ReturnType; 93 | }; 94 | 95 | for (const name in Property) { 96 | /* Assign each property functor to a TypeScript decorator. 97 | * This code extracts parameters and return type to provide proper 98 | * typings to the user. */ 99 | property[name as PropertyKeys] = (...args: PropertyArgs) => { 100 | const functor = Property[name as PropertyKeys] as ( 101 | ...args: unknown[] 102 | ) => ComponentProperty; 103 | return propertyDecorator(functor(...args)); 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /packages/api/src/global.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Emscripten exports. 3 | */ 4 | 5 | interface Window { 6 | instantiateWonderlandRuntime?: ( 7 | module?: Record 8 | ) => Promise>; 9 | _WL?: { 10 | runtimes: Record< 11 | string, 12 | (module?: Record) => Promise> 13 | >; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /packages/api/src/property.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Component property type. 3 | */ 4 | export enum Type { 5 | /** 6 | * **Native** 7 | * 8 | * Property of a native component. Must not be used in custom components. 9 | * 10 | * @hidden 11 | */ 12 | Native = 0, 13 | 14 | /** 15 | * **Bool**: 16 | * 17 | * Appears in the editor as a checkbox. 18 | * 19 | * Initial value is `false`, unless overridden by the `default` property. 20 | */ 21 | Bool = 1, 22 | 23 | /** 24 | * **Int**: 25 | * 26 | * Appears in the editor as an integer input field. 27 | * 28 | * Initial value is `0`, unless overridden by the `default` property. 29 | */ 30 | Int = 2, 31 | 32 | /** 33 | * **Float**: 34 | * 35 | * Appears in the editor as a floating point input field. 36 | * 37 | * Initial value is `0.0`, unless overridden by the `default` property. 38 | */ 39 | Float = 3, 40 | 41 | /** 42 | * **String / Text**: 43 | * 44 | * Appears in the editor as a single-line text input field. 45 | * 46 | * Initial value is an empty string, unless overridden by the `default` 47 | * property. 48 | */ 49 | String = 4, 50 | 51 | /** 52 | * **Enumeration**: 53 | * 54 | * Appears in the editor as a dropdown with given values. The additional 55 | * `values` parameter with selection options is mandatory. 56 | * 57 | * The property value is resolved to an **index** into the `values` array. 58 | * 59 | * Initial value is the first element in `values`, unless overridden by 60 | * the `default` property. The `default` value can be a string or an index 61 | * into `values`. 62 | * 63 | * @example 64 | * 65 | * ```js 66 | * camera: {type: Type.Enum, values: ['auto', 'back', 'front'], default: 'auto'}, 67 | * ``` 68 | */ 69 | Enum = 5, 70 | 71 | /** 72 | * **Object reference**: 73 | * 74 | * Appears in the editor as an object resource selection dropdown 75 | * with object picker. 76 | * 77 | * Initial value is `null`. 78 | */ 79 | Object = 6, 80 | 81 | /** 82 | * **Mesh reference**: 83 | * 84 | * Appears in the editor as a mesh resource selection dropdown. 85 | * 86 | * Initial value is `null`. 87 | */ 88 | Mesh = 7, 89 | 90 | /** 91 | * **Texture reference**: 92 | * 93 | * Appears in the editor as a texture resource selection dropdown. 94 | * 95 | * Initial value is `null`. 96 | */ 97 | Texture = 8, 98 | 99 | /** 100 | * **Material reference**: 101 | * 102 | * Appears in the editor as a material resource selection dropdown. 103 | * 104 | * Initial value is `null`. 105 | */ 106 | Material = 9, 107 | 108 | /** 109 | * **Animation reference**: 110 | * 111 | * Appears in the editor as an animation resource selection dropdown. 112 | * 113 | * Initial value is `null`. 114 | */ 115 | Animation = 10, 116 | 117 | /** 118 | * **Skin reference**: 119 | * 120 | * Appears in the editor as a skin resource selection dropdown. 121 | * 122 | * Initial value is `null`. 123 | */ 124 | Skin = 11, 125 | 126 | /** 127 | * **Color**: 128 | * 129 | * Appears in the editor as a color widget. 130 | * 131 | * Initial value is `[0.0, 0.0, 0.0, 1.0]`, unless overridden by the 132 | * `default` property. 133 | */ 134 | Color = 12, 135 | 136 | /** 137 | * **Vector of two floats**: 138 | * 139 | * Appears in the editor as a two-element floating point input field. 140 | * 141 | * Initial value is `[0.0, 0.0]`, unless overridden by the 142 | * `default` property. 143 | */ 144 | Vector2 = 13, 145 | 146 | /** 147 | * **Vector of three floats**: 148 | * 149 | * Appears in the editor as a three-element floating point input field. 150 | * 151 | * Initial value is `[0.0, 0.0, 0.0]`, unless overridden by the 152 | * `default` property. 153 | */ 154 | Vector3 = 14, 155 | 156 | /** 157 | * **Vector of four floats**: 158 | * 159 | * Appears in the editor as a four-element floating point input field. 160 | * 161 | * Initial value is `[0.0, 0.0, 0.0, 0.0]`, unless overridden by the 162 | * `default` property. 163 | */ 164 | Vector4 = 15, 165 | } 166 | 167 | /** 168 | * Cloning interface for component properties. 169 | * 170 | * Used for component initialization and cloning. 171 | */ 172 | export interface PropertyCloner { 173 | /** 174 | * Clone a property value. 175 | * @param type Type of the property. 176 | * @param value Value of the property. 177 | * @returns The cloned value. 178 | */ 179 | clone(type: Type, value: any): any; 180 | } 181 | 182 | /** 183 | * Default cloner implementation. 184 | * 185 | * Clones array-like properties and leaves all other types unchanged. 186 | */ 187 | export class DefaultPropertyCloner implements PropertyCloner { 188 | clone(type: Type, value: any): any { 189 | switch (type) { 190 | case Type.Color: 191 | case Type.Vector2: 192 | case Type.Vector3: 193 | case Type.Vector4: 194 | return value.slice(); 195 | default: 196 | return value; 197 | } 198 | } 199 | } 200 | 201 | /** Default cloner for property values. */ 202 | export const defaultPropertyCloner = new DefaultPropertyCloner(); 203 | 204 | /** 205 | * Custom component property. 206 | * 207 | * For more information about component properties, have a look 208 | * at the {@link Component.Properties} attribute. 209 | */ 210 | export interface ComponentProperty { 211 | /** Property type. */ 212 | type: Type; 213 | /** Default value, depending on type. */ 214 | default?: any; 215 | /** Values for {@link Type.Enum} */ 216 | values?: string[]; 217 | required?: boolean; 218 | /** 219 | * Cloner for the property. 220 | * 221 | * If not defined, falls back to {@link defaultPropertyCloner}. To prevent 222 | * any cloning, set a custom cloner that passes the original value back 223 | * from {@link PropertyCloner.clone}. */ 224 | cloner?: PropertyCloner; 225 | } 226 | 227 | /** 228 | * Component property namespace. 229 | * 230 | * Usage: 231 | * 232 | * ```js 233 | * import {Component, Property} from '@wonderlandengine/api'; 234 | * 235 | * class MyComponent extends Component { 236 | * static Properties = { 237 | * myBool: Property.bool(true), 238 | * myInt: Property.int(42), 239 | * myString: Property.string('Hello World!'), 240 | * myMesh: Property.mesh(), 241 | * } 242 | * } 243 | * ``` 244 | * 245 | * For TypeScript users, you can use the decorators instead. 246 | */ 247 | export const Property = { 248 | /** 249 | * Create an boolean property. 250 | * 251 | * @param defaultValue The default value. If not provided, defaults to `false`. 252 | */ 253 | bool(defaultValue: boolean = false): ComponentProperty { 254 | return {type: Type.Bool, default: defaultValue}; 255 | }, 256 | 257 | /** 258 | * Create an integer property. 259 | * 260 | * @param defaultValue The default value. If not provided, defaults to `0`. 261 | */ 262 | int(defaultValue: number = 0): ComponentProperty { 263 | return {type: Type.Int, default: defaultValue}; 264 | }, 265 | 266 | /** 267 | * Create an float property. 268 | * 269 | * @param defaultValue The default value. If not provided, defaults to `0.0`. 270 | */ 271 | float(defaultValue: number = 0.0): ComponentProperty { 272 | return {type: Type.Float, default: defaultValue}; 273 | }, 274 | 275 | /** 276 | * Create an string property. 277 | * 278 | * @param defaultValue The default value. If not provided, defaults to `''`. 279 | */ 280 | string(defaultValue = ''): ComponentProperty { 281 | return {type: Type.String, default: defaultValue}; 282 | }, 283 | 284 | /** 285 | * Create an enumeration property. 286 | * 287 | * @param values The list of values. 288 | * @param defaultValue The default value. Can be a string or an index into 289 | * `values`. If not provided, defaults to the first element. 290 | */ 291 | enum(values: string[], defaultValue?: string | number): ComponentProperty { 292 | return {type: Type.Enum, values, default: defaultValue}; 293 | }, 294 | 295 | /** Create an {@link Object3D} reference property. */ 296 | object(opts?: PropertyReferenceOptions): ComponentProperty { 297 | return {type: Type.Object, default: null, required: opts?.required ?? false}; 298 | }, 299 | 300 | /** Create a {@link Mesh} reference property. */ 301 | mesh(opts?: PropertyReferenceOptions): ComponentProperty { 302 | return {type: Type.Mesh, default: null, required: opts?.required ?? false}; 303 | }, 304 | 305 | /** Create a {@link Texture} reference property. */ 306 | texture(opts?: PropertyReferenceOptions): ComponentProperty { 307 | return {type: Type.Texture, default: null, required: opts?.required ?? false}; 308 | }, 309 | 310 | /** Create a {@link Material} reference property. */ 311 | material(opts?: PropertyReferenceOptions): ComponentProperty { 312 | return {type: Type.Material, default: null, required: opts?.required ?? false}; 313 | }, 314 | 315 | /** Create an {@link Animation} reference property. */ 316 | animation(opts?: PropertyReferenceOptions): ComponentProperty { 317 | return {type: Type.Animation, default: null, required: opts?.required ?? false}; 318 | }, 319 | 320 | /** Create a {@link Skin} reference property. */ 321 | skin(opts?: PropertyReferenceOptions): ComponentProperty { 322 | return {type: Type.Skin, default: null, required: opts?.required ?? false}; 323 | }, 324 | 325 | /** 326 | * Create a color property. 327 | * 328 | * @param r The red component, in the range [0; 1]. 329 | * @param g The green component, in the range [0; 1]. 330 | * @param b The blue component, in the range [0; 1]. 331 | * @param a The alpha component, in the range [0; 1]. 332 | */ 333 | color(r = 0.0, g = 0.0, b = 0.0, a = 1.0): ComponentProperty { 334 | return {type: Type.Color, default: [r, g, b, a]}; 335 | }, 336 | 337 | /** 338 | * Create a two-element vector property. 339 | * 340 | * @param x The x component. 341 | * @param y The y component. 342 | */ 343 | vector2(x = 0.0, y = 0.0): ComponentProperty { 344 | return {type: Type.Vector2, default: [x, y]}; 345 | }, 346 | 347 | /** 348 | * Create a three-element vector property. 349 | * 350 | * @param x The x component. 351 | * @param y The y component. 352 | * @param z The z component. 353 | */ 354 | vector3(x = 0.0, y = 0.0, z = 0.0): ComponentProperty { 355 | return {type: Type.Vector3, default: [x, y, z]}; 356 | }, 357 | 358 | /** 359 | * Create a four-element vector property. 360 | * 361 | * @param x The x component. 362 | * @param y The y component. 363 | * @param z The z component. 364 | * @param w The w component. 365 | */ 366 | vector4(x = 0.0, y = 0.0, z = 0.0, w = 0.0): ComponentProperty { 367 | return {type: Type.Vector4, default: [x, y, z, w]}; 368 | }, 369 | }; 370 | 371 | /** 372 | * Options to create a reference property, i.e., 373 | * object, mesh, animation, skin, etc... 374 | */ 375 | export interface PropertyReferenceOptions { 376 | /** If `true`, the component will throw if the property isn't initialized. */ 377 | required?: boolean; 378 | } 379 | 380 | /** All the keys that exists on the {@link Property} object. */ 381 | export type PropertyKeys = keyof typeof Property; 382 | 383 | /** Retrieve all the argument types of a {@link Property} function. */ 384 | export type PropertyArgs = Parameters<(typeof Property)[key]>; 385 | -------------------------------------------------------------------------------- /packages/api/src/resources/mesh-manager.ts: -------------------------------------------------------------------------------- 1 | import {WonderlandEngine} from '../index.js'; 2 | import {Mesh, MeshIndexType, MeshParameters, MeshSkinningType} from '../wonderland.js'; 3 | 4 | import {ResourceManager} from './resource.js'; 5 | 6 | /** 7 | * Manage meshes. 8 | * 9 | * #### Creation 10 | * 11 | * Creating a mesh is done using {@link MeshManager.create}: 12 | * 13 | * ```js 14 | * const mesh = engine.meshes.create({vertexCount: 3, indexData: [0, 1, 2]}); 15 | * ``` 16 | * 17 | * @since 1.2.0 18 | */ 19 | export class MeshManager extends ResourceManager { 20 | constructor(engine: WonderlandEngine) { 21 | super(engine, Mesh); 22 | } 23 | 24 | /** 25 | * Create a new mesh. 26 | * 27 | * @param params Vertex and index data. For more information, have a look 28 | * at the {@link MeshParameters} object. 29 | */ 30 | create(params: Partial) { 31 | if (!params.vertexCount) throw new Error("Missing parameter 'vertexCount'"); 32 | 33 | const wasm = this.engine.wasm; 34 | 35 | let indexData = 0; 36 | let indexType = 0; 37 | let indexDataSize = 0; 38 | if (params.indexData) { 39 | indexType = params.indexType || MeshIndexType.UnsignedShort; 40 | indexDataSize = params.indexData.length * indexType; 41 | indexData = wasm._malloc(indexDataSize); 42 | /* Copy the index data into wasm memory */ 43 | switch (indexType) { 44 | case MeshIndexType.UnsignedByte: 45 | wasm.HEAPU8.set(params.indexData, indexData); 46 | break; 47 | case MeshIndexType.UnsignedShort: 48 | wasm.HEAPU16.set(params.indexData, indexData >> 1); 49 | break; 50 | case MeshIndexType.UnsignedInt: 51 | wasm.HEAPU32.set(params.indexData, indexData >> 2); 52 | break; 53 | } 54 | } 55 | 56 | const {skinningType = MeshSkinningType.None} = params; 57 | 58 | const index = wasm._wl_mesh_create( 59 | indexData, 60 | indexDataSize, 61 | indexType, 62 | params.vertexCount, 63 | skinningType 64 | ); 65 | const instance = new Mesh(this._host, index); 66 | this._cache[instance.index] = instance; 67 | return instance; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/api/src/resources/resource.ts: -------------------------------------------------------------------------------- 1 | import {WonderlandEngine} from '../engine.js'; 2 | import {Prefab} from '../prefab.js'; 3 | import {Scene} from '../scene.js'; 4 | import {FirstConstructorParam} from '../types.js'; 5 | 6 | /** Interface for a resource class */ 7 | type ResourceConstructor = { 8 | new (host: FirstConstructorParam, index: number): T; 9 | }; 10 | 11 | /** 12 | * Create a proxy throwing destroyed errors upon access. 13 | * 14 | * @param type The type to display upon error 15 | * @returns The proxy instance 16 | */ 17 | function createDestroyedProxy( 18 | host: FirstConstructorParam, 19 | type: ResourceConstructor 20 | ) { 21 | return new Proxy( 22 | {}, 23 | { 24 | get(_, param: string) { 25 | if (param === 'isDestroyed') return true; 26 | throw new Error( 27 | `Cannot read '${param}' of destroyed '${type.name}' resource from ${host}` 28 | ); 29 | }, 30 | set(_, param: string) { 31 | throw new Error( 32 | `Cannot write '${param}' of destroyed '${type.name}' resource from ${host}` 33 | ); 34 | }, 35 | } 36 | ); 37 | } 38 | 39 | /** 40 | * Base class for engine resources, such as: 41 | * - {@link Texture} 42 | * - {@link Mesh} 43 | * - {@link Material} 44 | * 45 | * @since 1.2.0 46 | */ 47 | export abstract class Resource { 48 | /** Relative index in the host. @hidden */ 49 | readonly _index: number = -1; 50 | /** For compatibility with SceneResource. @hidden */ 51 | readonly _id: number = -1; 52 | /** @hidden */ 53 | private readonly _engine: WonderlandEngine; 54 | 55 | constructor(engine: WonderlandEngine, index: number) { 56 | this._engine = engine; 57 | this._index = index; 58 | this._id = index; 59 | } 60 | 61 | /** Hosting engine instance. */ 62 | get engine() { 63 | return this._engine; 64 | } 65 | 66 | /** Index of this resource in the {@link Scene}'s manager. */ 67 | get index() { 68 | return this._index; 69 | } 70 | 71 | /** 72 | * Checks equality by comparing ids and **not** the JavaScript reference. 73 | * 74 | * @deprecated Use JavaScript reference comparison instead: 75 | * 76 | * ```js 77 | * const meshA = engine.meshes.create({vertexCount: 1}); 78 | * const meshB = engine.meshes.create({vertexCount: 1}); 79 | * const meshC = meshB; 80 | * console.log(meshA === meshB); // false 81 | * console.log(meshA === meshA); // true 82 | * console.log(meshB === meshC); // true 83 | * ``` 84 | */ 85 | equals(other: this | undefined | null): boolean { 86 | if (!other) return false; 87 | return this._index === other._index; 88 | } 89 | 90 | /** 91 | * `true` if the object is destroyed, `false` otherwise. 92 | * 93 | * If {@link WonderlandEngine.erasePrototypeOnDestroy} is `true`, 94 | * reading a class attribute / method will throw. 95 | */ 96 | get isDestroyed() { 97 | return this._index <= 0; 98 | } 99 | } 100 | 101 | /** 102 | * Base class for scene resources, such as: 103 | * * - {@link Texture} 104 | * - {@link Mesh} 105 | * - {@link Material} 106 | * - {@link Skin} 107 | * - {@link Animation} 108 | * 109 | * @since 1.2.0 110 | */ 111 | export abstract class SceneResource { 112 | /** @hidden */ 113 | static _pack(scene: number, index: number) { 114 | return (scene << 22) | index; 115 | } 116 | 117 | /** Relative index in the host. @hidden */ 118 | readonly _index: number = -1; 119 | /** For compatibility with SceneResource. @hidden */ 120 | readonly _id: number = -1; 121 | /** @hidden */ 122 | protected readonly _scene: Prefab; 123 | 124 | constructor(scene: Prefab, index: number) { 125 | this._scene = scene; 126 | this._index = index; 127 | this._id = SceneResource._pack(scene._index, index); 128 | } 129 | 130 | /** 131 | * Checks equality by comparing ids and **not** the JavaScript reference. 132 | * 133 | * @deprecated Use JavaScript reference comparison instead: 134 | * 135 | * ```js 136 | * const meshA = engine.meshes.create({vertexCount: 1}); 137 | * const meshB = engine.meshes.create({vertexCount: 1}); 138 | * const meshC = meshB; 139 | * console.log(meshA === meshB); // false 140 | * console.log(meshA === meshA); // true 141 | * console.log(meshB === meshC); // true 142 | * ``` 143 | */ 144 | equals(other: this | undefined | null): boolean { 145 | if (!other) return false; 146 | return this._id === other._id; 147 | } 148 | 149 | /** Hosting instance. */ 150 | get scene() { 151 | return this._scene; 152 | } 153 | 154 | /** Hosting engine instance. */ 155 | get engine() { 156 | return this._scene.engine; 157 | } 158 | 159 | /** Index of this resource in the {@link Scene}'s manager. */ 160 | get index() { 161 | return this._index; 162 | } 163 | 164 | /** 165 | * `true` if the object is destroyed, `false` otherwise. 166 | * 167 | * If {@link WonderlandEngine.erasePrototypeOnDestroy} is `true`, 168 | * reading a class attribute / method will throw. 169 | */ 170 | get isDestroyed() { 171 | return this._id <= 0; 172 | } 173 | } 174 | 175 | /** 176 | * Manager for resources. 177 | * 178 | * Resources are accessed via the engine they belong to. 179 | * 180 | * @see {@link WonderlandEngine.textures}, {@link WonderlandEngine.meshes}, 181 | * and {@link WonderlandEngine.materials}. 182 | * 183 | * @since 1.2.0 184 | */ 185 | export class ResourceManager { 186 | /** @hidden */ 187 | protected readonly _host: FirstConstructorParam; 188 | /** Cache. @hidden */ 189 | protected readonly _cache: (T | null)[] = []; 190 | 191 | /** Resource class. @hidden */ 192 | private readonly _template: ResourceConstructor; 193 | 194 | /** Destructor proxy, used if {@link WonderlandEngine.erasePrototypeOnDestroy} is `true`. @hidden */ 195 | private _destructor: {} | null = null; 196 | 197 | private readonly _engine: WonderlandEngine; 198 | 199 | /** 200 | * Create a new manager 201 | * 202 | * @param host The host containing the managed resources. 203 | * @param Class The class to instantiate when wrapping an index. 204 | * 205 | * @hidden 206 | */ 207 | constructor(host: FirstConstructorParam, Class: ResourceConstructor) { 208 | this._host = host; 209 | this._template = Class; 210 | this._engine = (host as Prefab).engine ?? host; 211 | } 212 | 213 | /** 214 | * Wrap the index into a resource instance. 215 | * 216 | * @note The index is relative to the host, i.e., doesn't pack the host index (if any). 217 | * 218 | * @param index The resource index. 219 | * @returns 220 | */ 221 | wrap(index: number) { 222 | if (index <= 0) return null; 223 | const texture = 224 | this._cache[index] ?? 225 | (this._cache[index] = new this._template(this._host, index)); 226 | return texture; 227 | } 228 | 229 | /** 230 | * Retrieve the resource at the given index. 231 | * 232 | * @note The index is relative to the host, i.e., doesn't pack the host index. 233 | */ 234 | get(index: number): T | null { 235 | return this._cache[index] ?? null; 236 | } 237 | 238 | /** Number of textures allocated in the manager. */ 239 | get allocatedCount() { 240 | return this._cache.length; 241 | } 242 | 243 | /** 244 | * Number of textures in the manager. 245 | * 246 | * @note For performance reasons, avoid calling this method when possible. 247 | */ 248 | get count() { 249 | let count = 0; 250 | for (const res of this._cache) { 251 | if (res && res.index >= 0) ++count; 252 | } 253 | return count; 254 | } 255 | 256 | /** Hosting engine instance. */ 257 | get engine() { 258 | return this._engine; 259 | } 260 | 261 | /** 262 | * Destroy the instance. 263 | * 264 | * @note This method takes care of the prototype destruction. 265 | * 266 | * @hidden 267 | */ 268 | _destroy(instance: T) { 269 | const index = instance.index; 270 | (instance._index as number) = -1; 271 | (instance._id as number) = -1; 272 | this._cache[index] = null; 273 | 274 | if (!this.engine.erasePrototypeOnDestroy) return; 275 | 276 | if (!this._destructor) 277 | this._destructor = createDestroyedProxy(this._host, this._template); 278 | Object.setPrototypeOf(instance, this._destructor); 279 | } 280 | 281 | /** 282 | * Mark all instances as destroyed. 283 | * 284 | * @hidden 285 | */ 286 | _clear() { 287 | if (!this.engine.erasePrototypeOnDestroy) return; 288 | for (let i = 0; i < this._cache.length; ++i) { 289 | const instance = this._cache[i]; 290 | if (instance) this._destroy(instance); 291 | } 292 | this._cache.length = 0; 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /packages/api/src/resources/texture-manager.ts: -------------------------------------------------------------------------------- 1 | import {WonderlandEngine} from '../index.js'; 2 | import {ImageLike} from '../types.js'; 3 | import {Texture} from '../wonderland.js'; 4 | 5 | import {ResourceManager, SceneResource} from './resource.js'; 6 | 7 | /** 8 | * Manage textures. 9 | * 10 | * #### Creation 11 | * 12 | * Creating a texture is done using {@link TextureManager.load}: 13 | * 14 | * ```js 15 | * const texture = await engine.textures.load('my-image.png'); 16 | * ``` 17 | * 18 | * Alternatively, textures can be created directly via a loaded image using 19 | * {@link TextureManager.create}. 20 | * 21 | * @since 1.2.0 22 | */ 23 | export class TextureManager extends ResourceManager { 24 | constructor(engine: WonderlandEngine) { 25 | super(engine, Texture); 26 | } 27 | 28 | /** 29 | * Create a new texture from an image or video. 30 | * 31 | * #### Usage 32 | * 33 | * ```js 34 | * const img = new Image(); 35 | * img.load = function(img) { 36 | * const texture = engine.textures.create(img); 37 | * }; 38 | * img.src = 'my-image.png'; 39 | * ``` 40 | * 41 | * @note The media must already be loaded. To automatically 42 | * load the media and create a texture, use {@link TextureManager.load} instead. 43 | * 44 | * @param image Media element to create the texture from. 45 | * @ret\urns The new texture with the media content. 46 | */ 47 | create(image: ImageLike): Texture { 48 | const wasm = this.engine.wasm; 49 | 50 | const jsImageIndex = wasm._images.length; 51 | wasm._images.push(image); 52 | 53 | if (image instanceof HTMLImageElement && !image.complete) { 54 | throw new Error('image must be ready to create a texture'); 55 | } 56 | 57 | const width = (image as HTMLVideoElement).videoWidth ?? image.width; 58 | const height = (image as HTMLVideoElement).videoHeight ?? image.height; 59 | 60 | const imageIndex = wasm._wl_image_create(jsImageIndex, width, height); 61 | 62 | /* Required because the image isn't a resource by itself, but will eventually be one. */ 63 | const index = wasm._wl_texture_create(imageIndex); 64 | 65 | const instance = new Texture(this.engine, index); 66 | this._cache[instance.index] = instance; 67 | return instance; 68 | } 69 | 70 | /** 71 | * Load an image from URL as {@link Texture}. 72 | * 73 | * #### Usage 74 | * 75 | * ```js 76 | * const texture = await engine.textures.load('my-image.png'); 77 | * ``` 78 | * 79 | * @param filename URL to load from. 80 | * @param crossOrigin Cross origin flag for the image object. 81 | * @returns Loaded texture. 82 | */ 83 | load(filename: string, crossOrigin?: string): Promise { 84 | let image = new Image(); 85 | image.crossOrigin = crossOrigin ?? image.crossOrigin; 86 | image.src = filename; 87 | return new Promise((resolve, reject) => { 88 | image.onload = () => { 89 | resolve(this.create(image)); 90 | }; 91 | image.onerror = function () { 92 | reject('Failed to load image. Not found or no read access'); 93 | }; 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /packages/api/src/scene-gltf.ts: -------------------------------------------------------------------------------- 1 | import {WonderlandEngine} from './index.js'; 2 | import {InstantiateGltfResult, Scene} from './scene.js'; 3 | import {Prefab} from './prefab.js'; 4 | import {Object3D} from './wonderland.js'; 5 | 6 | /** GLTF-specific loading options. */ 7 | export type GLTFOptions = { 8 | /** If `true`, extensions will be parsed. */ 9 | extensions?: boolean; 10 | }; 11 | 12 | /** 13 | * Extension data obtained from glTF files. 14 | */ 15 | export interface GLTFExtensionsInstance { 16 | /** 17 | * Mesh extension objects. Key is {@link Object3D.objectId}, value is JSON 18 | * data indexed by extension name. 19 | */ 20 | mesh: Record>>; 21 | /** 22 | * Node extension objects. Key is {@link Object3D.objectId}, value is JSON 23 | * data indexed by extension name. 24 | */ 25 | node: Record>>; 26 | /** Remapping from glTF node index to {@link Object3D.objectId}. */ 27 | idMapping: number[]; 28 | } 29 | 30 | export class GLTFExtensions { 31 | objectCount: number; 32 | /** glTF root extensions object. JSON data indexed by extension name. */ 33 | root: Record = {}; 34 | /** 35 | * Mesh extension objects. Key is the gltf index, value is JSON 36 | * data indexed by extension name. 37 | */ 38 | mesh: Record> = {}; 39 | /** 40 | * Node extension objects. Key is a glTF index, value is JSON 41 | * data indexed by extension name. 42 | */ 43 | node: Record> = {}; 44 | 45 | constructor(count: number) { 46 | this.objectCount = count; 47 | } 48 | } 49 | 50 | /** 51 | * glTF scene. 52 | * 53 | * At the opposite of {@link Scene}, glTF scenes can be instantiated 54 | * in other scenes but can't: 55 | * - Be activated 56 | * - Be the destination of an instantiation 57 | * 58 | * #### Usage 59 | * 60 | * ```js 61 | * const prefab = await engine.loadGLTF('Zombie.glb'); 62 | * 63 | * const scene = engine.scene; 64 | * for (let i = 0; i < 100; ++i) { 65 | * scene.instantiate(prefab); 66 | * } 67 | * ``` 68 | * 69 | * Since this class inherits from {@link Prefab}, you can use the shared 70 | * API to modify the glTF before an instantiation: 71 | * 72 | * ```js 73 | * const prefab = await engine.loadGLTF('Zombie.glb'); 74 | * const zombie = prefab.findByName('Zombie')[0]; 75 | * 76 | * // The mesh is too small, we scale the root 77 | * zombie.setScalingWorld([2, 2, 2]); 78 | * // Add a custom js 'health' component to the root 79 | * zombie.addComponent('health', {value: 100}); 80 | * 81 | * // 'Zombie' is wrapped in a new root added during instantiation 82 | * const {root} = engine.scene.instantiate(prefab); 83 | * const instanceZombie = root.children[0]; 84 | * console.log(instanceZombie.getScalingWorld()); // Prints '[2, 2, 2]' 85 | * ``` 86 | * 87 | * @category scene 88 | * @since 1.2.0 89 | */ 90 | export class PrefabGLTF extends Prefab { 91 | /** 92 | * Raw extensions read from the glTF file. 93 | * 94 | * The extensions will be mapped to the hierarchy upon instantiation. 95 | * For more information, have a look at the {@link InstantiateGltfResult} type. 96 | * 97 | * @note The glTF must be loaded with `extensions` enabled. If not, this 98 | * field will be set to `null`. For more information, have a look at the 99 | * {@link GLTFOptions} type. 100 | */ 101 | extensions: GLTFExtensions | null = null; 102 | 103 | /** 104 | * @note This api is meant to be used internally. 105 | * 106 | * @hidden 107 | */ 108 | constructor(engine: WonderlandEngine, index: number) { 109 | super(engine, index); 110 | this.extensions = this._readExtensions(); 111 | } 112 | 113 | /** 114 | * Instantiate the glTF extensions on an active sub scene graph. 115 | * 116 | * @param id The root object id. 117 | * @param result The instantiation object result. 118 | * 119 | * @hidden 120 | */ 121 | _processInstantiaton(dest: Prefab, root: Object3D, result: InstantiateGltfResult) { 122 | if (!this.extensions) return null; 123 | 124 | const wasm = this.engine.wasm; 125 | 126 | const count = this.extensions.objectCount; 127 | const idMapping: number[] = new Array(count); 128 | 129 | /** @todo: We need some check to ensure that the gltf layout didn't change to retarget extensions. 130 | * At least a simple scene graph size check should be required to avoid a segfault. */ 131 | 132 | const activeRootIndex = wasm._wl_object_index(root._id); 133 | for (let i = 0; i < count; ++i) { 134 | const mappedId = wasm._wl_glTF_scene_extensions_gltfIndex_to_id( 135 | this._index, 136 | dest._index, 137 | activeRootIndex, 138 | i 139 | ); 140 | idMapping[i] = mappedId; 141 | } 142 | 143 | const remapped: GLTFExtensionsInstance = { 144 | mesh: {}, 145 | node: {}, 146 | idMapping, 147 | }; 148 | 149 | for (const gltfIndex in this.extensions.mesh) { 150 | const id = idMapping[gltfIndex]; 151 | remapped.mesh[id] = this.extensions.mesh[gltfIndex]; 152 | } 153 | for (const gltfIndex in this.extensions.node) { 154 | const id = idMapping[gltfIndex]; 155 | remapped.node[id] = this.extensions.node[gltfIndex]; 156 | } 157 | 158 | result.extensions = remapped; 159 | } 160 | 161 | /** 162 | * Unmarshalls gltf extensions. 163 | * 164 | * @hidden 165 | */ 166 | private _readExtensions() { 167 | const wasm = this.engine.wasm; 168 | 169 | const ptr = wasm._wl_glTF_scene_get_extensions(this._index); 170 | if (!ptr) return null; 171 | 172 | let index = ptr / 4; 173 | const data = wasm.HEAPU32; 174 | const readString = () => { 175 | const strPtr = data[index++]; 176 | const strLen = data[index++]; 177 | return wasm.UTF8ViewToString(strPtr, strPtr + strLen); 178 | }; 179 | 180 | const objectCount = data[index++]; 181 | const extensions = new GLTFExtensions(objectCount); 182 | 183 | const meshExtensionsSize = data[index++]; 184 | for (let i = 0; i < meshExtensionsSize; ++i) { 185 | const objectId = data[index++]; 186 | extensions.mesh[objectId] = JSON.parse(readString()); 187 | } 188 | const nodeExtensionsSize = data[index++]; 189 | for (let i = 0; i < nodeExtensionsSize; ++i) { 190 | const objectId = data[index++]; 191 | extensions.node[objectId] = JSON.parse(readString()); 192 | } 193 | const rootExtensionsStr = readString(); 194 | if (rootExtensionsStr) { 195 | extensions.root = JSON.parse(rootExtensionsStr); 196 | } 197 | 198 | return extensions; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /packages/api/src/types.ts: -------------------------------------------------------------------------------- 1 | /** Element that can be used as an image in the engine. */ 2 | export type ImageLike = HTMLImageElement | HTMLVideoElement | HTMLCanvasElement; 3 | 4 | /** 5 | * A type alias for any TypedArray constructor, except big-int arrays. 6 | */ 7 | export type TypedArrayCtor = 8 | | Int8ArrayConstructor 9 | | Uint8ArrayConstructor 10 | | Uint8ClampedArrayConstructor 11 | | Int16ArrayConstructor 12 | | Uint16ArrayConstructor 13 | | Int32ArrayConstructor 14 | | Uint32ArrayConstructor 15 | | Float32ArrayConstructor 16 | | Float64ArrayConstructor; 17 | 18 | /** 19 | * Typed array instance based on a given {@link TypedArrayCtor} constructor. 20 | * 21 | * @typeParam T - The TypedArray constructor. 22 | */ 23 | export type TypedArray = InstanceType; 24 | 25 | /** 26 | * Represents any object that can be used as an array for read / write. 27 | */ 28 | export interface NumberArray { 29 | length: number; 30 | 31 | [n: number]: number; 32 | } 33 | 34 | /** 35 | * Type to describe a constructor. 36 | */ 37 | export type Constructor = { 38 | new (...args: any[]): T; 39 | }; 40 | 41 | export type FirstConstructorParam = ConstructorParameters>[0]; 42 | 43 | /** Progress callback used when fetching data. */ 44 | export type ProgressCallback = (current: number, total: number) => void; 45 | -------------------------------------------------------------------------------- /packages/api/src/utils/bitset.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Assert that the given bit index is less than 32. 3 | * 4 | * @param bit The bit to test. 5 | */ 6 | function assert(bit: number): void { 7 | if (bit >= 32) { 8 | throw new Error(`BitSet.enable(): Value ${bit} is over 31`); 9 | } 10 | } 11 | 12 | /** 13 | * Stores a bit pattern to quickly test if an index is enabled / disabled. 14 | * 15 | * This class can store up to **32** different values in the range [0; 31]. 16 | * 17 | * #### Usage 18 | * 19 | * ```js 20 | * const bitset = new BitSet(); 21 | * bitset.enable(10); // Enable bit at index `10`. 22 | * console.log(bitset.enabled(10)); // Prints 'true'. 23 | * ``` 24 | * 25 | * #### TypeScript 26 | * 27 | * The set can be typed over an enum: 28 | * 29 | * ```ts 30 | * enum Tag { 31 | * First = 0, 32 | * Second = 1, 33 | * } 34 | * 35 | * const bitset = new BitSet(); 36 | * bitset.enable(Tag.First); 37 | * ``` 38 | */ 39 | export class BitSet { 40 | /** Enabled bits. @hidden */ 41 | private _bits: number = 0; 42 | 43 | /** 44 | * Enable the bit at the given index. 45 | * 46 | * @param bits A spread of all the bits to enable. 47 | * @returns Reference to self (for method chaining). 48 | */ 49 | enable(...bits: T[]) { 50 | for (const bit of bits) { 51 | assert(bit); 52 | /* Casts the result to an unsigned integer */ 53 | this._bits |= (1 << bit) >>> 0; 54 | } 55 | return this; 56 | } 57 | 58 | /** 59 | * Enable all bits. 60 | * 61 | * @returns Reference to self (for method chaining). 62 | */ 63 | enableAll() { 64 | this._bits = ~0; 65 | return this; 66 | } 67 | 68 | /** 69 | * Disable the bit at the given index. 70 | * 71 | * @param bits A spread of all the bits to disable. 72 | * @returns Reference to self (for method chaining). 73 | */ 74 | disable(...bits: T[]) { 75 | for (const bit of bits) { 76 | assert(bit); 77 | /* Casts the result to an unsigned integer */ 78 | this._bits &= ~((1 << bit) >>> 0); 79 | } 80 | return this; 81 | } 82 | 83 | /** 84 | * Disable all bits. 85 | * 86 | * @returns Reference to self (for method chaining). 87 | */ 88 | disableAll() { 89 | this._bits = 0; 90 | return this; 91 | } 92 | 93 | /** 94 | * Checker whether the bit is set or not. 95 | * 96 | * @param bit The bit to check. 97 | * @returns `true` if it's enabled, `false` otherwise. 98 | */ 99 | enabled(bit: T) { 100 | return !!(this._bits & ((1 << bit) >>> 0)); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /packages/api/src/utils/fetch.ts: -------------------------------------------------------------------------------- 1 | import {ImageLike, ProgressCallback} from '../types.js'; 2 | 3 | /** 4 | * Transformer for {@link TransformStream} that passes read progress to a 5 | * callback. 6 | * 7 | * Invokes the callback for each streamed chunk, and one final time when the 8 | * stream closes. 9 | * 10 | * @hidden 11 | */ 12 | class FetchProgressTransformer implements Transformer { 13 | #progress = 0; 14 | #callback: ProgressCallback; 15 | #totalSize: number; 16 | 17 | /** 18 | * Constructor. 19 | * @param callback Callback that receives the progress. 20 | * @param totalSize Total size of the data. Pass 0 to indicate that the 21 | * size is unknown, then the callback will only be called once after 22 | * all data was transferred. 23 | */ 24 | constructor(callback: ProgressCallback, totalSize = 0) { 25 | this.#callback = callback; 26 | this.#totalSize = totalSize; 27 | } 28 | 29 | transform(chunk: Uint8Array, controller: TransformStreamDefaultController) { 30 | controller.enqueue(chunk); 31 | this.#progress += chunk.length; 32 | if (this.#totalSize > 0) { 33 | this.#callback(this.#progress, this.#totalSize); 34 | } 35 | } 36 | 37 | flush() { 38 | this.#callback(this.#progress, this.#progress); 39 | } 40 | } 41 | 42 | /** 43 | * Sink for `WritableStream` that writes data to an `ArrayBuffer`. 44 | * 45 | * @hidden 46 | */ 47 | export class ArrayBufferSink implements UnderlyingSink { 48 | #buffer: Uint8Array; 49 | #offset = 0; 50 | 51 | /** 52 | * Constructor. 53 | * @param size Initial size of the buffer. If less than the received data, 54 | * the buffer is dynamically reallocated. 55 | */ 56 | constructor(size = 0) { 57 | this.#buffer = new Uint8Array(size); 58 | } 59 | 60 | /** Get the received data as an `ArrayBuffer`. */ 61 | get arrayBuffer() { 62 | const arrayBuffer = this.#buffer.buffer; 63 | if (this.#offset < arrayBuffer.byteLength) { 64 | return arrayBuffer.slice(0, this.#offset); 65 | } 66 | return arrayBuffer; 67 | } 68 | 69 | write(chunk: Uint8Array) { 70 | const newLength = this.#offset + chunk.length; 71 | if (newLength > this.#buffer.length) { 72 | const newBuffer = new Uint8Array( 73 | Math.max(this.#buffer.length * 1.5, newLength) 74 | ); 75 | newBuffer.set(this.#buffer); 76 | this.#buffer = newBuffer; 77 | } 78 | this.#buffer.set(chunk, this.#offset); 79 | this.#offset = newLength; 80 | } 81 | } 82 | 83 | /** 84 | * Source for `ReadableStream` that reads data from an`ArrayBuffer`. 85 | * 86 | * @hidden 87 | */ 88 | export class ArrayBufferSource implements UnderlyingSource { 89 | #buffer: ArrayBuffer; 90 | 91 | /** 92 | * Constructor. 93 | * @param buffer Buffer to read from. 94 | */ 95 | constructor(buffer: ArrayBuffer) { 96 | this.#buffer = buffer; 97 | } 98 | 99 | start(controller: ReadableStreamController) { 100 | if (this.#buffer.byteLength > 0) { 101 | controller.enqueue(new Uint8Array(this.#buffer)); 102 | } 103 | controller.close(); 104 | } 105 | } 106 | 107 | /** 108 | * Fetch a file as an `ArrayBuffer`, with fetch progress passed to a callback. 109 | * 110 | * @param path Path of the file to fetch. 111 | * @param onProgress Callback receiving the current fetch progress and total 112 | * size, in bytes. Also called a final time on completion. 113 | * @param signal Abort signal passed to `fetch()`. 114 | * @returns Promise that resolves when the fetch successfully completes. 115 | */ 116 | export async function fetchWithProgress( 117 | path: string, 118 | onProgress?: ProgressCallback, 119 | signal?: AbortSignal 120 | ): Promise { 121 | const res = await fetch(path, {signal}); 122 | if (!res.ok) throw res.statusText; 123 | if (!onProgress || !res.body) return res.arrayBuffer(); 124 | let size = Number(res.headers.get('Content-Length') ?? 0); 125 | if (Number.isNaN(size)) size = 0; 126 | const sink = new ArrayBufferSink(size); 127 | await res.body 128 | .pipeThrough(new TransformStream(new FetchProgressTransformer(onProgress, size))) 129 | .pipeTo(new WritableStream(sink)); 130 | return sink.arrayBuffer; 131 | } 132 | 133 | /** 134 | * Fetch a file as a `ReadableStream`, with fetch progress passed to a 135 | * callback. 136 | * 137 | * @param path Path of the file to fetch. 138 | * @param onProgress Callback receiving the current fetch progress and total 139 | * size, in bytes. Also called a final time on completion. 140 | * @param signal Abort signal passed to `fetch()`. 141 | * @returns Promise that resolves when the fetch successfully completes. 142 | */ 143 | export async function fetchStreamWithProgress( 144 | path: string, 145 | onProgress?: ProgressCallback, 146 | signal?: AbortSignal 147 | ): Promise> { 148 | const res = await fetch(path, {signal}); 149 | if (!res.ok) throw res.statusText; 150 | const body = res.body ?? new ReadableStream(); 151 | let size = Number(res.headers.get('Content-Length') ?? 0); 152 | if (Number.isNaN(size)) size = 0; 153 | if (!onProgress) return body; 154 | const stream = body.pipeThrough( 155 | new TransformStream(new FetchProgressTransformer(onProgress, size)) 156 | ); 157 | return stream; 158 | } 159 | 160 | /** 161 | * Get parent path from a URL. 162 | * 163 | * @param url URL to get the parent from. 164 | * @returns Parent URL without trailing slash. 165 | */ 166 | export function getBaseUrl(url: string): string { 167 | return url.substring(0, url.lastIndexOf('/')); 168 | } 169 | 170 | /** 171 | * Get the filename of a url. 172 | * 173 | * @param url The url to extract the name from. 174 | * @returns A string containing the filename. If no filename is found, 175 | * returns the input string. 176 | */ 177 | export function getFilename(url: string): string { 178 | if (url.endsWith('/')) { 179 | /* Remove trailing slash. */ 180 | url = url.substring(0, url.lastIndexOf('/')); 181 | } 182 | const lastSlash = url.lastIndexOf('/'); 183 | if (lastSlash < 0) return url; 184 | return url.substring(lastSlash + 1); 185 | } 186 | 187 | /** 188 | * Promise resolved once the image is ready to be used 189 | * 190 | * @param image The image, video, or canvas to wait for. 191 | * @returns A promise with the image, once it's ready to be used. 192 | */ 193 | export function onImageReady(image: T): Promise { 194 | return new Promise((res, rej) => { 195 | if (image instanceof HTMLCanvasElement) { 196 | res(image); 197 | } else if (image instanceof HTMLVideoElement) { 198 | if (image.readyState >= 2) { 199 | res(image as T); 200 | return; 201 | } 202 | image.addEventListener( 203 | 'loadeddata', 204 | () => { 205 | if (image.readyState >= 2) res(image); 206 | }, 207 | {once: true} 208 | ); 209 | return; 210 | } else if ((image as HTMLImageElement).complete) { 211 | res(image); 212 | return; 213 | } 214 | image.addEventListener('load', () => res(image), {once: true}); 215 | image.addEventListener('error', rej, {once: true}); 216 | }); 217 | } 218 | -------------------------------------------------------------------------------- /packages/api/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bitset.js'; 2 | export * from './cbor.js'; 3 | export * from './event.js'; 4 | export * from './fetch.js'; 5 | export * from './logger.js'; 6 | export * from './misc.js'; 7 | export * from './object.js'; 8 | -------------------------------------------------------------------------------- /packages/api/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import {BitSet} from './bitset.js'; 2 | 3 | /** 4 | * Logging levels supported by {@link Logger}. 5 | */ 6 | export enum LogLevel { 7 | Info = 0, 8 | Warn = 1, 9 | Error = 2, 10 | } 11 | 12 | /** 13 | * Logging wrapper. 14 | * 15 | * This is used to allow turning on/off: 16 | * - `console.log` 17 | * - `console.warn` 18 | * - `console.error` 19 | * 20 | * #### Usage 21 | * 22 | * ```js 23 | * import {Logger, LogLevel, LogTag} from '@wonderlandengine/api'; 24 | * 25 | * // Create a logger with only the "error" and "warn" levels activated 26 | * const logger = new Logger(LogLevel.Warn, LogLevel.Error); 27 | * 28 | * // Only the "error" and "warn" levels are activated, 29 | * // this message isn't logged. 30 | * logger.info(LogTag.Component, 'This message is shushed') 31 | * 32 | * // Prints 'Hello Error!' 33 | * logger.error(LogTag.Component, 'Hello Error!'); 34 | * 35 | * // Prints 'Hello Warning!' 36 | * logger.warn(LogTag.Component, 'Hello Warning!'); 37 | * ``` 38 | * 39 | * The log levels can be changed at anytime using the {@link BitSet} api: 40 | * 41 | * ```js 42 | * // Enable the "info" level 43 | * logger.levels.enable(LogLevel.Info); 44 | * * // Disable the "warn" level 45 | * logger.levels.disable(LogLevel.Warn); 46 | * ``` 47 | * 48 | * #### Tags 49 | * 50 | * In addition, the logger supports tagging messages: 51 | * 52 | * ```js 53 | * import {Logger, LogLevel, LogTag} from '@wonderlandengine/api'; 54 | * 55 | * const logger = new Logger(LogLevel.Info); 56 | * 57 | * logger.tags.disableAll(); 58 | * 59 | * // All tags are off, this message isn't logged 60 | * logger.info(LogTag.Component, 'This message is shushed'); 61 | * 62 | * logger.tags.enable(LogTag.Component); 63 | * logger.info(LogTag.Component, 'Hello World!') // Prints 'Hello World!' 64 | * ``` 65 | * 66 | * The tagging system gives another layer of control to enable / disable 67 | * some specific logs. 68 | */ 69 | export class Logger { 70 | /** 71 | * Bitset of enabled levels. 72 | * 73 | * @hidden 74 | */ 75 | levels: BitSet = new BitSet(); 76 | 77 | /** 78 | * Bitset of enabled tags. 79 | * 80 | * @hidden 81 | */ 82 | tags: BitSet = new BitSet().enableAll(); 83 | 84 | /** 85 | * Create a new logger instance. 86 | * 87 | * @param levels Default set of levels to enable. 88 | */ 89 | constructor(...levels: LogLevel[]) { 90 | this.levels.enable(...levels); 91 | } 92 | 93 | /** 94 | * Log the message(s) using `console.log`. 95 | * 96 | * @param tag Tag represented by a positive integer. 97 | * @param msg A spread of message to log. 98 | * @returns Reference to self (for method chaining). 99 | */ 100 | info(tag: number, ...msg: unknown[]): this { 101 | if (this.levels.enabled(LogLevel.Info) && this.tags.enabled(tag)) { 102 | console.log(...msg); 103 | } 104 | return this; 105 | } 106 | 107 | /** 108 | * Log the message(s) using `console.warn`. 109 | * 110 | * @param tag Tag represented by a positive integer. 111 | * @param msg A spread of message to log. 112 | * @returns Reference to self (for method chaining). 113 | */ 114 | warn(tag: number, ...msg: unknown[]): this { 115 | if (this.levels.enabled(LogLevel.Warn) && this.tags.enabled(tag)) { 116 | console.warn(...msg); 117 | } 118 | return this; 119 | } 120 | 121 | /** 122 | * Log the message(s) using `console.error`. 123 | * 124 | * @param tag Tag represented by a positive integer. 125 | * @param msg A spread of message to log. 126 | * @returns Reference to self (for method chaining). 127 | */ 128 | error(tag: number, ...msg: unknown[]): this { 129 | if (this.levels.enabled(LogLevel.Error) && this.tags.enabled(tag)) { 130 | console.error(...msg); 131 | } 132 | return this; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /packages/api/src/utils/misc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Schedule a timeout, resolving in `time` milliseconds. 3 | * 4 | * @note `setTimeout` being a macro-task, this method can 5 | * be use as a debounce call. 6 | * 7 | * @param time The time until it resolves, in milliseconds. 8 | * @returns A promise resolving in `time` ms. 9 | */ 10 | export function timeout(time: number): Promise { 11 | return new Promise((res) => setTimeout(res, time)); 12 | } 13 | 14 | /** 15 | * Clamp the value in the range [min; max]. 16 | * 17 | * @param val The value to clamp. 18 | * @param min The minimum value (inclusive). 19 | * @param max The maximum value (inclusive). 20 | * @returns The clamped value. 21 | */ 22 | export function clamp(val: number, min: number, max: number): number { 23 | return Math.max(Math.min(max, val), min); 24 | } 25 | 26 | /** 27 | * Capitalize the first letter in a string. 28 | * 29 | * @note The string must be UTF-8. 30 | * 31 | * @param str The string to format. 32 | * @returns The string with the first letter capitalized. 33 | */ 34 | export function capitalizeFirstUTF8(str: string) { 35 | return `${str[0].toUpperCase()}${str.substring(1)}`; 36 | } 37 | 38 | /** 39 | * Create a proxy throwing destroyed errors upon access. 40 | * 41 | * @param type The type to display upon error 42 | * @returns The proxy instance 43 | */ 44 | export function createDestroyedProxy(type: string) { 45 | return new Proxy( 46 | {}, 47 | { 48 | get(_, param: string) { 49 | if (param === 'isDestroyed') return true; 50 | throw new Error(`Cannot read '${param}' of destroyed ${type}`); 51 | }, 52 | set(_, param: string) { 53 | throw new Error(`Cannot write '${param}' of destroyed ${type}`); 54 | }, 55 | } 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /packages/api/src/utils/object.ts: -------------------------------------------------------------------------------- 1 | import {ImageLike} from '../types.js'; 2 | 3 | /** 4 | * Check if a given value is a native string or a `String` instance. 5 | * 6 | * @param value The value to check. 7 | * @returns `true` if the `value` has type string literal or `String`, `false` otherwise. 8 | */ 9 | export function isString(value: any): value is string { 10 | if (value === '') return true; 11 | return value && (typeof value === 'string' || value.constructor === String); 12 | } 13 | 14 | /** 15 | * Check if a given value is a native number or a `Number` instance. 16 | * 17 | * @param value The value to check. 18 | * @returns `true` if the `value` has type number literal or `Number`, `false` otherwise. 19 | */ 20 | export function isNumber(value: any): value is number { 21 | if (value === null || value === undefined) return false; 22 | return typeof value === 'number' || value.constructor === Number; 23 | } 24 | 25 | /** 26 | * Check whether a given value is a visual media. 27 | * 28 | * @param value The value to check 29 | * @returns `true` if the `value` is an image, video, or canvas. 30 | */ 31 | export function isImageLike(value: any): value is ImageLike { 32 | return ( 33 | value instanceof HTMLImageElement || 34 | value instanceof HTMLVideoElement || 35 | value instanceof HTMLCanvasElement 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "nodenext", 5 | "moduleResolution": "nodenext", 6 | "outDir": "dist", 7 | "declaration": true, 8 | "newLine": "LF", 9 | "allowJs": false, 10 | "strict": true, 11 | "experimentalDecorators": true, 12 | }, 13 | 14 | "include": [ "./src/**/*.ts" ], 15 | "exclude": [ "node_modules" ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/test-e2e/.gitignore: -------------------------------------------------------------------------------- 1 | # Test projects generated resources 2 | resources/projects/ 3 | 4 | projects/**/package-lock.json 5 | -------------------------------------------------------------------------------- /packages/test-e2e/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Wonderland Engine Logo 6 | 7 | 8 | # End-to-End Testing 9 | 10 | ## Test Projects 11 | 12 | Some tests use projects as inputs, e.g., 13 | * `animation.tests.ts` 14 | * `component.tests.ts` 15 | * `mesh.tests.ts` 16 | * `scene.test.ts` 17 | * `scene-gltf.test.ts` 18 | 19 | Those projects are required to be built before the tests can run. 20 | 21 | First reach the `test` folder: 22 | 23 | ```sh 24 | cd ./test 25 | ``` 26 | 27 | You can then build all projects using: 28 | 29 | ```sh 30 | npm run build -- path/to/wonderlandeditor-binary 31 | ``` 32 | 33 | Each projects `deploy/` will be copied into `test/resources/projects`. 34 | 35 | ## Running 36 | 37 | To run all the end-to-end tests from the api root, use: 38 | 39 | ```sh 40 | npm run test:e2e -- --deploy path/to/wonderland/deploy 41 | ``` 42 | 43 | It's required to pass the Wonderland Editor deploy folder as a CLI argument using 44 | the `--deploy` argument. 45 | 46 | If no such flag is passed, the test framework will assume 47 | that the deploy folder is located in `../../deploy`. 48 | 49 | `npm run test:e2e` is a shortcut for: 50 | 51 | ```sh 52 | cd test-2e2 53 | npm run test 54 | ``` 55 | 56 | ## Test File 57 | 58 | To run the tests in a file, use: 59 | 60 | ```sh 61 | npm run test:e2e -- [PATH] --deploy path/to/wonderland/deploy 62 | ``` 63 | 64 | Example with the component file: 65 | 66 | ```sh 67 | npm run test:e2e -- component.test.ts --deploy path/to/wonderland/deploy 68 | ``` 69 | 70 | > Note: Since test:e2e forwards the npm script to the test package, 71 | > paths are relative to the test folder, and not to the api root. 72 | 73 | ### Watch 74 | 75 | It's possible to watch one/multiple test(s) using `--watch`: 76 | 77 | ```sh 78 | npm run test:e2e -- --deploy path/to/wonderland/deploy --watch 79 | ``` 80 | 81 | ### Grep 82 | 83 | The runner uses Mocha, which supports filtering tests using [regexp](https://mochajs.org/api/mocha#grep). 84 | 85 | You can provide a regexp using `--grep` or `-g`: 86 | 87 | ```sh 88 | npm run test:e2e -- -g 'MeshComponent' 89 | ``` 90 | 91 | The `grep` flag can be mixed with the positional file argument: 92 | 93 | ```sh 94 | npm run test:e2e -- component.test.ts -g 'MeshComponent' 95 | ``` 96 | 97 | This command will only run the tests matching the "MeshComponent" string in the `test/component.test.ts` file. 98 | 99 | ## License 100 | 101 | Wonderland Engine API TypeScript and JavaScript code is released under MIT license. 102 | The runtime and editor are licensed under the [Wonderland Engine EULA](https://wonderlandengine.com/eula) 103 | -------------------------------------------------------------------------------- /packages/test-e2e/cbor-test-vectors.ts: -------------------------------------------------------------------------------- 1 | type TestVector = { 2 | hex: string; 3 | tag?: number; 4 | decoded: any; 5 | }; 6 | 7 | /* Examples of encoded CBOR data items given in RFC 8949 Appendix A. Adapted 8 | * from https://github.com/cbor/test-vectors/blob/aba89b653e484bc8573c22f3ff35641d79dfd8c1/appendix_a.json */ 9 | export const CborTestVectors: TestVector[] = [ 10 | { 11 | hex: '00', 12 | decoded: 0, 13 | }, 14 | { 15 | hex: '01', 16 | decoded: 1, 17 | }, 18 | { 19 | hex: '0a', 20 | decoded: 10, 21 | }, 22 | { 23 | hex: '17', 24 | decoded: 23, 25 | }, 26 | { 27 | hex: '1818', 28 | decoded: 24, 29 | }, 30 | { 31 | hex: '1819', 32 | decoded: 25, 33 | }, 34 | { 35 | hex: '1864', 36 | decoded: 100, 37 | }, 38 | { 39 | hex: '1903e8', 40 | decoded: 1000, 41 | }, 42 | { 43 | hex: '1a000f4240', 44 | decoded: 1000000, 45 | }, 46 | { 47 | hex: '1b000000e8d4a51000', 48 | decoded: 1000000000000, 49 | }, 50 | { 51 | hex: '1bffffffffffffffff', 52 | decoded: 18446744073709551615n, 53 | }, 54 | { 55 | hex: 'c249010000000000000000', 56 | decoded: 18446744073709551616n, 57 | }, 58 | { 59 | hex: '3bffffffffffffffff', 60 | decoded: -18446744073709551616n, 61 | }, 62 | { 63 | hex: 'c349010000000000000000', 64 | decoded: -18446744073709551617n, 65 | }, 66 | { 67 | hex: '20', 68 | decoded: -1, 69 | }, 70 | { 71 | hex: '29', 72 | decoded: -10, 73 | }, 74 | { 75 | hex: '3863', 76 | decoded: -100, 77 | }, 78 | { 79 | hex: '3903e7', 80 | decoded: -1000, 81 | }, 82 | { 83 | hex: 'f90000', 84 | decoded: 0.0, 85 | }, 86 | { 87 | hex: 'f98000', 88 | decoded: -0.0, 89 | }, 90 | { 91 | hex: 'f93c00', 92 | decoded: 1.0, 93 | }, 94 | { 95 | hex: 'fb3ff199999999999a', 96 | decoded: 1.1, 97 | }, 98 | { 99 | hex: 'f93e00', 100 | decoded: 1.5, 101 | }, 102 | { 103 | hex: 'f97bff', 104 | decoded: 65504.0, 105 | }, 106 | { 107 | hex: 'fa47c35000', 108 | decoded: 100000.0, 109 | }, 110 | { 111 | hex: 'fa7f7fffff', 112 | decoded: 3.4028234663852886e38, 113 | }, 114 | { 115 | hex: 'fb7e37e43c8800759c', 116 | decoded: 1.0e300, 117 | }, 118 | { 119 | hex: 'f90001', 120 | decoded: 5.960464477539063e-8, 121 | }, 122 | { 123 | hex: 'f90400', 124 | decoded: 6.103515625e-5, 125 | }, 126 | { 127 | hex: 'f9c400', 128 | decoded: -4.0, 129 | }, 130 | { 131 | hex: 'fbc010666666666666', 132 | decoded: -4.1, 133 | }, 134 | { 135 | hex: 'f97c00', 136 | decoded: Infinity, 137 | }, 138 | { 139 | hex: 'f97e00', 140 | decoded: NaN, 141 | }, 142 | { 143 | hex: 'f9fc00', 144 | decoded: -Infinity, 145 | }, 146 | { 147 | hex: 'fa7f800000', 148 | decoded: Infinity, 149 | }, 150 | { 151 | hex: 'fa7fc00000', 152 | decoded: NaN, 153 | }, 154 | { 155 | hex: 'faff800000', 156 | decoded: -Infinity, 157 | }, 158 | { 159 | hex: 'fb7ff0000000000000', 160 | decoded: Infinity, 161 | }, 162 | { 163 | hex: 'fb7ff8000000000000', 164 | decoded: NaN, 165 | }, 166 | { 167 | hex: 'fbfff0000000000000', 168 | decoded: -Infinity, 169 | }, 170 | { 171 | hex: 'f4', 172 | decoded: false, 173 | }, 174 | { 175 | hex: 'f5', 176 | decoded: true, 177 | }, 178 | { 179 | hex: 'f6', 180 | decoded: null, 181 | }, 182 | { 183 | hex: 'f7', 184 | decoded: undefined, 185 | }, 186 | { 187 | hex: 'f0', 188 | decoded: 16, 189 | }, 190 | { 191 | hex: 'f818', 192 | decoded: 24, 193 | }, 194 | { 195 | hex: 'f8ff', 196 | decoded: 255, 197 | }, 198 | { 199 | hex: 'c074323031332d30332d32315432303a30343a30305a', 200 | tag: 0, 201 | decoded: '2013-03-21T20:04:00Z', 202 | }, 203 | { 204 | hex: 'c11a514b67b0', 205 | tag: 1, 206 | decoded: 1363896240, 207 | }, 208 | { 209 | hex: 'c1fb41d452d9ec200000', 210 | tag: 1, 211 | decoded: 1363896240.5, 212 | }, 213 | { 214 | hex: 'd74401020304', 215 | tag: 23, 216 | decoded: Uint8Array.from([0x01, 0x02, 0x03, 0x04]), 217 | }, 218 | { 219 | hex: 'd818456449455446', 220 | tag: 24, 221 | decoded: Uint8Array.from([0x64, 0x49, 0x45, 0x54, 0x46]), 222 | }, 223 | { 224 | hex: 'd82076687474703a2f2f7777772e6578616d706c652e636f6d', 225 | tag: 32, 226 | decoded: 'http://www.example.com', 227 | }, 228 | { 229 | hex: '40', 230 | decoded: Uint8Array.from([]), 231 | }, 232 | { 233 | hex: '4401020304', 234 | decoded: Uint8Array.from([0x01, 0x02, 0x03, 0x04]), 235 | }, 236 | { 237 | hex: '60', 238 | decoded: '', 239 | }, 240 | { 241 | hex: '6161', 242 | decoded: 'a', 243 | }, 244 | { 245 | hex: '6449455446', 246 | decoded: 'IETF', 247 | }, 248 | { 249 | hex: '62225c', 250 | decoded: '"\\', 251 | }, 252 | { 253 | hex: '62c3bc', 254 | decoded: 'ü', 255 | }, 256 | { 257 | hex: '63e6b0b4', 258 | decoded: '水', 259 | }, 260 | { 261 | hex: '64f0908591', 262 | decoded: '𐅑', 263 | }, 264 | { 265 | hex: '80', 266 | decoded: [], 267 | }, 268 | { 269 | hex: '83010203', 270 | decoded: [1, 2, 3], 271 | }, 272 | { 273 | hex: '8301820203820405', 274 | decoded: [1, [2, 3], [4, 5]], 275 | }, 276 | { 277 | hex: '98190102030405060708090a0b0c0d0e0f101112131415161718181819', 278 | decoded: [ 279 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 280 | 23, 24, 25, 281 | ], 282 | }, 283 | { 284 | hex: 'a0', 285 | decoded: new Map(Object.entries({})), 286 | }, 287 | { 288 | hex: 'a201020304', 289 | decoded: new Map([ 290 | [1, 2], 291 | [3, 4], 292 | ]), 293 | }, 294 | { 295 | hex: 'a26161016162820203', 296 | decoded: new Map( 297 | Object.entries({ 298 | a: 1, 299 | b: [2, 3], 300 | }) 301 | ), 302 | }, 303 | { 304 | hex: '826161a161626163', 305 | decoded: [ 306 | 'a', 307 | new Map( 308 | Object.entries({ 309 | b: 'c', 310 | }) 311 | ), 312 | ], 313 | }, 314 | { 315 | hex: 'a56161614161626142616361436164614461656145', 316 | decoded: new Map( 317 | Object.entries({ 318 | a: 'A', 319 | b: 'B', 320 | c: 'C', 321 | d: 'D', 322 | e: 'E', 323 | }) 324 | ), 325 | }, 326 | { 327 | hex: '5f42010243030405ff', 328 | decoded: Uint8Array.from([0x01, 0x02, 0x03, 0x04, 0x05]), 329 | }, 330 | { 331 | hex: '7f657374726561646d696e67ff', 332 | decoded: 'streaming', 333 | }, 334 | { 335 | hex: '9fff', 336 | decoded: [], 337 | }, 338 | { 339 | hex: '9f018202039f0405ffff', 340 | decoded: [1, [2, 3], [4, 5]], 341 | }, 342 | { 343 | hex: '9f01820203820405ff', 344 | decoded: [1, [2, 3], [4, 5]], 345 | }, 346 | { 347 | hex: '83018202039f0405ff', 348 | decoded: [1, [2, 3], [4, 5]], 349 | }, 350 | { 351 | hex: '83019f0203ff820405', 352 | decoded: [1, [2, 3], [4, 5]], 353 | }, 354 | { 355 | hex: '9f0102030405060708090a0b0c0d0e0f101112131415161718181819ff', 356 | decoded: [ 357 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 358 | 23, 24, 25, 359 | ], 360 | }, 361 | { 362 | hex: 'bf61610161629f0203ffff', 363 | decoded: new Map( 364 | Object.entries({ 365 | a: 1, 366 | b: [2, 3], 367 | }) 368 | ), 369 | }, 370 | { 371 | hex: '826161bf61626163ff', 372 | decoded: [ 373 | 'a', 374 | new Map( 375 | Object.entries({ 376 | b: 'c', 377 | }) 378 | ), 379 | ], 380 | }, 381 | { 382 | hex: 'bf6346756ef563416d7421ff', 383 | decoded: new Map( 384 | Object.entries({ 385 | Fun: true, 386 | Amt: -2, 387 | }) 388 | ), 389 | }, 390 | ]; 391 | -------------------------------------------------------------------------------- /packages/test-e2e/chai/almost.ts: -------------------------------------------------------------------------------- 1 | const ObjectConstructor = {}.constructor; 2 | 3 | type GenericMap = {[key: string]: any}; 4 | type ChaiFlag = (obj: object, key: string, value?: any) => any; 5 | 6 | function isArray(val: any): val is [unknown] { 7 | if (!val) return false; 8 | return Array.isArray(val) || (val.buffer && val.buffer instanceof ArrayBuffer); 9 | } 10 | 11 | function isObject(val: any): val is Record { 12 | return val && val.constructor == ObjectConstructor; 13 | } 14 | 15 | function almost(a: number, b: number, tolerance: number) { 16 | return Math.abs(a - b) <= tolerance; 17 | } 18 | 19 | function almostEqualArray(a: [any], b: [any], tolerance: number) { 20 | if (a.length !== b.length) return false; 21 | for (let i = 0; i < a.length; ++i) { 22 | const eq = almostDeepEqual(a[i], b[i], tolerance); 23 | if (eq == false) return `index ${i}`; 24 | else if (typeof eq == 'string') return `index ${i} -> ${i}`; 25 | } 26 | return true; 27 | } 28 | 29 | function almostEqualObject(a: GenericMap, b: GenericMap, tolerance: number) { 30 | const keys = Object.keys(a); 31 | for (const key of keys) { 32 | const eq = almostDeepEqual(a[key], b[key], tolerance); 33 | if (typeof eq === 'boolean' && !eq) return `key ${key}`; 34 | else if (typeof eq == 'string') return `key ${key} -> ${key}`; 35 | } 36 | } 37 | 38 | /** 39 | * Recursively checks two values with a tolerance threshold. 40 | * 41 | * @param a First value for comparison 42 | * @param b Second value for comparison 43 | * @param tolerance Floating point tolerance for numbers 44 | * 45 | * @returns `true` if values are the same with some tolerance, `false` otherwise. 46 | * In addition, the method can return a string when an error occurs to give feedback 47 | * about where the difference happened. 48 | */ 49 | function almostDeepEqual(a: any, b: any, tolerance: number) { 50 | if (isArray(a) && isArray(b)) return almostEqualArray(a, b, tolerance); 51 | else if (isObject(a) && isObject(b)) return almostEqualObject(a, b, tolerance); 52 | else if (a instanceof Map && b instanceof Map) 53 | return almostEqualObject(a, b, tolerance); 54 | else if (!isNaN(a) && !isNaN(b)) return almost(a, b, tolerance); 55 | return a === b; 56 | } 57 | 58 | /** 59 | * Main Chai's assert. This function checks the input / expected 60 | * and performs formatting. In addition, it notifies chai if 61 | * the assert fails. 62 | * 63 | * @param {Function} flag Chai's flag function 64 | * @param {Object} val The value to check 65 | * @param {string} msg Chai's message 66 | */ 67 | function chaiAlmostAssert(_super: Chai.Assertion, flag: ChaiFlag, deep = false) { 68 | return function (this: Chai.AssertionStatic, val: any, msg: string) { 69 | if (msg) flag(this, 'message', msg); 70 | 71 | const needsAlmost = flag(this, 'almost'); 72 | const tolerance = flag(this, 'tolerance'); 73 | if (!needsAlmost || tolerance === undefined) { 74 | /* The user is trying to perform an exact comparison */ 75 | return _super.apply(this, arguments as unknown as [string, string | undefined]); 76 | } 77 | 78 | let value = null; 79 | let message = ''; 80 | 81 | if (deep || flag(this, 'deep')) { 82 | const deepEq = almostDeepEqual(val, this._obj, tolerance); 83 | value = deepEq; 84 | if (typeof deepEq === 'string') { 85 | message = `\n\t\tDifference path: ${deepEq}`; 86 | value = false; 87 | } 88 | } else if (tolerance && !isNaN(val) && !isNaN(this._obj)) { 89 | value = almost(val, this._obj, tolerance); 90 | } else { 91 | return _super.apply(this, arguments as unknown as [string, string | undefined]); 92 | } 93 | this.assert( 94 | value, 95 | `expected #{this} to deeply almost equal #{exp}${message}`, 96 | `expected #{this} to not deeply almost equal #{exp}${message}`, 97 | val, 98 | this._obj, 99 | true 100 | ); 101 | }; 102 | } 103 | 104 | /** 105 | * Chai plugin for almost equality 106 | * 107 | * It can be registered using: 108 | * 109 | * ```js 110 | * import { use } from 'chai'; 111 | * import { chaiAlmost } from './utils/almost.js'; 112 | * 113 | * use(chaiAlmost); 114 | * ``` 115 | * 116 | * @param {number} tolerance The default tolerance to use for every assert 117 | * @returns 118 | */ 119 | export function chaiAlmost(tolerance: number = 1e-4) { 120 | return function (chai: Chai.ChaiStatic, utils: Chai.ChaiUtils): void { 121 | const flag = utils.flag; 122 | 123 | /** 124 | * Override for methods of the form: 'equal', 'equals', 'eq'. 125 | */ 126 | function overridenEqualityAssert(_super: Chai.Assertion) { 127 | return chaiAlmostAssert(_super, flag); 128 | } 129 | 130 | /** 131 | * Override for methods of the form: 'eql', 'eqls'. 132 | * 133 | * According to Chai's documentation, `eql` and `eqls` must be deep. 134 | * This function thus force the `deep` flag. 135 | */ 136 | function overridenDeepEqualityAssert(_super: Chai.Assertion) { 137 | return chaiAlmostAssert(_super, flag, true); 138 | } 139 | 140 | function assert(this: Chai.Assertion, val: any, toleranceOverride: number) { 141 | flag(this, 'tolerance', toleranceOverride || tolerance); 142 | flag(this, 'almost', true); 143 | return this.equal(val); 144 | } 145 | 146 | function chain(this: Chai.Assertion) { 147 | flag(this, 'tolerance', tolerance); 148 | flag(this, 'almost', true); 149 | } 150 | 151 | chai.Assertion.addChainableMethod('almost', assert as any, chain); 152 | chai.Assertion.overwriteMethod('equal', overridenEqualityAssert); 153 | chai.Assertion.overwriteMethod('equals', overridenEqualityAssert); 154 | chai.Assertion.overwriteMethod('eq', overridenEqualityAssert); 155 | chai.Assertion.overwriteMethod('eql', overridenDeepEqualityAssert); 156 | chai.Assertion.overwriteMethod('eqls', overridenDeepEqualityAssert); 157 | }; 158 | } 159 | -------------------------------------------------------------------------------- /packages/test-e2e/chai/promise.ts: -------------------------------------------------------------------------------- 1 | import {expect} from '@esm-bundle/chai'; 2 | 3 | /** 4 | * Expects the promise to fulfill. 5 | * 6 | * @param {Promise} promise The promise to check. 7 | * @param {number} timeout Maximum amount of time it can take to fulfill, in ms. 8 | * @returns The promise result. 9 | */ 10 | export function expectSuccess(promise: Promise, timeout = 1500) { 11 | return new Promise((res, rej) => { 12 | setTimeout(() => { 13 | rej(`expected promise to fulfill before timeout of ${timeout}`); 14 | }, timeout); 15 | promise.then(res).catch(rej); 16 | }).catch((e) => { 17 | expect.fail( 18 | 'promise expected to succeed but failed.\n' + 19 | '\tExpected: Success\n' + 20 | '\tBut rejected with: ' + 21 | e 22 | ); 23 | }); 24 | } 25 | 26 | /** 27 | * Expects the promise to fail. 28 | * 29 | * @param {Promise} promise The promise to check. 30 | * @param {number} timeout Maximum amount of time it can take to fail, in ms. 31 | * @returns The promise result. 32 | */ 33 | export function expectFail(promise: Promise, timeout = 1500) { 34 | return new Promise((res, rej) => { 35 | setTimeout(() => { 36 | rej(`expected promise to fail before timeout of ${timeout}`); 37 | }, timeout); 38 | promise.then(rej).catch(res); 39 | }).catch((data) => { 40 | expect.fail( 41 | 'promise expected to fail but resolved.\n' + 42 | '\tExpected: Error\n' + 43 | '\tBut resolved with: ' + 44 | data 45 | ); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /packages/test-e2e/deploy: -------------------------------------------------------------------------------- 1 | /Users/davidpeicho/Dev/wonderland/wonderland-engine/deploy -------------------------------------------------------------------------------- /packages/test-e2e/engine.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from '@esm-bundle/chai'; 2 | 3 | import {init, WL} from './setup.js'; 4 | 5 | import { 6 | checkRuntimeCompatibility, 7 | APIVersion, 8 | loadRuntime, 9 | Component, 10 | Version, 11 | LogLevel, 12 | } from '@wonderlandengine/api'; 13 | import * as promise from './chai/promise.js'; 14 | 15 | const TestCanvas = '_WL_test_canvas_'; 16 | const OriginalAPIVersion = {...APIVersion}; 17 | 18 | /* major, minor, patch, and rc version of the runtime. */ 19 | let runtimeVersion: Version = {major: -1, minor: -1, patch: -1, rc: -1}; 20 | 21 | /** 22 | * Sets the global API version. 23 | * 24 | * @note This override the API version export. It must be properly 25 | * reset to avoid any issue with other callers. Each test file being 26 | * isolated (loaded separately from scratch), the issue wouldn't 27 | * be able to spread outside this file. 28 | * 29 | * @param major Major component 30 | * @param minor Minor component 31 | * @param patch Patch component 32 | * @param rc RC component 33 | */ 34 | function setAPIVersion(major: number, minor: number, patch: number, rc: number) { 35 | APIVersion.major = major; 36 | APIVersion.minor = minor; 37 | APIVersion.patch = patch; 38 | APIVersion.rc = rc; 39 | } 40 | 41 | before(async function () { 42 | await init(); 43 | runtimeVersion = WL.runtimeVersion; 44 | }); 45 | 46 | beforeEach(function () { 47 | /* Before each test, append a debug canvas. */ 48 | const canvas = document.createElement('canvas'); 49 | canvas.id = TestCanvas; 50 | document.body.append(canvas); 51 | }); 52 | 53 | afterEach(function () { 54 | /* Delete debug canvas after each test to claim GPU memory. */ 55 | document.getElementById(TestCanvas)?.remove(); 56 | setAPIVersion( 57 | OriginalAPIVersion.major, 58 | OriginalAPIVersion.minor, 59 | OriginalAPIVersion.patch, 60 | OriginalAPIVersion.rc 61 | ); 62 | }); 63 | 64 | describe('Engine', function () { 65 | it('ar / vr supported', async function () { 66 | const [arSupported, vrSupported] = await Promise.all([ 67 | navigator?.xr?.isSessionSupported('immersive-ar') ?? Promise.resolve(false), 68 | navigator?.xr?.isSessionSupported('immersive-vr') ?? Promise.resolve(false), 69 | ]); 70 | expect(WL.arSupported).to.equal(arSupported); 71 | expect(WL.vrSupported).to.equal(vrSupported); 72 | }); 73 | 74 | it('.switchTo()', async function () { 75 | const sceneA = WL._createEmpty(); 76 | const sceneB = WL._createEmpty(); 77 | 78 | expect(sceneA.isActive).to.be.false; 79 | expect(sceneB.isActive).to.be.false; 80 | 81 | WL.switchTo(sceneA); 82 | expect(sceneA.isActive).to.be.true; 83 | expect(sceneB.isActive).to.be.false; 84 | 85 | WL.switchTo(sceneB); 86 | expect(sceneB.isActive).to.be.true; 87 | expect(sceneA.isActive).to.be.false; 88 | }); 89 | 90 | it('multiple instances', async function () { 91 | this.timeout(20000); 92 | 93 | class TestComponent extends Component { 94 | static TypeName = 'test-component'; 95 | } 96 | 97 | const engine = await loadRuntime('deploy/WonderlandRuntime', { 98 | simd: false, 99 | threads: false, 100 | canvas: TestCanvas, 101 | logs: [LogLevel.Error], 102 | }); 103 | expect(engine.scene).to.not.equal(WL.scene); 104 | 105 | expect(WL.isRegistered(TestComponent)).to.be.false; 106 | expect(engine.isRegistered(TestComponent)).to.be.false; 107 | engine.registerComponent(TestComponent); 108 | expect(WL.isRegistered(TestComponent)).to.be.false; 109 | expect(engine.isRegistered(TestComponent)).to.be.true; 110 | 111 | /* Loading screen event test */ 112 | 113 | const events: ('loading-screen' | 'loaded')[] = []; 114 | engine.onLoadingScreenEnd.add(() => events.push('loading-screen')); 115 | const scene = engine._createEmpty(); 116 | engine.switchTo(scene); 117 | expect(events).to.eql(['loading-screen']); 118 | }); 119 | 120 | describe('Runtime <> API compatibility', function () { 121 | /* Helper function to load a runtime. */ 122 | 123 | this.timeout(10000); 124 | 125 | function load() { 126 | return loadRuntime('deploy/WonderlandRuntime', { 127 | simd: false, 128 | threads: false, 129 | canvas: TestCanvas, 130 | logs: [LogLevel.Error], 131 | }); 132 | } 133 | 134 | it('checkRuntimeCompatibility()', function () { 135 | const {major, minor, patch, rc} = runtimeVersion; 136 | for (const bump of [-1, 1]) { 137 | setAPIVersion(major + bump, minor, patch, rc); 138 | expect(checkRuntimeCompatibility.bind(null, runtimeVersion)).to.throw(); 139 | setAPIVersion(major, minor + bump, patch, rc); 140 | expect(checkRuntimeCompatibility.bind(null, runtimeVersion)).to.throw(); 141 | setAPIVersion(major, minor, patch + bump, rc); 142 | expect(checkRuntimeCompatibility.bind(null, runtimeVersion)).to.not.throw(); 143 | } 144 | }); 145 | 146 | it('loadRuntime() with older API', async function () { 147 | /* Non-matching major version. */ 148 | const {major, minor, patch, rc} = runtimeVersion; 149 | setAPIVersion(major + 1, minor + 1, patch, rc); 150 | await promise.expectFail(load()); 151 | }); 152 | 153 | it('loadRuntime() with newer API', async function () { 154 | /* Non-matching major version. */ 155 | const {major, minor, patch, rc} = runtimeVersion; 156 | setAPIVersion(major - 1, minor - 1, patch, rc); 157 | await promise.expectFail(load()); 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /packages/test-e2e/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module Chai { 2 | interface AlmostEqual { 3 | (value: any, tolerance?: number | null, message?: string): Assertion; 4 | } 5 | interface Assertion { 6 | almost: AlmostEqual; 7 | } 8 | interface Deep { 9 | almost: AlmostEqual; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/test-e2e/i18n.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from '@esm-bundle/chai'; 2 | 3 | import {init, projectURL, reset, WL} from './setup.js'; 4 | import {InMemoryLoadOptions, TextComponent} from '@wonderlandengine/api'; 5 | import {fetchWithProgress} from '@wonderlandengine/api/utils/fetch.js'; 6 | import {TestComponentTranslate} from './projects/language-switching/js/test-component-translate.js'; 7 | 8 | before(init); 9 | beforeEach(reset); 10 | 11 | /* Use in-memory .bin as much as possible to speed up the tests. */ 12 | const bin: InMemoryLoadOptions = {buffer: null!, baseURL: projectURL('')}; 13 | try { 14 | bin.buffer = await fetchWithProgress(projectURL('LanguageSwitching.bin')); 15 | } catch (e) { 16 | console.error('Failed to load required test scenes'); 17 | throw e; 18 | } 19 | 20 | describe('I18N', function () { 21 | it('defaults', function () { 22 | const i18n = WL.i18n; 23 | 24 | const langCount = i18n.languageCount(); 25 | expect(langCount).to.equal(0); 26 | 27 | const langIndex = i18n.languageIndex('non-existing-code'); 28 | expect(langIndex).to.equal(-1); 29 | 30 | const translation = i18n.translate('non-existing-term'); 31 | expect(translation).to.equal(null); 32 | 33 | const lang = i18n.language; 34 | expect(lang).to.equal(null); 35 | 36 | const langCode = i18n.languageCode(0); 37 | expect(langCode).to.equal(null); 38 | 39 | const langName = i18n.languageName(0); 40 | expect(langName).to.equal(null); 41 | 42 | expect(i18n.language).to.be.null; 43 | }); 44 | 45 | it('language', async function () { 46 | WL.registerComponent(TestComponentTranslate); 47 | 48 | await WL.scene.load(bin); 49 | 50 | expect(WL.i18n.languageCount()).to.equal(2); 51 | expect(WL.i18n.language).to.equal('en'); 52 | expect(WL.i18n.languageIndex(WL.i18n.language!)).to.equal(0); 53 | 54 | expect(WL.scene.children).to.have.a.lengthOf(5); 55 | 56 | const textObjects = WL.scene.findByNameDirect('Text'); 57 | expect(textObjects).to.have.a.lengthOf(1); 58 | const text = textObjects[0].getComponent(TextComponent)!; 59 | expect(text).to.not.be.null; 60 | 61 | const jsObjects = WL.scene.findByNameDirect('JS'); 62 | expect(jsObjects).to.have.a.lengthOf(1); 63 | const js = jsObjects[0].getComponent(TestComponentTranslate)!; 64 | expect(js).to.not.be.null; 65 | 66 | const mixedObjects = WL.scene.findByNameDirect('Mixed'); 67 | expect(mixedObjects).to.have.a.lengthOf(1); 68 | const mixedText = mixedObjects[0].getComponent(TextComponent)!; 69 | expect(mixedText).to.not.be.null; 70 | const mixedJs = mixedObjects[0].getComponents(TestComponentTranslate)!; 71 | expect(mixedJs).to.have.a.lengthOf(2); 72 | 73 | const textUnchangedObjects = WL.scene.findByNameDirect('Text unchanged'); 74 | expect(textUnchangedObjects).to.have.a.lengthOf(1); 75 | const textUnchanged = textUnchangedObjects[0].getComponent(TextComponent)!; 76 | expect(textUnchanged).to.not.be.null; 77 | 78 | /* Translation should be applied to inactive components */ 79 | mixedText.active = false; 80 | /* Translation should not be applied to destroyed components */ 81 | mixedJs[0].destroy(); 82 | 83 | expect(WL.i18n.translate('48-text-0')).to.equal('Hello Wonderland!'); 84 | expect(WL.i18n.translate('49-js-0-stringProp')).to.equal('Yahallo Wonderland!'); 85 | expect(WL.i18n.translate('50-js-2-stringProp')).to.equal('First Wonderland!'); 86 | expect(WL.i18n.translate('50-js-4-stringProp')).to.equal('Second Wonderland!'); 87 | expect(WL.i18n.translate('50-text-1')).to.equal('Hi Wonderland?'); 88 | expect(WL.i18n.translate('51-text-0')).to.equal('No translation'); 89 | expect(WL.i18n.translate('projectName')).to.equal('LanguageSwitching'); 90 | 91 | expect(text.text).to.equal('Hello Wonderland!'); 92 | expect(js.stringProp).to.equal('Yahallo Wonderland!'); 93 | expect(mixedText.text).to.equal('Hi Wonderland?'); 94 | expect(mixedJs[1].stringProp).to.equal( 95 | mixedJs[1].floatProp > 0.0 ? 'Second Wonderland!' : 'First Wonderland!' 96 | ); 97 | expect(textUnchanged.text).to.equal('No translation'); 98 | 99 | /* We have to wait for the language switch to make sure the language bin was loaded */ 100 | const promise = WL.i18n.onLanguageChanged.promise(); 101 | 102 | WL.i18n.language = 'nl'; 103 | expect(WL.i18n.language).to.equal('nl'); 104 | expect(WL.i18n.languageIndex(WL.i18n.language!)).to.equal(1); 105 | 106 | const [oldLanguage, newLanguage] = await promise; 107 | 108 | expect(oldLanguage).to.equal(0); 109 | expect(newLanguage).to.equal(1); 110 | 111 | expect(WL.i18n.translate('48-text-0')).to.equal('Hallo Wonderland!'); 112 | expect(WL.i18n.translate('49-js-0-stringProp')).to.equal('Yuhullo Wonderland!'); 113 | expect(WL.i18n.translate('50-js-2-stringProp')).to.equal('Erst Wonderland!'); 114 | expect(WL.i18n.translate('50-js-4-stringProp')).to.equal('Zweijt Wonderland!'); 115 | expect(WL.i18n.translate('50-text-1')).to.equal('Hej Wynderland?'); 116 | /* null in nl.json, no translation applied */ 117 | expect(WL.i18n.translate('51-text-0')).to.equal('No translation'); 118 | expect(WL.i18n.translate('projectName')).to.equal('LanguageSwitching but Dutch!'); 119 | 120 | expect(text.text).to.equal('Hallo Wonderland!'); 121 | expect(js.stringProp).to.equal('Yuhullo Wonderland!'); 122 | expect(mixedText.text).to.equal('Hej Wynderland?'); 123 | expect(mixedJs[1].stringProp).to.equal( 124 | mixedJs[1].floatProp > 0.0 ? 'Zweijt Wonderland!' : 'Erst Wonderland!' 125 | ); 126 | expect(textUnchanged.text).to.equal('No translation'); 127 | }); 128 | 129 | it('language, unregistered component', async function () { 130 | await WL.scene.load(bin); 131 | 132 | expect(WL.i18n.language).to.equal('en'); 133 | expect(WL.isRegistered(TestComponentTranslate)).to.be.false; 134 | 135 | const textObjects = WL.scene.findByNameDirect('Text'); 136 | expect(textObjects).to.have.a.lengthOf(1); 137 | const text = textObjects[0].getComponent(TextComponent)!; 138 | expect(text).to.not.be.null; 139 | 140 | const jsObjects = WL.scene.findByNameDirect('JS'); 141 | expect(jsObjects).to.have.a.lengthOf(1); 142 | const js = jsObjects[0].getComponent(TestComponentTranslate); 143 | expect(js).to.be.null; 144 | 145 | const promise = WL.i18n.onLanguageChanged.promise(); 146 | 147 | WL.i18n.language = 'nl'; 148 | 149 | const [oldLanguage, newLanguage] = await promise; 150 | 151 | expect(oldLanguage).to.equal(0); 152 | expect(newLanguage).to.equal(1); 153 | 154 | /* Translation using the term always works */ 155 | expect(WL.i18n.translate('48-text-0')).to.equal('Hallo Wonderland!'); 156 | expect(WL.i18n.translate('49-js-0-stringProp')).to.equal('Yuhullo Wonderland!'); 157 | expect(WL.i18n.translate('projectName')).to.equal('LanguageSwitching but Dutch!'); 158 | 159 | expect(text.text).to.equal('Hallo Wonderland!'); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /packages/test-e2e/logger.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from '@esm-bundle/chai'; 2 | 3 | import {Logger, LogLevel} from '@wonderlandengine/api'; 4 | 5 | /** Tag enumeration to use throughout the test. */ 6 | enum Tag { 7 | First = 0, 8 | Last = 31, 9 | } 10 | 11 | describe('Logger', function () { 12 | const consoleLog = console.log; 13 | const consoleWarn = console.warn; 14 | const consoleErr = console.error; 15 | after(function () { 16 | console.log = consoleLog; 17 | console.warn = consoleWarn; 18 | console.error = consoleErr; 19 | }); 20 | 21 | let logs = {info: [] as any[], warn: [] as any[], error: [] as any[]}; 22 | console.log = function (...data: any[]) { 23 | logs.info.push(...data); 24 | }; 25 | console.warn = function (...data: any[]) { 26 | logs.warn.push(...data); 27 | }; 28 | console.error = function (...data: any[]) { 29 | logs.error.push(...data); 30 | }; 31 | 32 | beforeEach(function () { 33 | logs = {info: [], warn: [], error: []}; 34 | }); 35 | 36 | /** 37 | * Create a test for a given log level 38 | * 39 | * @param level The level to create the test for 40 | * @param tags The tags to check 41 | */ 42 | function levelTest(level: keyof typeof LogLevel, tags: (keyof typeof Tag)[]) { 43 | const logger = new Logger(LogLevel[level]); 44 | 45 | const logFunction = level.toLowerCase() as 'info' | 'warn' | 'error'; 46 | const messages = {info: 'Hello', warn: 'World', error: 'How are you?'}; 47 | 48 | const expected = { 49 | info: [] as string[], 50 | warn: [] as string[], 51 | error: [] as string[], 52 | }; 53 | expected[logFunction].push(messages[logFunction]); 54 | 55 | for (const name of tags) { 56 | const tag = Tag[name]; 57 | it(`LogLevel.${level} > Tag ${name}`, function () { 58 | expect(logs).to.deep.equal({info: [], warn: [], error: []}); 59 | logger.info(tag, messages.info); 60 | logger.warn(tag, messages.warn); 61 | logger.error(tag, messages.error); 62 | expect(logs).to.deep.equal(expected); 63 | }); 64 | } 65 | } 66 | 67 | /* Create tests for each log level: info, warn & error. */ 68 | levelTest('Info', ['First', 'Last']); 69 | levelTest('Warn', ['First', 'Last']); 70 | levelTest('Error', ['First', 'Last']); 71 | 72 | it('LogLevel.Info | LogLevel.Warn | LogLevel.Error', async function () { 73 | const logger = new Logger(LogLevel.Info, LogLevel.Warn, LogLevel.Error); 74 | expect(logs).to.deep.equal({info: [], warn: [], error: []}); 75 | logger.info(Tag.First, 'Hello'); 76 | logger.warn(Tag.First, 'World'); 77 | logger.error(Tag.First, 'How are you?'); 78 | expect(logs).to.deep.equal({ 79 | info: ['Hello'], 80 | warn: ['World'], 81 | error: ['How are you?'], 82 | }); 83 | }); 84 | 85 | it('non matching tags', function () { 86 | expect(logs).to.deep.equal({info: [], warn: [], error: []}); 87 | 88 | const logger = new Logger(LogLevel.Info); 89 | logger.tags.disableAll(); 90 | 91 | logger.info(Tag.First, 'First'); 92 | expect(logs).to.deep.equal({info: [], warn: [], error: []}); 93 | logger.info(Tag.Last, 'Last'); 94 | expect(logs).to.deep.equal({info: [], warn: [], error: []}); 95 | 96 | logger.tags.enable(Tag.First); 97 | logger.info(Tag.First, 'First'); 98 | logger.info(Tag.Last, 'Last'); 99 | expect(logs).to.deep.equal({info: ['First'], warn: [], error: []}); 100 | 101 | logger.tags.enable(Tag.Last); 102 | logger.info(Tag.Last, 'Last'); 103 | expect(logs).to.deep.equal({info: ['First', 'Last'], warn: [], error: []}); 104 | }); 105 | 106 | it('multiple arguments', function () { 107 | const logger = new Logger(LogLevel.Info, LogLevel.Warn, LogLevel.Error); 108 | expect(logs).to.deep.equal({info: [], warn: [], error: []}); 109 | logger.info(Tag.First, 'Hello', 'you'); 110 | logger.warn(Tag.First, 'How', 'are', 'you'); 111 | logger.error(Tag.First, 'today', '?'); 112 | expect(logs).to.deep.equal({ 113 | info: ['Hello', 'you'], 114 | warn: ['How', 'are', 'you'], 115 | error: ['today', '?'], 116 | }); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /packages/test-e2e/material.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, use} from '@esm-bundle/chai'; 2 | import {chaiAlmost} from './chai/almost.js'; 3 | 4 | import {init, WL} from './setup.js'; 5 | import {Material, NumberArray, MaterialConstructor, Texture} from '@wonderlandengine/api'; 6 | 7 | use(chaiAlmost()); 8 | before(init); 9 | /* We intentionally don't reset such that the 'Phong Opaque' pipeline 10 | * from the loading screen is available to create materials from. */ 11 | 12 | interface PhongMaterial extends Material { 13 | ambientColor: Float32Array | number[]; 14 | diffuseColor: Float32Array | number[]; 15 | specularColor: Float32Array | number[]; 16 | shininess: number; 17 | alphaMaskThreshold: number; 18 | alphaMaskTexture: Texture; 19 | 20 | getAmbientColor(out?: NumberArray): NumberArray; 21 | setAmbientColor(value: NumberArray): void; 22 | getDiffuseColor(out?: NumberArray): NumberArray; 23 | setDiffuseColor(value: NumberArray): void; 24 | getSpecularColor(out?: NumberArray): NumberArray; 25 | setSpecularColor(value: NumberArray): void; 26 | 27 | getShininess(): number; 28 | setShininess(value: number): number; 29 | 30 | getAlphaMaskThreshold(): number; 31 | setAlphaMaskThreshold(value: number): void; 32 | getAlphaMaskTexture(): Texture; 33 | setAlphaMaskTexture(value: Texture): void; 34 | } 35 | 36 | interface MeshVisualizerMaterial extends Material { 37 | getColor(out?: NumberArray): NumberArray; 38 | setColor(value: NumberArray): void; 39 | getWireframeColor(out?: NumberArray): NumberArray; 40 | setWireframeColor(value: NumberArray): void; 41 | } 42 | 43 | describe('Material', function () { 44 | it('deprecated constructor', function () { 45 | expect(() => new Material(WL, undefined as any)).to.throw( 46 | Error, 47 | "Missing parameter 'pipeline'" 48 | ); 49 | expect(() => new Material(WL, {pipeline: 'Northstream'})).to.throw( 50 | Error, 51 | `Pipeline \'Northstream\' doesn\'t exist in the scene` 52 | ); 53 | 54 | const mat = new Material(WL, {pipeline: 'Phong Opaque'}) as PhongMaterial; 55 | expect((mat as Record).color).to.be.undefined; 56 | expect(mat.diffuseColor).to.deep.almost(new Float32Array([0, 0, 0, 1])); 57 | expect(mat.pipeline).to.equal('Phong Opaque'); 58 | }); 59 | 60 | it('deprecated accessors', function () { 61 | const mat = new Material(WL, {pipeline: 'Phong Opaque'}) as PhongMaterial; 62 | 63 | expect(mat.diffuseColor).to.deep.equal(new Float32Array([0, 0, 0, 1])); 64 | mat.diffuseColor = [1.0, 0.0, 0.0, 1.0]; 65 | expect(mat.diffuseColor).to.deep.equal(new Float32Array([1.0, 0.0, 0.0, 1.0])); 66 | 67 | mat.shininess = 10; 68 | expect(mat.shininess).to.eql(10); 69 | 70 | mat.alphaMaskThreshold = 0.5; 71 | expect(mat.alphaMaskThreshold).to.eql(0.5); 72 | expect(mat.alphaMaskTexture).to.be.null; 73 | }); 74 | 75 | it('colors out parameter', function () { 76 | const PhongMaterial = WL.materials.getTemplate( 77 | 'Phong Opaque' 78 | ) as MaterialConstructor; 79 | 80 | const mat = new PhongMaterial(); 81 | 82 | expect(mat.getAmbientColor()).to.deep.almost([0, 0, 0, 1]); 83 | mat.setAmbientColor([0.1, 0.2, 0.3, 0.4]); 84 | { 85 | const out = [-1, -1, -1, -1]; 86 | expect(mat.getAmbientColor(out)).to.equal(out); 87 | expect(out).to.deep.almost([0.1, 0.2, 0.3, 0.4], 0.01); 88 | } 89 | 90 | expect(mat.getDiffuseColor()).to.deep.almost([0, 0, 0, 1]); 91 | mat.setDiffuseColor([0.9, 0.8, 0.7, 1.0]); 92 | { 93 | const out = [-1, -1, -1, -1]; 94 | expect(mat.getDiffuseColor(out)).to.equal(out); 95 | expect(out).to.deep.almost([0.9, 0.8, 0.7, 1.0], 0.01); 96 | } 97 | }); 98 | 99 | it('deprecated equals()', function () { 100 | const mat1 = new Material(WL, {pipeline: 'Phong Opaque'}); 101 | const mat2 = new Material(WL, {pipeline: 'Phong Opaque'}); 102 | const mat3 = new Material(WL, mat1._index); 103 | expect(mat1.equals(null)).to.be.false; 104 | expect(mat1.equals(undefined)).to.be.false; 105 | expect(mat1.equals(mat1)).to.be.true; 106 | expect(mat1.equals(mat2)).to.be.false; 107 | expect(mat2.equals(mat1)).to.be.false; 108 | expect(mat1.equals(mat3)).to.be.true; 109 | expect(mat3.equals(mat1)).to.be.true; 110 | }); 111 | }); 112 | 113 | describe('Phong', function () { 114 | it('definition', function () { 115 | const PhongMaterial = WL.materials.getTemplate('Phong Opaque'); 116 | for (const param of [ 117 | 'ambientColor', 118 | 'diffuseColor', 119 | 'shininess', 120 | 'alphaMaskThreshold', 121 | 'alphaMaskTexture', 122 | ]) { 123 | expect(PhongMaterial.Parameters.has(param), `missing parameter '${param}'`).to 124 | .be.true; 125 | } 126 | 127 | expect(PhongMaterial.prototype.getAmbientColor).to.be.instanceOf(Function); 128 | expect(PhongMaterial.prototype.getDiffuseColor).to.be.instanceOf(Function); 129 | expect(PhongMaterial.prototype.setDiffuseColor).to.be.instanceOf(Function); 130 | expect(PhongMaterial.prototype.getAlphaMaskThreshold).to.be.instanceOf(Function); 131 | expect(PhongMaterial.prototype.setAlphaMaskThreshold).to.be.instanceOf(Function); 132 | expect(PhongMaterial.prototype.getAlphaMaskTexture).to.be.instanceOf(Function); 133 | expect(PhongMaterial.prototype.setAlphaMaskTexture).to.be.instanceOf(Function); 134 | }); 135 | 136 | it('properties', function () { 137 | const PhongMaterial = WL.materials.getTemplate( 138 | 'Phong Opaque' 139 | ) as MaterialConstructor; 140 | 141 | const mat = new PhongMaterial(); 142 | 143 | expect(mat.getAmbientColor()).to.deep.almost([0, 0, 0, 1]); 144 | mat.setAmbientColor([0.1, 0.2, 0.3, 0.4]); 145 | expect(mat.getAmbientColor()).to.deep.almost([0.1, 0.2, 0.3, 0.4], 0.01); 146 | 147 | expect(mat.getDiffuseColor()).to.deep.almost([0, 0, 0, 1]); 148 | mat.setDiffuseColor([0.9, 0.8, 0.7, 1.0]); 149 | expect(mat.getDiffuseColor()).to.deep.almost([0.9, 0.8, 0.7, 1.0], 0.01); 150 | 151 | expect(mat.getSpecularColor()).to.deep.almost([0, 0, 0, 1]); 152 | mat.setSpecularColor([0.5, 0.4, 0.7, 0.5]); 153 | expect(mat.getSpecularColor()).to.deep.almost([0.5, 0.4, 0.7, 0.5], 0.01); 154 | 155 | expect(mat.getShininess()).to.almost(0); 156 | mat.setShininess(5); 157 | expect(mat.getShininess()).to.almost(5); 158 | 159 | expect(mat.getAlphaMaskThreshold()).to.almost(0.0); 160 | mat.setAlphaMaskThreshold(0.75); 161 | expect(mat.getAlphaMaskThreshold()).to.almost(0.75); 162 | }); 163 | }); 164 | 165 | describe('MeshVisualizer', function () { 166 | it('definition', function () { 167 | const materials = WL.materials; 168 | 169 | const MeshVisualizerMaterial = 170 | materials.getTemplate('MeshVisualizer'); 171 | for (const param of [ 172 | 'color', 173 | 'wireframeColor', 174 | 'alphaMaskThreshold', 175 | 'alphaMaskTexture', 176 | ]) { 177 | expect( 178 | MeshVisualizerMaterial.Parameters.has(param), 179 | `missing parameter '${param}'` 180 | ).to.be.true; 181 | } 182 | 183 | expect(MeshVisualizerMaterial.prototype.getColor).to.be.instanceOf(Function); 184 | expect(MeshVisualizerMaterial.prototype.setColor).to.be.instanceOf(Function); 185 | expect(MeshVisualizerMaterial.prototype.getWireframeColor).to.be.instanceOf( 186 | Function 187 | ); 188 | expect(MeshVisualizerMaterial.prototype.setWireframeColor).to.be.instanceOf( 189 | Function 190 | ); 191 | }); 192 | }); 193 | -------------------------------------------------------------------------------- /packages/test-e2e/memory.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, use} from '@esm-bundle/chai'; 2 | import {chaiAlmost} from './chai/almost.js'; 3 | 4 | import {init, reset, WL} from './setup.js'; 5 | 6 | use(chaiAlmost()); 7 | 8 | before(init); 9 | beforeEach(reset); 10 | 11 | describe('Memory', function () { 12 | let ptr: number | null = null; 13 | 14 | after(function () { 15 | if (ptr !== null) WL.wasm._free(ptr); 16 | }); 17 | 18 | it('grow memory, ensure views are updated', async function () { 19 | const oldHEAP8 = WL.wasm.HEAP8; 20 | const oldHEAP16 = WL.wasm.HEAP16; 21 | const oldHEAP32 = WL.wasm.HEAP32; 22 | const oldHEAPU8 = WL.wasm.HEAPU8; 23 | const oldHEAPU16 = WL.wasm.HEAPU16; 24 | const oldHEAPU32 = WL.wasm.HEAPU32; 25 | const oldHEAPF32 = WL.wasm.HEAPF32; 26 | const oldHEAPF64 = WL.wasm.HEAPF64; 27 | 28 | /* Allocate 24 MB to grow beyond the 24 initial MB 29 | * This should never fail with out of memory. If it does, 30 | * read the error closely, it could fail in our updateMemoryViews() */ 31 | ptr = WL.wasm._malloc(1024 * 1024 * 24); 32 | 33 | expect(oldHEAP8 == WL.wasm.HEAP8).to.be.false; 34 | expect(oldHEAP16 == WL.wasm.HEAP16).to.be.false; 35 | expect(oldHEAP32 == WL.wasm.HEAP32).to.be.false; 36 | expect(oldHEAPU8 == WL.wasm.HEAPU8).to.be.false; 37 | expect(oldHEAPU16 == WL.wasm.HEAPU16).to.be.false; 38 | expect(oldHEAPU32 == WL.wasm.HEAPU32).to.be.false; 39 | expect(oldHEAPF32 == WL.wasm.HEAPF32).to.be.false; 40 | expect(oldHEAPF64 == WL.wasm.HEAPF64).to.be.false; 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/test-e2e/morphtargets.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, use} from '@esm-bundle/chai'; 2 | import {chaiAlmost} from './chai/almost.js'; 3 | 4 | import {MeshComponent, InMemoryLoadOptions, Scene} from '@wonderlandengine/api'; 5 | import {init, projectURL, reset, WL} from './setup.js'; 6 | import {loadProjectBins} from './utils.js'; 7 | 8 | use(chaiAlmost(0.001)); 9 | 10 | before(init); 11 | beforeEach(reset); 12 | 13 | /* Use in-memory .bin as much as possible to speed up the tests. */ 14 | let bins: InMemoryLoadOptions[] = []; 15 | try { 16 | bins = await loadProjectBins('MorphTargets.bin'); 17 | } catch (e) { 18 | console.error('Failed to load required test scenes'); 19 | throw e; 20 | } 21 | const morphTargetsBin = bins[0]; 22 | 23 | describe('MorphTargets', function () { 24 | it('get / set targets', async function () { 25 | const scene = await WL.loadMainSceneFromBuffer(morphTargetsBin); 26 | 27 | const primitivesObject = scene.findByName('MorphPrimitives')[0]; 28 | expect(primitivesObject).to.not.be.undefined; 29 | let meshes = primitivesObject.getComponents('mesh'); 30 | expect(meshes.length).to.equal(2); 31 | expect(meshes[0].morphTargets).to.not.be.null; 32 | expect(meshes[0].morphTargets).to.not.equal(meshes[1].morphTargets); 33 | for (const mesh of meshes) { 34 | const targets = mesh.morphTargets!; 35 | expect(targets).to.not.be.null; 36 | expect(targets.count).to.equal(1); 37 | expect(targets.getTargetName(0)).to.equal('target_0'); 38 | expect(() => targets.getTargetName(5000)).to.throw( 39 | Error, 40 | 'Index 5000 is out of bounds for 1 targets' 41 | ); 42 | expect(targets.getTargetIndex('target_0')).to.equal(0); 43 | expect(() => targets.getTargetIndex('target_1')).to.throw( 44 | Error, 45 | "Missing target 'target_1'" 46 | ); 47 | } 48 | 49 | const stressTestObject = scene.findByName('MorphStressTest')[0]; 50 | expect(stressTestObject).to.not.be.undefined; 51 | meshes = stressTestObject.getComponents('mesh'); 52 | expect(meshes.length).to.equal(2); 53 | expect(meshes[0].morphTargets).to.not.be.null; 54 | expect(meshes[0].morphTargets).to.not.equal(meshes[1].morphTargets); 55 | for (const mesh of meshes) { 56 | const targets = mesh.morphTargets!; 57 | expect(targets).to.not.be.null; 58 | expect(targets.count).to.equal(8); 59 | expect(() => targets.getTargetIndex('unknown')).to.throw( 60 | Error, 61 | "Missing target 'unknown'" 62 | ); 63 | for (let t = 0; t < targets.count; ++t) { 64 | const targetName = 'Key ' + (t + 1); 65 | expect(targets.getTargetName(t)).to.equal(targetName); 66 | expect(targets.getTargetIndex(targetName)).to.equal(t); 67 | } 68 | } 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /packages/test-e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wonderlandengine/api-test-e2e", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "author": "Wonderland GmbH", 6 | "license": "MIT", 7 | "private": true, 8 | "prettier": "@wonderlandengine/prettier-config", 9 | "scripts": { 10 | "test": "npm run typecheck && node ./scripts/run-tests.mjs", 11 | "typecheck": "tsc --project ../api/tsconfig.json --noEmit", 12 | "build": "node ./scripts/build-projects.mjs" 13 | }, 14 | "dependencies": { 15 | "@esm-bundle/chai": "^4.3.4-fix.0", 16 | "@types/mocha": "^10.0.1", 17 | "@types/node": "^18.11.9", 18 | "@web/dev-server-esbuild": "^0.4.1", 19 | "@web/test-runner": "0.17.0", 20 | "@wonderlandengine/api": "file:../api" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/test-e2e/projects/advanced/assets/SimpleSkin.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/projects/advanced/assets/SimpleSkin.glb -------------------------------------------------------------------------------- /packages/test-e2e/projects/advanced/assets/spree_bank_32x16.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/projects/advanced/assets/spree_bank_32x16.hdr -------------------------------------------------------------------------------- /packages/test-e2e/projects/advanced/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/projects/advanced/image.png -------------------------------------------------------------------------------- /packages/test-e2e/projects/advanced/js/test-component-retarget.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This component uses the local api on purpose. 3 | * 4 | * This greatly simplify the testing process, since we don't need 5 | * to install the `wonderlandengine/api` package in the project. 6 | */ 7 | import { 8 | Animation, 9 | Component, 10 | Material, 11 | Mesh, 12 | Object3D, 13 | Property, 14 | Skin, 15 | Texture, 16 | } from '@wonderlandengine/api'; 17 | 18 | export class TestComponentRetarget extends Component { 19 | static TypeName = 'test-component-retarget-params'; 20 | 21 | static Properties = { 22 | animationProp: Property.animation(), 23 | materialProp: Property.material(), 24 | meshProp: Property.mesh(), 25 | textureProp: Property.texture(), 26 | objectProp: Property.object(), 27 | skinProp: Property.skin(), 28 | 29 | /* Reference properties set to `null` to ensure no retargeting occurs */ 30 | animationPropUnset: Property.animation(), 31 | materialPropUnset: Property.material(), 32 | meshPropUnset: Property.mesh(), 33 | texturePropUnset: Property.texture(), 34 | objectPropUnset: Property.object(), 35 | skinPropUnset: Property.skin(), 36 | 37 | /* Non-reference properties to ensure no retargeting occurs */ 38 | boolProp: Property.bool(), 39 | intProp: Property.int(), 40 | floatProp: Property.float(), 41 | enumProp: Property.enum(['a', 'b']), 42 | colorProp: Property.color(), 43 | vector2Prop: Property.vector2(), 44 | vector3Prop: Property.vector3(), 45 | vector4Prop: Property.vector4(), 46 | 47 | /* Defaulted number properties to ensure serializing as undefined works */ 48 | boolPropUnset: Property.bool(true), 49 | intPropUnset: Property.int(7), 50 | floatPropUnset: Property.float(1.2), 51 | enumPropUnset: Property.enum(['a', 'b', 'c'], 'c'), 52 | colorPropUnset: Property.color(0.1, 0.2, 0.3, 0.4), 53 | vector2PropUnset: Property.vector2(1.0, 2.0), 54 | vector3PropUnset: Property.vector3(3.0, 4.0, 5.0), 55 | vector4PropUnset: Property.vector4(6.0, 7.0, 8.0, 9.0), 56 | }; 57 | 58 | animationProp!: Animation; 59 | materialProp!: Material; 60 | meshProp!: Mesh; 61 | skinProp!: Skin; 62 | textureProp!: Texture; 63 | objectProp!: Object3D; 64 | 65 | animationPropUnset!: Animation | null; 66 | materialPropUnset!: Material | null; 67 | meshPropUnset!: Mesh | null; 68 | skinPropUnset!: Skin | null; 69 | texturePropUnset!: Texture | null; 70 | objectPropUnset!: Object3D | null; 71 | 72 | boolProp!: boolean; 73 | intProp!: number; 74 | floatProp!: number; 75 | enumProp!: number; 76 | colorProp!: Float32Array; 77 | vector2Prop!: Float32Array; 78 | vector3Prop!: Float32Array; 79 | vector4Prop!: Float32Array; 80 | 81 | boolPropUnset!: boolean; 82 | intPropUnset!: number; 83 | floatPropUnset!: number; 84 | enumPropUnset!: number; 85 | colorPropUnset!: Float32Array; 86 | vector2PropUnset!: Float32Array; 87 | vector3PropUnset!: Float32Array; 88 | vector4PropUnset!: Float32Array; 89 | } 90 | -------------------------------------------------------------------------------- /packages/test-e2e/projects/advanced/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-advanced", 3 | "version": "1.0.0", 4 | "description": "Test project for resources retargeting", 5 | "type": "module", 6 | "keywords": ["wonderland-engine"], 7 | "dependencies": { 8 | "@wonderlandengine/api": "file:../../.." 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/test-e2e/projects/animations/animation.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/projects/animations/animation.glb -------------------------------------------------------------------------------- /packages/test-e2e/projects/animations/animation2.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/projects/animations/animation2.glb -------------------------------------------------------------------------------- /packages/test-e2e/projects/animations/animation3.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/projects/animations/animation3.glb -------------------------------------------------------------------------------- /packages/test-e2e/projects/animations/animation4.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/projects/animations/animation4.glb -------------------------------------------------------------------------------- /packages/test-e2e/projects/components/TestJsComponentsMain.wlp: -------------------------------------------------------------------------------- 1 | { 2 | "objects": { 3 | "47": { 4 | "name": "Test", 5 | "components": [ 6 | { 7 | "type": "test-component-activated-with-param", 8 | "test-component-activated-with-param": { 9 | "param": 42 10 | } 11 | }, 12 | { 13 | "type": "test-component-deactivating-other" 14 | }, 15 | { 16 | "type": "test-component-inactive-on-start", 17 | "active": false 18 | }, 19 | { 20 | "type": "test-component-getting-activated", 21 | "active": false 22 | }, 23 | { 24 | "type": "test-component-activating-other" 25 | }, 26 | { 27 | "type": "test-component-getting-deactivated" 28 | }, 29 | { 30 | "type": "test-component-destroying-other", 31 | "test-component-destroying-other": { 32 | "target": "47" 33 | } 34 | }, 35 | { 36 | "type": "test-component-getting-destroyed" 37 | } 38 | ] 39 | }, 40 | "48": { 41 | "name": "Views" 42 | }, 43 | "49": { 44 | "name": "Left", 45 | "parent": "48", 46 | "components": [ 47 | { 48 | "type": "view" 49 | } 50 | ] 51 | }, 52 | "50": { 53 | "name": "Right", 54 | "parent": "48", 55 | "components": [ 56 | { 57 | "type": "view" 58 | } 59 | ] 60 | } 61 | }, 62 | "meshes": { 63 | "p0": { 64 | "link": { 65 | "name": "PrimitivePlane", 66 | "file": "default" 67 | } 68 | }, 69 | "p1": { 70 | "link": { 71 | "name": "PrimitiveCube", 72 | "file": "default" 73 | } 74 | }, 75 | "p2": { 76 | "link": { 77 | "name": "PrimitiveSphere", 78 | "file": "default" 79 | } 80 | }, 81 | "p3": { 82 | "link": { 83 | "name": "PrimitiveCone", 84 | "file": "default" 85 | } 86 | }, 87 | "p4": { 88 | "link": { 89 | "name": "PrimitiveCylinder", 90 | "file": "default" 91 | } 92 | }, 93 | "p5": { 94 | "link": { 95 | "name": "PrimitiveCircle", 96 | "file": "default" 97 | } 98 | } 99 | }, 100 | "textures": {}, 101 | "images": {}, 102 | "materials": { 103 | "DefaultFontMaterial": { 104 | "link": { 105 | "name": "DefaultFontMaterial", 106 | "file": "default" 107 | } 108 | } 109 | }, 110 | "shaders": { 111 | "1": { 112 | "link": { 113 | "name": "Background.frag", 114 | "file": "default" 115 | } 116 | }, 117 | "2": { 118 | "link": { 119 | "name": "lib\\Color.glsl", 120 | "file": "default" 121 | } 122 | }, 123 | "3": { 124 | "link": { 125 | "name": "lib\\Compatibility.frag", 126 | "file": "default" 127 | } 128 | }, 129 | "4": { 130 | "link": { 131 | "name": "lib\\CoordinateSystems.glsl", 132 | "file": "default" 133 | } 134 | }, 135 | "5": { 136 | "link": { 137 | "name": "Depth.frag", 138 | "file": "default" 139 | } 140 | }, 141 | "7": { 142 | "link": { 143 | "name": "DistanceFieldVector.frag", 144 | "file": "default" 145 | } 146 | }, 147 | "9": { 148 | "link": { 149 | "name": "Dynamic.vert", 150 | "file": "default" 151 | } 152 | }, 153 | "10": { 154 | "link": { 155 | "name": "Flat.frag", 156 | "file": "default" 157 | } 158 | }, 159 | "13": { 160 | "link": { 161 | "name": "FullScreenTriangle.vert", 162 | "file": "default" 163 | } 164 | }, 165 | "14": { 166 | "link": { 167 | "name": "lib\\GI.frag", 168 | "file": "default" 169 | } 170 | }, 171 | "15": { 172 | "link": { 173 | "name": "lib\\Inputs.frag", 174 | "file": "default" 175 | } 176 | }, 177 | "16": { 178 | "link": { 179 | "name": "lib\\Lights.frag", 180 | "file": "default" 181 | } 182 | }, 183 | "17": { 184 | "link": { 185 | "name": "lib\\Materials.frag", 186 | "file": "default" 187 | } 188 | }, 189 | "18": { 190 | "link": { 191 | "name": "lib\\Math.glsl", 192 | "file": "default" 193 | } 194 | }, 195 | "19": { 196 | "link": { 197 | "name": "MeshVisualizer.frag", 198 | "file": "default" 199 | } 200 | }, 201 | "21": { 202 | "link": { 203 | "name": "lib\\Packing.frag", 204 | "file": "default" 205 | } 206 | }, 207 | "22": { 208 | "link": { 209 | "name": "Phong.frag", 210 | "file": "default" 211 | } 212 | }, 213 | "25": { 214 | "link": { 215 | "name": "Physical.frag", 216 | "file": "default" 217 | } 218 | }, 219 | "28": { 220 | "link": { 221 | "name": "lib\\Quaternion.glsl", 222 | "file": "default" 223 | } 224 | }, 225 | "29": { 226 | "link": { 227 | "name": "lib\\ShaderType.glsl", 228 | "file": "default" 229 | } 230 | }, 231 | "30": { 232 | "link": { 233 | "name": "Skinning.vert", 234 | "file": "default" 235 | } 236 | }, 237 | "31": { 238 | "link": { 239 | "name": "Sky.frag", 240 | "file": "default" 241 | } 242 | }, 243 | "32": { 244 | "link": { 245 | "name": "lib\\Slug.frag", 246 | "file": "default" 247 | } 248 | }, 249 | "33": { 250 | "link": { 251 | "name": "lib\\Slug.vert", 252 | "file": "default" 253 | } 254 | }, 255 | "34": { 256 | "link": { 257 | "name": "lib\\Surface.frag", 258 | "file": "default" 259 | } 260 | }, 261 | "35": { 262 | "link": { 263 | "name": "Text.frag", 264 | "file": "default" 265 | } 266 | }, 267 | "37": { 268 | "link": { 269 | "name": "Text.vert", 270 | "file": "default" 271 | } 272 | }, 273 | "38": { 274 | "link": { 275 | "name": "lib\\Textures.frag", 276 | "file": "default" 277 | } 278 | }, 279 | "39": { 280 | "link": { 281 | "name": "lib\\Uniforms.glsl", 282 | "file": "default" 283 | } 284 | }, 285 | "40": { 286 | "link": { 287 | "name": "Particle.frag", 288 | "file": "default" 289 | } 290 | }, 291 | "51": { 292 | "link": { 293 | "name": "TileFeedback.frag", 294 | "file": "default" 295 | } 296 | } 297 | }, 298 | "animations": {}, 299 | "skins": {}, 300 | "pipelines": { 301 | "6": { 302 | "link": { 303 | "name": "Depth", 304 | "file": "default" 305 | } 306 | }, 307 | "8": { 308 | "link": { 309 | "name": "DistanceFieldVector", 310 | "file": "default" 311 | } 312 | }, 313 | "11": { 314 | "link": { 315 | "name": "Flat Opaque", 316 | "file": "default" 317 | } 318 | }, 319 | "12": { 320 | "link": { 321 | "name": "Flat Opaque Textured", 322 | "file": "default" 323 | } 324 | }, 325 | "20": { 326 | "link": { 327 | "name": "MeshVisualizer", 328 | "file": "default" 329 | } 330 | }, 331 | "23": { 332 | "link": { 333 | "name": "Phong Opaque", 334 | "file": "default" 335 | } 336 | }, 337 | "24": { 338 | "link": { 339 | "name": "Phong Opaque Textured", 340 | "file": "default" 341 | } 342 | }, 343 | "26": { 344 | "link": { 345 | "name": "Physical Opaque", 346 | "file": "default" 347 | } 348 | }, 349 | "27": { 350 | "link": { 351 | "name": "Physical Opaque Textured", 352 | "file": "default" 353 | } 354 | }, 355 | "36": { 356 | "link": { 357 | "name": "Text", 358 | "file": "default" 359 | } 360 | }, 361 | "41": { 362 | "link": { 363 | "name": "Phong Normalmapped", 364 | "file": "default" 365 | } 366 | }, 367 | "42": { 368 | "link": { 369 | "name": "Phong Lightmapped", 370 | "file": "default" 371 | } 372 | }, 373 | "43": { 374 | "link": { 375 | "name": "Foliage", 376 | "file": "default" 377 | } 378 | }, 379 | "44": { 380 | "link": { 381 | "name": "Particle", 382 | "file": "default" 383 | } 384 | }, 385 | "45": { 386 | "link": { 387 | "name": "Sky", 388 | "file": "default" 389 | } 390 | }, 391 | "52": { 392 | "link": { 393 | "name": "TileFeedback", 394 | "file": "default" 395 | } 396 | } 397 | }, 398 | "fonts": { 399 | "46": { 400 | "link": { 401 | "name": "DejaVuSans.ttf", 402 | "file": "default" 403 | } 404 | } 405 | }, 406 | "languages": {}, 407 | "settings": { 408 | "project": { 409 | "name": "TestJsComponentsMain", 410 | "version": [ 411 | 1, 412 | 1, 413 | 4 414 | ] 415 | }, 416 | "scripting": { 417 | "sourcePaths": [ 418 | "js" 419 | ], 420 | "entryPoint": null, 421 | "bundlingType": "none" 422 | }, 423 | "vr": { 424 | "leftEyeObject": "49", 425 | "rightEyeObject": "50", 426 | "enable": false 427 | } 428 | }, 429 | "files": {} 430 | } -------------------------------------------------------------------------------- /packages/test-e2e/projects/components/js/test-component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This component uses the local api on purpose. 3 | * 4 | * This greatly simplify the testing process, since we don't need 5 | * to install the `wonderlandengine/api` package in the project. 6 | */ 7 | import {Component, Property} from '@wonderlandengine/api'; 8 | 9 | export class TestComponentBase extends Component { 10 | deactivated = false; 11 | order: ('init' | 'started' | 'activated' | 'deactivated' | 'destroyed')[] = []; 12 | init() { 13 | this.order.push('init'); 14 | } 15 | start() { 16 | this.order.push('started'); 17 | } 18 | onActivate() { 19 | this.order.push('activated'); 20 | } 21 | onDeactivate() { 22 | this.deactivated = true; 23 | this.order.push('deactivated'); 24 | } 25 | onDestroy(): void { 26 | this.order.push('destroyed'); 27 | } 28 | } 29 | 30 | export class TestComponentActivatedWithParam extends TestComponentBase { 31 | static TypeName = 'test-component-activated-with-param'; 32 | 33 | static Properties = { 34 | param: Property.int(), 35 | }; 36 | 37 | param = 0; 38 | hi: string = ''; 39 | 40 | init() { 41 | this.hi = 'Hello'; 42 | super.init(); 43 | } 44 | start() { 45 | this.hi = `${this.hi} ${this.param}`; 46 | super.start(); 47 | } 48 | } 49 | 50 | export class TestComponentInactiveOnStart extends TestComponentBase { 51 | static TypeName = 'test-component-inactive-on-start'; 52 | } 53 | 54 | export class TestComponentGettingActivated extends TestComponentBase { 55 | static TypeName = 'test-component-getting-activated'; 56 | } 57 | 58 | export class TestComponentActivatingOther extends TestComponentBase { 59 | static TypeName = 'test-component-activating-other'; 60 | 61 | start(): void { 62 | const other = this.object.getComponent(TestComponentGettingActivated)!; 63 | other.active = true; 64 | /* Last to skip count increment if an error is thrown */ 65 | super.start(); 66 | } 67 | } 68 | 69 | export class TestComponentGettingDeactivated extends TestComponentBase { 70 | static TypeName = 'test-component-getting-deactivated'; 71 | } 72 | 73 | export class TestComponentDeactivatingOther extends TestComponentBase { 74 | static TypeName = 'test-component-deactivating-other'; 75 | 76 | start(): void { 77 | const other = this.object.getComponent(TestComponentGettingDeactivated)!; 78 | other.active = false; 79 | /* Last to skip count increment if an error is thrown */ 80 | super.start(); 81 | } 82 | } 83 | 84 | export class TestComponentGettingDestroyed extends TestComponentBase { 85 | static TypeName = 'test-component-getting-destroyed'; 86 | } 87 | 88 | export class TestComponentDestroyingOther extends TestComponentBase { 89 | static TypeName = 'test-component-destroying-other'; 90 | 91 | /** Store the target to be able to access it post-destruction */ 92 | target: TestComponentGettingDestroyed = null!; 93 | 94 | start(): void { 95 | this.target = this.object.getComponent(TestComponentGettingDestroyed)!; 96 | this.target.destroy(); 97 | /* Last to skip count increment if an error is thrown */ 98 | super.start(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /packages/test-e2e/projects/components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-js-components", 3 | "version": "1.0.0", 4 | "description": "My Wonderland project", 5 | "main": "js/index.js", 6 | "type": "module", 7 | "module": "js/index.js", 8 | "scripts": { 9 | "build": "echo \"The 'build' script is run by the editor and should produce your application bundle\"" 10 | }, 11 | "keywords": [ 12 | "wonderland-engine" 13 | ], 14 | "dependencies": { 15 | "@wonderlandengine/api": "file:../../.." 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/test-e2e/projects/language-switching/LanguageSwitching.wlp: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "project": { 4 | "name": "LanguageSwitching", 5 | "version": [ 6 | 1, 7 | 0, 8 | 4 9 | ] 10 | }, 11 | "vr": { 12 | "enable": false 13 | }, 14 | "localization": { 15 | "defaultLanguage": "en", 16 | "enableZipCompression": false 17 | }, 18 | "scripting": { 19 | "sourcePaths": [ 20 | "js" 21 | ], 22 | "bundlingType": "none", 23 | "entryPoint": null 24 | } 25 | }, 26 | "files": {}, 27 | "images": {}, 28 | "shaders": { 29 | "1": { 30 | "link": { 31 | "name": "Background.frag", 32 | "file": "default" 33 | } 34 | }, 35 | "2": { 36 | "link": { 37 | "name": "lib\\Color.glsl", 38 | "file": "default" 39 | } 40 | }, 41 | "3": { 42 | "link": { 43 | "name": "lib\\Compatibility.frag", 44 | "file": "default" 45 | } 46 | }, 47 | "4": { 48 | "link": { 49 | "name": "lib\\CoordinateSystems.glsl", 50 | "file": "default" 51 | } 52 | }, 53 | "5": { 54 | "link": { 55 | "name": "Depth.frag", 56 | "file": "default" 57 | } 58 | }, 59 | "7": { 60 | "link": { 61 | "name": "DistanceFieldVector.frag", 62 | "file": "default" 63 | } 64 | }, 65 | "9": { 66 | "link": { 67 | "name": "Dynamic.vert", 68 | "file": "default" 69 | } 70 | }, 71 | "10": { 72 | "link": { 73 | "name": "Flat.frag", 74 | "file": "default" 75 | } 76 | }, 77 | "13": { 78 | "link": { 79 | "name": "FullScreenTriangle.vert", 80 | "file": "default" 81 | } 82 | }, 83 | "14": { 84 | "link": { 85 | "name": "lib\\GI.frag", 86 | "file": "default" 87 | } 88 | }, 89 | "15": { 90 | "link": { 91 | "name": "lib\\Inputs.frag", 92 | "file": "default" 93 | } 94 | }, 95 | "16": { 96 | "link": { 97 | "name": "lib\\Lights.frag", 98 | "file": "default" 99 | } 100 | }, 101 | "17": { 102 | "link": { 103 | "name": "lib\\Materials.frag", 104 | "file": "default" 105 | } 106 | }, 107 | "18": { 108 | "link": { 109 | "name": "lib\\Math.glsl", 110 | "file": "default" 111 | } 112 | }, 113 | "19": { 114 | "link": { 115 | "name": "MeshVisualizer.frag", 116 | "file": "default" 117 | } 118 | }, 119 | "21": { 120 | "link": { 121 | "name": "lib\\Packing.frag", 122 | "file": "default" 123 | } 124 | }, 125 | "22": { 126 | "link": { 127 | "name": "Phong.frag", 128 | "file": "default" 129 | } 130 | }, 131 | "25": { 132 | "link": { 133 | "name": "Physical.frag", 134 | "file": "default" 135 | } 136 | }, 137 | "28": { 138 | "link": { 139 | "name": "lib\\Quaternion.glsl", 140 | "file": "default" 141 | } 142 | }, 143 | "29": { 144 | "link": { 145 | "name": "lib\\ShaderType.glsl", 146 | "file": "default" 147 | } 148 | }, 149 | "30": { 150 | "link": { 151 | "name": "Skinning.vert", 152 | "file": "default" 153 | } 154 | }, 155 | "31": { 156 | "link": { 157 | "name": "Sky.frag", 158 | "file": "default" 159 | } 160 | }, 161 | "32": { 162 | "link": { 163 | "name": "lib\\Slug.frag", 164 | "file": "default" 165 | } 166 | }, 167 | "33": { 168 | "link": { 169 | "name": "lib\\Slug.vert", 170 | "file": "default" 171 | } 172 | }, 173 | "34": { 174 | "link": { 175 | "name": "lib\\Surface.frag", 176 | "file": "default" 177 | } 178 | }, 179 | "35": { 180 | "link": { 181 | "name": "Text.frag", 182 | "file": "default" 183 | } 184 | }, 185 | "37": { 186 | "link": { 187 | "name": "Text.vert", 188 | "file": "default" 189 | } 190 | }, 191 | "38": { 192 | "link": { 193 | "name": "lib\\Textures.frag", 194 | "file": "default" 195 | } 196 | }, 197 | "39": { 198 | "link": { 199 | "name": "lib\\Uniforms.glsl", 200 | "file": "default" 201 | } 202 | }, 203 | "40": { 204 | "link": { 205 | "name": "Particle.frag", 206 | "file": "default" 207 | } 208 | } 209 | }, 210 | "meshes": { 211 | "p0": { 212 | "link": { 213 | "name": "PrimitivePlane", 214 | "file": "default" 215 | } 216 | }, 217 | "p1": { 218 | "link": { 219 | "name": "PrimitiveCube", 220 | "file": "default" 221 | } 222 | }, 223 | "p2": { 224 | "link": { 225 | "name": "PrimitiveSphere", 226 | "file": "default" 227 | } 228 | }, 229 | "p3": { 230 | "link": { 231 | "name": "PrimitiveCone", 232 | "file": "default" 233 | } 234 | }, 235 | "p4": { 236 | "link": { 237 | "name": "PrimitiveCylinder", 238 | "file": "default" 239 | } 240 | }, 241 | "p5": { 242 | "link": { 243 | "name": "PrimitiveCircle", 244 | "file": "default" 245 | } 246 | } 247 | }, 248 | "textures": {}, 249 | "materials": { 250 | "DefaultFontMaterial": { 251 | "link": { 252 | "name": "DefaultFontMaterial", 253 | "file": "default" 254 | } 255 | } 256 | }, 257 | "objects": { 258 | "47": { 259 | "name": "View", 260 | "components": [ 261 | { 262 | "type": "view" 263 | } 264 | ] 265 | }, 266 | "48": { 267 | "name": "Text", 268 | "components": [ 269 | { 270 | "type": "text", 271 | "text": { 272 | "text": "Hello Wonderland!" 273 | } 274 | } 275 | ] 276 | }, 277 | "49": { 278 | "name": "JS", 279 | "components": [ 280 | { 281 | "type": "test-component-translate-params", 282 | "test-component-translate-params": { 283 | "animationProp": null, 284 | "stringProp": "Yahallo Wonderland!", 285 | "floatProp": 4.5 286 | } 287 | } 288 | ] 289 | }, 290 | "50": { 291 | "name": "Mixed", 292 | "components": [ 293 | { 294 | "type": "input" 295 | }, 296 | { 297 | "type": "text", 298 | "text": { 299 | "text": "Hi Wonderland?" 300 | } 301 | }, 302 | { 303 | "type": "test-component-translate-params", 304 | "test-component-translate-params": { 305 | "floatProp": 0.0, 306 | "stringProp": "First Wonderland!" 307 | } 308 | }, 309 | { 310 | "type": "view" 311 | }, 312 | { 313 | "type": "test-component-translate-params", 314 | "test-component-translate-params": { 315 | "floatProp": 1.0, 316 | "stringProp": "Second Wonderland!" 317 | } 318 | } 319 | ] 320 | }, 321 | "51": { 322 | "name": "Text unchanged", 323 | "components": [ 324 | { 325 | "type": "text", 326 | "text": { 327 | "text": "No translation" 328 | } 329 | } 330 | ] 331 | } 332 | }, 333 | "animations": {}, 334 | "skins": {}, 335 | "pipelines": { 336 | "6": { 337 | "link": { 338 | "name": "Depth", 339 | "file": "default" 340 | } 341 | }, 342 | "8": { 343 | "link": { 344 | "name": "DistanceFieldVector", 345 | "file": "default" 346 | } 347 | }, 348 | "11": { 349 | "link": { 350 | "name": "Flat Opaque", 351 | "file": "default" 352 | } 353 | }, 354 | "12": { 355 | "link": { 356 | "name": "Flat Opaque Textured", 357 | "file": "default" 358 | } 359 | }, 360 | "20": { 361 | "link": { 362 | "name": "MeshVisualizer", 363 | "file": "default" 364 | } 365 | }, 366 | "23": { 367 | "link": { 368 | "name": "Phong Opaque", 369 | "file": "default" 370 | } 371 | }, 372 | "24": { 373 | "link": { 374 | "name": "Phong Opaque Textured", 375 | "file": "default" 376 | } 377 | }, 378 | "26": { 379 | "link": { 380 | "name": "Physical Opaque", 381 | "file": "default" 382 | } 383 | }, 384 | "27": { 385 | "link": { 386 | "name": "Physical Opaque Textured", 387 | "file": "default" 388 | } 389 | }, 390 | "36": { 391 | "link": { 392 | "name": "Text", 393 | "file": "default" 394 | } 395 | }, 396 | "41": { 397 | "link": { 398 | "name": "Phong Normalmapped", 399 | "file": "default" 400 | } 401 | }, 402 | "42": { 403 | "link": { 404 | "name": "Phong Lightmapped", 405 | "file": "default" 406 | } 407 | }, 408 | "43": { 409 | "link": { 410 | "name": "Foliage", 411 | "file": "default" 412 | } 413 | }, 414 | "44": { 415 | "link": { 416 | "name": "Particle", 417 | "file": "default" 418 | } 419 | }, 420 | "45": { 421 | "link": { 422 | "name": "Sky", 423 | "file": "default" 424 | } 425 | } 426 | }, 427 | "fonts": { 428 | "46": { 429 | "link": { 430 | "name": "DejaVuSans.ttf", 431 | "file": "default" 432 | }, 433 | "outline": true, 434 | "outlineSize": 0.2 435 | } 436 | }, 437 | "languages": { 438 | "en": { 439 | "name": "English" 440 | }, 441 | "nl": { 442 | "name": "Dutch" 443 | } 444 | } 445 | } -------------------------------------------------------------------------------- /packages/test-e2e/projects/language-switching/js/test-component-translate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This component uses the local api on purpose. 3 | * 4 | * This greatly simplify the testing process, since we don't need 5 | * to install the `wonderlandengine/api` package in the project. 6 | */ 7 | import {Animation, Component, Property} from '@wonderlandengine/api'; 8 | 9 | export class TestComponentTranslate extends Component { 10 | static TypeName = 'test-component-translate-params'; 11 | 12 | static Properties = { 13 | animationProp: Property.animation(), 14 | stringProp: Property.string(), 15 | floatProp: Property.float(), 16 | }; 17 | 18 | animationProp!: Animation | null; 19 | stringProp!: string; 20 | floatProp!: number; 21 | } 22 | -------------------------------------------------------------------------------- /packages/test-e2e/projects/language-switching/languages/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "translation": { 3 | "48-text-0": "Hello Wonderland!", 4 | "49-js-0-stringProp": "Yahallo Wonderland!", 5 | "50-js-2-stringProp": "First Wonderland!", 6 | "50-js-4-stringProp": "Second Wonderland!", 7 | "50-text-1": "Hi Wonderland?", 8 | "51-text-0": "No translation", 9 | "projectName": "LanguageSwitching" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/test-e2e/projects/language-switching/languages/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "translation": { 3 | "48-text-0": "Hallo Wonderland!", 4 | "49-js-0-stringProp": "Yuhullo Wonderland!", 5 | "50-js-2-stringProp": "Erst Wonderland!", 6 | "50-js-4-stringProp": "Zweijt Wonderland!", 7 | "50-text-1": "Hej Wynderland?", 8 | "51-text-0": null, 9 | "projectName": "LanguageSwitching but Dutch!" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/test-e2e/projects/language-switching/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-js-language-switching", 3 | "version": "1.0.0", 4 | "description": "My Wonderland project", 5 | "main": "js/index.js", 6 | "type": "module", 7 | "module": "js/index.js", 8 | "scripts": { 9 | "build": "echo \"The 'build' script is run by the editor and should produce your application bundle\"" 10 | }, 11 | "keywords": [ 12 | "wonderland-engine" 13 | ], 14 | "dependencies": { 15 | "@wonderlandengine/api": "file:../../.." 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/test-e2e/projects/morph-targets/assets/MorphPrimitivesTest.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/projects/morph-targets/assets/MorphPrimitivesTest.glb -------------------------------------------------------------------------------- /packages/test-e2e/projects/morph-targets/assets/MorphStressTest.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/projects/morph-targets/assets/MorphStressTest.glb -------------------------------------------------------------------------------- /packages/test-e2e/projects/preferences.json: -------------------------------------------------------------------------------- 1 | { 2 | "notifications": { 3 | "projectSaveFailure": false, 4 | "packageSuccess": false, 5 | "packageFailure": false, 6 | "javaScriptError": false 7 | }, 8 | "paths": { 9 | "projectDefault": "", 10 | "nodePath": "", 11 | "npmEntryPath": "" 12 | }, 13 | "server": { 14 | "ssl": { 15 | "enabled": false, 16 | "certFile": "", 17 | "keyFile": "", 18 | "keyPassphrase": "", 19 | "dhParamsFile": "" 20 | } 21 | }, 22 | "startup": { 23 | "startServer": false, 24 | "autoReloadBrowser": false, 25 | "forceFullPageReloads": false, 26 | "watchJavaScript": false 27 | }, 28 | "package": { 29 | "onSave": false, 30 | "onWindowSwitch": false, 31 | "onXRHeadsetActivated": false, 32 | "onTemplateChange": false 33 | }, 34 | "editor": { 35 | "mouseLookSensitivity": 1.0, 36 | "mousePanSensitivity": 0.019999999552965164, 37 | "autoNpmInstall": false, 38 | "useLocalApiJs": false, 39 | "enablePlugins": false 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/test-e2e/projects/streaming/4x4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/projects/streaming/4x4.png -------------------------------------------------------------------------------- /packages/test-e2e/projects/streaming/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/projects/streaming/image.png -------------------------------------------------------------------------------- /packages/test-e2e/projects/tiny/Tiny.wlp: -------------------------------------------------------------------------------- 1 | { 2 | "objects": { 3 | "39": { 4 | "name": "View", 5 | "components": [ 6 | { 7 | "type": "view" 8 | } 9 | ] 10 | } 11 | }, 12 | "meshes": { 13 | "p0": { 14 | "link": { 15 | "name": "PrimitivePlane", 16 | "file": "default" 17 | } 18 | }, 19 | "p1": { 20 | "link": { 21 | "name": "PrimitiveCube", 22 | "file": "default" 23 | } 24 | }, 25 | "p2": { 26 | "link": { 27 | "name": "PrimitiveSphere", 28 | "file": "default" 29 | } 30 | }, 31 | "p3": { 32 | "link": { 33 | "name": "PrimitiveCone", 34 | "file": "default" 35 | } 36 | }, 37 | "p4": { 38 | "link": { 39 | "name": "PrimitiveCylinder", 40 | "file": "default" 41 | } 42 | }, 43 | "p5": { 44 | "link": { 45 | "name": "PrimitiveCircle", 46 | "file": "default" 47 | } 48 | } 49 | }, 50 | "textures": {}, 51 | "images": {}, 52 | "materials": { 53 | "DefaultFontMaterial": { 54 | "link": { 55 | "name": "DefaultFontMaterial", 56 | "file": "default" 57 | } 58 | } 59 | }, 60 | "shaders": { 61 | "1": { 62 | "link": { 63 | "name": "Background.frag", 64 | "file": "default" 65 | } 66 | }, 67 | "2": { 68 | "link": { 69 | "name": "Depth.frag", 70 | "file": "default" 71 | } 72 | }, 73 | "4": { 74 | "link": { 75 | "name": "DistanceFieldVector.frag", 76 | "file": "default" 77 | } 78 | }, 79 | "6": { 80 | "link": { 81 | "name": "Dynamic.vert", 82 | "file": "default" 83 | } 84 | }, 85 | "7": { 86 | "link": { 87 | "name": "Flat.frag", 88 | "file": "default" 89 | } 90 | }, 91 | "10": { 92 | "link": { 93 | "name": "FullScreenTriangle.vert", 94 | "file": "default" 95 | } 96 | }, 97 | "11": { 98 | "link": { 99 | "name": "MeshVisualizer.frag", 100 | "file": "default" 101 | } 102 | }, 103 | "13": { 104 | "link": { 105 | "name": "Phong.frag", 106 | "file": "default" 107 | } 108 | }, 109 | "16": { 110 | "link": { 111 | "name": "Physical.frag", 112 | "file": "default" 113 | } 114 | }, 115 | "19": { 116 | "link": { 117 | "name": "Skinning.vert", 118 | "file": "default" 119 | } 120 | }, 121 | "20": { 122 | "link": { 123 | "name": "Sky.frag", 124 | "file": "default" 125 | } 126 | }, 127 | "21": { 128 | "link": { 129 | "name": "Text.frag", 130 | "file": "default" 131 | } 132 | }, 133 | "23": { 134 | "link": { 135 | "name": "Text.vert", 136 | "file": "default" 137 | } 138 | }, 139 | "24": { 140 | "link": { 141 | "name": "TileFeedback.frag", 142 | "file": "default" 143 | } 144 | }, 145 | "25": { 146 | "link": { 147 | "name": "Particle.frag", 148 | "file": "default" 149 | } 150 | }, 151 | "40": { 152 | "link": { 153 | "name": "lib\\Color.glsl", 154 | "file": "default" 155 | } 156 | }, 157 | "41": { 158 | "link": { 159 | "name": "lib\\Compatibility.frag", 160 | "file": "default" 161 | } 162 | }, 163 | "42": { 164 | "link": { 165 | "name": "lib\\CoordinateSystems.glsl", 166 | "file": "default" 167 | } 168 | }, 169 | "43": { 170 | "link": { 171 | "name": "lib\\GI.frag", 172 | "file": "default" 173 | } 174 | }, 175 | "44": { 176 | "link": { 177 | "name": "lib\\Inputs.frag", 178 | "file": "default" 179 | } 180 | }, 181 | "45": { 182 | "link": { 183 | "name": "lib\\Lights.frag", 184 | "file": "default" 185 | } 186 | }, 187 | "46": { 188 | "link": { 189 | "name": "lib\\Materials.frag", 190 | "file": "default" 191 | } 192 | }, 193 | "47": { 194 | "link": { 195 | "name": "lib\\Math.glsl", 196 | "file": "default" 197 | } 198 | }, 199 | "48": { 200 | "link": { 201 | "name": "lib\\Packing.frag", 202 | "file": "default" 203 | } 204 | }, 205 | "49": { 206 | "link": { 207 | "name": "lib\\Quaternion.glsl", 208 | "file": "default" 209 | } 210 | }, 211 | "50": { 212 | "link": { 213 | "name": "lib\\Slug.frag", 214 | "file": "default" 215 | } 216 | }, 217 | "51": { 218 | "link": { 219 | "name": "lib\\Slug.vert", 220 | "file": "default" 221 | } 222 | }, 223 | "52": { 224 | "link": { 225 | "name": "lib\\Surface.frag", 226 | "file": "default" 227 | } 228 | }, 229 | "53": { 230 | "link": { 231 | "name": "lib\\Textures.frag", 232 | "file": "default" 233 | } 234 | }, 235 | "55": { 236 | "link": { 237 | "name": "lib\\Uniforms.glsl", 238 | "file": "default" 239 | } 240 | } 241 | }, 242 | "animations": {}, 243 | "skins": {}, 244 | "pipelines": { 245 | "3": { 246 | "link": { 247 | "name": "Depth", 248 | "file": "default" 249 | } 250 | }, 251 | "5": { 252 | "link": { 253 | "name": "DistanceFieldVector", 254 | "file": "default" 255 | } 256 | }, 257 | "8": { 258 | "link": { 259 | "name": "Flat Opaque", 260 | "file": "default" 261 | } 262 | }, 263 | "9": { 264 | "link": { 265 | "name": "Flat Opaque Textured", 266 | "file": "default" 267 | } 268 | }, 269 | "12": { 270 | "link": { 271 | "name": "MeshVisualizer", 272 | "file": "default" 273 | } 274 | }, 275 | "14": { 276 | "link": { 277 | "name": "Phong Opaque", 278 | "file": "default" 279 | }, 280 | "features": { 281 | "DEPRECATED_LIGHT_ATTENUATION": true, 282 | "WITH_EMISSIVE": true 283 | } 284 | }, 285 | "15": { 286 | "link": { 287 | "name": "Phong Opaque Textured", 288 | "file": "default" 289 | }, 290 | "features": { 291 | "DEPRECATED_LIGHT_ATTENUATION": true 292 | } 293 | }, 294 | "17": { 295 | "link": { 296 | "name": "Physical Opaque", 297 | "file": "default" 298 | } 299 | }, 300 | "18": { 301 | "link": { 302 | "name": "Physical Opaque Textured", 303 | "file": "default" 304 | } 305 | }, 306 | "22": { 307 | "link": { 308 | "name": "Text", 309 | "file": "default" 310 | } 311 | }, 312 | "26": { 313 | "link": { 314 | "name": "Phong Normalmapped", 315 | "file": "default" 316 | }, 317 | "features": { 318 | "DEPRECATED_LIGHT_ATTENUATION": true 319 | } 320 | }, 321 | "27": { 322 | "link": { 323 | "name": "Phong Lightmapped", 324 | "file": "default" 325 | }, 326 | "features": { 327 | "DEPRECATED_LIGHT_ATTENUATION": true 328 | } 329 | }, 330 | "28": { 331 | "link": { 332 | "name": "Foliage", 333 | "file": "default" 334 | }, 335 | "features": { 336 | "DEPRECATED_LIGHT_ATTENUATION": true 337 | } 338 | }, 339 | "29": { 340 | "link": { 341 | "name": "Particle", 342 | "file": "default" 343 | } 344 | }, 345 | "30": { 346 | "link": { 347 | "name": "Sky", 348 | "file": "default" 349 | }, 350 | "features": { 351 | "GRADIENT": true, 352 | "TEXTURED": false 353 | } 354 | }, 355 | "54": { 356 | "link": { 357 | "name": "TileFeedback", 358 | "file": "default" 359 | } 360 | } 361 | }, 362 | "languages": {}, 363 | "settings": { 364 | "project": { 365 | "name": "Tiny", 366 | "version": [ 367 | 1, 368 | 1, 369 | 6 370 | ] 371 | }, 372 | "vr": { 373 | "enable": false 374 | }, 375 | "scripting": { 376 | "bundlingType": "none", 377 | "entryPoint": null, 378 | "sourcePaths": [ 379 | "js" 380 | ] 381 | } 382 | }, 383 | "files": {}, 384 | "fonts": {} 385 | } -------------------------------------------------------------------------------- /packages/test-e2e/resources/2x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/resources/2x2.png -------------------------------------------------------------------------------- /packages/test-e2e/resources/Box.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/resources/Box.glb -------------------------------------------------------------------------------- /packages/test-e2e/resources/BoxWithExtensions.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/resources/BoxWithExtensions.glb -------------------------------------------------------------------------------- /packages/test-e2e/resources/Broken.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/test-e2e/resources/Cube.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/resources/Cube.glb -------------------------------------------------------------------------------- /packages/test-e2e/resources/Rigged.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/resources/Rigged.glb -------------------------------------------------------------------------------- /packages/test-e2e/resources/SimpleSkin.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/resources/SimpleSkin.glb -------------------------------------------------------------------------------- /packages/test-e2e/resources/TwoPlanesWithTextures.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/resources/TwoPlanesWithTextures.glb -------------------------------------------------------------------------------- /packages/test-e2e/resources/UVcube.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WonderlandEngine/api/e90c61c4f5541c4c101c0d6c238652d703fb19f0/packages/test-e2e/resources/UVcube.glb -------------------------------------------------------------------------------- /packages/test-e2e/scene-gltf.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from '@esm-bundle/chai'; 2 | 3 | import {init, projectURL, reset, resourceURL, WL} from './setup.js'; 4 | 5 | import { 6 | AnimationComponent, 7 | CollisionComponent, 8 | Component, 9 | ComponentConstructor, 10 | InputComponent, 11 | LightComponent, 12 | MeshComponent, 13 | Object3D, 14 | PhysXComponent, 15 | Scene, 16 | SceneAppendResultWithExtensions, 17 | PrefabGLTF, 18 | Texture, 19 | ViewComponent, 20 | } from '@wonderlandengine/api'; 21 | import { 22 | fetchWithProgress, 23 | fetchStreamWithProgress, 24 | } from '@wonderlandengine/api/utils/fetch.js'; 25 | 26 | import {dummyImage} from './utils.js'; 27 | import {PhongMaterial} from './types.js'; 28 | 29 | before(() => init({loader: true, physx: true})); 30 | 31 | /* Use in-memory .bin as much as possible to speed up the tests. */ 32 | const bins: ArrayBuffer[] = []; 33 | try { 34 | bins.push( 35 | ...(await Promise.all([ 36 | fetchWithProgress(projectURL('Advanced.bin')), 37 | fetchWithProgress(projectURL('Tiny.bin')), 38 | fetchWithProgress(resourceURL('Box.glb')), 39 | fetchWithProgress(resourceURL('BoxWithExtensions.glb')), 40 | ])) 41 | ); 42 | } catch (e) { 43 | console.error('Failed to load required test scenes'); 44 | throw e; 45 | } 46 | const advancedBin = {buffer: bins[0], baseURL: projectURL('')}; 47 | const tinyBin = {buffer: bins[1], baseURL: projectURL('')}; 48 | const boxGLB = {buffer: bins[2], baseURL: resourceURL('')}; 49 | const boxExtensionsGLB = {buffer: bins[3], baseURL: resourceURL('')}; 50 | 51 | describe('PrefabGLTF', function () { 52 | let scene: PrefabGLTF; 53 | before(async function () { 54 | reset(); 55 | scene = WL.loadGLTFFromBuffer(boxGLB); 56 | }); 57 | 58 | it('can modify scene graph', function () { 59 | class TestComponent extends Component { 60 | static TypeName = 'test-component'; 61 | started = 0; 62 | start() { 63 | ++this.started; 64 | } 65 | } 66 | WL.registerComponent(TestComponent); 67 | 68 | const count = scene.children.length; 69 | const obj = scene.addObject(); 70 | obj.name = 'wonderland'; 71 | expect(obj.name).to.equal('wonderland'); 72 | expect(scene.children).to.have.lengthOf(count + 1); 73 | 74 | /* Ensure we can add a component of each type */ 75 | const classes: ComponentConstructor[] = [ 76 | AnimationComponent, 77 | CollisionComponent, 78 | InputComponent, 79 | ViewComponent, 80 | LightComponent, 81 | MeshComponent, 82 | PhysXComponent, 83 | TestComponent, 84 | ]; 85 | for (const ctor of classes) { 86 | const added = obj.addComponent(ctor)!; 87 | const comp = obj.getComponent(ctor)!; 88 | expect(comp).to.not.be.undefined; 89 | expect(added._id).to.equal(comp._id); 90 | } 91 | }); 92 | }); 93 | 94 | describe('PrefabGLTF > Extensions', function () { 95 | let mainScene: Scene; 96 | let scene: PrefabGLTF; 97 | 98 | before(async function () { 99 | reset(); 100 | 101 | mainScene = await WL.loadMainSceneFromBuffer(tinyBin); 102 | scene = WL.loadGLTFFromBuffer({ 103 | ...boxExtensionsGLB, 104 | extensions: true, 105 | }); 106 | }); 107 | 108 | it('raw extensions', function () { 109 | const rawExtensions = scene.extensions!; 110 | expect(rawExtensions, 'extensions not marshalled').to.not.be.null; 111 | expect(rawExtensions.root).to.eql({'TEST_root_extension': {}}); 112 | }); 113 | 114 | for (let i = 0; i < 2; ++i) { 115 | it(`instantiation ${i}`, function () { 116 | const result = mainScene.instantiate(scene)!; 117 | expect(result, 'instantiation in non active root').to.not.be.undefined; 118 | 119 | const {root, extensions} = result; 120 | 121 | expect(root).to.be.an.instanceof(Object3D); 122 | expect(extensions, 'extensions not remapped').to.not.be.null; 123 | expect(extensions).to.have.all.keys('node', 'mesh', 'idMapping'); 124 | 125 | const child = root.children[0]; 126 | const grandChild = child.children[0]; 127 | 128 | expect(extensions!.node).to.have.all.keys(grandChild._localId); 129 | expect(extensions!.node[grandChild._localId]).to.eql({ 130 | 'TEST_node_extension': {}, 131 | }); 132 | expect(extensions!.mesh).to.have.all.keys(grandChild._localId); 133 | expect(extensions!.mesh[grandChild._localId]).to.eql({ 134 | 'TEST_mesh_extension': {}, 135 | }); 136 | }); 137 | } 138 | }); 139 | 140 | describe('Scene GLTF > Legacy', function () { 141 | beforeEach(reset); 142 | 143 | describe('append()', function () { 144 | it('.glb', async function () { 145 | /* Make sure we have all the pipelines used in the streamed .bin */ 146 | await WL.scene.load(advancedBin); 147 | const root = (await WL.scene.append(boxGLB.buffer)) as Object3D; 148 | 149 | expect(root).to.be.an.instanceof(Object3D); 150 | expect(root.children).to.have.a.lengthOf(1); 151 | const child = root.children[0]; 152 | expect(child.children).to.have.a.lengthOf(1); 153 | const grandChild = child.children[0]; 154 | 155 | const meshComponents = grandChild.getComponents('mesh') as MeshComponent[]; 156 | expect(meshComponents).to.have.a.lengthOf(1); 157 | const mesh = meshComponents[0].mesh; 158 | expect(mesh).to.not.be.null; 159 | expect(mesh!.vertexCount).to.equal(24); 160 | }); 161 | 162 | it('.glb with textures', async function () { 163 | /* Make sure we have all the pipelines used in the streamed .bin */ 164 | await WL.scene.load(advancedBin); 165 | 166 | /* Appends two dummy textures to ensure images indices are correct. */ 167 | const dummyImages = await Promise.all([dummyImage(42, 42), dummyImage(43, 43)]); 168 | new Texture(WL, dummyImages[0]); 169 | new Texture(WL, dummyImages[1]); 170 | 171 | const root = (await WL.scene.append( 172 | resourceURL('TwoPlanesWithTextures.glb') 173 | )) as Object3D; 174 | expect(root).to.be.an.instanceof(Object3D); 175 | expect(root.children).to.have.a.lengthOf(2); 176 | 177 | /* Ensure images are properly retargeted. */ 178 | const images = await WL.imagesPromise; 179 | expect([images[0].width, images[0].height]).to.eql([2, 2]); 180 | expect(images[1]).to.equal(dummyImages[0]); 181 | expect(images[2]).to.equal(dummyImages[1]); 182 | 183 | /* The .glb contains two textures: 2x2 and 4x4 */ 184 | expect([images[3].width, images[3].height]).to.eql([2, 2]); 185 | expect([images[4].width, images[4].height]).to.eql([4, 4]); 186 | 187 | const objA = root.findByName('PlaneA')[0]; 188 | expect(objA).to.not.be.undefined; 189 | const objB = root.findByName('PlaneB')[0]; 190 | expect(objB).to.not.be.undefined; 191 | 192 | const compA = objA.getComponent('mesh'); 193 | expect(compA).to.not.be.undefined; 194 | const materialA = compA?.material as PhongMaterial; 195 | expect(materialA).to.not.be.undefined; 196 | expect(materialA.pipeline).to.equal('Phong Opaque Textured'); 197 | expect(materialA.shininess).to.be.lessThanOrEqual(20); 198 | { 199 | const texture = materialA.diffuseTexture; 200 | expect([texture.width, texture.height]).to.eql([2, 2]); 201 | } 202 | 203 | const compB = objB.getComponent('mesh'); 204 | expect(compB).to.not.be.undefined; 205 | const materialB = compB?.material as PhongMaterial; 206 | expect(materialB).to.not.be.undefined; 207 | expect(materialB.pipeline).to.equal('Phong Opaque Textured'); 208 | expect(materialB.shininess).to.be.greaterThanOrEqual(140); 209 | { 210 | const texture = materialB.diffuseTexture; 211 | expect([texture.width, texture.height]).to.eql([4, 4]); 212 | } 213 | }); 214 | 215 | it('.glb, loadGltfExtensions', async function () { 216 | /* Make sure we have all the pipelines used in the streamed .bin */ 217 | await WL.scene.load(advancedBin); 218 | 219 | const {root, extensions} = (await WL.scene.append(boxExtensionsGLB.buffer, { 220 | loadGltfExtensions: true, 221 | })) as SceneAppendResultWithExtensions; 222 | 223 | expect(root).to.be.an.instanceof(Object3D); 224 | 225 | expect(extensions).to.have.all.keys('root', 'node', 'mesh', 'idMapping'); 226 | expect(extensions.root).to.eql({'TEST_root_extension': {}}); 227 | 228 | const child = root!.children[0]; 229 | const grandChild = child.children[0]; 230 | expect(extensions.node).to.have.all.keys(grandChild.objectId); 231 | expect(extensions.node[grandChild.objectId]).to.eql({ 232 | 'TEST_node_extension': {}, 233 | }); 234 | expect(extensions.mesh).to.have.all.keys(grandChild.objectId); 235 | expect(extensions.mesh[grandChild.objectId]).to.eql({ 236 | 'TEST_mesh_extension': {}, 237 | }); 238 | }); 239 | }); 240 | }); 241 | -------------------------------------------------------------------------------- /packages/test-e2e/scripts/build-projects.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* This script is used to build the Wonderland Engine test projects 4 | * located in test/projects/ */ 5 | 6 | import {spawn} from 'node:child_process'; 7 | import {readdir} from 'node:fs/promises'; 8 | import {basename, dirname, resolve} from 'node:path'; 9 | import {fileURLToPath} from 'node:url'; 10 | 11 | if (!process.argv[2]) { 12 | console.error('WonderlandEditor executable not provided'); 13 | console.error('Usage: npm run test:build -- path/to/wonderlandengine'); 14 | process.exit(1); 15 | } 16 | 17 | const scriptPath = dirname(fileURLToPath(import.meta.url)); 18 | const paths = { 19 | editor: resolve(process.argv[2]), 20 | test: resolve(scriptPath, '..'), 21 | projects: resolve(scriptPath, '..', 'projects'), 22 | output: resolve(scriptPath, '..', 'resources', 'projects'), 23 | }; 24 | 25 | let commands = []; 26 | try { 27 | /* Read each project's directory. This doesn't recursively search for projects 28 | * on purpose, but instead search only the first layer of directories. */ 29 | const wlps = await readdir(paths.projects, {withFileTypes: true}).then((dirs) => { 30 | const promises = dirs 31 | .filter((dir) => dir.isDirectory()) 32 | .map((dir) => { 33 | const path = resolve(dir.path, dir.name); 34 | return readdir(path).then((files) => 35 | files.filter((f) => f.endsWith('.wlp')).map((f) => resolve(path, f)) 36 | ); 37 | }); 38 | return Promise.all(promises).then((data) => data.flat()); 39 | }); 40 | commands = wlps.map((path) => ({path, filename: basename(path), logs: []})); 41 | } catch (e) { 42 | console.error('Failed to search for .wlp files in test/projects/, reason:', e); 43 | process.exit(1); 44 | } 45 | 46 | const promises = []; 47 | for (const command of commands) { 48 | promises.push( 49 | new Promise((res, rej) => { 50 | const cmd = spawn(paths.editor, [ 51 | '--project', 52 | command.path, 53 | '--windowless', 54 | '--package', 55 | '--preferences', 56 | `${paths.projects}/preferences.json`, 57 | '--output', 58 | paths.output, 59 | ]); 60 | cmd.stderr.on('data', (data) => { 61 | command.logs.push(data); 62 | }); 63 | cmd.stdout.on('data', (data) => { 64 | command.logs.push(data); 65 | }); 66 | cmd.on('error', rej); 67 | cmd.on('close', res); 68 | cmd.on('exit', res); 69 | }).then(() => { 70 | console.log(`================ ${command.filename} ================`); 71 | console.log(command.logs.join('')); 72 | command.logs.length = 0; 73 | }) 74 | ); 75 | } 76 | 77 | let failed = false; 78 | const results = await Promise.allSettled(promises); 79 | for (let i = 0; i < results.length; ++i) { 80 | const result = results[i]; 81 | if (result.status !== 'fulfilled') { 82 | console.error('Failed to run editor command, reason: ', result.reason.message); 83 | failed = true; 84 | continue; 85 | } 86 | if (!result.value) continue; 87 | 88 | const cmd = commands[i]; 89 | console.error(`Project '${cmd.filename}' failed with exit code '${result.value}'`); 90 | failed = true; 91 | } 92 | 93 | process.exit(failed ? 1 : 0); 94 | -------------------------------------------------------------------------------- /packages/test-e2e/scripts/run-tests.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import {startTestRunner} from '@web/test-runner'; 4 | 5 | /* This is required to prevent web-test-runner to parse 6 | * the cli arguments, leading to errors with `--grep` and `--deploy`. */ 7 | startTestRunner({readCliArgs: false}); 8 | -------------------------------------------------------------------------------- /packages/test-e2e/setup.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InMemoryLoadOptions, 3 | loadRuntime, 4 | LoadRuntimeOptions, 5 | LogLevel, 6 | Scene, 7 | Version, 8 | WonderlandEngine, 9 | } from '@wonderlandengine/api'; 10 | 11 | export type DescribeSceneContext = {active: boolean; scene: Scene}; 12 | export type DescribeSceneTestCallback = (ctx: DescribeSceneContext) => void; 13 | 14 | export let WL: WonderlandEngine = null!; 15 | 16 | /** 17 | * Creates a new engine: 18 | * - Load emscripten code + wasm 19 | * - Wait until the engine is ready to be use 20 | * 21 | * For now, engines are stored globally in the page, you **must** 22 | * thus call this function before and test, in order to clean any 23 | * previous engine instance running and create a new one. 24 | */ 25 | export async function init(options: Partial = {}) { 26 | const canvas = document.createElement('canvas'); 27 | canvas.id = 'canvas'; 28 | canvas.style.width = '100%'; 29 | canvas.style.height = '100%'; 30 | document.body.append(canvas); 31 | 32 | const {simd = false, threads = false, loader = false, physx = false} = options; 33 | 34 | const engine = await loadRuntime('deploy/WonderlandRuntime', { 35 | simd, 36 | threads, 37 | loader, 38 | physx, 39 | loadingScreen: 'deploy/WonderlandRuntime-LoadingScreen.bin', 40 | canvas: 'canvas', 41 | logs: [LogLevel.Error], 42 | }); 43 | engine.autoResizeCanvas = false; 44 | engine.resize(canvas.clientWidth, canvas.clientHeight); 45 | 46 | WL = engine; 47 | } 48 | 49 | /** 50 | * Resets the runtime, i.e., 51 | * - Removes all loaded textures 52 | * - Clears the scene 53 | * - Clears component cache 54 | * 55 | * Should be called before running a test to prevent side effects. 56 | */ 57 | export function reset() { 58 | if (!WL) return; 59 | WL.erasePrototypeOnDestroy = false; 60 | (WL._useChunkLoading as boolean) = true; 61 | WL.log.levels.disableAll().enable(LogLevel.Error); 62 | return WL._reset(); 63 | } 64 | 65 | /** 66 | * Create a URL pointing inside the test projects folder. 67 | * 68 | * @param filename The name of the file to point to 69 | * @returns A string pointing inside `test/resources/projects` 70 | */ 71 | export function projectURL(filename: string) { 72 | return `resources/projects/${filename}`; 73 | } 74 | 75 | /** 76 | * Create a URL pointing inside the test resources folder. 77 | * 78 | * @param filename The name of the file to point to 79 | * @returns A string pointing inside `test/resources` 80 | */ 81 | export function resourceURL(filename: string) { 82 | return `resources/${filename}`; 83 | } 84 | 85 | /* 86 | * Check whether the `target` version is anterior to `base`. 87 | * 88 | * @param target The version to check. 89 | * @param base The base version to compare against. 90 | * @returns `true` if `target` is less than `base`, `false` otherwise. 91 | */ 92 | export function versionLess(target: Version, base: Version) { 93 | for (const component of ['major', 'minor', 'patch'] as (keyof Version)[]) { 94 | if (target[component] == base[component]) continue; 95 | if (target[component] < base[component]) return true; 96 | if (target[component] > base[component]) return false; 97 | } 98 | return target.rc !== 0 && (base.rc == 0 || target.rc < base.rc); 99 | } 100 | 101 | /** 102 | * Create two describe blocks, for active and inactive scene tests. 103 | * 104 | * @note The scene will be reset between each test. 105 | * 106 | * @param cb The test callback to register. 107 | */ 108 | export function describeScene(name: string, cb: DescribeSceneTestCallback) { 109 | describe('Active Scene', function () { 110 | describe(name, function () { 111 | const ctx: {active: boolean; scene: Scene} = {active: true, scene: null!}; 112 | beforeEach(() => { 113 | reset(); 114 | ctx.active = true; 115 | ctx.scene = WL.scene; 116 | }); 117 | cb(ctx); 118 | }); 119 | }); 120 | describe('Inactive Scene', function () { 121 | describe(name, function () { 122 | const ctx: {active: boolean; scene: Scene} = {active: false, scene: null!}; 123 | beforeEach(() => { 124 | reset(); 125 | ctx.active = false; 126 | ctx.scene = WL._createEmpty(); 127 | }); 128 | cb(ctx); 129 | }); 130 | }); 131 | } 132 | 133 | /** 134 | * Create two describe blocks, for active and inactive scene tests. 135 | * 136 | * @note The bin is loaded as a new scene group, and the loading is performed 137 | * only once before tests start. Scene isn't discarded between two tests. 138 | * 139 | * @param name The name of the inner describe. 140 | * @param bin The options to load the group from memory. 141 | * @param cb The test callback to register. 142 | */ 143 | export function describeMainScene( 144 | name: string, 145 | bin: InMemoryLoadOptions, 146 | cb: DescribeSceneTestCallback 147 | ) { 148 | describe('Active Scene', function () { 149 | describe(name, function () { 150 | const ctx: {active: boolean; scene: Scene} = {active: true, scene: null!}; 151 | before(async function () { 152 | reset(); 153 | ctx.scene = await WL.loadMainSceneFromBuffer(bin); 154 | return WL.switchTo(ctx.scene); 155 | }); 156 | cb(ctx); 157 | }); 158 | }); 159 | describe('Inactive Scene', function () { 160 | describe(name, function () { 161 | const ctx: {active: boolean; scene: Scene} = {active: false, scene: null!}; 162 | before(async function () { 163 | reset(); 164 | ctx.scene = await WL.loadMainSceneFromBuffer(bin); 165 | const dummyScene = WL._createEmpty(); 166 | return WL.switchTo(dummyScene); 167 | }); 168 | cb(ctx); 169 | }); 170 | }); 171 | } 172 | -------------------------------------------------------------------------------- /packages/test-e2e/skin.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from '@esm-bundle/chai'; 2 | 3 | import {Scene, Skin} from '@wonderlandengine/api'; 4 | 5 | describe('Skin', function () { 6 | it('deprecated equals', function () { 7 | const host = {_index: 0} as unknown as Scene; 8 | const skin1 = new Skin(host, 1); 9 | const skin2 = new Skin(host, 2); 10 | const skin3 = new Skin(host, 1); 11 | expect(skin1.equals(null)).to.be.false; 12 | expect(skin1.equals(undefined)).to.be.false; 13 | expect(skin1.equals(skin1)).to.be.true; 14 | expect(skin1.equals(skin2)).to.be.false; 15 | expect(skin2.equals(skin1)).to.be.false; 16 | expect(skin1.equals(skin3)).to.be.true; 17 | expect(skin3.equals(skin1)).to.be.true; 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/test-e2e/texture.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from '@esm-bundle/chai'; 2 | 3 | import {Texture} from '@wonderlandengine/api'; 4 | import {init, reset, resourceURL, WL} from './setup.js'; 5 | import {dummyImage} from './utils.js'; 6 | 7 | before(init); 8 | beforeEach(reset); 9 | 10 | describe('Texture Legacy', function () { 11 | it('create', async function () { 12 | const testImage = await dummyImage(20, 30); 13 | const tex = new Texture(WL, testImage); 14 | expect(tex.width).to.equal(20); 15 | expect(tex.height).to.equal(30); 16 | }); 17 | 18 | it('load from url', async function () { 19 | const texture = await WL.textures.load(resourceURL('2x2.png')); 20 | expect(texture.width).to.equal(2); 21 | expect(texture.height).to.equal(2); 22 | }); 23 | 24 | it('.destroy()', async function () { 25 | const textureA = new Texture(WL, await dummyImage(1, 2)); 26 | const textureB = new Texture(WL, await dummyImage(3, 4)); 27 | 28 | expect([textureA.width, textureA.height]).to.eql([1, 2]); 29 | expect([textureB.width, textureB.height]).to.eql([3, 4]); 30 | 31 | textureB.destroy(); 32 | expect(textureB._index).to.equal(-1); 33 | expect([textureA.width, textureA.height]).to.eql([1, 2]); 34 | 35 | textureA.destroy(); 36 | expect(textureA._index).to.equal(-1); 37 | 38 | const textureC = new Texture(WL, await dummyImage(5, 6)); 39 | expect([textureC.width, textureC.height]).to.eql([5, 6]); 40 | textureC.destroy(); 41 | expect(textureC._index).to.equal(-1); 42 | }); 43 | }); 44 | 45 | describe('Texture', function () { 46 | it('create from Image', async function () { 47 | const testImage = await dummyImage(20, 30); 48 | 49 | const tex = WL.textures.create(testImage); 50 | expect(tex.width).to.equal(20); 51 | expect(tex.height).to.equal(30); 52 | }); 53 | 54 | it('.equals()', async function () { 55 | const testImage = await dummyImage(20, 30); 56 | const tex1 = WL.textures.create(testImage); 57 | const tex2 = WL.textures.create(testImage); 58 | const tex3 = new Texture(WL, tex1._index); 59 | expect(tex1.equals(null)).to.be.false; 60 | expect(tex1.equals(undefined)).to.be.false; 61 | expect(tex1.equals(tex1)).to.be.true; 62 | expect(tex1.equals(tex2)).to.be.false; 63 | expect(tex2.equals(tex1)).to.be.false; 64 | expect(tex1.equals(tex3)).to.be.true; 65 | expect(tex3.equals(tex1)).to.be.true; 66 | }); 67 | 68 | it('.destroy()', async function () { 69 | const images = await Promise.all([ 70 | dummyImage(2, 4), 71 | dummyImage(6, 8), 72 | dummyImage(10, 12), 73 | ]); 74 | 75 | const manager = WL.textures; 76 | 77 | const tex1 = manager.create(images[0]); 78 | const tex2 = manager.create(images[1]); 79 | expect(tex1.index).to.equal(1); 80 | expect(tex2.index).to.equal(2); 81 | 82 | tex1.destroy(); 83 | expect(tex1.index).to.equal(-1); 84 | expect(tex2.index).to.equal(2); 85 | expect(manager.get(tex2._index)?.width).to.equal(6); 86 | expect(manager.get(tex2._index)?.height).to.equal(8); 87 | 88 | const tex3 = manager.create(images[2]); 89 | expect(tex3.index).to.equal(1); 90 | expect(manager.get(tex3._index)?.width).to.equal(10); 91 | expect(manager.get(tex3._index)?.height).to.equal(12); 92 | }); 93 | 94 | it('.destroy() with prototype destruction', async function () { 95 | WL.erasePrototypeOnDestroy = true; 96 | 97 | const image = await dummyImage(2, 2); 98 | const a = WL.textures.create(image); 99 | const b = WL.textures.create(image); 100 | 101 | a.destroy(); 102 | expect(() => a.equals(b)).to.throw( 103 | `Cannot read 'equals' of destroyed 'Texture' resource from ${WL}` 104 | ); 105 | expect(() => a.valid).to.throw( 106 | `Cannot read 'valid' of destroyed 'Texture' resource from ${WL}` 107 | ); 108 | 109 | /* Ensure destroying `a` didn't destroy `b` as well */ 110 | expect(b._id).to.be.greaterThan(0); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /packages/test-e2e/types.ts: -------------------------------------------------------------------------------- 1 | import {Material, Texture} from '@wonderlandengine/api'; 2 | 3 | export interface PhongMaterial extends Material { 4 | shininess: number; 5 | diffuseTexture: Texture; 6 | } 7 | -------------------------------------------------------------------------------- /packages/test-e2e/utils.ts: -------------------------------------------------------------------------------- 1 | import {InMemoryLoadOptions, Scene, WonderlandEngine} from '@wonderlandengine/api'; 2 | import {fetchWithProgress, onImageReady} from '@wonderlandengine/api/utils/fetch.js'; 3 | import {projectURL} from './setup.js'; 4 | 5 | class ResourceLike { 6 | _index = -1; 7 | constructor(index: number) { 8 | this._index = index; 9 | } 10 | equals(other: ResourceLike) { 11 | return this._index === other._index; 12 | } 13 | } 14 | 15 | /** 16 | * Create a dummy image with dimensions `width * height`. 17 | * 18 | * @param width The image width 19 | * @param height The image height 20 | * @returns The image 21 | */ 22 | export function dummyImage(width: number, height: number): Promise { 23 | const canvas = document.createElement('canvas'); 24 | canvas.width = width; 25 | canvas.height = height; 26 | 27 | const img = new Image(width, height); 28 | img.src = canvas.toDataURL(); 29 | 30 | return onImageReady(img); 31 | } 32 | 33 | /** 34 | * Create a mocked {@link Scene} object. 35 | * 36 | * @param index Index of the scene 37 | * @returns An object with a similar structure to {@link Scene}. 38 | */ 39 | export function mockedScene(index: number = 0) { 40 | return new ResourceLike(index) as unknown as Scene; 41 | } 42 | 43 | /** 44 | * Load multiple project bins. 45 | * 46 | * @param filenames Name of each bin to load 47 | * @returns A promise that resolve with an array of object 48 | * that can be used with {@link WonderlandEngine.loadSceneAsGroupFromMemory} 49 | * and {@link SceneGroup.loadSceneFromBuffer}. 50 | */ 51 | export async function loadProjectBins(...filenames: string[]) { 52 | const bins: InMemoryLoadOptions[] = []; 53 | const promises: Promise[] = []; 54 | for (const filename of filenames) { 55 | promises.push( 56 | fetchWithProgress(projectURL(filename)).then((buffer) => { 57 | return {filename, baseURL: projectURL(''), buffer}; 58 | }) 59 | ); 60 | } 61 | return Promise.all(promises); 62 | } 63 | -------------------------------------------------------------------------------- /packages/test-e2e/web-test-runner.config.js: -------------------------------------------------------------------------------- 1 | import {symlinkSync, existsSync, unlinkSync, lstatSync} from 'node:fs'; 2 | import {relative, resolve} from 'node:path'; 3 | import {fileURLToPath} from 'node:url'; 4 | import {parseArgs} from 'node:util'; 5 | 6 | import {chromeLauncher} from '@web/test-runner'; 7 | import {esbuildPlugin} from '@web/dev-server-esbuild'; 8 | 9 | let args = null; 10 | let positionals = null; 11 | try { 12 | ({values: args, positionals: positionals} = parseArgs({ 13 | options: { 14 | grep: {type: 'string', short: 'g'}, 15 | watch: {type: 'boolean', short: 'w'}, 16 | deploy: {type: 'string', short: 'd'}, 17 | 'no-headless': {type: 'boolean', default: false}, 18 | }, 19 | allowPositionals: true, 20 | })); 21 | } catch (e) { 22 | console.error('Failed to parse command line arguments, reason:', e); 23 | process.exit(1); 24 | } 25 | args.deploy = args.deploy ?? process.env['DEPLOY_FOLDER'] ?? '../../../../deploy/'; 26 | 27 | /* We need to relink every run in case the env var changed, 28 | * so first remove old link (or old deploy copy) */ 29 | if (existsSync('deploy') && lstatSync('deploy').isSymbolicLink()) { 30 | unlinkSync('deploy'); 31 | } 32 | 33 | const deployRoot = resolve(args.deploy); 34 | if (!lstatSync(deployRoot, {throwIfNoEntry: false})?.isDirectory()) { 35 | console.error(`deploy folder '${deployRoot}' isn't a directory`); 36 | process.exit(1); 37 | } 38 | 39 | console.log(`Creating symlink 'deploy' to '${deployRoot}'`); 40 | symlinkSync(deployRoot, 'deploy', 'junction'); 41 | 42 | /* When running in docker on Ubuntu, headless set to `true` always forces the 43 | * browser to use the SwiftShader backend which we don't want. 44 | * 45 | * The 'new' mode also create animation loop issues, we do not use it. */ 46 | const headless = !args['no-headless']; 47 | 48 | const Config = { 49 | nodeResolve: true, 50 | files: positionals.length > 0 ? positionals : ['*.test.ts', '!**/node_modules/**/*'], 51 | watch: args.watch, 52 | 53 | browsers: [ 54 | chromeLauncher({ 55 | launchOptions: { 56 | headless, 57 | devtools: false, 58 | args: ['--no-sandbox', '--use-gl=angle', '--ignore-gpu-blocklist'] 59 | }, 60 | createPage: async ({context}) => { 61 | /* By default, tests are run in separate pages, in the same browser context. 62 | * However, the entire engine relies on RAF, which is throttled in inactive page. 63 | * 64 | * Running in an unfocused tab can cause the animation loop to be stuck (an so our job system). 65 | * 66 | * Creating one browser context per test allows to run tests concurrently without 67 | * having focusing issues. */ 68 | return (await context.browser().createIncognitoBrowserContext()).newPage(); 69 | }, 70 | }), 71 | ], 72 | 73 | /* Mocha configuration */ 74 | testFramework: { 75 | config: { 76 | ui: 'bdd', 77 | timeout: '15000', 78 | allowUncaught: false, 79 | grep: args.grep 80 | }, 81 | }, 82 | 83 | plugins: [ 84 | esbuildPlugin({ 85 | ts: true, 86 | tsconfig: fileURLToPath(new URL('../api/tsconfig.json', import.meta.url)), 87 | }) 88 | ], 89 | }; 90 | 91 | export default Config; 92 | --------------------------------------------------------------------------------