├── .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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------