├── .editorconfig ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ ├── docs.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── devui ├── README.md ├── example │ ├── index.html │ ├── index.js │ ├── package-lock.json │ ├── package.json │ └── webpack.config.cjs ├── package-lock.json ├── package.json ├── rollup.config.js ├── src │ ├── components │ │ ├── analog.tsx │ │ ├── binary.tsx │ │ ├── controller.tsx │ │ ├── controls.tsx │ │ ├── hand.tsx │ │ ├── header.tsx │ │ ├── headset.tsx │ │ ├── icons.tsx │ │ ├── joystick.tsx │ │ ├── keys.tsx │ │ ├── mapper.tsx │ │ ├── pinch.tsx │ │ ├── pose.tsx │ │ ├── styled.tsx │ │ └── vec3.tsx │ ├── index.tsx │ └── scene.ts └── tsconfig.json ├── docs ├── .vitepress │ ├── config.ts │ └── theme │ │ ├── index.ts │ │ └── style.css ├── about.md ├── action.md ├── api │ ├── config-interfaces.md │ ├── xr-controller.md │ ├── xr-device.md │ └── xr-hand-input.md ├── getting-started.md ├── index.md ├── public │ ├── cap.mp4 │ ├── iwer-dark.svg │ ├── iwer-locomotion.gif │ ├── iwer-open-menu.gif │ ├── iwer-photo-drop.gif │ ├── iwer-seed-selection.gif │ ├── iwer-text.svg │ ├── iwer.png │ └── iwer.svg └── xplat.md ├── example ├── cap.min.js ├── index.html ├── index.js ├── package-lock.json ├── package.json └── webpack.config.js ├── jest.config.cjs ├── package-lock.json ├── package.json ├── rollup.config.js ├── sem ├── .gitignore ├── README.md ├── captures │ ├── living_room.json │ ├── meeting_room.json │ ├── music_room.json │ ├── office_large.json │ └── office_small.json ├── package-lock.json ├── package.json ├── rollup.config.js ├── scripts │ └── prebuild.cjs ├── src │ ├── generated │ │ ├── google │ │ │ └── protobuf │ │ │ │ ├── descriptor.ts │ │ │ │ ├── duration.ts │ │ │ │ └── timestamp.ts │ │ └── protos │ │ │ ├── openxr_core.ts │ │ │ ├── openxr_scene.ts │ │ │ └── validate.ts │ ├── globals.d.ts │ ├── index.ts │ ├── native │ │ ├── components │ │ │ ├── bounded2d.ts │ │ │ ├── bounded3d.ts │ │ │ ├── component.ts │ │ │ ├── locatable.ts │ │ │ ├── semanticlabel.ts │ │ │ └── trianglemesh.ts │ │ └── entity.ts │ └── sem.ts └── tsconfig.json ├── src ├── action │ ├── ActionPlayer.ts │ └── ActionRecorder.ts ├── anchors │ └── XRAnchor.ts ├── device │ ├── XRController.ts │ ├── XRDevice.ts │ ├── XRHandInput.ts │ ├── XRTrackedInput.ts │ └── configs │ │ ├── controller │ │ └── meta.ts │ │ ├── hand │ │ ├── pinch.ts │ │ ├── point.ts │ │ └── relaxed.ts │ │ └── headset │ │ └── meta.ts ├── events │ ├── XRInputSourceEvent.ts │ ├── XRInputSourcesChangeEvent.ts │ ├── XRReferenceSpaceEvent.ts │ └── XRSessionEvent.ts ├── frameloop │ └── XRFrame.ts ├── gamepad │ └── Gamepad.ts ├── hittest │ ├── XRHitTest.ts │ └── XRRay.ts ├── index.ts ├── initialization │ └── XRSystem.ts ├── input │ ├── XRHand.ts │ └── XRInputSource.ts ├── labels │ └── labels.ts ├── layers │ └── XRWebGLLayer.ts ├── meshes │ └── XRMesh.ts ├── planes │ └── XRPlane.ts ├── pose │ ├── XRJointPose.ts │ ├── XRPose.ts │ └── XRViewerPose.ts ├── primitives │ └── XRRigidTransform.ts ├── private.ts ├── session │ ├── XRRenderState.ts │ └── XRSession.ts ├── spaces │ ├── XRJointSpace.ts │ ├── XRReferenceSpace.ts │ └── XRSpace.ts ├── utils │ └── Math.ts └── views │ ├── XRView.ts │ └── XRViewport.ts ├── tests ├── anchors │ └── XRAnchor.test.ts ├── device │ ├── XRController.test.ts │ ├── XRDevice.test.ts │ ├── XRHandInput.test.ts │ └── XRTrackedInput.test.ts ├── events │ ├── XRInputSourceChangeEvent.test.ts │ ├── XRInputSourceEvent.test.ts │ ├── XRReferenceSpaceEvent.test.ts │ └── XRSessionEvent.test.ts ├── frameloop │ └── XRFrame.test.ts ├── gamepad │ └── Gamepad.test.ts ├── hittest │ ├── XRHitTest.test.ts │ └── XRRay.test.ts ├── initialization │ └── XRSystem.test.ts ├── input │ └── XRInputSource.test.ts ├── jsconfig.json ├── polyfill.ts ├── primitives │ └── XRRigidTransform.test.ts ├── session │ └── XRSession.test.ts └── spaces │ ├── XRReferenceSpace.test.ts │ └── XRSpace.test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier', 11 | ], 12 | parser: '@typescript-eslint/parser', 13 | plugins: ['@typescript-eslint', 'prettier'], 14 | parserOptions: { 15 | ecmaVersion: 12, 16 | sourceType: 'module', 17 | }, 18 | rules: { 19 | 'sort-imports': [ 20 | 'error', 21 | { 22 | ignoreCase: false, 23 | ignoreDeclarationSort: false, 24 | ignoreMemberSort: false, 25 | memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], 26 | allowSeparatedGroups: false, 27 | }, 28 | ], 29 | 'no-unused-vars': [ 30 | 'error', 31 | { vars: 'all', args: 'all', argsIgnorePattern: '^_' }, 32 | ], 33 | 'lines-between-class-members': ['warn', 'always'], 34 | 'prettier/prettier': 'error', 35 | }, 36 | root: true, 37 | }; 38 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | open-pull-requests-limit: 10 8 | commit-message: 9 | prefix: 'chore' 10 | prefix-development: 'chore' 11 | include: 'scope' 12 | - package-ecosystem: 'npm' 13 | directory: '/devui' 14 | schedule: 15 | interval: 'weekly' 16 | open-pull-requests-limit: 10 17 | commit-message: 18 | prefix: 'chore' 19 | prefix-development: 'chore' 20 | include: 'scope' 21 | - package-ecosystem: 'npm' 22 | directory: '/sem' 23 | schedule: 24 | interval: 'weekly' 25 | open-pull-requests-limit: 10 26 | commit-message: 27 | prefix: 'chore' 28 | prefix-development: 'chore' 29 | include: 'scope' 30 | - package-ecosystem: 'github-actions' 31 | directory: '/' 32 | schedule: 33 | interval: 'weekly' 34 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy docs to Pages 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: 'pages' 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | deploy: 19 | environment: 20 | name: github-pages 21 | url: ${{ steps.deployment.outputs.page_url }} 22 | 23 | runs-on: ubuntu-latest 24 | 25 | strategy: 26 | matrix: 27 | node-version: [20.x] 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - name: Use Node.js ${{ matrix.node-version }} 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | 37 | - name: Install dependencies 38 | run: npm ci 39 | 40 | - name: Build library 41 | run: npm run build 42 | 43 | - name: Build doc site 44 | run: npm run docs:build 45 | 46 | - name: Upload artifact 47 | uses: actions/upload-pages-artifact@v3 48 | with: 49 | path: ./docs/.vitepress/dist 50 | 51 | - name: Deploy to GitHub Pages 52 | id: deployment 53 | uses: actions/deploy-pages@v4 54 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [20.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - run: npm ci 20 | - run: npm run build 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.yarn/* 2 | !/.yarn/releases 3 | !/.yarn/plugins 4 | !/.yarn/sdks 5 | 6 | # Swap the comments on the following lines if you don't wish to use zero-installs 7 | # Documentation here: https://yarnpkg.com/features/zero-installs 8 | !/.yarn/cache 9 | #/.pnp.* 10 | 11 | lib/ 12 | build/ 13 | dist/ 14 | node_modules/ 15 | 16 | /.vscode/ 17 | 18 | # blender backups 19 | *.blend? 20 | 21 | .DS_Store 22 | 23 | docs/.vitepress/dist/ 24 | docs/.vitepress/cache/ 25 | 26 | **/src/version.ts 27 | 28 | *.tgz 29 | 30 | /coverage/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /src/assets/* 2 | /src/style/* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "endOfLine": "lf", 4 | "useTabs": true, 5 | "trailingComma": "all", 6 | "arrowParens": "always", 7 | "printWidth": 80, 8 | "tabWidth": 2, 9 | "singleQuote": true, 10 | "jsxSingleQuote": false 11 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when there is a 56 | reasonable belief that an individual's behavior may have a negative impact on 57 | the project or its community. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported by contacting the project team at . All 63 | complaints will be reviewed and investigated and will result in a response that 64 | is deemed necessary and appropriate to the circumstances. The project team is 65 | obligated to maintain confidentiality with regard to the reporter of an incident. 66 | Further details of specific enforcement policies may be posted separately. 67 | 68 | Project maintainers who do not follow or enforce the Code of Conduct in good 69 | faith may face temporary or permanent repercussions as determined by other 70 | members of the project's leadership. 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 75 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 76 | 77 | [homepage]: https://www.contributor-covenant.org 78 | 79 | For answers to common questions about this code of conduct, see 80 | https://www.contributor-covenant.org/faq 81 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Immersive Web Emulation Runtime 2 | 3 | We want to make contributing to this project as easy and transparent as 4 | possible. 5 | 6 | ## Pull Requests 7 | 8 | We actively welcome your pull requests. 9 | 10 | 1. Fork the repo and create your branch from `main`. 11 | 2. If you've added code that should be tested, add tests. 12 | 3. Make sure your code lints. 13 | 4. If you haven't already, complete the Contributor License Agreement ("CLA"). 14 | 15 | ## Contributor License Agreement ("CLA") 16 | 17 | In order to accept your pull request, we need you to submit a CLA. You only need 18 | to do this once to work on any of Meta's open source projects. 19 | 20 | Complete your CLA here: 21 | 22 | ## Issues 23 | 24 | We use GitHub issues to track public bugs. Please ensure your description is 25 | clear and has sufficient instructions to be able to reproduce the issue. 26 | 27 | Meta has a [bounty program](https://www.facebook.com/whitehat/) for the safe 28 | disclosure of security bugs. In those cases, please go through the process 29 | outlined on that page and do not file a public issue. 30 | 31 | ## Coding Style 32 | 33 | Immersive Web Emulation Runtime uses `eslint` and `prettier` to lint and format code. 34 | 35 | You can format manually by running: 36 | 37 | ``` 38 | $ npm run format 39 | ``` 40 | 41 | There are also VSCode extensions that can run those linters / formatters for you. Prettier has a [VSCode Plugin](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode), which allows you to format on save. 42 | 43 | If you don't want to format on save, and want to format a single file, you can use `Ctrl+Shift+P` in VS Code (`Cmd+Shift+P` on macs) to bring up the command palette, then type `Format Document` to format the currently active file. Note that you should have the Prettier VS code plugin installed to make sure it formats according to the project's guidelines. 44 | 45 | ## Testing 46 | 47 | Immersive Web Emulation Runtime uses `jest` to conduct unit testing for all its classes. Before contributing, please make sure your change does not fail any of the existing jest tests. If you are adding new classes or new features, we request that you add relevant tests to ensure a high test coverage of the Immersive Web Emulation Runtime. 48 | 49 | You can run tests manually by running: 50 | 51 | ``` 52 | $ npm run test 53 | ``` 54 | 55 | ## License 56 | 57 | By contributing to Immersive Web Emulation Runtime, you agree that your contributions will be licensed 58 | under the [LICENSE](./LICENSE) file in the root directory of this source tree. 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Meta Platforms, Inc. and affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

Immersive Web Emulation Runtime

4 |

5 | 6 |

7 | npm version 8 | npm download 9 | language 10 | license 11 |

12 | 13 | The Immersive Web Emulation Runtime (IWER) is a TypeScript-based tool designed to enable the running of WebXR applications in modern browsers without requiring native WebXR support. By emulating the WebXR Device API, IWER provides developers with the ability to test and run WebXR projects across a wide range of devices, ensuring compatibility and enhancing the development process. 14 | 15 | ## Documentation 16 | 17 | For detailed information about using IWER, including concepts, guides, and API references, please visit our documentation site: 18 | 19 | - [IWER Documentation](https://meta-quest.github.io/immersive-web-emulation-runtime) 20 | 21 | ## License 22 | 23 | IWER is licensed under the MIT License. For more details, see the [LICENSE](https://github.com/meta-quest/immersive-web-emulation-runtime/blob/main/LICENSE) file in this repository. 24 | 25 | ## Contributing 26 | 27 | Your contributions are welcome! Please feel free to submit issues and pull requests. Before contributing, make sure to review our [Contributing Guidelines](https://github.com/meta-quest/immersive-web-emulation-runtime/blob/main/CONTRIBUTING.md) and [Code of Conduct](https://github.com/meta-quest/immersive-web-emulation-runtime/blob/main/CODE_OF_CONDUCT.md). 28 | -------------------------------------------------------------------------------- /devui/example/index.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | IWER DevUI Showcase 11 | 12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /devui/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webxr-first-steps", 3 | "private": true, 4 | "devDependencies": { 5 | "@types/three": "0.165.0", 6 | "copy-webpack-plugin": "12.0.2", 7 | "eslint": "9.10.0", 8 | "eslint-config-prettier": "9.1.0", 9 | "eslint-webpack-plugin": "4.2.0", 10 | "html-webpack-plugin": "5.6.0", 11 | "prettier": "3.3.3", 12 | "webpack": "5.94.0", 13 | "webpack-cli": "5.1.4", 14 | "webpack-dev-server": "5.1.0" 15 | }, 16 | "scripts": { 17 | "build": "webpack", 18 | "dev": "webpack serve", 19 | "format": "prettier --write ./src/**/*" 20 | }, 21 | "dependencies": { 22 | "@iwer/devui": "file:..", 23 | "@iwer/sem": "^0.2.4", 24 | "iwer": "^2.0.1", 25 | "three": "0.165.0", 26 | "troika-three-text": "^0.52.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /devui/example/webpack.config.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const path = require('path'); 9 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 10 | 11 | module.exports = { 12 | mode: 'development', 13 | entry: './index.js', 14 | output: { 15 | filename: '[name].[contenthash].js', 16 | path: path.resolve(__dirname, 'dist'), 17 | clean: true, 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.css$/i, 23 | use: ['style-loader', 'css-loader'], 24 | }, 25 | ], 26 | }, 27 | devServer: { 28 | static: { 29 | directory: path.join(__dirname, 'dist'), 30 | }, 31 | host: '0.0.0.0', 32 | server: 'https', 33 | port: 8081, 34 | client: { 35 | overlay: { warnings: false, errors: true }, 36 | }, 37 | }, 38 | plugins: [ 39 | new HtmlWebpackPlugin({ 40 | template: './index.html', 41 | }), 42 | ], 43 | }; 44 | -------------------------------------------------------------------------------- /devui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iwer/devui", 3 | "version": "1.1.2", 4 | "description": "Dev UI for IWER", 5 | "type": "module", 6 | "main": "lib/index.js", 7 | "types": "lib/index.d.ts", 8 | "files": [ 9 | "build", 10 | "lib" 11 | ], 12 | "scripts": { 13 | "prebuild": "node -p \"'export const VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts", 14 | "build": "tsc && rollup -c" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/meta-quest/immersive-web-emulation-runtime.git" 19 | }, 20 | "keywords": [], 21 | "author": "Felix Zhang ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/meta-quest/immersive-web-emulation-runtime/issues" 25 | }, 26 | "homepage": "https://github.com/meta-quest/immersive-web-emulation-runtime#readme", 27 | "dependencies": { 28 | "@fortawesome/fontawesome-svg-core": "6.6.0", 29 | "@fortawesome/free-solid-svg-icons": "6.6.0", 30 | "@fortawesome/react-fontawesome": "0.2.2", 31 | "@pmndrs/handle": "^6.6.17", 32 | "@pmndrs/pointer-events": "^6.6.17", 33 | "react": ">=18.3.1", 34 | "react-dom": ">=18.3.1", 35 | "styled-components": "^6.1.13", 36 | "three": "^0.165.0" 37 | }, 38 | "peerDependencies": { 39 | "iwer": "^2.0.1" 40 | }, 41 | "devDependencies": { 42 | "@rollup/plugin-babel": "^6.0.4", 43 | "@rollup/plugin-commonjs": "^28.0.1", 44 | "@rollup/plugin-node-resolve": "^15.3.0", 45 | "@rollup/plugin-replace": "^6.0.1", 46 | "@rollup/plugin-terser": "^0.4.4", 47 | "@types/react": "^18.3.12", 48 | "@types/react-dom": "^18.3.1", 49 | "rollup": "^4.24.3", 50 | "rollup-plugin-peer-deps-external": "^2.2.4", 51 | "typescript": "^5.6.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /devui/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import peerDepsExternal from 'rollup-plugin-peer-deps-external'; 3 | import replace from '@rollup/plugin-replace'; 4 | import resolve from '@rollup/plugin-node-resolve'; 5 | import terser from '@rollup/plugin-terser'; 6 | 7 | const globals = { 8 | iwer: 'IWER', // Change this to the global variable name for the iwer module 9 | }; 10 | 11 | export default { 12 | input: 'lib/index.js', 13 | external: ['iwer'], 14 | plugins: [ 15 | peerDepsExternal(), 16 | resolve(), 17 | commonjs(), 18 | replace({ 19 | 'process.env.NODE_ENV': JSON.stringify('production'), 20 | preventAssignment: true, 21 | }), 22 | ], 23 | output: [ 24 | // UMD build 25 | { 26 | file: 'build/iwer-devui.js', 27 | format: 'umd', 28 | name: 'IWER_DevUI', 29 | globals, 30 | }, 31 | // Minified UMD build 32 | { 33 | file: 'build/iwer-devui.min.js', 34 | format: 'umd', 35 | name: 'IWER_DevUI', 36 | globals, 37 | plugins: [terser()], 38 | }, 39 | // ES module build 40 | { 41 | file: 'build/iwer-devui.module.js', 42 | format: 'es', 43 | globals, 44 | }, 45 | // Minified ES module build 46 | { 47 | file: 'build/iwer-devui.module.min.js', 48 | format: 'es', 49 | globals, 50 | plugins: [terser()], 51 | }, 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /devui/src/components/binary.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { 9 | Button, 10 | ButtonContainer, 11 | ButtonGroup, 12 | Colors, 13 | ControlButtonStyles, 14 | FAIcon, 15 | MappedKeyBlock, 16 | } from './styled.js'; 17 | import React, { useEffect, useState } from 'react'; 18 | 19 | import { GamepadIcon } from './icons.js'; 20 | import { MappedKeyDisplay } from './keys.js'; 21 | import { XRController } from 'iwer/lib/device/XRController'; 22 | import { faFingerprint } from '@fortawesome/free-solid-svg-icons'; 23 | 24 | interface BinaryButtonProps { 25 | xrController: XRController; 26 | buttonId: string; 27 | pointerLocked: boolean; 28 | mappedKey: string; 29 | } 30 | 31 | export const BinaryButton: React.FC = ({ 32 | xrController, 33 | buttonId, 34 | pointerLocked, 35 | mappedKey, 36 | }) => { 37 | const [isTouched, setIsTouched] = useState(false); 38 | const [isOnHold, setIsOnHold] = useState(false); 39 | const [isPressed, setIsPressed] = useState(false); 40 | const [isKeyPressed, setIsKeyPressed] = useState(false); 41 | 42 | const handedness = xrController.inputSource.handedness; 43 | 44 | useEffect(() => { 45 | const handleKeyDown = (event: KeyboardEvent) => { 46 | if (event.code === mappedKey) { 47 | xrController.updateButtonValue(buttonId, 1); 48 | setIsKeyPressed(true); 49 | } 50 | }; 51 | 52 | const handleKeyUp = (event: KeyboardEvent) => { 53 | if (event.code === mappedKey) { 54 | xrController.updateButtonValue(buttonId, 0); 55 | setIsKeyPressed(false); 56 | } 57 | }; 58 | 59 | if (pointerLocked) { 60 | window.addEventListener('keydown', handleKeyDown); 61 | window.addEventListener('keyup', handleKeyUp); 62 | } else { 63 | window.removeEventListener('keydown', handleKeyDown); 64 | window.removeEventListener('keyup', handleKeyUp); 65 | } 66 | 67 | return () => { 68 | window.removeEventListener('keydown', handleKeyDown); 69 | window.removeEventListener('keyup', handleKeyUp); 70 | }; 71 | }, [mappedKey, pointerLocked, buttonId, xrController]); 72 | 73 | return ( 74 | 75 | 76 | 77 | {pointerLocked ? ( 78 | 79 | {MappedKeyDisplay[mappedKey]} 80 | 81 | ) : ( 82 | <> 83 | 102 | 119 | 134 | 135 | )} 136 | 137 | 138 | ); 139 | }; 140 | -------------------------------------------------------------------------------- /devui/src/components/controls.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { ControllerUI } from './controller.js'; 9 | import { HandUI } from './hand.js'; 10 | import { InputLayer } from '../scene.js'; 11 | import React from 'react'; 12 | import { XRDevice } from 'iwer'; 13 | import { create } from 'zustand'; 14 | 15 | interface ControlsProps { 16 | xrDevice: XRDevice; 17 | inputLayer: InputLayer; 18 | pointerLocked: boolean; 19 | } 20 | 21 | type InputModeStore = { 22 | inputMode: string; 23 | setInputMode: (mode: 'controller' | 'hand') => void; 24 | }; 25 | 26 | export const useInputModeStore = create((set) => ({ 27 | inputMode: 'controller', 28 | setInputMode: (mode: 'controller' | 'hand') => 29 | set(() => ({ 30 | inputMode: mode, 31 | })), 32 | })); 33 | 34 | export const ControlsUI: React.FC = ({ 35 | xrDevice, 36 | inputLayer, 37 | pointerLocked, 38 | }) => { 39 | const { inputMode } = useInputModeStore(); 40 | return ( 41 | <> 42 | {inputMode === 'controller' 43 | ? Object.entries(xrDevice.controllers).map( 44 | ([handedness, controller]) => ( 45 | 56 | ), 57 | ) 58 | : Object.entries(xrDevice.hands).map(([handedness, hand]) => ( 59 | 68 | ))} 69 | 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /devui/src/components/hand.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { 9 | ControlButtonStyles, 10 | ControlPanel, 11 | FAIcon, 12 | PanelHeaderButton, 13 | SectionBreak, 14 | } from './styled.js'; 15 | import { ControlsMapper, useKeyMapStore } from './mapper.js'; 16 | import { 17 | faCircleXmark, 18 | faGear, 19 | faHand, 20 | faPlug, 21 | } from '@fortawesome/free-solid-svg-icons'; 22 | 23 | import { PinchControl } from './pinch.js'; 24 | import { PoseSelector } from './pose.js'; 25 | import React from 'react'; 26 | import { TransformHandles } from '@pmndrs/handle'; 27 | import { Vector3Input } from './vec3.js'; 28 | import type { XRHandInput } from 'iwer/lib/device/XRHandInput.js'; 29 | 30 | interface HandProps { 31 | hand: XRHandInput; 32 | handle: TransformHandles; 33 | handedness: string; 34 | pointerLocked: boolean; 35 | } 36 | 37 | export const HandUI: React.FC = ({ 38 | hand, 39 | handle, 40 | handedness, 41 | pointerLocked, 42 | }) => { 43 | const { keyMap } = useKeyMapStore(); 44 | const [connected, setConnected] = React.useState(hand.connected); 45 | const [settingsOpen, setSettingsOpen] = React.useState(false); 46 | React.useEffect(() => { 47 | if (pointerLocked) { 48 | setSettingsOpen(false); 49 | } 50 | }, [pointerLocked]); 51 | return ( 52 | 60 | {!pointerLocked && ( 61 | <> 62 |
70 |
78 | 83 | Hand  84 | 85 | [{handedness === 'left' ? 'L' : 'R'}] 86 | 87 |
88 |
95 | {connected ? ( 96 | <> 97 | { 102 | setSettingsOpen(!settingsOpen); 103 | }} 104 | > 105 | 106 | 107 | { 111 | hand.connected = false; 112 | setConnected(false); 113 | }} 114 | > 115 | 116 | 117 | 118 | ) : ( 119 | { 122 | hand.connected = true; 123 | setConnected(true); 124 | }} 125 | style={{ marginLeft: '5px' }} 126 | > 127 | 128 | 129 | )} 130 |
131 |
132 | 133 | )} 134 | {connected && !pointerLocked && ( 135 | <> 136 | {!settingsOpen && ( 137 | <> 138 | 139 | 144 | 145 | 146 | )} 147 | 148 | 149 | )} 150 | {connected && 151 | (settingsOpen ? ( 152 | 156 | ) : ( 157 | <> 158 | 163 | 168 | 169 | ))} 170 |
171 | ); 172 | }; 173 | -------------------------------------------------------------------------------- /devui/src/components/headset.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { 9 | Colors, 10 | ControlButtonStyles, 11 | ControlPanel, 12 | FAIcon, 13 | InputSuffix, 14 | SectionBreak, 15 | ValueInput, 16 | ValuesContainer, 17 | } from './styled.js'; 18 | import { 19 | faStreetView, 20 | faVideo, 21 | faVrCardboard, 22 | } from '@fortawesome/free-solid-svg-icons'; 23 | 24 | import { InputLayer } from '../scene.js'; 25 | import React from 'react'; 26 | import { Vector3Input } from './vec3.js'; 27 | import { XRDevice } from 'iwer'; 28 | import { styled } from 'styled-components'; 29 | 30 | interface HeadsetProps { 31 | xrDevice: XRDevice; 32 | inputLayer: InputLayer; 33 | pointerLocked: boolean; 34 | } 35 | 36 | const HeadsetOptionContainer = styled.div` 37 | width: 100%; 38 | display: flex; 39 | flex-direction: row; 40 | justify-content: space-between; 41 | align-items: center; 42 | margin-top: ${ControlButtonStyles.gap}; 43 | font-size: 12px; 44 | `; 45 | 46 | const RangeSelector = styled.input.attrs({ type: 'range' })` 47 | -webkit-appearance: none; 48 | appearance: none; 49 | background: ${Colors.gradientGrey}; 50 | border: 1px solid transparent; 51 | height: 25px; 52 | color: ${Colors.textWhite}; 53 | width: ${ControlButtonStyles.widthLong}; 54 | cursor: pointer; 55 | margin: 0; 56 | border-radius: 5px; 57 | padding: 0 10px 0 5px; 58 | box-sizing: border-box; 59 | font-size: 10px; 60 | 61 | &::-webkit-slider-thumb { 62 | -webkit-appearance: none; 63 | appearance: none; 64 | width: 8px; 65 | height: 25px; 66 | background-color: ${Colors.textWhite}; 67 | border-radius: ${ControlButtonStyles.radiusMiddle}; 68 | } 69 | 70 | &::-moz-range-thumb { 71 | width: 8px; 72 | height: 25px; 73 | background-color: ${Colors.textWhite}; 74 | border-radius: ${ControlButtonStyles.radiusMiddle}; 75 | } 76 | 77 | &::-ms-thumb { 78 | width: 8px; 79 | height: 25px; 80 | background-color: ${Colors.textWhite}; 81 | border-radius: ${ControlButtonStyles.radiusMiddle}; 82 | } 83 | `; 84 | 85 | export const HeadsetUI: React.FC = ({ 86 | xrDevice, 87 | inputLayer, 88 | pointerLocked, 89 | }) => { 90 | const [fovy, setFovy] = React.useState(xrDevice.fovy); 91 | return ( 92 | 93 |
102 |
111 | 112 |
{xrDevice.name}
113 |
114 |
121 |
122 | 123 | 127 | 128 | {!pointerLocked && ( 129 | 130 | 131 | 132 |
139 | 145 | FOV-Y 146 |
147 |
153 | { 156 | const value = Number(e.target.value); 157 | setFovy(value); 158 | xrDevice.fovy = value; 159 | }} 160 | min={Math.PI / 6} 161 | max={Math.PI / 1.5} 162 | step={Math.PI / 48} 163 | style={{ width: '80px' }} 164 | /> 165 |
166 |
167 |
168 | )} 169 |
170 | ); 171 | }; 172 | -------------------------------------------------------------------------------- /devui/src/components/keys.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { MouseLeft, MouseRight } from './icons.js'; 9 | import { 10 | faAngleUp, 11 | faArrowRightToBracket, 12 | faArrowTurnDown, 13 | faCaretDown, 14 | faCaretLeft, 15 | faCaretRight, 16 | faCaretUp, 17 | faDeleteLeft, 18 | } from '@fortawesome/free-solid-svg-icons'; 19 | 20 | import { FAIcon } from './styled.js'; 21 | 22 | export const MappedKeyDisplay: { 23 | [keyCode: string]: string | JSX.Element; 24 | } = { 25 | KeyA: 'A', 26 | KeyB: 'B', 27 | KeyC: 'C', 28 | KeyD: 'D', 29 | KeyE: 'E', 30 | KeyF: 'F', 31 | KeyG: 'G', 32 | KeyH: 'H', 33 | KeyI: 'I', 34 | KeyJ: 'J', 35 | KeyK: 'K', 36 | KeyL: 'L', 37 | KeyM: 'M', 38 | KeyN: 'N', 39 | KeyO: 'O', 40 | KeyP: 'P', 41 | KeyQ: 'Q', 42 | KeyR: 'R', 43 | KeyS: 'S', 44 | KeyT: 'T', 45 | KeyU: 'U', 46 | KeyV: 'V', 47 | KeyW: 'W', 48 | KeyX: 'X', 49 | KeyY: 'Y', 50 | KeyZ: 'Z', 51 | Digit0: '0', 52 | Digit1: '1', 53 | Digit2: '2', 54 | Digit3: '3', 55 | Digit4: '4', 56 | Digit5: '5', 57 | Digit6: '6', 58 | Digit7: '7', 59 | Digit8: '8', 60 | Digit9: '9', 61 | Tab: , 62 | Backspace: , 63 | Enter: ( 64 | 70 | ), 71 | ShiftLeft: , 72 | ShiftRight: , 73 | Space: ' ', 74 | ArrowUp: , 75 | ArrowDown: , 76 | ArrowLeft: , 77 | ArrowRight: , 78 | Semicolon: ';', 79 | Equal: '=', 80 | Comma: ',', 81 | Minus: '-', 82 | Period: '.', 83 | Slash: '/', 84 | Backquote: '`', 85 | BracketLeft: '[', 86 | Backslash: '\\', 87 | BracketRight: ']', 88 | Quote: "'", 89 | MouseLeft: , 90 | MouseRight: , 91 | }; 92 | -------------------------------------------------------------------------------- /devui/src/components/pinch.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { 9 | Button, 10 | ButtonContainer, 11 | ButtonGroup, 12 | Colors, 13 | ControlButtonStyles, 14 | FAControlIcon, 15 | MappedKeyBlock, 16 | RangeSelector, 17 | } from './styled.js'; 18 | import React, { useEffect, useState } from 'react'; 19 | 20 | import { MappedKeyDisplay } from './keys.js'; 21 | import { XRHandInput } from 'iwer/lib/device/XRHandInput.js'; 22 | import { faHandLizard } from '@fortawesome/free-solid-svg-icons'; 23 | 24 | interface PinchControlProps { 25 | hand: XRHandInput; 26 | pointerLocked: boolean; 27 | mappedKey: string; 28 | } 29 | 30 | const pinchSliderWidth = `calc(${ControlButtonStyles.widthLong} + ${ControlButtonStyles.widthShort} + ${ControlButtonStyles.gap})`; 31 | 32 | export const PinchControl: React.FC = ({ 33 | hand, 34 | pointerLocked, 35 | mappedKey, 36 | }) => { 37 | const [isPressed, setIsPressed] = useState(false); 38 | const [isKeyPressed, setIsKeyPressed] = useState(false); 39 | const [analogValue, setAnalogValue] = useState(0); 40 | 41 | const handedness = hand.inputSource.handedness; 42 | 43 | useEffect(() => { 44 | const handleKeyDown = (event: KeyboardEvent) => { 45 | if (event.code === mappedKey) { 46 | hand.updatePinchValue(1); 47 | setIsKeyPressed(true); 48 | } 49 | }; 50 | 51 | const handleKeyUp = (event: KeyboardEvent) => { 52 | if (event.code === mappedKey) { 53 | hand.updatePinchValue(0); 54 | setIsKeyPressed(false); 55 | } 56 | }; 57 | 58 | const handleMouseDown = (event: MouseEvent) => { 59 | if ( 60 | (mappedKey === 'MouseLeft' && event.button === 0) || 61 | (mappedKey === 'MouseRight' && event.button === 2) 62 | ) { 63 | hand.updatePinchValue(1); 64 | setIsKeyPressed(true); 65 | } 66 | }; 67 | 68 | const handleMouseUp = (event: MouseEvent) => { 69 | if ( 70 | (mappedKey === 'MouseLeft' && event.button === 0) || 71 | (mappedKey === 'MouseRight' && event.button === 2) 72 | ) { 73 | hand.updatePinchValue(0); 74 | setIsKeyPressed(false); 75 | } 76 | }; 77 | 78 | if (pointerLocked) { 79 | if (mappedKey === 'MouseLeft' || mappedKey === 'MouseRight') { 80 | window.addEventListener('mousedown', handleMouseDown); 81 | window.addEventListener('mouseup', handleMouseUp); 82 | } else { 83 | window.addEventListener('keydown', handleKeyDown); 84 | window.addEventListener('keyup', handleKeyUp); 85 | } 86 | } else { 87 | if (mappedKey === 'MouseLeft' || mappedKey === 'MouseRight') { 88 | window.removeEventListener('mousedown', handleMouseDown); 89 | window.removeEventListener('mouseup', handleMouseUp); 90 | } else { 91 | window.removeEventListener('keydown', handleKeyDown); 92 | window.removeEventListener('keyup', handleKeyUp); 93 | } 94 | } 95 | 96 | return () => { 97 | if (mappedKey === 'MouseLeft' || mappedKey === 'MouseRight') { 98 | window.removeEventListener('mousedown', handleMouseDown); 99 | window.removeEventListener('mouseup', handleMouseUp); 100 | } else { 101 | window.removeEventListener('keydown', handleKeyDown); 102 | window.removeEventListener('keyup', handleKeyUp); 103 | } 104 | }; 105 | }, [mappedKey, pointerLocked, hand]); 106 | 107 | return ( 108 | 109 | 110 | 111 | {pointerLocked ? ( 112 | 113 | {MappedKeyDisplay[mappedKey]} 114 | 115 | ) : ( 116 | <> 117 | 136 | { 140 | const value = Number(e.target.value); 141 | setAnalogValue(value); 142 | hand.updatePinchValue(value / 100); 143 | }} 144 | style={{ width: pinchSliderWidth }} 145 | min="0" 146 | max="100" 147 | /> 148 | 149 | )} 150 | 151 | 152 | ); 153 | }; 154 | -------------------------------------------------------------------------------- /devui/src/components/pose.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { 9 | Button, 10 | ButtonContainer, 11 | ButtonGroup, 12 | ControlButtonStyles, 13 | FAControlIcon, 14 | FAIcon, 15 | MappedKeyBlock, 16 | } from './styled.js'; 17 | import React, { useEffect, useState } from 'react'; 18 | import { 19 | faChevronLeft, 20 | faChevronRight, 21 | faHandScissors, 22 | } from '@fortawesome/free-solid-svg-icons'; 23 | 24 | import { MappedKeyDisplay } from './keys.js'; 25 | import { XRHandInput } from 'iwer/lib/device/XRHandInput.js'; 26 | 27 | interface PoseSelectorProps { 28 | hand: XRHandInput; 29 | pointerLocked: boolean; 30 | mappedKey: string; 31 | } 32 | 33 | const poses = ['default', 'point']; 34 | 35 | const poseButtonWidth = `calc(2 * ${ControlButtonStyles.widthLong} - ${ControlButtonStyles.widthShort})`; 36 | 37 | export const PoseSelector: React.FC = ({ 38 | hand, 39 | pointerLocked, 40 | mappedKey, 41 | }) => { 42 | const [poseId, setPoseId] = useState(hand.poseId); 43 | const [isKeyPressed, setIsKeyPressed] = useState(false); 44 | 45 | const handedness = hand.inputSource.handedness; 46 | const cyclePose = (delta: number) => { 47 | const poseIdx = poses.indexOf(hand.poseId); 48 | const newPoseIdx = (poseIdx + poses.length + delta) % poses.length; 49 | setPoseId(poses[newPoseIdx]); 50 | hand.poseId = poses[newPoseIdx]; 51 | }; 52 | const layoutReverse = handedness === 'right'; 53 | 54 | useEffect(() => { 55 | const handleKeyDown = (event: KeyboardEvent) => { 56 | if (event.code === mappedKey) { 57 | cyclePose(1); 58 | setIsKeyPressed(true); 59 | } 60 | }; 61 | 62 | const handleKeyUp = (event: KeyboardEvent) => { 63 | if (event.code === mappedKey) { 64 | setIsKeyPressed(false); 65 | } 66 | }; 67 | 68 | if (pointerLocked) { 69 | window.addEventListener('keydown', handleKeyDown); 70 | window.addEventListener('keyup', handleKeyUp); 71 | } else { 72 | window.removeEventListener('keydown', handleKeyDown); 73 | window.removeEventListener('keyup', handleKeyUp); 74 | } 75 | 76 | return () => { 77 | window.removeEventListener('keydown', handleKeyDown); 78 | window.removeEventListener('keyup', handleKeyUp); 79 | }; 80 | }, [mappedKey, pointerLocked, hand]); 81 | 82 | return ( 83 | 84 | 85 | 86 | {pointerLocked ? ( 87 | 88 | {MappedKeyDisplay[mappedKey]} 89 | 90 | ) : ( 91 | <> 92 | 103 | 112 | 123 | 124 | )} 125 | 126 | 127 | ); 128 | }; 129 | -------------------------------------------------------------------------------- /devui/src/components/vec3.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { FAIcon, InputSuffix, ValueInput, ValuesContainer } from './styled.js'; 9 | import { useEffect, useRef, useState } from 'react'; 10 | 11 | import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; 12 | import { styled } from 'styled-components'; 13 | 14 | export function round(number: number, decimalPlaces: number) { 15 | const factor = Math.pow(10, decimalPlaces); 16 | return Math.round(number * factor) / factor; 17 | } 18 | 19 | type Axis = 'x' | 'y' | 'z'; 20 | 21 | type Vector3Like = { 22 | x: number; 23 | y: number; 24 | z: number; 25 | }; 26 | 27 | interface Vector3InputProps { 28 | vector: Vector3Like; 29 | label?: string; 30 | icon?: IconDefinition; 31 | multiplier?: number; 32 | precision?: number; 33 | onValidInput?: () => void; 34 | marginBottom?: string; 35 | } 36 | 37 | const Vector3Container = styled.div` 38 | width: 100%; 39 | display: flex; 40 | flex-direction: row; 41 | justify-content: space-between; 42 | align-items: center; 43 | margin: 0; 44 | font-size: 12px; 45 | `; 46 | 47 | export const Vector3Input = ({ 48 | vector, 49 | label = '', 50 | icon, 51 | multiplier = 1, 52 | precision = 2, 53 | onValidInput = () => {}, 54 | marginBottom = '0', 55 | }: Vector3InputProps) => { 56 | const [displayValues, setDisplayValues] = useState({ 57 | x: (vector.x / multiplier).toFixed(precision), 58 | y: (vector.y / multiplier).toFixed(precision), 59 | z: (vector.z / multiplier).toFixed(precision), 60 | }); 61 | 62 | const actualValuesRef = useRef({ 63 | x: round(vector.x / multiplier, precision), 64 | y: round(vector.y / multiplier, precision), 65 | z: round(vector.z / multiplier, precision), 66 | }); 67 | 68 | const animationFrameId = useRef(null); 69 | 70 | // Sync display values with actual values (optimized) 71 | const syncValues = () => { 72 | const currentActualValues = { 73 | x: round(vector.x / multiplier, precision), 74 | y: round(vector.y / multiplier, precision), 75 | z: round(vector.z / multiplier, precision), 76 | }; 77 | 78 | const { x, y, z } = actualValuesRef.current; 79 | 80 | // Only update state if actual values have changed 81 | if ( 82 | currentActualValues.x !== x || 83 | currentActualValues.y !== y || 84 | currentActualValues.z !== z 85 | ) { 86 | actualValuesRef.current = currentActualValues; 87 | setDisplayValues({ 88 | x: currentActualValues.x.toFixed(precision), 89 | y: currentActualValues.y.toFixed(precision), 90 | z: currentActualValues.z.toFixed(precision), 91 | }); 92 | } 93 | 94 | // Schedule the next frame 95 | animationFrameId.current = requestAnimationFrame(syncValues); 96 | }; 97 | 98 | useEffect(() => { 99 | // Start the synchronization loop 100 | animationFrameId.current = requestAnimationFrame(syncValues); 101 | 102 | return () => { 103 | // Cleanup the animation frame on unmount 104 | if (animationFrameId.current) { 105 | cancelAnimationFrame(animationFrameId.current); 106 | } 107 | }; 108 | }, [vector, multiplier, precision]); 109 | 110 | // Handle user input changes 111 | const handleInputChange = 112 | (axis: Axis) => (event: React.ChangeEvent) => { 113 | const newValue = event.target.value; 114 | const parsedValue = parseFloat(newValue); 115 | 116 | // Update display values immediately 117 | setDisplayValues((prev) => ({ ...prev, [axis]: newValue })); 118 | 119 | // If valid, update the actual values and the vector 120 | if (!isNaN(parsedValue)) { 121 | actualValuesRef.current[axis] = parsedValue; 122 | vector[axis] = parsedValue * multiplier; 123 | onValidInput(); 124 | } 125 | }; 126 | 127 | return ( 128 | 129 | {icon ? ( 130 | 131 | ) : ( 132 | {label} 133 | )} 134 | 135 | {['x', 'y', 'z'].map((axis) => ( 136 |
144 | 154 | {axis.toUpperCase()} 155 |
156 | ))} 157 |
158 |
159 | ); 160 | }; 161 | -------------------------------------------------------------------------------- /devui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "outDir": "./lib", 6 | }, 7 | "include": ["src/**/*"] 8 | } -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { VERSION } from '../../src/version'; 2 | import { defineConfig } from 'vitepress'; 3 | 4 | // https://vitepress.dev/reference/site-config 5 | export default defineConfig({ 6 | title: 'IWER', 7 | description: 'Javascript WebXR Runtime Library for Emulation', 8 | head: [ 9 | [ 10 | 'link', 11 | { rel: 'icon', href: '/immersive-web-emulation-runtime/iwer.png' }, 12 | ], 13 | ], 14 | base: '/immersive-web-emulation-runtime/', 15 | themeConfig: { 16 | // https://vitepress.dev/reference/default-theme-config 17 | logo: { light: '/iwer-dark.svg', dark: '/iwer.svg' }, 18 | nav: [ 19 | { text: 'About', link: '/about' }, 20 | { text: 'Guide', link: '/getting-started' }, 21 | { 22 | text: 'Terms', 23 | link: 'https://opensource.fb.com/legal/terms/', 24 | }, 25 | { 26 | text: 'Privacy', 27 | link: 'https://opensource.fb.com/legal/privacy/', 28 | }, 29 | { 30 | text: `v${VERSION}`, 31 | items: [ 32 | { text: 'NPM', link: 'https://www.npmjs.com/package/iwer' }, 33 | { 34 | text: 'License', 35 | link: 'https://github.com/meta-quest/immersive-web-emulation-runtime/blob/main/LICENSE', 36 | }, 37 | ], 38 | }, 39 | ], 40 | 41 | sidebar: [ 42 | { 43 | text: 'Getting Started', 44 | items: [ 45 | { text: 'About IWER', link: '/about' }, 46 | { 47 | text: 'Installation', 48 | link: '/getting-started#adding-iwer-to-your-project', 49 | }, 50 | { 51 | text: 'Runtime Injection', 52 | link: '/getting-started#creating-an-xrdevice-and-installing-the-runtime', 53 | }, 54 | { 55 | text: '🥽 Emulated Headset', 56 | link: '/getting-started#emulated-headset', 57 | }, 58 | { 59 | text: '🎮 Emulated Controllers', 60 | link: '/getting-started#emulated-controllers', 61 | }, 62 | { 63 | text: '🖐️ Emulated Hands', 64 | link: '/getting-started#emulated-hands', 65 | }, 66 | { 67 | text: 'Platform Features', 68 | link: '/getting-started#platform-features', 69 | }, 70 | { 71 | text: 'XPlat Controls', 72 | link: '/xplat', 73 | }, 74 | ], 75 | }, 76 | { 77 | text: 'IWER in Action', 78 | link: '/action#action-recording-playback', 79 | items: [ 80 | { text: 'Live Demo', link: '/action#live-webxr-demo' }, 81 | { text: 'Action Recording', link: '/action#how-does-recording-work' }, 82 | { text: 'Action Playback', link: '/action#how-does-playback-work' }, 83 | ], 84 | }, 85 | { 86 | text: 'API Reference', 87 | items: [ 88 | { text: 'XRDevice Class', link: '/api/xr-device' }, 89 | { text: 'XRController Class', link: '/api/xr-controller' }, 90 | { text: 'XRHandInput Class', link: '/api/xr-hand-input' }, 91 | { text: 'Config Interfaces', link: '/api/config-interfaces' }, 92 | ], 93 | }, 94 | ], 95 | 96 | socialLinks: [ 97 | { 98 | icon: 'github', 99 | link: 'https://github.com/meta-quest/immersive-web-emulation-runtime/', 100 | }, 101 | ], 102 | 103 | search: { 104 | provider: 'local', 105 | }, 106 | 107 | footer: { 108 | copyright: 'MIT License | Copyright © Meta Platforms, Inc', 109 | }, 110 | }, 111 | }); 112 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | // https://vitepress.dev/guide/custom-theme 2 | import { h } from 'vue' 3 | import type { Theme } from 'vitepress' 4 | import DefaultTheme from 'vitepress/theme' 5 | import './style.css' 6 | 7 | export default { 8 | extends: DefaultTheme, 9 | Layout: () => { 10 | return h(DefaultTheme.Layout, null, { 11 | // https://vitepress.dev/guide/extending-default-theme#layout-slots 12 | }) 13 | }, 14 | enhanceApp({ app, router, siteData }) { 15 | // ... 16 | } 17 | } satisfies Theme 18 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Customize default theme styling by overriding CSS variables: 3 | * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css 4 | */ 5 | 6 | /** 7 | * Colors 8 | * 9 | * Each colors have exact same color scale system with 3 levels of solid 10 | * colors with different brightness, and 1 soft color. 11 | * 12 | * - `XXX-1`: The most solid color used mainly for colored text. It must 13 | * satisfy the contrast ratio against when used on top of `XXX-soft`. 14 | * 15 | * - `XXX-2`: The color used mainly for hover state of the button. 16 | * 17 | * - `XXX-3`: The color for solid background, such as bg color of the button. 18 | * It must satisfy the contrast ratio with pure white (#ffffff) text on 19 | * top of it. 20 | * 21 | * - `XXX-soft`: The color used for subtle background such as custom container 22 | * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors 23 | * on top of it. 24 | * 25 | * The soft color must be semi transparent alpha channel. This is crucial 26 | * because it allows adding multiple "soft" colors on top of each other 27 | * to create a accent, such as when having inline code block inside 28 | * custom containers. 29 | * 30 | * - `default`: The color used purely for subtle indication without any 31 | * special meanings attched to it such as bg color for menu hover state. 32 | * 33 | * - `brand`: Used for primary brand colors, such as link text, button with 34 | * brand theme, etc. 35 | * 36 | * - `tip`: Used to indicate useful information. The default theme uses the 37 | * brand color for this by default. 38 | * 39 | * - `warning`: Used to indicate warning to the users. Used in custom 40 | * container, badges, etc. 41 | * 42 | * - `danger`: Used to show error, or dangerous message to the users. Used 43 | * in custom container, badges, etc. 44 | * -------------------------------------------------------------------------- */ 45 | 46 | :root { 47 | --vp-c-default-1: var(--vp-c-gray-1); 48 | --vp-c-default-2: var(--vp-c-gray-2); 49 | --vp-c-default-3: var(--vp-c-gray-3); 50 | --vp-c-default-soft: var(--vp-c-gray-soft); 51 | 52 | --vp-c-brand-1: var(--vp-c-indigo-1); 53 | --vp-c-brand-2: var(--vp-c-indigo-2); 54 | --vp-c-brand-3: var(--vp-c-indigo-3); 55 | --vp-c-brand-soft: var(--vp-c-indigo-soft); 56 | 57 | --vp-c-tip-1: var(--vp-c-brand-1); 58 | --vp-c-tip-2: var(--vp-c-brand-2); 59 | --vp-c-tip-3: var(--vp-c-brand-3); 60 | --vp-c-tip-soft: var(--vp-c-brand-soft); 61 | 62 | --vp-c-warning-1: var(--vp-c-yellow-1); 63 | --vp-c-warning-2: var(--vp-c-yellow-2); 64 | --vp-c-warning-3: var(--vp-c-yellow-3); 65 | --vp-c-warning-soft: var(--vp-c-yellow-soft); 66 | 67 | --vp-c-danger-1: var(--vp-c-red-1); 68 | --vp-c-danger-2: var(--vp-c-red-2); 69 | --vp-c-danger-3: var(--vp-c-red-3); 70 | --vp-c-danger-soft: var(--vp-c-red-soft); 71 | } 72 | 73 | /** 74 | * Component: Button 75 | * -------------------------------------------------------------------------- */ 76 | 77 | :root { 78 | --vp-button-brand-border: transparent; 79 | --vp-button-brand-text: var(--vp-c-white); 80 | --vp-button-brand-bg: var(--vp-c-brand-3); 81 | --vp-button-brand-hover-border: transparent; 82 | --vp-button-brand-hover-text: var(--vp-c-white); 83 | --vp-button-brand-hover-bg: var(--vp-c-brand-2); 84 | --vp-button-brand-active-border: transparent; 85 | --vp-button-brand-active-text: var(--vp-c-white); 86 | --vp-button-brand-active-bg: var(--vp-c-brand-1); 87 | } 88 | 89 | /** 90 | * Component: Home 91 | * -------------------------------------------------------------------------- */ 92 | 93 | :root { 94 | --vp-home-hero-name-color: transparent; 95 | --vp-home-hero-name-background: -webkit-linear-gradient( 96 | 120deg, 97 | #bd34fe 30%, 98 | #41d1ff 99 | ); 100 | 101 | --vp-home-hero-image-background-image: linear-gradient( 102 | -45deg, 103 | #bd34fe 50%, 104 | #47caff 50% 105 | ); 106 | --vp-home-hero-image-filter: blur(44px); 107 | } 108 | 109 | @media (min-width: 640px) { 110 | :root { 111 | --vp-home-hero-image-filter: blur(56px); 112 | } 113 | } 114 | 115 | @media (min-width: 960px) { 116 | :root { 117 | --vp-home-hero-image-filter: blur(68px); 118 | } 119 | } 120 | 121 | /** 122 | * Component: Custom Block 123 | * -------------------------------------------------------------------------- */ 124 | 125 | :root { 126 | --vp-custom-block-tip-border: transparent; 127 | --vp-custom-block-tip-text: var(--vp-c-text-1); 128 | --vp-custom-block-tip-bg: var(--vp-c-brand-soft); 129 | --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft); 130 | } 131 | 132 | /** 133 | * Component: Algolia 134 | * -------------------------------------------------------------------------- */ 135 | 136 | .DocSearch { 137 | --docsearch-primary-color: var(--vp-c-brand-1) !important; 138 | } 139 | 140 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | title: About 4 | --- 5 | 6 | # What is IWER? 7 | 8 | The Immersive Web Emulation Runtime **(IWER)** is a TypeScript-based tool designed to enable the running of WebXR applications in modern browsers without requiring native WebXR support. By emulating the WebXR Device API, IWER provides developers with the ability to test and run WebXR projects across a wide range of devices, ensuring compatibility and enhancing the development process. 9 | 10 | IWER utilizes a universal `XRDevice` interface, configurable to emulate the vast majority of XR hardware with WebXR support, to offer direct and comprehensive control over the XR device in the emulated WebXR experience; Its potential applications include: 11 | 12 | - **Custom Development Tools**: Empowers developers to craft specific WebXR tools tailored to their project requirements, bypassing the need for browser extensions. 13 | - **Cross-Platform Compatibility**: Functions as an input remapping layer over existing WebXR projects, facilitating seamless cross-platform functionality and allowing the recycling of XR interaction codes. 14 | - **Scalable Testing Solutions**: Offers action capture and playback features that unlocks automated testing of WebXR projects for the first time in environments without the necessity for physical headsets, enhancing testing accessibility and scalability. 15 | -------------------------------------------------------------------------------- /docs/action.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | title: IWER in Action 4 | --- 5 | 6 | # IWER in Action 7 | 8 | The following section introduces the Immersive Web Emulation Runtime (IWER) implemented within a three.js-powered WebXR demo. This demo smartly detects whether you are accessing from a browser lacking native WebXR support and automatically installs IWER's WebXR runtime, allowing entry into XR in emulation mode. In this mode, a simple GUI at the top right corner lets you interact with a selection of IWER's features like toggling stereo rendering and managing XRVisibilityState, alongside exploring an **experimental input playback** feature. 9 | 10 | ## Live WebXR Demo 11 | 12 | Below is the live demo which uses three.js. It includes preloaded actions captured on a Meta Quest 3 device. For those with WebXR-ready browsers, it's possible to capture your own actions within this demo environment. 13 | 14 | 22 | 23 | ## Comparative Video 24 | 25 | Here's a screen recording from the same session captured for the demo, providing a visual reference of the actions included in the live demo. 26 | 27 | 31 | 32 | ## Action Recording & Playback 33 | 34 | Recording and replaying user actions in WebXR are some of the most requested features among the WebXR developer community for their potential to simplify debugging and automate testing. The primary use cases include: 35 | 36 | - **Debugging**: Capture user actions that lead to issues or crashes in your WebXR apps. Replay these actions in a developer-friendly environment to robustly reproduce and troubleshoot issues. 37 | - **Automated Testing**: Automate end-to-end testing, reducing the reliance on manual QA, which is traditionally labor-intensive for WebXR applications. 38 | - **Motion Capture**: Record brief sequences of user actions to generate motion capture data, enhancing the realism and interactivity of your WebXR content. 39 | 40 | ### How Does Recording Work? 41 | 42 | IWER’s `ActionRecorder` functions independently from its WebXR runtime and is typically utilized in environments where native WebXR support is available. During each animation frame, `ActionRecorder` captures data directly from the `XRSession` and `XRFrame`. This includes recording the transforms of the headset and input sources relative to the root `XRReferenceSpace`. It also captures the component states within the input sources, such as button presses and joystick movements from the associated `Gamepad` objects. 43 | 44 | #### Handling Input Source Switching 45 | 46 | To handle scenarios where input sources may change during a session, `ActionRecorder` listens for input source events. Upon detecting a new input source, it constructs an input schema that includes details like the profile ID and input type, and then it starts tracking data for that input source. 47 | 48 | ### How to Record an Input Session? 49 | 50 | 1. **Initialize the Recorder**: 51 | 52 | ```javascript 53 | import { ActionRecorder } from 'iwer'; 54 | 55 | onSessionStarted(xrSession) { 56 | const refSpace = await xrSession.requestReferenceSpace('local-floor'); 57 | recorder = new ActionRecorder(xrSession, refSpace); 58 | } 59 | ``` 60 | 61 | 2. **Start / Stop the Recording**: 62 | 63 | ```javascript 64 | let recording = false; 65 | 66 | onButtonPress() { 67 | recording = !recording; // Toggle recording state 68 | } 69 | 70 | onFrame(xrFrame) { 71 | if (recording) { 72 | recorder.recordFrame(xrFrame); 73 | } 74 | } 75 | ``` 76 | 77 | 3. **Access the Recorded Data**: 78 | ```javascript 79 | recorder.log(); // Outputs the recorded data as a JSON string in the console 80 | ``` 81 | 82 | ### How does Playback Work? 83 | 84 | `ActionPlayer`, unlike `ActionRecorder`, integrates closely with IWER’s WebXR runtime and connects directly to the `XRDevice` class. It uses the root `XRReferenceSpace` from your WebXR app to accurately reconstruct input data for playback. 85 | 86 | #### Achieving Playback Accuracy 87 | 88 | Due to hardware and framerate variations, achieving perfect playback accuracy can be challenging. For example, discrepancies in frame rates between recording (e.g., 72 fps) and playback (e.g., 60 fps) devices can cause subtle speed variations in the playback of recorded sessions. `ActionPlayer` mitigates this by interpolating between frames based on timestamps to maintain consistent motion during playback. 89 | 90 | ### How to Play a Captured Session? 91 | 92 | 1. **Initialize the Player**: 93 | 94 | ```javascript 95 | let capture = JSON.parse(capturedJSONString); 96 | let player; 97 | 98 | onSessionStarted(xrSession) { 99 | player = xrdevice.createActionPlayer(refSpace, capture); 100 | } 101 | ``` 102 | 103 | 2. **Control Playback**: 104 | ```javascript 105 | player.play(); // Starts playback from the beginning 106 | player.stop(); // Stops the playback 107 | ``` 108 | -------------------------------------------------------------------------------- /docs/api/config-interfaces.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # Configuration Interfaces 6 | 7 | ## XRDeviceConfig 8 | 9 | - **`name`**: **string** 10 | The name of the XR device. 11 | 12 | - **`controllerConfig`**: **XRControllerConfig | undefined** 13 | Configuration for the device's controllers. It is `undefined` if no controllers are used. 14 | 15 | - **`supportedSessionModes`**: **XRSessionMode[]** 16 | Session modes the device supports (e.g., `inline`, `immersive-vr`, `immersive-ar`). 17 | 18 | - **`supportedFeatures`**: **WebXRFeatures[]** 19 | Features supported by the device, such as `hand-tracking`, `hit-test`, etc. 20 | 21 | - **`supportedFrameRates`**: **number[]** 22 | Frame rates supported by the device for rendering. 23 | 24 | - **`isSystemKeyboardSupported`**: **boolean** 25 | Indicates if system keyboard input is supported. 26 | 27 | - **`internalNominalFrameRate`**: **number** 28 | The device's nominal internal frame rate. 29 | 30 | ## XRDeviceOptions 31 | 32 | - **`ipd`**: **number** 33 | Interpupillary distance in meters, affecting stereo rendering. 34 | 35 | - **`fovy`**: **number** 36 | Field of view on the Y-axis in radians. 37 | 38 | - **`stereoEnabled`**: **boolean** 39 | Enables or disables stereo rendering. 40 | 41 | - **`headsetPosition`**: **Vector3** 42 | The initial position of the headset in 3D space. 43 | 44 | - **`headsetQuaternion`**: **Quaternion** 45 | The initial orientation of the headset. 46 | 47 | - **`canvasContainer`**: **HTMLDivElement** 48 | The HTML container for the rendering canvas. 49 | 50 | ## XRControllerConfig 51 | 52 | - **`profileId`**: **string** 53 | Identifier for the controller profile. 54 | 55 | - **`fallbackProfileIds`**: **string[]** 56 | Identifiers for fallback controller profiles. 57 | 58 | - **`layout`**: **{ [handedness in XRHandedness]?: { gamepad: GamepadConfig; gripOffsetMatrix?: mat4; numHapticActuators: number; } }** 59 | Maps handedness to controller configurations, including gamepad layout and optional grip offset. 60 | 61 | ## HandPose 62 | 63 | - **`jointTransforms`**: **{ [joint in XRHandJoint]: { offsetMatrix: mat4; radius: number; } }** 64 | Pose data for each joint in the hand, including transformation matrix and joint radius. 65 | 66 | - **`gripOffsetMatrix`**: **mat4?** 67 | Optional matrix to offset the grip position. 68 | 69 | ## XRHandInputConfig 70 | 71 | - **`profileId`**: **string** 72 | Identifier for the hand input profile. 73 | 74 | - **`fallbackProfileIds`**: **string[]** 75 | Identifiers for fallback hand input profiles. 76 | 77 | - **`poses`**: **{ default: HandPose; pinch: HandPose; [poseId: string]: HandPose; }** 78 | Contains hand pose configurations for default, pinch, and additional customizable poses. 79 | -------------------------------------------------------------------------------- /docs/api/xr-controller.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # XRController Class 6 | 7 | The `XRController` class represents an emulated XR input device, such as a handheld controller, used within WebXR projects. 8 | 9 | ## Properties 10 | 11 | Properties of the `XRController` class provide access to the controller's current state, including its connection status, position, orientation, and the associated input source. 12 | 13 | ### `connected` 14 | 15 | Indicates whether the controller is currently connected and active. 16 | 17 | - **Type**: `boolean` 18 | 19 | ### `position` 20 | 21 | Provides the current position of the controller in the 3D space, enabling position tracking within the virtual environment. 22 | 23 | - **Type**: `Vector3` 24 | - **Readonly** 25 | 26 | ### `quaternion` 27 | 28 | Describes the current orientation of the controller as a quaternion, essential for accurate directional tracking. 29 | 30 | - **Type**: `Quaternion` 31 | - **Readonly** 32 | 33 | ### `inputSource` 34 | 35 | Accesses the underlying `XRInputSource` object for this controller, linking it to the broader XR input system. 36 | 37 | - **Type**: `XRInputSource` 38 | - **Readonly** 39 | 40 | ## Methods 41 | 42 | Methods of the `XRController` class allow for dynamic updates to the controller's state, simulating user interactions such as button presses, touch, and movement along axes. 43 | 44 | ### `updateButtonValue` 45 | 46 | Simulates the press or release of a button on the controller by updating its value. 47 | 48 | ```typescript 49 | updateButtonValue(id: string, value: number): void 50 | ``` 51 | 52 | - `id`: **string** - The identifier for the button to update. 53 | - `value`: **number** - The new value for the button, with `0` indicating unpressed and `1` indicating fully pressed. 54 | 55 | ### `updateButtonTouch` 56 | 57 | Simulates touching or releasing a touch-sensitive button on the controller. 58 | 59 | ```typescript 60 | updateButtonTouch(id: string, touched: boolean): void 61 | ``` 62 | 63 | - `id`: **string** - The identifier for the button to update. 64 | - `touched`: **boolean** - Indicates whether the button is being touched. 65 | 66 | ### `updateAxis` 67 | 68 | Updates the value of a specific axis, such as a thumbstick or touchpad direction. 69 | 70 | ```typescript 71 | updateAxis(id: string, type: 'x-axis' | 'y-axis', value: number): void 72 | ``` 73 | 74 | - `id`: **string** - The identifier for the axis group to update. 75 | - `type`: **'x-axis' | 'y-axis'** - Specifies which axis to update. 76 | - `value`: **number** - The new value for the axis, between `-1` (full negative) and `1` (full positive). 77 | 78 | ### `updateAxes` 79 | 80 | Simultaneously updates the values of both axes for a specific control element on the controller. 81 | 82 | ```typescript 83 | updateAxes(id: string, x: number, y: number): void 84 | ``` 85 | 86 | - `id`: **string** - The identifier for the axis group to update. 87 | - `x`: **number** - The new value for the x-axis. 88 | - `y`: **number** - The new value for the y-axis. 89 | -------------------------------------------------------------------------------- /docs/api/xr-hand-input.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # XRHandInput Class 6 | 7 | The `XRHandInput` class emulates hand input within WebXR environments. It supports detailed hand pose emulation and gesture recognition, allowing for immersive interactions within the virtual space. 8 | 9 | ## Properties 10 | 11 | ### `poseId` 12 | 13 | Specifies the current hand pose being emulated, such as `default`, `pinch`, or other custom poses defined in the configuration. 14 | 15 | - **Type**: `string` 16 | - Allows dynamic update to the hand pose through a setter. Changing the `poseId` triggers an update in the hand's pose based on the configuration associated with the new pose ID. 17 | 18 | ### `pinchValue` 19 | 20 | Indicates the current level of the pinch gesture being emulated, with a range from `0` (no pinch) to `1` (full pinch). 21 | 22 | - **Type**: `number` 23 | - **Readonly** 24 | 25 | ### `connected` 26 | 27 | Reflects the connection status of the hand input, showing whether it's actively recognized and tracked within the XR session. 28 | 29 | - **Type**: `boolean` 30 | 31 | ### `position` 32 | 33 | The current 3D position of the hand input, allowing for movement tracking within the virtual environment. 34 | 35 | - **Type**: `Vector3` 36 | - **Readonly** 37 | 38 | ### `quaternion` 39 | 40 | The current orientation of the hand input, represented as a quaternion for accurate gesture and direction tracking. 41 | 42 | - **Type**: `Quaternion` 43 | - **Readonly** 44 | 45 | ### `inputSource` 46 | 47 | Provides access to the `XRInputSource` associated with this hand input, integrating it with the XR input system. 48 | 49 | - **Type**: `XRInputSource` 50 | - **Readonly** 51 | 52 | ## Methods 53 | 54 | ### `updateHandPose` 55 | 56 | Executes the pose update logic, adjusting the virtual hand's current pose based on the active `poseId` and `pinchValue`. This method facilitates the interpolation between different hand poses and pinch gestures for dynamic hand gesture emulation. 57 | 58 | ```typescript 59 | updateHandPose(): void 60 | ``` 61 | 62 | ### `updatePinchValue` 63 | 64 | Modifies the emulation level of the pinch gesture. This method allows for the simulation of pinch gestures to various degrees, enhancing interaction realism within the virtual environment. 65 | 66 | ```typescript 67 | updatePinchValue(value: number): void 68 | ``` 69 | 70 | - `value`: **number** - The new pinch intensity, where `0` indicates no pinch and `1` indicates a fully engaged pinch gesture. 71 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: Immersive Web Emulation Runtime 7 | text: 🔓 Unlock WebXR Emulation Everywhere 8 | actions: 9 | - theme: brand 10 | text: Getting Started 11 | link: /getting-started 12 | - theme: alt 13 | text: API Reference 14 | link: /api/xr-device 15 | 16 | features: 17 | - title: 🌐  Emulate WebXR Anywhere 18 | details: IWER provides a comprehensive WebXR runtime that unlocks WebXR emulation on any browser, enabling developers to create bespoke WebXR development tools. Develop seamlessly across all browsers, no native WebXR needed. 19 | linkText: Get Started 20 | link: /getting-started 21 | - title: 🎮  Recycle XR Controls across Platforms 22 | details: IWER is a lightweight, high-performance layer capable of input remapping directly atop your WebXR projects. It enables seamless recycling of your XR input stack, enhancing cross-platform compatibility without the overhead. 23 | linkText: Read Story 24 | link: /xplat 25 | - title: 🎥  Action Capture and Playback 26 | details: With IWER, automated testing in WebXR becomes a breeze. Capture user interactions within XR environments and replay them across various devices effortlessly, thanks to our action capture and playback feature. 27 | linkText: Live Demo 28 | link: /action 29 | --- 30 | -------------------------------------------------------------------------------- /docs/public/cap.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meta-quest/immersive-web-emulation-runtime/4b05359a07f9b92086bc09d3e5be0971b2e01dd1/docs/public/cap.mp4 -------------------------------------------------------------------------------- /docs/public/iwer-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/public/iwer-locomotion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meta-quest/immersive-web-emulation-runtime/4b05359a07f9b92086bc09d3e5be0971b2e01dd1/docs/public/iwer-locomotion.gif -------------------------------------------------------------------------------- /docs/public/iwer-open-menu.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meta-quest/immersive-web-emulation-runtime/4b05359a07f9b92086bc09d3e5be0971b2e01dd1/docs/public/iwer-open-menu.gif -------------------------------------------------------------------------------- /docs/public/iwer-photo-drop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meta-quest/immersive-web-emulation-runtime/4b05359a07f9b92086bc09d3e5be0971b2e01dd1/docs/public/iwer-photo-drop.gif -------------------------------------------------------------------------------- /docs/public/iwer-seed-selection.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meta-quest/immersive-web-emulation-runtime/4b05359a07f9b92086bc09d3e5be0971b2e01dd1/docs/public/iwer-seed-selection.gif -------------------------------------------------------------------------------- /docs/public/iwer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meta-quest/immersive-web-emulation-runtime/4b05359a07f9b92086bc09d3e5be0971b2e01dd1/docs/public/iwer.png -------------------------------------------------------------------------------- /docs/public/iwer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/xplat.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | title: Cross-Platform Controls with IWER 4 | --- 5 | 6 | # Bringing Project Flowerbed to PC: Cross-Platform Controls with IWER 7 | 8 | ## Motivation 9 | 10 | The web’s inherent cross-platform nature offers unique opportunities for developers. However, adapting WebXR experiences, which are rich in nuanced interactions, to non-VR platforms like PCs and mobile devices presents distinct challenges. Typically, developers might resort to creating multiple input mechanisms for different platforms, leading to complex and hard-to-maintain code. The Immersive Web Emulation Runtime (IWER) offers a streamlined solution by serving as an input remapping layer, simplifying the adaptation process. This article details my journey in implementing intuitive cross-platform controls for [Project Flowerbed](https://flowerbed.metademolab.com/), a WebXR showcase with intricate interactions, using IWER. 11 | 12 | ## Locomotion 13 | 14 | ![locomotion](/iwer-locomotion.gif) 15 | 16 | One of the first challenges I faced was enabling players to move around in the gorgeous environment of Project Flowerbed. On PC, first-person movements are typically controlled using the WASD keys for movement and the mouse for camera direction. Project Flowerbed, originally designed for VR, utilizes sliding locomotion where movement is controlled by a joystick and the camera by the player’s head movement. To adapt this, I mapped the WASD keys to control the emulated joystick via IWER and the mouse movement to control the emulated headset's rotation. This setup allows PC players to navigate Project Flowerbed as smoothly as they would in any first-person PC game. 17 | 18 | ## Simple One-Handed Interactions 19 | 20 | ![open-menu](/iwer-open-menu.gif) 21 | 22 | Most interactions in Project Flowerbed, as with many XR applications, involve simple one-handed interactions that consist of two basic steps: point and click. This includes indirect interactions such as raycasting or direct interactions like grabbing virtual objects. These interactions are straightforward to adapt to PC controls as well. 23 | 24 | ### Pointing: 25 | 26 | In VR, pointing is usually done with controllers. In Project Flowerbed, this translates to PC by coupling the mouse with the camera movement. By constructing a player rig that includes controllers and a headset, with controllers parented under the headset, pointing on a PC becomes intuitive. As the player rotates the headset view with the mouse, the controllers follow, allowing for accurate pointing. 27 | 28 | ### Clicking: 29 | 30 | I use "clicking" to refer broadly to actions like pressing, holding, and grabbing. On PC, these can be mapped to keyboard and mouse buttons in a way that is ergonomic and logical. For instance, in Project Flowerbed, I mapped the F key to trigger the main action wheel (originally the A button in VR), and the left mouse button to mimic shooting a seed, corresponding to the right trigger in VR. 31 | 32 | ### One-Handed Gestures: 33 | 34 | ![photo-drop](/iwer-photo-drop.gif) 35 | 36 | Addressing straightforward point-and-click interactions laid a solid foundation for handling more complex one-handed gestures. For example, the drag-and-drop action to save or delete a photo in Project Flowerbed breaks down into a sequence of point-and-click actions. By leveraging IWER, implementing these gestures on PC became straightforward. 37 | 38 | ## Complex Gesture-Based Interactions 39 | 40 | ![seed-selection](/iwer-seed-selection.gif) 41 | 42 | More intricate interactions that involve relative motion between two hands, like selecting a seedbag with one hand from a seedbox attached to the other, required a creative approach. On PC, since both hands are typically attached to the camera rig, achieving this relative motion means temporarily decoupling one hand. In planting mode, holding the right mouse button decouples the left hand (seedbox), allowing the player to select a seedbag with the other hand. Releasing the right mouse button then snaps the left hand back under the camera rig. 43 | 44 | For scenarios requiring complex gestures or patterns to trigger specific actions in XR, consider using the Action Recorder to pre-record these sequences. These can then be played back on PC by mapping them to keyboard or mouse events. For more details on how to use the Action Recorder, check out [this guide](/action). 45 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | IWER Demo 17 | 46 | 47 | 48 |
49 |
50 |

Immersive Web Emulation Runtime Demo

51 | 52 |
    53 |
  • 54 | No Native WebXR Support Detected: 55 | IWER has created an emulated Meta Quest 3 device and injected its 56 | WebXR runtime, you can now enter WebXR in emulation mode and control 57 | the emulated device via the GUI. 58 |
  • 59 | 60 |
  • 61 | Action Playback Demo: 62 | This demo is loaded with a pre-recorded input session captured on a 63 | Meta Quest 3 device, you can view the playback by entering VR in 64 | emulation mode and click the "Play Action" button. 65 |
  • 66 | 67 |
  • 68 | Native WebXR Support Detected: 69 | Enter VR and record your input session by pressing trigger on your 70 | right controller to start/end recording 71 |
  • 72 |
73 |
74 | 75 | 76 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iwer-example", 3 | "private": true, 4 | "devDependencies": { 5 | "css-loader": "^5.2.1", 6 | "html-webpack-plugin": "^5.3.1", 7 | "pre-commit": "^1.2.2", 8 | "style-loader": "3.3.4", 9 | "webpack": "^5.91.0", 10 | "webpack-cli": "5.1.4", 11 | "webpack-dev-server": "5.0.4" 12 | }, 13 | "scripts": { 14 | "build": "webpack", 15 | "ci-build": "NODE_ENV=development npm install && NODE_ENV=production npm run build", 16 | "serve": "webpack serve" 17 | }, 18 | "dependencies": { 19 | "@iwer/devui": "file:../devui", 20 | "dat.gui": "0.7.9", 21 | "iwer": "file:../", 22 | "three": "npm:super-three@0.158.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: process.env.NODE_ENV ?? 'development', 6 | entry: { 7 | index: './index.js', 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.css$/i, 13 | use: ['style-loader', 'css-loader'], 14 | }, 15 | ], 16 | }, 17 | devServer: { 18 | static: { 19 | directory: path.join(__dirname, 'dist'), 20 | }, 21 | host: '0.0.0.0', 22 | server: 'https', 23 | compress: true, 24 | port: 8081, 25 | }, 26 | output: { 27 | filename: '[name].bundle.js', 28 | path: path.resolve(__dirname, 'dist'), 29 | clean: true, 30 | }, 31 | plugins: [ 32 | new HtmlWebpackPlugin({ 33 | template: './index.html', 34 | }), 35 | ], 36 | devtool: process.env.NODE_ENV === 'production' ? false : 'source-map', 37 | }; 38 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest/presets/default-esm', 4 | extensionsToTreatAsEsm: ['.ts'], 5 | setupFiles: ['/tests/polyfill.ts'], 6 | moduleNameMapper: { 7 | '^(\\.{1,2}/.*)\\.js$': '$1', 8 | }, 9 | transform: { 10 | '^.+\\.ts$': ['ts-jest', { useESM: true }], 11 | }, 12 | testEnvironment: 'jsdom', 13 | }; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iwer", 3 | "version": "2.0.1", 4 | "description": "Javascript WebXR Runtime for Emulation", 5 | "type": "module", 6 | "main": "lib/index.js", 7 | "types": "lib/index.d.ts", 8 | "files": [ 9 | "build", 10 | "lib" 11 | ], 12 | "scripts": { 13 | "prebuild": "node -p \"'export const VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts", 14 | "build": "tsc && rollup -c", 15 | "doc": "typedoc", 16 | "format": "prettier --write ./src/**/*", 17 | "test": "jest --coverage", 18 | "prepublishOnly": "npm run build", 19 | "docs:dev": "vitepress dev docs", 20 | "docs:build": "vitepress build docs", 21 | "docs:preview": "vitepress preview docs" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/meta-quest/immersive-web-emulation-runtime.git" 26 | }, 27 | "keywords": [], 28 | "author": "Felix Zhang ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/meta-quest/immersive-web-emulation-runtime/issues" 32 | }, 33 | "homepage": "https://github.com/meta-quest/immersive-web-emulation-runtime#readme", 34 | "devDependencies": { 35 | "@rollup/plugin-commonjs": "^25.0.7", 36 | "@rollup/plugin-node-resolve": "^15.2.3", 37 | "@rollup/plugin-terser": "^0.4.4", 38 | "@types/gl-matrix": "^3.2.0", 39 | "@types/jest": "^29.5.11", 40 | "@types/node": "^18.7.13", 41 | "@types/three": "^0.149.0", 42 | "@types/uuid": "^9.0.8", 43 | "@types/webxr": "^0.5.8", 44 | "@typescript-eslint/eslint-plugin": "^5.55.0", 45 | "@typescript-eslint/parser": "^5.55.0", 46 | "eslint": "^8.36.0", 47 | "eslint-config-prettier": "^8.10.0", 48 | "eslint-plugin-prettier": "^5.0.1", 49 | "jest": "^29.7.0", 50 | "jest-environment-jsdom": "^29.7.0", 51 | "prettier": "^3.0.3", 52 | "renamer": "^4.0.0", 53 | "rimraf": "^5.0.5", 54 | "rollup": "^2.79.1", 55 | "ts-jest": "^29.1.1", 56 | "typescript": "^4.9.5", 57 | "vitepress": "1.0.1" 58 | }, 59 | "dependencies": { 60 | "gl-matrix": "^3.4.3" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import terser from '@rollup/plugin-terser'; 4 | 5 | export default { 6 | input: 'lib/index.js', 7 | plugins: [resolve(), commonjs()], 8 | output: [ 9 | // UMD build 10 | { 11 | file: 'build/iwer.js', 12 | format: 'umd', 13 | name: 'IWER', 14 | }, 15 | // Minified UMD build 16 | { 17 | file: 'build/iwer.min.js', 18 | format: 'umd', 19 | name: 'IWER', 20 | plugins: [terser()], 21 | }, 22 | // ES module build 23 | { 24 | file: 'build/iwer.module.js', 25 | format: 'es', 26 | }, 27 | // Minified ES module build 28 | { 29 | file: 'build/iwer.module.min.js', 30 | format: 'es', 31 | plugins: [terser()], 32 | }, 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /sem/.gitignore: -------------------------------------------------------------------------------- 1 | src/registry.ts -------------------------------------------------------------------------------- /sem/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

@iwer/sem

4 |

5 | 6 |

7 | npm version 8 | npm download 9 | language 10 | license 11 |

12 | 13 | `@iwer/sem` (**Synthetic Environment Module**) is an extension for the Immersive Web Emulation Runtime (**IWER**). It enables advanced Mixed Reality features in IWER by emulating real-world environments with high-fidelity, including video passthrough, plane and mesh detection, semantic labels, and hit testing. 14 | 15 | ## Key Features 16 | 17 | - **Video Passthrough Emulation:** Injects a video passthrough layer, rendering the captured scene behind the emulated app canvas. 18 | - **Plane & Mesh Detection:** Provides `XRPlane` and `XRMesh` information via the `plane-detection` and `mesh-detection` APIs in IWER. 19 | - **High Fidelity Scene Mesh:** Supports emulation of high-fidelity scene meshes exposed on Meta Quest 3 and 3S devices. 20 | - **Hit-Test:** Enables `hit-test` APIs through IWER by conducting real-time raycasting spatial queries with the loaded environment. 21 | 22 | ## Current Status 23 | 24 | `@iwer/sem` is currently in `0.x` status and is under active development. This is an early build, changes may occur before the official v1.0 release. 25 | 26 | ## Installation 27 | 28 | To install `@iwer/sem`, use the following npm command: 29 | 30 | ```bash 31 | npm install @iwer/sem 32 | ``` 33 | 34 | ## Usage 35 | 36 | `@iwer/sem` requires an active IWER runtime. If you are new to IWER, refer to the [IWER Getting Started Guide](https://meta-quest.github.io/immersive-web-emulation-runtime/getting-started.html). Here is a quick example: 37 | 38 | ```javascript 39 | import { XRDevice, metaQuest3 } from 'iwer'; 40 | 41 | // Initialize the XR device with a preset configuration (e.g., Meta Quest 3) 42 | const xrDevice = new XRDevice(metaQuest3); 43 | 44 | // Install the IWER runtime to enable WebXR emulation 45 | xrDevice.installRuntime(); 46 | ``` 47 | 48 | Integrate `@iwer/sem`: 49 | 50 | ```javascript 51 | import { SyntheticEnvironmentModule } from '@iwer/sem'; 52 | 53 | const sem = new SyntheticEnvironmentModule(); 54 | xrDevice.installSyntheticEnvironmentModule(sem); 55 | ``` 56 | 57 | Load an environment using a JSON object: 58 | 59 | ```javascript 60 | sem.loadEnvironment(sceneJSON); 61 | ``` 62 | 63 | Or fetch from an external source: 64 | 65 | ```javascript 66 | const url = 'path/to/your/scene.json'; 67 | fetch(url) 68 | .then((response) => response.json()) 69 | .then((data) => { 70 | sem.loadEnvironment(data); 71 | }) 72 | .catch((error) => { 73 | console.error('Error loading JSON:', error); 74 | }); 75 | ``` 76 | 77 | ## License 78 | 79 | `@iwer/sem` is licensed under the MIT License. For more details, see the [LICENSE](https://github.com/meta-quest/immersive-web-emulation-runtime/blob/main/LICENSE) file in this repository. 80 | 81 | ## Contributing 82 | 83 | Your contributions are welcome! Please feel free to submit issues and pull requests. Before contributing, make sure to review our [Contributing Guidelines](https://github.com/meta-quest/immersive-web-emulation-runtime/blob/main/CONTRIBUTING.md) and [Code of Conduct](https://github.com/meta-quest/immersive-web-emulation-runtime/blob/main/CODE_OF_CONDUCT.md). 84 | -------------------------------------------------------------------------------- /sem/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iwer/sem", 3 | "version": "0.2.5", 4 | "description": "Synthetic Environment Module for IWER", 5 | "type": "module", 6 | "main": "lib/index.js", 7 | "types": "lib/index.d.ts", 8 | "files": [ 9 | "build", 10 | "lib", 11 | "captures" 12 | ], 13 | "scripts": { 14 | "prebuild": "node scripts/prebuild.cjs", 15 | "build": "tsc && rollup -c" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/meta-quest/immersive-web-emulation-runtime.git" 20 | }, 21 | "keywords": [], 22 | "author": "Felix Zhang ", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/meta-quest/immersive-web-emulation-runtime/issues" 26 | }, 27 | "homepage": "https://github.com/meta-quest/immersive-web-emulation-runtime#readme", 28 | "dependencies": { 29 | "three": "^0.165.0", 30 | "ts-proto": "^2.6.0" 31 | }, 32 | "peerDependencies": { 33 | "iwer": "^2.0.0" 34 | }, 35 | "devDependencies": { 36 | "@rollup/plugin-commonjs": "^28.0.1", 37 | "@rollup/plugin-json": "^6.1.0", 38 | "@rollup/plugin-node-resolve": "^15.3.0", 39 | "@rollup/plugin-replace": "^6.0.1", 40 | "@rollup/plugin-terser": "^0.4.4", 41 | "@types/three": "^0.170.0", 42 | "rollup": "^4.24.3", 43 | "rollup-plugin-peer-deps-external": "^2.2.4", 44 | "typescript": "^5.6.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /sem/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import json from '@rollup/plugin-json'; 3 | import peerDepsExternal from 'rollup-plugin-peer-deps-external'; 4 | import replace from '@rollup/plugin-replace'; 5 | import resolve from '@rollup/plugin-node-resolve'; 6 | import terser from '@rollup/plugin-terser'; 7 | 8 | const globals = { 9 | iwer: 'IWER', 10 | }; 11 | 12 | const basePlugins = [peerDepsExternal(), resolve(), commonjs(), json()]; 13 | 14 | const esPlugins = [ 15 | ...basePlugins, 16 | replace({ 17 | __IS_UMD__: 'false', // Set to false for ES builds 18 | preventAssignment: true, 19 | }), 20 | ]; 21 | 22 | const umdPlugins = [ 23 | ...basePlugins, 24 | replace({ 25 | __IS_UMD__: 'true', // Set to true for UMD builds 26 | preventAssignment: true, 27 | }), 28 | ]; 29 | 30 | export default [ 31 | // UMD builds 32 | { 33 | input: 'lib/index.js', 34 | external: ['iwer'], 35 | plugins: umdPlugins, 36 | output: [ 37 | { 38 | file: 'build/iwer-sem.js', 39 | format: 'umd', 40 | name: 'IWER_SEM', 41 | globals, 42 | }, 43 | { 44 | file: 'build/iwer-sem.min.js', 45 | format: 'umd', 46 | name: 'IWER_SEM', 47 | globals, 48 | plugins: [terser()], 49 | }, 50 | ], 51 | }, 52 | // ES module builds 53 | { 54 | input: 'lib/index.js', 55 | external: ['iwer'], 56 | plugins: esPlugins, 57 | output: [ 58 | { 59 | dir: 'build/es', 60 | format: 'es', 61 | entryFileNames: '[name].js', 62 | chunkFileNames: '[name]-[hash].js', 63 | globals, 64 | }, 65 | { 66 | dir: 'build/es-min', 67 | format: 'es', 68 | entryFileNames: '[name].min.js', 69 | chunkFileNames: '[name]-[hash].min.js', 70 | plugins: [terser()], 71 | globals, 72 | }, 73 | ], 74 | }, 75 | ]; 76 | -------------------------------------------------------------------------------- /sem/scripts/prebuild.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | 11 | const jsonDirectory = path.join(__dirname, '../captures'); 12 | const outputFilePath = path.join(__dirname, '../src', 'registry.ts'); 13 | 14 | // Read all JSON files in the directory 15 | const files = fs 16 | .readdirSync(jsonDirectory) 17 | .filter((file) => file.endsWith('.json')); 18 | 19 | // Generate the content for registry.ts 20 | const imports = files 21 | .map((file) => { 22 | const envId = path.basename(file, '.json'); 23 | return ` ${envId}: () => import('../captures/${file}'),`; 24 | }) 25 | .join('\n'); 26 | 27 | const content = `export const Environments: { [envId: string]: () => Promise } = {\n${imports}\n};\n`; 28 | 29 | // Write the content to registry.ts 30 | fs.writeFileSync(outputFilePath, content, 'utf8'); 31 | 32 | console.log('registry.ts has been generated successfully.'); 33 | // Generate version.ts 34 | const packageJson = require('../package.json'); 35 | const versionOutputFilePath = path.join(__dirname, '../src', 'version.ts'); 36 | const versionContent = `export const VERSION = ${JSON.stringify( 37 | packageJson.version, 38 | )};\n`; 39 | // Write the content to version.ts 40 | fs.writeFileSync(versionOutputFilePath, versionContent, 'utf8'); 41 | console.log('version.ts has been generated successfully.'); 42 | -------------------------------------------------------------------------------- /sem/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare const __IS_UMD__: boolean; 2 | -------------------------------------------------------------------------------- /sem/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | export { SpatialEntity, SpatialEntityType } from './native/entity.js'; 9 | export { Bounded2DComponent } from './native/components/bounded2d.js'; 10 | export { Bounded3DComponent } from './native/components/bounded3d.js'; 11 | export { 12 | SpatialEntityComponent, 13 | SpatialEntityComponentType, 14 | } from './native/components/component.js'; 15 | export { LocatableComponent } from './native/components/locatable.js'; 16 | export { SemanticLabelComponent } from './native/components/semanticlabel.js'; 17 | export { TriangleMeshComponent } from './native/components/trianglemesh.js'; 18 | export { SyntheticEnvironmentModule } from './sem.js'; 19 | export { VERSION } from './version.js'; 20 | -------------------------------------------------------------------------------- /sem/src/native/components/bounded2d.ts: -------------------------------------------------------------------------------- 1 | import { PlaneGeometry, Vector2 } from 'three'; 2 | import { 3 | SpatialEntityComponent, 4 | SpatialEntityComponentType, 5 | } from './component.js'; 6 | 7 | import { Rect2D } from '../../generated/protos/openxr_core.js'; 8 | import { SpatialEntity } from '../entity.js'; 9 | 10 | export class Bounded2DComponent extends SpatialEntityComponent { 11 | private _offset: Vector2 = new Vector2(); 12 | private _extent: Vector2 = new Vector2(); 13 | type = SpatialEntityComponentType.Bounded2D; 14 | 15 | constructor(spatialEntity: SpatialEntity, initData: Rect2D) { 16 | super(spatialEntity); 17 | const { offset, extent } = initData; 18 | this._offset.set(offset!.x, offset!.y); 19 | this._extent.set(extent!.width, extent!.height); 20 | this.buildGeometry(); 21 | } 22 | 23 | buildGeometry() { 24 | const geometry = new PlaneGeometry(this._extent.x, this._extent.y); 25 | geometry.translate( 26 | this._offset.x + this._extent.x / 2, 27 | this._offset.y + this._extent.y / 2, 28 | 0, 29 | ); 30 | geometry.rotateX(Math.PI / 2); 31 | this._spatialEntity.geometry?.dispose(); 32 | this._spatialEntity.geometry = geometry; 33 | } 34 | 35 | get offset() { 36 | return this._offset; 37 | } 38 | 39 | get extent() { 40 | return this._extent; 41 | } 42 | 43 | get initData() { 44 | return { 45 | offset: this.offset, 46 | extent: this.extent, 47 | }; 48 | } 49 | 50 | get pbData(): Rect2D { 51 | return { 52 | offset: { x: this._offset.x, y: this._offset.y }, 53 | extent: { width: this._extent.x, height: this._extent.y }, 54 | } as Rect2D; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sem/src/native/components/bounded3d.ts: -------------------------------------------------------------------------------- 1 | import { BoxGeometry, Mesh, Vector3 } from 'three'; 2 | import { 3 | SpatialEntityComponent, 4 | SpatialEntityComponentType, 5 | } from './component.js'; 6 | 7 | import { Rect3D } from '../../generated/protos/openxr_core.js'; 8 | 9 | export class Bounded3DComponent extends SpatialEntityComponent { 10 | private _offset: Vector3 = new Vector3(); 11 | private _extent: Vector3 = new Vector3(); 12 | type = SpatialEntityComponentType.Bounded3D; 13 | 14 | constructor(spatialEntity: Mesh, initData: Rect3D) { 15 | super(spatialEntity); 16 | const { offset, extent } = initData; 17 | this._offset.set(offset!.x, offset!.y, offset!.z); 18 | this._extent.set(extent!.width, extent!.height, extent!.depth); 19 | this.buildGeometry(); 20 | } 21 | 22 | buildGeometry() { 23 | const geometry = new BoxGeometry( 24 | this._extent.x, 25 | this._extent.y, 26 | this._extent.z, 27 | ); 28 | geometry.translate( 29 | this._offset.x + this._extent.x / 2, 30 | this._offset.y + this._extent.y / 2, 31 | this._offset.z + this._extent.z / 2, 32 | ); 33 | this._spatialEntity.geometry?.dispose(); 34 | this._spatialEntity.geometry = geometry; 35 | } 36 | 37 | get offset() { 38 | return this._offset; 39 | } 40 | 41 | get extent() { 42 | return this._extent; 43 | } 44 | 45 | get initData() { 46 | return { 47 | offset: this.offset, 48 | extent: this.extent, 49 | }; 50 | } 51 | 52 | get pbData() { 53 | return { 54 | offset: { x: this._offset.x, y: this._offset.y, z: this._offset.z }, 55 | extent: { 56 | width: this._extent.x, 57 | height: this._extent.y, 58 | depth: this._extent.z, 59 | }, 60 | } as Rect3D; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /sem/src/native/components/component.ts: -------------------------------------------------------------------------------- 1 | import { Mesh } from 'three'; 2 | 3 | export enum SpatialEntityComponentType { 4 | Locatable = 'locatable', 5 | Bounded3D = 'bounded3D', 6 | Bounded2D = 'bounded2D', 7 | TriangleMesh = 'triangleMesh', 8 | SemanticLabel = 'semanticLabel', 9 | } 10 | 11 | export abstract class SpatialEntityComponent extends EventTarget { 12 | constructor(protected _spatialEntity: Mesh) { 13 | super(); 14 | } 15 | abstract get initData(): any; 16 | abstract get pbData(): any; 17 | abstract type: SpatialEntityComponentType; 18 | } 19 | -------------------------------------------------------------------------------- /sem/src/native/components/locatable.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SpatialEntityComponent, 3 | SpatialEntityComponentType, 4 | } from './component.js'; 5 | 6 | import { Mesh } from 'three'; 7 | import { Pose } from '../../generated/protos/openxr_core.js'; 8 | 9 | export class LocatableComponent extends SpatialEntityComponent { 10 | type = SpatialEntityComponentType.Locatable; 11 | 12 | constructor(spatialEntity: Mesh, initData: Pose) { 13 | super(spatialEntity); 14 | if (initData.position) { 15 | this.position.copy(initData.position); 16 | } 17 | if (initData.orientation) { 18 | this.orientation.copy(initData.orientation); 19 | } 20 | } 21 | 22 | get position() { 23 | return this._spatialEntity.position; 24 | } 25 | 26 | get rotation() { 27 | return this._spatialEntity.rotation; 28 | } 29 | 30 | get orientation() { 31 | return this._spatialEntity.quaternion; 32 | } 33 | 34 | get initData() { 35 | return { 36 | position: { 37 | x: this.position.x, 38 | y: this.position.y, 39 | z: this.position.z, 40 | }, 41 | orientation: { 42 | x: this.orientation.x, 43 | y: this.orientation.y, 44 | z: this.orientation.z, 45 | w: this.orientation.w, 46 | }, 47 | }; 48 | } 49 | 50 | get pbData() { 51 | return { 52 | position: { 53 | x: this._spatialEntity.position.x, 54 | y: this._spatialEntity.position.y, 55 | z: this._spatialEntity.position.z, 56 | }, 57 | orientation: { 58 | x: this._spatialEntity.quaternion.x, 59 | y: this._spatialEntity.quaternion.y, 60 | z: this._spatialEntity.quaternion.z, 61 | w: this._spatialEntity.quaternion.w, 62 | }, 63 | } as Pose; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /sem/src/native/components/semanticlabel.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SemanticLabelMETA, 3 | semanticLabelMETAToJSON, 4 | } from '../../generated/protos/openxr_scene.js'; 5 | import { 6 | SpatialEntityComponent, 7 | SpatialEntityComponentType, 8 | } from './component.js'; 9 | 10 | import { Mesh } from 'three'; 11 | 12 | function convertToReadableString(str: string): string { 13 | return str.toLowerCase().replace(/_/g, ' ').trim(); 14 | } 15 | 16 | export class SemanticLabelComponent extends SpatialEntityComponent { 17 | type = SpatialEntityComponentType.SemanticLabel; 18 | 19 | constructor( 20 | spatialEntity: Mesh, 21 | private _semanticLabel: SemanticLabelMETA, 22 | ) { 23 | super(spatialEntity); 24 | this._spatialEntity.name = convertToReadableString( 25 | semanticLabelMETAToJSON(_semanticLabel), 26 | ); 27 | } 28 | 29 | get semanticLabel(): SemanticLabelMETA { 30 | return this._semanticLabel; 31 | } 32 | 33 | set semanticLabel(value: SemanticLabelMETA) { 34 | if (Object.values(SemanticLabelMETA).includes(value)) { 35 | this._semanticLabel = value; 36 | } else { 37 | this._semanticLabel = SemanticLabelMETA.UNRECOGNIZED; 38 | } 39 | this._spatialEntity.name = convertToReadableString( 40 | semanticLabelMETAToJSON(this._semanticLabel), 41 | ); 42 | } 43 | 44 | get initData() { 45 | return this._semanticLabel; 46 | } 47 | 48 | get pbData() { 49 | return this._semanticLabel; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /sem/src/native/components/trianglemesh.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | Mesh, 5 | MeshBasicMaterial, 6 | } from 'three'; 7 | import { 8 | SpatialEntityComponent, 9 | SpatialEntityComponentType, 10 | } from './component.js'; 11 | 12 | import { TriangleMeshMETA } from '../../generated/protos/openxr_scene.js'; 13 | import { Vector3 } from '../../generated/protos/openxr_core.js'; 14 | 15 | function vec3ArrayToFloat32Array(arr: Vector3[]): Float32Array { 16 | const result = new Float32Array(arr.length * 3); 17 | let index = 0; 18 | for (const vec of arr) { 19 | result[index++] = vec.x; 20 | result[index++] = vec.y; 21 | result[index++] = vec.z; 22 | } 23 | return result; 24 | } 25 | 26 | export class TriangleMeshComponent extends SpatialEntityComponent { 27 | private _vertices: Vector3[]; 28 | private _indices: number[]; 29 | private _polygonCount: number = 0; 30 | private _vertexCount: number = 0; 31 | private _dimensions: Vector3 = { x: 0, y: 0, z: 0 }; 32 | type = SpatialEntityComponentType.TriangleMesh; 33 | 34 | constructor(spatialEntity: Mesh, initData: TriangleMeshMETA) { 35 | super(spatialEntity); 36 | const { vertices, indices } = initData; 37 | const verticesArray = new Float32Array(vertices.buffer); 38 | const indicesArray = new Uint32Array(indices.buffer); 39 | const vec3Array = []; 40 | for (let i = 0; i < verticesArray.length / 3; i++) { 41 | vec3Array.push({ 42 | x: verticesArray[3 * i], 43 | y: verticesArray[3 * i + 1], 44 | z: verticesArray[3 * i + 2], 45 | }); 46 | } 47 | this._vertices = vec3Array; 48 | this._indices = [...indicesArray]; 49 | this.buildGeometry(); 50 | const material = spatialEntity.material as MeshBasicMaterial; 51 | material.polygonOffset = true; 52 | material.polygonOffsetFactor = 1; 53 | material.polygonOffsetUnits = 0.005; 54 | material.color.setHex(0xd4d4d4); 55 | spatialEntity.renderOrder = 999; 56 | } 57 | 58 | private buildGeometry() { 59 | const geometry = new BufferGeometry(); 60 | const vertices = vec3ArrayToFloat32Array(this._vertices); 61 | geometry.setAttribute('position', new BufferAttribute(vertices, 3)); 62 | geometry.setIndex(new BufferAttribute(new Uint16Array(this._indices), 1)); 63 | this._spatialEntity.geometry?.dispose(); 64 | this._spatialEntity.geometry = geometry; 65 | geometry.computeVertexNormals(); 66 | this._vertexCount = geometry.attributes.position.count; 67 | this._polygonCount = geometry.index 68 | ? geometry.index.count / 3 69 | : this._vertexCount / 3; 70 | geometry.computeBoundingBox(); 71 | const boundingBox = geometry.boundingBox!; 72 | this._dimensions = { 73 | x: boundingBox.max.x - boundingBox.min.x, 74 | y: boundingBox.max.y - boundingBox.min.y, 75 | z: boundingBox.max.z - boundingBox.min.z, 76 | }; 77 | } 78 | 79 | get vertexCount() { 80 | return this._vertexCount; 81 | } 82 | 83 | get polygonCount() { 84 | return this._polygonCount; 85 | } 86 | 87 | get dimensions() { 88 | return this._dimensions; 89 | } 90 | 91 | get initData() { 92 | return { 93 | vertices: this._vertices, 94 | indices: this._indices, 95 | }; 96 | } 97 | 98 | get pbData() { 99 | const verticesArray = vec3ArrayToFloat32Array(this._vertices); 100 | const indicesArray = new Uint32Array(this._indices); 101 | return { 102 | vertices: new Uint8Array(verticesArray.buffer), 103 | indices: new Uint8Array(indicesArray.buffer), 104 | } as TriangleMeshMETA; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /sem/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /src/anchors/XRAnchor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { P_ANCHOR, P_DEVICE, P_SESSION, P_SPACE } from '../private.js'; 9 | 10 | import { XRSession } from '../session/XRSession.js'; 11 | import { XRSpace } from '../spaces/XRSpace.js'; 12 | import { mat4 } from 'gl-matrix'; 13 | 14 | export class XRAnchor { 15 | [P_ANCHOR]: { 16 | anchorSpace: XRSpace | null; 17 | session: XRSession; 18 | deleted: boolean; 19 | }; 20 | 21 | constructor(anchorSpace: XRSpace, session: XRSession) { 22 | this[P_ANCHOR] = { 23 | anchorSpace, 24 | session, 25 | deleted: false, 26 | }; 27 | session[P_SESSION].trackedAnchors.add(this); 28 | } 29 | 30 | get anchorSpace() { 31 | if (this[P_ANCHOR].deleted) { 32 | throw new DOMException( 33 | 'XRAnchor has already been deleted.', 34 | 'InvalidStateError', 35 | ); 36 | } 37 | return this[P_ANCHOR].anchorSpace!; 38 | } 39 | 40 | requestPersistentHandle() { 41 | return new Promise((resolve, reject) => { 42 | if (this[P_ANCHOR].deleted) { 43 | reject( 44 | new DOMException( 45 | 'XRAnchor has already been deleted.', 46 | 'InvalidStateError', 47 | ), 48 | ); 49 | } else { 50 | const persistentAnchors = 51 | this[P_ANCHOR].session[P_SESSION].persistentAnchors; 52 | for (const [uuid, anchor] of persistentAnchors.entries()) { 53 | if (anchor === this) { 54 | resolve(uuid); 55 | return; 56 | } 57 | } 58 | const uuid = crypto.randomUUID(); 59 | XRAnchorUtils.createPersistentAnchor( 60 | this[P_ANCHOR].session, 61 | this, 62 | uuid, 63 | ); 64 | resolve(uuid); 65 | } 66 | }); 67 | } 68 | 69 | delete() { 70 | if (this[P_ANCHOR].deleted) { 71 | return; 72 | } 73 | this[P_ANCHOR].anchorSpace = null; 74 | this[P_ANCHOR].deleted = true; 75 | this[P_ANCHOR].session[P_SESSION].trackedAnchors.delete(this); 76 | } 77 | } 78 | 79 | export class XRAnchorSet extends Set {} 80 | 81 | const PersistentAnchorsStorageKey = 82 | '@immersive-web-emulation-runtime/persistent-anchors'; 83 | 84 | export class XRAnchorUtils { 85 | static recoverPersistentAnchorsFromStorage(session: XRSession) { 86 | const persistentAnchors = JSON.parse( 87 | localStorage.getItem(PersistentAnchorsStorageKey) || '{}', 88 | ) as { [uuid: string]: mat4 }; 89 | Object.entries(persistentAnchors).forEach(([uuid, offsetMatrix]) => { 90 | const globalSpace = session[P_SESSION].device[P_DEVICE].globalSpace; 91 | const anchorSpace = new XRSpace(globalSpace, offsetMatrix); 92 | const anchor = new XRAnchor(anchorSpace, session); 93 | session[P_SESSION].persistentAnchors.set(uuid, anchor); 94 | }); 95 | } 96 | 97 | static createPersistentAnchor( 98 | session: XRSession, 99 | anchor: XRAnchor, 100 | uuid: string, 101 | ) { 102 | session[P_SESSION].trackedAnchors.add(anchor); 103 | session[P_SESSION].persistentAnchors.set(uuid, anchor); 104 | const persistentAnchors = JSON.parse( 105 | localStorage.getItem(PersistentAnchorsStorageKey) || '{}', 106 | ) as { [uuid: string]: mat4 }; 107 | persistentAnchors[uuid] = Array.from( 108 | anchor[P_ANCHOR].anchorSpace![P_SPACE].offsetMatrix, 109 | ) as mat4; 110 | localStorage.setItem( 111 | PersistentAnchorsStorageKey, 112 | JSON.stringify(persistentAnchors), 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/device/XRController.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { Gamepad, GamepadConfig } from '../gamepad/Gamepad.js'; 9 | import { GlobalSpace, XRSpace } from '../spaces/XRSpace.js'; 10 | import { P_CONTROLLER, P_GAMEPAD, P_TRACKED_INPUT } from '../private.js'; 11 | import { 12 | XRHandedness, 13 | XRInputSource, 14 | XRTargetRayMode, 15 | } from '../input/XRInputSource.js'; 16 | 17 | import { XRTrackedInput } from './XRTrackedInput.js'; 18 | import { mat4 } from 'gl-matrix'; 19 | 20 | export interface XRControllerConfig { 21 | profileId: string; 22 | fallbackProfileIds: string[]; 23 | layout: { 24 | [handedness in XRHandedness]?: { 25 | gamepad: GamepadConfig; 26 | gripOffsetMatrix?: mat4; 27 | numHapticActuators: number; 28 | }; 29 | }; 30 | } 31 | 32 | export class XRController extends XRTrackedInput { 33 | [P_CONTROLLER]: { 34 | profileId: string; 35 | gamepadConfig: GamepadConfig; 36 | }; 37 | 38 | constructor( 39 | controllerConfig: XRControllerConfig, 40 | handedness: XRHandedness, 41 | globalSpace: GlobalSpace, 42 | ) { 43 | if (!controllerConfig.layout[handedness]) { 44 | throw new DOMException('Handedness not supported', 'InvalidStateError'); 45 | } 46 | const targetRaySpace = new XRSpace(globalSpace); 47 | const gripSpace = controllerConfig.layout[handedness]!.gripOffsetMatrix 48 | ? new XRSpace( 49 | targetRaySpace, 50 | controllerConfig.layout[handedness]!.gripOffsetMatrix, 51 | ) 52 | : undefined; 53 | const profiles = [ 54 | controllerConfig.profileId, 55 | ...controllerConfig.fallbackProfileIds, 56 | ]; 57 | const inputSource = new XRInputSource( 58 | handedness, 59 | XRTargetRayMode.TrackedPointer, 60 | profiles, 61 | targetRaySpace, 62 | new Gamepad(controllerConfig.layout[handedness]!.gamepad), 63 | gripSpace, 64 | ); 65 | 66 | super(inputSource); 67 | this[P_CONTROLLER] = { 68 | profileId: controllerConfig.profileId, 69 | gamepadConfig: controllerConfig.layout[handedness]!.gamepad, 70 | }; 71 | } 72 | 73 | get gamepadConfig() { 74 | return this[P_CONTROLLER].gamepadConfig; 75 | } 76 | 77 | get profileId() { 78 | return this[P_CONTROLLER].profileId; 79 | } 80 | 81 | updateButtonValue(id: string, value: number) { 82 | if (value > 1 || value < 0) { 83 | console.warn(`Out-of-range value ${value} provided for button ${id}.`); 84 | return; 85 | } 86 | const gamepadButton = 87 | this[P_TRACKED_INPUT].inputSource.gamepad![P_GAMEPAD].buttonsMap[id]; 88 | if (gamepadButton) { 89 | if ( 90 | gamepadButton[P_GAMEPAD].type === 'binary' && 91 | value != 1 && 92 | value != 0 93 | ) { 94 | console.warn( 95 | `Non-binary value ${value} provided for binary button ${id}.`, 96 | ); 97 | return; 98 | } 99 | gamepadButton[P_GAMEPAD].pendingValue = value; 100 | } else { 101 | console.warn(`Current controller does not have button ${id}.`); 102 | } 103 | } 104 | 105 | updateButtonTouch(id: string, touched: boolean) { 106 | const gamepadButton = 107 | this[P_TRACKED_INPUT].inputSource.gamepad![P_GAMEPAD].buttonsMap[id]; 108 | if (gamepadButton) { 109 | gamepadButton[P_GAMEPAD].touched = touched; 110 | } else { 111 | console.warn(`Current controller does not have button ${id}.`); 112 | } 113 | } 114 | 115 | updateAxis(id: string, type: 'x-axis' | 'y-axis', value: number) { 116 | if (value > 1 || value < -1) { 117 | console.warn(`Out-of-range value ${value} provided for ${id} axes.`); 118 | return; 119 | } 120 | const axesById = 121 | this[P_TRACKED_INPUT].inputSource.gamepad![P_GAMEPAD].axesMap[id]; 122 | if (axesById) { 123 | if (type === 'x-axis') { 124 | axesById.x = value; 125 | } else if (type === 'y-axis') { 126 | axesById.y = value; 127 | } 128 | } else { 129 | console.warn(`Current controller does not have ${id} axes.`); 130 | } 131 | } 132 | 133 | updateAxes(id: string, x: number, y: number) { 134 | if (x > 1 || x < -1 || y > 1 || y < -1) { 135 | console.warn( 136 | `Out-of-range value x:${x}, y:${y} provided for ${id} axes.`, 137 | ); 138 | return; 139 | } 140 | const axesById = 141 | this[P_TRACKED_INPUT].inputSource.gamepad![P_GAMEPAD].axesMap[id]; 142 | if (axesById) { 143 | axesById.x = x; 144 | axesById.y = y; 145 | } else { 146 | console.warn(`Current controller does not have ${id} axes.`); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/device/XRTrackedInput.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { P_GAMEPAD, P_SPACE, P_TRACKED_INPUT } from '../private.js'; 9 | import { Quaternion, Vector3 } from '../utils/Math.js'; 10 | import { XRHandedness, XRInputSource } from '../input/XRInputSource.js'; 11 | 12 | import { GamepadButton } from '../gamepad/Gamepad.js'; 13 | import { XRFrame } from '../frameloop/XRFrame.js'; 14 | import { XRInputSourceEvent } from '../events/XRInputSourceEvent.js'; 15 | import { mat4 } from 'gl-matrix'; 16 | 17 | const DEFAULT_TRANSFORM = { 18 | [XRHandedness.Left]: { 19 | position: new Vector3(-0.25, 1.5, -0.4), 20 | quaternion: new Quaternion(), 21 | }, 22 | [XRHandedness.Right]: { 23 | position: new Vector3(0.25, 1.5, -0.4), 24 | quaternion: new Quaternion(), 25 | }, 26 | [XRHandedness.None]: { 27 | position: new Vector3(0.25, 1.5, -0.4), 28 | quaternion: new Quaternion(), 29 | }, 30 | }; 31 | 32 | export class XRTrackedInput { 33 | [P_TRACKED_INPUT]: { 34 | inputSource: XRInputSource; 35 | // input state 36 | position: Vector3; 37 | quaternion: Quaternion; 38 | connected: boolean; 39 | lastFrameConnected: boolean; 40 | inputSourceChanged: boolean; 41 | }; 42 | 43 | constructor(inputSource: XRInputSource) { 44 | this[P_TRACKED_INPUT] = { 45 | inputSource, 46 | position: DEFAULT_TRANSFORM[inputSource.handedness].position.clone(), 47 | quaternion: DEFAULT_TRANSFORM[inputSource.handedness].quaternion.clone(), 48 | connected: true, 49 | lastFrameConnected: false, 50 | inputSourceChanged: true, 51 | }; 52 | } 53 | 54 | get position(): Vector3 { 55 | return this[P_TRACKED_INPUT].position; 56 | } 57 | 58 | get quaternion(): Quaternion { 59 | return this[P_TRACKED_INPUT].quaternion; 60 | } 61 | 62 | get inputSource(): XRInputSource { 63 | return this[P_TRACKED_INPUT].inputSource; 64 | } 65 | 66 | get connected() { 67 | return this[P_TRACKED_INPUT].connected; 68 | } 69 | 70 | set connected(value: boolean) { 71 | this[P_TRACKED_INPUT].connected = value; 72 | this[P_TRACKED_INPUT].inputSource.gamepad![P_GAMEPAD].connected = value; 73 | } 74 | 75 | onFrameStart(frame: XRFrame) { 76 | const targetRaySpace = this[P_TRACKED_INPUT].inputSource.targetRaySpace; 77 | mat4.fromRotationTranslation( 78 | targetRaySpace[P_SPACE].offsetMatrix, 79 | this[P_TRACKED_INPUT].quaternion.quat, 80 | this[P_TRACKED_INPUT].position.vec3, 81 | ); 82 | 83 | const session = frame.session; 84 | this[P_TRACKED_INPUT].inputSource.gamepad!.buttons.forEach((button) => { 85 | if (button instanceof GamepadButton) { 86 | // apply pending values and record last frame values 87 | button[P_GAMEPAD].lastFrameValue = button[P_GAMEPAD].value; 88 | if (button[P_GAMEPAD].pendingValue != null) { 89 | button[P_GAMEPAD].value = button[P_GAMEPAD].pendingValue; 90 | button[P_GAMEPAD].pendingValue = null; 91 | } 92 | // trigger input source events 93 | if (button[P_GAMEPAD].eventTrigger != null) { 94 | if ( 95 | button[P_GAMEPAD].lastFrameValue === 0 && 96 | button[P_GAMEPAD].value > 0 97 | ) { 98 | session.dispatchEvent( 99 | new XRInputSourceEvent(button[P_GAMEPAD].eventTrigger, { 100 | frame, 101 | inputSource: this[P_TRACKED_INPUT].inputSource, 102 | }), 103 | ); 104 | session.dispatchEvent( 105 | new XRInputSourceEvent(button[P_GAMEPAD].eventTrigger + 'start', { 106 | frame, 107 | inputSource: this[P_TRACKED_INPUT].inputSource, 108 | }), 109 | ); 110 | } else if ( 111 | button[P_GAMEPAD].lastFrameValue > 0 && 112 | button[P_GAMEPAD].value === 0 113 | ) { 114 | session.dispatchEvent( 115 | new XRInputSourceEvent(button[P_GAMEPAD].eventTrigger + 'end', { 116 | frame, 117 | inputSource: this[P_TRACKED_INPUT].inputSource, 118 | }), 119 | ); 120 | } 121 | } 122 | } 123 | }); 124 | 125 | this[P_TRACKED_INPUT].inputSourceChanged = 126 | this.connected !== this[P_TRACKED_INPUT].lastFrameConnected; 127 | this[P_TRACKED_INPUT].lastFrameConnected = this.connected; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/device/configs/headset/meta.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { 9 | XREnvironmentBlendMode, 10 | XRInteractionMode, 11 | } from '../../../session/XRSession.js'; 12 | import { 13 | metaQuestTouchPlus, 14 | metaQuestTouchPro, 15 | oculusTouchV2, 16 | oculusTouchV3, 17 | } from '../controller/meta.js'; 18 | 19 | import { XRDeviceConfig } from '../../XRDevice.js'; 20 | 21 | export const oculusQuest1: XRDeviceConfig = { 22 | name: 'Oculus Quest 1', 23 | controllerConfig: oculusTouchV2, 24 | supportedSessionModes: ['inline', 'immersive-vr', 'immersive-ar'], 25 | supportedFeatures: [ 26 | 'viewer', 27 | 'local', 28 | 'local-floor', 29 | 'bounded-floor', 30 | 'unbounded', 31 | 'anchors', 32 | 'plane-detection', 33 | 'hand-tracking', 34 | ], 35 | supportedFrameRates: [72, 80, 90], 36 | isSystemKeyboardSupported: true, 37 | internalNominalFrameRate: 72, 38 | environmentBlendModes: { 39 | ['immersive-vr']: XREnvironmentBlendMode.Opaque, 40 | ['immersive-ar']: XREnvironmentBlendMode.AlphaBlend, 41 | }, 42 | interactionMode: XRInteractionMode.WorldSpace, 43 | userAgent: 44 | 'Mozilla/5.0 (X11; Linux x86_64; Quest 1) AppleWebKit/537.36 (KHTML, like Gecko) OculusBrowser/33.0.0.x.x.x Chrome/126.0.6478.122 VR Safari/537.36', 45 | }; 46 | 47 | export const metaQuest2: XRDeviceConfig = { 48 | name: 'Meta Quest 2', 49 | controllerConfig: oculusTouchV3, 50 | supportedSessionModes: ['inline', 'immersive-vr', 'immersive-ar'], 51 | supportedFeatures: [ 52 | 'viewer', 53 | 'local', 54 | 'local-floor', 55 | 'bounded-floor', 56 | 'unbounded', 57 | 'anchors', 58 | 'plane-detection', 59 | 'mesh-detection', 60 | 'hit-test', 61 | 'hand-tracking', 62 | ], 63 | supportedFrameRates: [72, 80, 90, 120], 64 | isSystemKeyboardSupported: true, 65 | internalNominalFrameRate: 72, 66 | environmentBlendModes: { 67 | ['immersive-vr']: XREnvironmentBlendMode.Opaque, 68 | ['immersive-ar']: XREnvironmentBlendMode.AlphaBlend, 69 | }, 70 | interactionMode: XRInteractionMode.WorldSpace, 71 | userAgent: 72 | 'Mozilla/5.0 (X11; Linux x86_64; Quest 2) AppleWebKit/537.36 (KHTML, like Gecko) OculusBrowser/33.0.0.x.x.x Chrome/126.0.6478.122 VR Safari/537.36', 73 | }; 74 | 75 | export const metaQuestPro: XRDeviceConfig = { 76 | name: 'Meta Quest Pro', 77 | controllerConfig: metaQuestTouchPro, 78 | supportedSessionModes: ['inline', 'immersive-vr', 'immersive-ar'], 79 | supportedFeatures: [ 80 | 'viewer', 81 | 'local', 82 | 'local-floor', 83 | 'bounded-floor', 84 | 'unbounded', 85 | 'anchors', 86 | 'plane-detection', 87 | 'mesh-detection', 88 | 'hit-test', 89 | 'hand-tracking', 90 | ], 91 | supportedFrameRates: [72, 80, 90, 120], 92 | isSystemKeyboardSupported: true, 93 | internalNominalFrameRate: 90, 94 | environmentBlendModes: { 95 | ['immersive-vr']: XREnvironmentBlendMode.Opaque, 96 | ['immersive-ar']: XREnvironmentBlendMode.AlphaBlend, 97 | }, 98 | interactionMode: XRInteractionMode.WorldSpace, 99 | userAgent: 100 | 'Mozilla/5.0 (X11; Linux x86_64; Quest Pro) AppleWebKit/537.36 (KHTML, like Gecko) OculusBrowser/33.0.0.x.x.x Chrome/126.0.6478.122 VR Safari/537.36', 101 | }; 102 | 103 | export const metaQuest3: XRDeviceConfig = { 104 | name: 'Meta Quest 3', 105 | controllerConfig: metaQuestTouchPlus, 106 | supportedSessionModes: ['inline', 'immersive-vr', 'immersive-ar'], 107 | supportedFeatures: [ 108 | 'viewer', 109 | 'local', 110 | 'local-floor', 111 | 'bounded-floor', 112 | 'unbounded', 113 | 'anchors', 114 | 'plane-detection', 115 | 'mesh-detection', 116 | 'hit-test', 117 | 'hand-tracking', 118 | 'depth-sensing', 119 | ], 120 | supportedFrameRates: [72, 80, 90, 120], 121 | isSystemKeyboardSupported: true, 122 | internalNominalFrameRate: 90, 123 | environmentBlendModes: { 124 | ['immersive-vr']: XREnvironmentBlendMode.Opaque, 125 | ['immersive-ar']: XREnvironmentBlendMode.AlphaBlend, 126 | }, 127 | interactionMode: XRInteractionMode.WorldSpace, 128 | userAgent: 129 | 'Mozilla/5.0 (X11; Linux x86_64; Quest 3) AppleWebKit/537.36 (KHTML, like Gecko) OculusBrowser/33.0.0.x.x.x Chrome/126.0.6478.122 VR Safari/537.36', 130 | }; 131 | -------------------------------------------------------------------------------- /src/events/XRInputSourceEvent.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { XRFrame } from '../frameloop/XRFrame.js'; 9 | import { XRInputSource } from '../input/XRInputSource.js'; 10 | 11 | interface XRInputSourceEventInit extends EventInit { 12 | frame: XRFrame; 13 | inputSource: XRInputSource; 14 | } 15 | 16 | export class XRInputSourceEvent extends Event { 17 | public readonly frame: XRFrame; 18 | public readonly inputSource: XRInputSource; 19 | 20 | constructor(type: string, eventInitDict: XRInputSourceEventInit) { 21 | super(type, eventInitDict); 22 | if (!eventInitDict.frame) { 23 | throw new Error('XRInputSourceEventInit.frame is required'); 24 | } 25 | if (!eventInitDict.inputSource) { 26 | throw new Error('XRInputSourceEventInit.inputSource is required'); 27 | } 28 | this.frame = eventInitDict.frame; 29 | this.inputSource = eventInitDict.inputSource; 30 | } 31 | } 32 | 33 | export interface XRInputSourceEventHandler { 34 | (evt: XRInputSourceEvent): any; 35 | } 36 | -------------------------------------------------------------------------------- /src/events/XRInputSourcesChangeEvent.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { XRInputSource } from '../input/XRInputSource.js'; 9 | import { XRSession } from '../session/XRSession.js'; 10 | 11 | interface XRInputSourcesChangeEventInit extends EventInit { 12 | session: XRSession; 13 | added: XRInputSource[]; 14 | removed: XRInputSource[]; 15 | } 16 | 17 | export class XRInputSourcesChangeEvent extends Event { 18 | public readonly session: XRSession; 19 | public readonly added: XRInputSource[]; 20 | public readonly removed: XRInputSource[]; 21 | 22 | constructor(type: string, eventInitDict: XRInputSourcesChangeEventInit) { 23 | super(type, eventInitDict); 24 | if (!eventInitDict.session) { 25 | throw new Error('XRInputSourcesChangeEventInit.session is required'); 26 | } 27 | if (!eventInitDict.added) { 28 | throw new Error('XRInputSourcesChangeEventInit.added is required'); 29 | } 30 | if (!eventInitDict.removed) { 31 | throw new Error('XRInputSourcesChangeEventInit.removed is required'); 32 | } 33 | this.session = eventInitDict.session; 34 | this.added = eventInitDict.added; 35 | this.removed = eventInitDict.removed; 36 | } 37 | } 38 | 39 | export interface XRInputSourcesChangeEventHandler { 40 | (evt: XRInputSourcesChangeEvent): any; 41 | } 42 | -------------------------------------------------------------------------------- /src/events/XRReferenceSpaceEvent.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { XRReferenceSpace } from '../spaces/XRReferenceSpace.js'; 9 | import { XRRigidTransform } from '../primitives/XRRigidTransform.js'; 10 | 11 | interface XRReferenceSpaceEventInit extends EventInit { 12 | referenceSpace: XRReferenceSpace; 13 | transform?: XRRigidTransform; 14 | } 15 | 16 | export class XRReferenceSpaceEvent extends Event { 17 | public readonly referenceSpace: XRReferenceSpace; 18 | public readonly transform?: XRRigidTransform; 19 | 20 | constructor(type: string, eventInitDict: XRReferenceSpaceEventInit) { 21 | super(type, eventInitDict); 22 | if (!eventInitDict.referenceSpace) { 23 | throw new Error('XRReferenceSpaceEventInit.referenceSpace is required'); 24 | } 25 | this.referenceSpace = eventInitDict.referenceSpace; 26 | this.transform = eventInitDict.transform; 27 | } 28 | } 29 | 30 | export interface XRReferenceSpaceEventHandler { 31 | (evt: XRReferenceSpaceEvent): any; 32 | } 33 | -------------------------------------------------------------------------------- /src/events/XRSessionEvent.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { XRSession } from '../session/XRSession.js'; 9 | 10 | interface XRSessionEventInit extends EventInit { 11 | session: XRSession; 12 | } 13 | 14 | export class XRSessionEvent extends Event { 15 | public readonly session: XRSession; 16 | 17 | constructor(type: string, eventInitDict: XRSessionEventInit) { 18 | super(type, eventInitDict); 19 | if (!eventInitDict.session) { 20 | throw new Error('XRSessionEventInit.session is required'); 21 | } 22 | this.session = eventInitDict.session; 23 | } 24 | } 25 | 26 | export interface XRSessionEventHandler { 27 | (evt: XRSessionEvent): any; 28 | } 29 | -------------------------------------------------------------------------------- /src/gamepad/Gamepad.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { P_GAMEPAD } from '../private.js'; 9 | 10 | export enum GamepadMappingType { 11 | None = '', 12 | Standard = 'standard', 13 | XRStandard = 'xr-standard', 14 | } 15 | 16 | export interface Button { 17 | id: string; 18 | type: 'binary' | 'analog' | 'manual'; 19 | eventTrigger?: 'select' | 'squeeze'; 20 | } 21 | 22 | export interface Axis { 23 | id: string; 24 | type: 'x-axis' | 'y-axis' | 'manual'; 25 | } 26 | 27 | export interface GamepadConfig { 28 | mapping: GamepadMappingType; 29 | buttons: (Button | null)[]; 30 | axes: (Axis | null)[]; 31 | } 32 | 33 | export class GamepadButton { 34 | [P_GAMEPAD]: { 35 | type: 'analog' | 'binary' | 'manual'; 36 | eventTrigger: 'select' | 'squeeze' | null; 37 | pressed: boolean; 38 | touched: boolean; 39 | value: number; 40 | lastFrameValue: number; 41 | pendingValue: number | null; 42 | }; 43 | 44 | constructor( 45 | type: 'analog' | 'binary' | 'manual', 46 | eventTrigger: 'select' | 'squeeze' | null, 47 | ) { 48 | this[P_GAMEPAD] = { 49 | type, 50 | eventTrigger, 51 | pressed: false, 52 | touched: false, 53 | value: 0, 54 | lastFrameValue: 0, 55 | pendingValue: null, 56 | }; 57 | } 58 | 59 | get pressed() { 60 | if (this[P_GAMEPAD].type === 'manual') { 61 | return this[P_GAMEPAD].pressed; 62 | } else { 63 | return this[P_GAMEPAD].value > 0; 64 | } 65 | } 66 | 67 | get touched() { 68 | if (this[P_GAMEPAD].type === 'manual') { 69 | return this[P_GAMEPAD].touched; 70 | } else { 71 | return this[P_GAMEPAD].touched || this.pressed; 72 | } 73 | } 74 | 75 | get value() { 76 | return this[P_GAMEPAD].value; 77 | } 78 | } 79 | 80 | export class EmptyGamepadButton { 81 | pressed = false; 82 | touched = false; 83 | value = 0; 84 | } 85 | 86 | export class Gamepad { 87 | [P_GAMEPAD]: { 88 | id: string; 89 | index: number; 90 | connected: boolean; 91 | timestamp: DOMHighResTimeStamp; 92 | mapping: GamepadMappingType; 93 | buttonsMap: { 94 | [id: string]: GamepadButton | null; 95 | }; 96 | buttonsSequence: (string | null)[]; 97 | axesMap: { 98 | [id: string]: { x: number; y: number }; 99 | }; 100 | axesSequence: (string | null)[]; 101 | hapticActuators: GamepadHapticActuator[]; 102 | }; 103 | 104 | constructor( 105 | gamepadConfig: GamepadConfig, 106 | id: string = '', 107 | index: number = -1, 108 | ) { 109 | this[P_GAMEPAD] = { 110 | id, 111 | index, 112 | connected: false, 113 | timestamp: performance.now(), 114 | mapping: gamepadConfig.mapping, 115 | buttonsMap: {}, 116 | buttonsSequence: [], 117 | axesMap: {}, 118 | axesSequence: [], 119 | hapticActuators: [], 120 | }; 121 | gamepadConfig.buttons.forEach((buttonConfig) => { 122 | if (buttonConfig === null) { 123 | this[P_GAMEPAD].buttonsSequence.push(null); 124 | } else { 125 | this[P_GAMEPAD].buttonsSequence.push(buttonConfig.id); 126 | this[P_GAMEPAD].buttonsMap[buttonConfig.id] = new GamepadButton( 127 | buttonConfig.type, 128 | buttonConfig.eventTrigger ?? null, 129 | ); 130 | } 131 | }); 132 | gamepadConfig.axes.forEach((axisConfig) => { 133 | if (axisConfig === null) { 134 | this[P_GAMEPAD].axesSequence.push(null); 135 | } else { 136 | this[P_GAMEPAD].axesSequence.push(axisConfig.id + axisConfig.type); 137 | if (!this[P_GAMEPAD].axesMap[axisConfig.id]) { 138 | this[P_GAMEPAD].axesMap[axisConfig.id] = { x: 0, y: 0 }; 139 | } 140 | } 141 | }); 142 | } 143 | 144 | get id() { 145 | return this[P_GAMEPAD].id; 146 | } 147 | 148 | get index() { 149 | return this[P_GAMEPAD].index; 150 | } 151 | 152 | get connected() { 153 | return this[P_GAMEPAD].connected; 154 | } 155 | 156 | get timestamp() { 157 | return this[P_GAMEPAD].timestamp; 158 | } 159 | 160 | get mapping() { 161 | return this[P_GAMEPAD].mapping; 162 | } 163 | 164 | get axes() { 165 | const axes: (number | null)[] = []; 166 | this[P_GAMEPAD].axesSequence.forEach((id) => { 167 | if (id === null) { 168 | axes.push(null); 169 | } else { 170 | const axisId = id.substring(0, id.length - 6); 171 | const axisType = id.substring(id.length - 6); 172 | axes.push( 173 | // if axis type is manual, then return the x value 174 | axisType === 'y-axis' 175 | ? this[P_GAMEPAD].axesMap[axisId].y 176 | : this[P_GAMEPAD].axesMap[axisId].x, 177 | ); 178 | } 179 | }); 180 | return axes; 181 | } 182 | 183 | get buttons() { 184 | return this[P_GAMEPAD].buttonsSequence.map((id) => 185 | id === null ? new EmptyGamepadButton() : this[P_GAMEPAD].buttonsMap[id], 186 | ); 187 | } 188 | 189 | get hapticActuators() { 190 | return this[P_GAMEPAD].hapticActuators; 191 | } 192 | 193 | get vibrationActuator() { 194 | return null; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/hittest/XRHitTest.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { P_HIT_TEST, P_SESSION } from '../private.js'; 9 | 10 | import { XRFrame } from '../frameloop/XRFrame.js'; 11 | import { XRPose } from '../pose/XRPose.js'; 12 | import { XRRay } from './XRRay.js'; 13 | import { XRRigidTransform } from '../primitives/XRRigidTransform.js'; 14 | import { XRSession } from '../session/XRSession.js'; 15 | import { XRSpace } from '../spaces/XRSpace.js'; 16 | 17 | export interface XRHitTestOptionsInit { 18 | space: XRSpace; 19 | offsetRay: XRRay; 20 | } 21 | 22 | export class XRHitTestSource { 23 | [P_HIT_TEST]: { 24 | session: XRSession; 25 | space: XRSpace; 26 | offsetRay: XRRay; 27 | }; 28 | 29 | constructor(session: XRSession, options: XRHitTestOptionsInit) { 30 | this[P_HIT_TEST] = { 31 | session, 32 | space: options.space, 33 | offsetRay: options.offsetRay ?? new XRRay(), 34 | }; 35 | } 36 | 37 | cancel() { 38 | this[P_HIT_TEST].session[P_SESSION].hitTestSources.delete(this); 39 | } 40 | } 41 | 42 | export class XRHitTestResult { 43 | [P_HIT_TEST]: { 44 | frame: XRFrame; 45 | offsetSpace: XRSpace; 46 | }; 47 | 48 | constructor(frame: XRFrame, offsetSpace: XRSpace) { 49 | this[P_HIT_TEST] = { frame, offsetSpace }; 50 | } 51 | 52 | getPose(baseSpace: XRSpace): XRPose | undefined { 53 | return this[P_HIT_TEST].frame.getPose( 54 | this[P_HIT_TEST].offsetSpace, 55 | baseSpace, 56 | ); 57 | } 58 | 59 | createAnchor() { 60 | return this[P_HIT_TEST].frame.createAnchor( 61 | new XRRigidTransform(), 62 | this[P_HIT_TEST].offsetSpace, 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/hittest/XRRay.ts: -------------------------------------------------------------------------------- 1 | import { mat4, vec3, vec4 } from 'gl-matrix'; 2 | 3 | import { P_RAY } from '../private.js'; 4 | import { XRRigidTransform } from '../primitives/XRRigidTransform.js'; 5 | 6 | class DOMPointReadOnly { 7 | constructor( 8 | public x: number = 0, 9 | public y: number = 0, 10 | public z: number = 0, 11 | public w: number = 1, 12 | ) {} 13 | } 14 | 15 | export class XRRay { 16 | [P_RAY]: { 17 | origin: DOMPointReadOnly; 18 | direction: DOMPointReadOnly; 19 | matrix: Float32Array | null; 20 | }; 21 | 22 | constructor( 23 | origin?: DOMPointInit | XRRigidTransform, 24 | direction?: DOMPointInit, 25 | ) { 26 | const _origin: DOMPointInit = { x: 0, y: 0, z: 0, w: 1 }; 27 | const _direction: DOMPointInit = { x: 0, y: 0, z: -1, w: 0 }; 28 | 29 | if (origin instanceof XRRigidTransform) { 30 | const transform = origin; 31 | const matrix = transform.matrix; 32 | const originVec4 = vec4.set( 33 | vec4.create(), 34 | _origin.x!, 35 | _origin.y!, 36 | _origin.z!, 37 | _origin.w!, 38 | ); 39 | const directionVec4 = vec4.set( 40 | vec4.create(), 41 | _direction.x!, 42 | _direction.y!, 43 | _direction.z!, 44 | _direction.w!, 45 | ); 46 | vec4.transformMat4(originVec4, originVec4, matrix); 47 | vec4.transformMat4(directionVec4, directionVec4, matrix); 48 | _origin.x = originVec4[0]; 49 | _origin.y = originVec4[1]; 50 | _origin.z = originVec4[2]; 51 | _origin.w = originVec4[3]; 52 | _direction.x = directionVec4[0]; 53 | _direction.y = directionVec4[1]; 54 | _direction.z = directionVec4[2]; 55 | _direction.w = directionVec4[3]; 56 | } else { 57 | if (origin) { 58 | _origin.x = origin.x; 59 | _origin.y = origin.y; 60 | _origin.z = origin.z; 61 | _origin.w = origin.w; 62 | } 63 | if (direction) { 64 | if ( 65 | (direction.x === 0 && direction.y === 0 && direction.z === 0) || 66 | direction.w !== 0 67 | ) { 68 | throw new DOMException( 69 | 'Invalid direction value to construct XRRay', 70 | 'TypeError', 71 | ); 72 | } 73 | _direction.x = direction.x; 74 | _direction.y = direction.y; 75 | _direction.z = direction.z; 76 | _direction.w = direction.w; 77 | } 78 | } 79 | 80 | const length = 81 | Math.sqrt( 82 | _direction.x! * _direction.x! + 83 | _direction.y! * _direction.y! + 84 | _direction.z! * _direction.z!, 85 | ) || 1; 86 | _direction.x = _direction.x! / length; 87 | _direction.y = _direction.y! / length; 88 | _direction.z = _direction.z! / length; 89 | 90 | this[P_RAY] = { 91 | origin: new DOMPointReadOnly( 92 | _origin.x!, 93 | _origin.y!, 94 | _origin.z!, 95 | _origin.w!, 96 | ), 97 | direction: new DOMPointReadOnly( 98 | _direction.x!, 99 | _direction.y!, 100 | _direction.z!, 101 | _direction.w!, 102 | ), 103 | matrix: null, 104 | }; 105 | } 106 | 107 | get origin(): DOMPointReadOnly { 108 | return this[P_RAY].origin; 109 | } 110 | 111 | get direction(): DOMPointReadOnly { 112 | return this[P_RAY].direction; 113 | } 114 | 115 | get matrix(): Float32Array { 116 | if (this[P_RAY].matrix) { 117 | return this[P_RAY].matrix; 118 | } 119 | const z = vec3.set(vec3.create(), 0, 0, -1); 120 | const origin = vec3.set( 121 | vec3.create(), 122 | this[P_RAY].origin.x, 123 | this[P_RAY].origin.y, 124 | this[P_RAY].origin.z, 125 | ); 126 | const direction = vec3.set( 127 | vec3.create(), 128 | this[P_RAY].direction.x, 129 | this[P_RAY].direction.y, 130 | this[P_RAY].direction.z, 131 | ); 132 | const axis = vec3.cross(vec3.create(), direction, z); 133 | const cosAngle = vec3.dot(direction, z); 134 | const rotation = mat4.create(); 135 | if (cosAngle > -1 && cosAngle < 1) { 136 | mat4.fromRotation(rotation, Math.acos(cosAngle), axis); 137 | } else if (cosAngle === -1) { 138 | mat4.fromRotation( 139 | rotation, 140 | Math.acos(cosAngle), 141 | vec3.set(vec3.create(), 1, 0, 0), 142 | ); 143 | } else { 144 | mat4.identity(rotation); 145 | } 146 | 147 | const translation = mat4.fromTranslation(mat4.create(), origin); 148 | const matrix = mat4.multiply(mat4.create(), translation, rotation); 149 | this[P_RAY].matrix = new Float32Array(matrix); 150 | return this[P_RAY].matrix; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // model 9 | export { XRDevice, XRDeviceConfig } from './device/XRDevice.js'; 10 | export { 11 | metaQuest2, 12 | metaQuest3, 13 | metaQuestPro, 14 | oculusQuest1, 15 | } from './device/configs/headset/meta.js'; 16 | 17 | // Initialization 18 | export { XRSystem } from './initialization/XRSystem.js'; 19 | 20 | // Session 21 | export { XRRenderState } from './session/XRRenderState.js'; 22 | export { XRSession } from './session/XRSession.js'; 23 | 24 | // Frame Loop 25 | export { XRFrame } from './frameloop/XRFrame.js'; 26 | 27 | // Spaces 28 | export { XRSpace } from './spaces/XRSpace.js'; 29 | export { XRReferenceSpace } from './spaces/XRReferenceSpace.js'; 30 | export { XRJointSpace } from './spaces/XRJointSpace.js'; 31 | 32 | // Views 33 | export { XRView } from './views/XRView.js'; 34 | export { XRViewport } from './views/XRViewport.js'; 35 | 36 | // Primitives 37 | export { XRRigidTransform } from './primitives/XRRigidTransform.js'; 38 | 39 | // Pose 40 | export { XRPose } from './pose/XRPose.js'; 41 | export { XRViewerPose } from './pose/XRViewerPose.js'; 42 | export { XRJointPose } from './pose/XRJointPose.js'; 43 | 44 | // Input 45 | export { XRInputSource, XRInputSourceArray } from './input/XRInputSource.js'; 46 | export { XRHand } from './input/XRHand.js'; 47 | 48 | // Layers 49 | export { XRWebGLLayer, XRLayer } from './layers/XRWebGLLayer.js'; 50 | 51 | // Planes 52 | export { XRPlane, XRPlaneSet, NativePlane } from './planes/XRPlane.js'; 53 | 54 | // Meshes 55 | export { XRMesh, XRMeshSet, NativeMesh } from './meshes/XRMesh.js'; 56 | export { XRSemanticLabels } from './labels/labels.js'; 57 | 58 | // Anchors 59 | export { XRAnchor, XRAnchorSet } from './anchors/XRAnchor.js'; 60 | 61 | // Hit Test 62 | export { XRRay } from './hittest/XRRay.js'; 63 | 64 | // Events 65 | export { XRSessionEvent } from './events/XRSessionEvent.js'; 66 | export { XRInputSourceEvent } from './events/XRInputSourceEvent.js'; 67 | export { XRInputSourcesChangeEvent } from './events/XRInputSourcesChangeEvent.js'; 68 | export { XRReferenceSpaceEvent } from './events/XRReferenceSpaceEvent.js'; 69 | 70 | // Action Recording/Playback 71 | export { ActionRecorder } from './action/ActionRecorder.js'; 72 | 73 | // Private Keys 74 | export * from './private.js'; 75 | -------------------------------------------------------------------------------- /src/initialization/XRSystem.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import type { WebXRFeature, XRDevice } from '../device/XRDevice.js'; 9 | import { 10 | XRSession, 11 | XRSessionInit, 12 | XRSessionMode, 13 | } from '../session/XRSession.js'; 14 | 15 | import { P_SYSTEM } from '../private.js'; 16 | 17 | type SessionGrantConfig = { 18 | resolve: (value: XRSession) => void; 19 | reject: (reason?: any) => void; 20 | mode: XRSessionMode; 21 | options: XRSessionInit; 22 | }; 23 | 24 | export class XRSystem extends EventTarget { 25 | [P_SYSTEM]: { 26 | device: XRDevice; 27 | activeSession?: XRSession; 28 | grantSession: (SessionGrantConfig: SessionGrantConfig) => void; 29 | offeredSessionConfig?: SessionGrantConfig; 30 | }; 31 | 32 | constructor(device: XRDevice) { 33 | super(); 34 | this[P_SYSTEM] = { 35 | device, 36 | grantSession: ({ resolve, reject, mode, options }) => { 37 | // Check for active sessions and other constraints here 38 | if (this[P_SYSTEM].activeSession) { 39 | reject( 40 | new DOMException( 41 | 'An active XRSession already exists.', 42 | 'InvalidStateError', 43 | ), 44 | ); 45 | return; 46 | } 47 | 48 | // Handle required and optional features 49 | const { requiredFeatures = [], optionalFeatures = [] } = options; 50 | const { supportedFeatures } = this[P_SYSTEM].device; 51 | 52 | // Check if all required features are supported 53 | const allRequiredSupported = requiredFeatures.every((feature) => 54 | supportedFeatures.includes(feature), 55 | ); 56 | if (!allRequiredSupported) { 57 | reject( 58 | new Error( 59 | 'One or more required features are not supported by the device.', 60 | ), 61 | ); 62 | return; 63 | } 64 | 65 | // Filter out unsupported optional features 66 | const supportedOptionalFeatures = optionalFeatures.filter((feature) => 67 | supportedFeatures.includes(feature), 68 | ); 69 | 70 | // Combine required and supported optional features into enabled features 71 | const enabledFeatures: WebXRFeature[] = Array.from( 72 | new Set([ 73 | ...requiredFeatures, 74 | ...supportedOptionalFeatures, 75 | 'viewer', 76 | 'local', 77 | ]), 78 | ); 79 | 80 | // Proceed with session creation 81 | const session = new XRSession( 82 | this[P_SYSTEM].device, 83 | mode, 84 | enabledFeatures, 85 | ); 86 | this[P_SYSTEM].activeSession = session; 87 | 88 | // Listen for session end to clear the active session 89 | session.addEventListener('end', () => { 90 | this[P_SYSTEM].activeSession = undefined; 91 | }); 92 | 93 | resolve(session); 94 | }, 95 | }; 96 | // Initialize device change monitoring here if applicable 97 | } 98 | 99 | isSessionSupported(mode: XRSessionMode): Promise { 100 | return new Promise((resolve, _reject) => { 101 | if (mode === 'inline') { 102 | resolve(true); 103 | } else { 104 | // Check for spatial tracking permission if necessary 105 | resolve(this[P_SYSTEM].device.supportedSessionModes.includes(mode)); 106 | } 107 | }); 108 | } 109 | 110 | requestSession( 111 | mode: XRSessionMode, 112 | options: XRSessionInit = {}, 113 | ): Promise { 114 | return new Promise((resolve, reject) => { 115 | this.isSessionSupported(mode) 116 | .then((isSupported) => { 117 | if (!isSupported) { 118 | reject( 119 | new DOMException( 120 | 'The requested XRSession mode is not supported.', 121 | 'NotSupportedError', 122 | ), 123 | ); 124 | return; 125 | } 126 | 127 | const sessionGrantConfig = { 128 | resolve, 129 | reject, 130 | mode, 131 | options, 132 | }; 133 | 134 | this[P_SYSTEM].grantSession(sessionGrantConfig); 135 | }) 136 | .catch(reject); 137 | }); 138 | } 139 | 140 | offerSession( 141 | mode: XRSessionMode, 142 | options: XRSessionInit = {}, 143 | ): Promise { 144 | return new Promise((resolve, reject) => { 145 | this.isSessionSupported(mode) 146 | .then((isSupported) => { 147 | if (!isSupported) { 148 | reject( 149 | new DOMException( 150 | 'The requested XRSession mode is not supported.', 151 | 'NotSupportedError', 152 | ), 153 | ); 154 | return; 155 | } 156 | 157 | this[P_SYSTEM].offeredSessionConfig = { 158 | resolve, 159 | reject, 160 | mode, 161 | options, 162 | }; 163 | }) 164 | .catch(reject); 165 | }); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/input/XRHand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { XRJointSpace } from '../spaces/XRJointSpace.js'; 9 | 10 | export enum XRHandJoint { 11 | Wrist = 'wrist', 12 | 13 | ThumbMetacarpal = 'thumb-metacarpal', 14 | ThumbPhalanxProximal = 'thumb-phalanx-proximal', 15 | ThumbPhalanxDistal = 'thumb-phalanx-distal', 16 | ThumbTip = 'thumb-tip', 17 | 18 | IndexFingerMetacarpal = 'index-finger-metacarpal', 19 | IndexFingerPhalanxProximal = 'index-finger-phalanx-proximal', 20 | IndexFingerPhalanxIntermediate = 'index-finger-phalanx-intermediate', 21 | IndexFingerPhalanxDistal = 'index-finger-phalanx-distal', 22 | IndexFingerTip = 'index-finger-tip', 23 | 24 | MiddleFingerMetacarpal = 'middle-finger-metacarpal', 25 | MiddleFingerPhalanxProximal = 'middle-finger-phalanx-proximal', 26 | MiddleFingerPhalanxIntermediate = 'middle-finger-phalanx-intermediate', 27 | MiddleFingerPhalanxDistal = 'middle-finger-phalanx-distal', 28 | MiddleFingerTip = 'middle-finger-tip', 29 | 30 | RingFingerMetacarpal = 'ring-finger-metacarpal', 31 | RingFingerPhalanxProximal = 'ring-finger-phalanx-proximal', 32 | RingFingerPhalanxIntermediate = 'ring-finger-phalanx-intermediate', 33 | RingFingerPhalanxDistal = 'ring-finger-phalanx-distal', 34 | RingFingerTip = 'ring-finger-tip', 35 | 36 | PinkyFingerMetacarpal = 'pinky-finger-metacarpal', 37 | PinkyFingerPhalanxProximal = 'pinky-finger-phalanx-proximal', 38 | PinkyFingerPhalanxIntermediate = 'pinky-finger-phalanx-intermediate', 39 | PinkyFingerPhalanxDistal = 'pinky-finger-phalanx-distal', 40 | PinkyFingerTip = 'pinky-finger-tip', 41 | } 42 | 43 | export class XRHand extends Map {} 44 | -------------------------------------------------------------------------------- /src/input/XRInputSource.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { Gamepad } from '../gamepad/Gamepad.js'; 9 | import { P_INPUT_SOURCE } from '../private.js'; 10 | import { XRHand } from './XRHand.js'; 11 | import { XRSpace } from '../spaces/XRSpace.js'; 12 | 13 | export enum XRHandedness { 14 | None = 'none', 15 | Left = 'left', 16 | Right = 'right', 17 | } 18 | 19 | export enum XRTargetRayMode { 20 | Gaze = 'gaze', 21 | TrackedPointer = 'tracked-pointer', 22 | Screen = 'screen', 23 | TransientPointer = 'transient-pointer', 24 | } 25 | 26 | export class XRInputSourceArray extends Array {} 27 | 28 | export class XRInputSource { 29 | [P_INPUT_SOURCE]: { 30 | handedness: XRHandedness; 31 | targetRayMode: XRTargetRayMode; 32 | targetRaySpace: XRSpace; 33 | gripSpace?: XRSpace; 34 | profiles: Array; 35 | gamepad?: Gamepad; 36 | hand?: XRHand; 37 | }; 38 | 39 | constructor( 40 | handedness: XRHandedness, 41 | targetRayMode: XRTargetRayMode, 42 | profiles: string[], 43 | targetRaySpace: XRSpace, 44 | gamepad?: Gamepad, 45 | gripSpace?: XRSpace, 46 | hand?: XRHand, 47 | ) { 48 | this[P_INPUT_SOURCE] = { 49 | handedness, 50 | targetRayMode, 51 | targetRaySpace, 52 | gripSpace, 53 | profiles, 54 | gamepad, 55 | hand, 56 | }; 57 | } 58 | 59 | get handedness() { 60 | return this[P_INPUT_SOURCE].handedness; 61 | } 62 | 63 | get targetRayMode() { 64 | return this[P_INPUT_SOURCE].targetRayMode; 65 | } 66 | 67 | get targetRaySpace() { 68 | return this[P_INPUT_SOURCE].targetRaySpace; 69 | } 70 | 71 | get gripSpace() { 72 | return this[P_INPUT_SOURCE].gripSpace; 73 | } 74 | 75 | get profiles() { 76 | return this[P_INPUT_SOURCE].profiles; 77 | } 78 | 79 | get gamepad() { 80 | return this[P_INPUT_SOURCE].gamepad; 81 | } 82 | 83 | get hand() { 84 | return this[P_INPUT_SOURCE].hand; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/labels/labels.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Source: https://github.com/immersive-web/semantic-labels/blob/master/labels.json 9 | 10 | /** 11 | * Enum for semantic labels. 12 | * For more details, see the {@link https://github.com/immersive-web/semantic-labels | Semantic Labels Documentation}. 13 | */ 14 | export enum XRSemanticLabels { 15 | Desk = 'desk', 16 | Couch = 'couch', 17 | Floor = 'floor', 18 | Ceiling = 'ceiling', 19 | Wall = 'wall', 20 | Door = 'door', 21 | Window = 'window', 22 | Table = 'table', 23 | Shelf = 'shelf', 24 | Bed = 'bed', 25 | Screen = 'screen', 26 | Lamp = 'lamp', 27 | Plant = 'plant', 28 | WallArt = 'wall art', 29 | GlobalMesh = 'global mesh', 30 | Other = 'other', 31 | } 32 | -------------------------------------------------------------------------------- /src/layers/XRWebGLLayer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { P_DEVICE, P_SESSION, P_VIEW, P_WEBGL_LAYER } from '../private.js'; 9 | 10 | import { XRSession } from '../session/XRSession.js'; 11 | import { XRView } from '../views/XRView.js'; 12 | 13 | export class XRLayer extends EventTarget {} 14 | 15 | type LayerInit = { 16 | antialias?: boolean; 17 | depth?: boolean; 18 | stencil?: boolean; 19 | alpha?: boolean; 20 | ignoreDepthValues?: boolean; 21 | framebufferScaleFactor?: number; 22 | }; 23 | 24 | const defaultLayerInit: LayerInit = { 25 | antialias: true, 26 | depth: true, 27 | stencil: false, 28 | alpha: true, 29 | ignoreDepthValues: false, 30 | framebufferScaleFactor: 1.0, 31 | }; 32 | 33 | export class XRWebGLLayer extends XRLayer { 34 | [P_WEBGL_LAYER]: { 35 | session: XRSession; 36 | context: WebGLRenderingContext | WebGL2RenderingContext; 37 | antialias: boolean; 38 | }; 39 | 40 | constructor( 41 | session: XRSession, 42 | context: WebGLRenderingContext | WebGL2RenderingContext, 43 | layerInit: LayerInit = {}, 44 | ) { 45 | super(); 46 | 47 | if (session[P_SESSION].ended) { 48 | throw new DOMException('Session has ended', 'InvalidStateError'); 49 | } 50 | 51 | // TO-DO: Check that the context attribute has xrCompatible set to true 52 | // may require polyfilling the context and perhaps canvas.getContext 53 | 54 | // Default values for XRWebGLLayerInit, can be overridden by layerInit 55 | const config = { ...defaultLayerInit, ...layerInit }; 56 | 57 | this[P_WEBGL_LAYER] = { 58 | session, 59 | context, 60 | antialias: config.antialias!, 61 | }; 62 | } 63 | 64 | get context() { 65 | return this[P_WEBGL_LAYER].context; 66 | } 67 | 68 | get antialias() { 69 | return this[P_WEBGL_LAYER].antialias; 70 | } 71 | 72 | get ignoreDepthValues() { 73 | return true; 74 | } 75 | 76 | get framebuffer() { 77 | return null; 78 | } 79 | 80 | get framebufferWidth() { 81 | return this[P_WEBGL_LAYER].context.drawingBufferWidth; 82 | } 83 | 84 | get framebufferHeight() { 85 | return this[P_WEBGL_LAYER].context.drawingBufferHeight; 86 | } 87 | 88 | getViewport(view: XRView) { 89 | if (view[P_VIEW].session !== this[P_WEBGL_LAYER].session) { 90 | throw new DOMException( 91 | "View's session differs from Layer's session", 92 | 'InvalidStateError', 93 | ); 94 | } 95 | // TO-DO: check frame 96 | return this[P_WEBGL_LAYER].session[P_SESSION].device[P_DEVICE].getViewport( 97 | this, 98 | view, 99 | ); 100 | } 101 | 102 | static getNativeFramebufferScaleFactor(session: XRSession): number { 103 | if (!(session instanceof XRSession)) { 104 | throw new TypeError( 105 | 'getNativeFramebufferScaleFactor must be passed a session.', 106 | ); 107 | } 108 | 109 | if (session[P_SESSION].ended) { 110 | return 0.0; 111 | } 112 | 113 | // Return 1.0 for simplicity, actual implementation might vary based on the device capabilities 114 | return 1.0; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/meshes/XRMesh.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { P_MESH } from '../private.js'; 9 | import type { XRFrame } from '../frameloop/XRFrame.js'; 10 | import { XRRigidTransform } from '../primitives/XRRigidTransform.js'; 11 | import { XRSemanticLabels } from '../labels/labels.js'; 12 | import { XRSpace } from '../spaces/XRSpace.js'; 13 | 14 | export class XRMesh { 15 | [P_MESH]: { 16 | nativeMesh: NativeMesh; 17 | frame: XRFrame; 18 | meshSpace: XRSpace; 19 | vertices: Float32Array; 20 | indices: Uint32Array; 21 | lastChangedTime: DOMHighResTimeStamp; 22 | semanticLabel?: XRSemanticLabels; 23 | }; 24 | 25 | constructor( 26 | nativeMesh: NativeMesh, 27 | meshSpace: XRSpace, 28 | vertices: Float32Array, 29 | indices: Uint32Array, 30 | semanticLabel?: XRSemanticLabels, 31 | ) { 32 | this[P_MESH] = { 33 | nativeMesh, 34 | frame: undefined!, 35 | meshSpace, 36 | vertices, 37 | indices, 38 | lastChangedTime: performance.now(), 39 | semanticLabel, 40 | }; 41 | } 42 | 43 | get meshSpace() { 44 | return this[P_MESH].meshSpace; 45 | } 46 | 47 | get vertices(): Readonly { 48 | return this[P_MESH].vertices; 49 | } 50 | 51 | get indices(): Readonly { 52 | return this[P_MESH].indices; 53 | } 54 | 55 | get lastChangedTime() { 56 | return this[P_MESH].lastChangedTime; 57 | } 58 | 59 | get semanticLabel() { 60 | return this[P_MESH].semanticLabel; 61 | } 62 | } 63 | 64 | export class XRMeshSet extends Set {} 65 | 66 | export class NativeMesh { 67 | constructor( 68 | public transform: XRRigidTransform, 69 | public vertices: Float32Array, 70 | public indices: Uint32Array, 71 | public semanticLabel: XRSemanticLabels, 72 | ) {} 73 | } 74 | -------------------------------------------------------------------------------- /src/planes/XRPlane.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { P_PLANE } from '../private.js'; 9 | import type { XRFrame } from '../frameloop/XRFrame.js'; 10 | import { XRRigidTransform } from '../primitives/XRRigidTransform.js'; 11 | import { XRSemanticLabels } from '../labels/labels.js'; 12 | import { XRSpace } from '../spaces/XRSpace.js'; 13 | 14 | export enum XRPlaneOrientation { 15 | Horizontal = 'horizontal', 16 | Vertical = 'vertical', 17 | } 18 | 19 | /** 20 | * XRPlane orientation mapping from semantic labels. 21 | * For more details, see the {@link https://github.com/immersive-web/semantic-labels | Semantic Labels Documentation}. 22 | */ 23 | export const XREntityOrientation: Partial< 24 | Record 25 | > = { 26 | [XRSemanticLabels.Desk]: XRPlaneOrientation.Horizontal, 27 | [XRSemanticLabels.Couch]: XRPlaneOrientation.Horizontal, 28 | [XRSemanticLabels.Floor]: XRPlaneOrientation.Horizontal, 29 | [XRSemanticLabels.Ceiling]: XRPlaneOrientation.Horizontal, 30 | [XRSemanticLabels.Wall]: XRPlaneOrientation.Vertical, 31 | [XRSemanticLabels.Door]: XRPlaneOrientation.Vertical, 32 | [XRSemanticLabels.Window]: XRPlaneOrientation.Vertical, 33 | [XRSemanticLabels.Table]: XRPlaneOrientation.Horizontal, 34 | [XRSemanticLabels.Shelf]: XRPlaneOrientation.Horizontal, 35 | [XRSemanticLabels.Bed]: XRPlaneOrientation.Horizontal, 36 | [XRSemanticLabels.Screen]: XRPlaneOrientation.Horizontal, 37 | [XRSemanticLabels.Lamp]: XRPlaneOrientation.Horizontal, 38 | [XRSemanticLabels.Plant]: XRPlaneOrientation.Horizontal, 39 | [XRSemanticLabels.WallArt]: XRPlaneOrientation.Vertical, 40 | }; 41 | 42 | export class XRPlane { 43 | [P_PLANE]: { 44 | nativePlane: NativePlane; 45 | frame: XRFrame; 46 | planeSpace: XRSpace; 47 | polygon: DOMPointReadOnly[]; 48 | lastChangedTime: DOMHighResTimeStamp; 49 | semanticLabel?: XRSemanticLabels; 50 | orientation?: XRPlaneOrientation; 51 | }; 52 | 53 | constructor( 54 | nativePlane: NativePlane, 55 | planeSpace: XRSpace, 56 | polygon: DOMPointReadOnly[], 57 | semanticLabel?: XRSemanticLabels, 58 | ) { 59 | this[P_PLANE] = { 60 | nativePlane, 61 | frame: undefined!, 62 | planeSpace, 63 | polygon, 64 | lastChangedTime: performance.now(), 65 | semanticLabel, 66 | orientation: semanticLabel 67 | ? XREntityOrientation[semanticLabel] 68 | : undefined, 69 | }; 70 | } 71 | 72 | get planeSpace() { 73 | return this[P_PLANE].planeSpace; 74 | } 75 | 76 | get polygon(): ReadonlyArray { 77 | return this[P_PLANE].polygon; 78 | } 79 | 80 | get orientation() { 81 | return this[P_PLANE].orientation; 82 | } 83 | 84 | get lastChangedTime() { 85 | return this[P_PLANE].lastChangedTime; 86 | } 87 | 88 | get semanticLabel() { 89 | return this[P_PLANE].semanticLabel; 90 | } 91 | } 92 | 93 | export class XRPlaneSet extends Set {} 94 | 95 | export class NativePlane { 96 | constructor( 97 | public transform: XRRigidTransform, 98 | public polygon: DOMPointReadOnly[], 99 | public semanticLabel: XRSemanticLabels, 100 | ) {} 101 | } 102 | -------------------------------------------------------------------------------- /src/pose/XRJointPose.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { P_JOINT_POSE } from '../private.js'; 9 | import { XRPose } from './XRPose.js'; 10 | import { XRRigidTransform } from '../primitives/XRRigidTransform.js'; 11 | 12 | export class XRJointPose extends XRPose { 13 | [P_JOINT_POSE]: { 14 | radius: number; 15 | }; 16 | 17 | constructor( 18 | transform: XRRigidTransform, 19 | radius: number, 20 | emulatedPosition: boolean = false, 21 | linearVelocity: DOMPointReadOnly | undefined = undefined, 22 | angularVelocity: DOMPointReadOnly | undefined = undefined, 23 | ) { 24 | super(transform, emulatedPosition, linearVelocity, angularVelocity); 25 | this[P_JOINT_POSE] = { radius }; 26 | } 27 | 28 | get radius() { 29 | return this[P_JOINT_POSE].radius; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/pose/XRPose.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { P_POSE } from '../private.js'; 9 | import { XRRigidTransform } from '../primitives/XRRigidTransform.js'; 10 | 11 | export class XRPose { 12 | [P_POSE]: { 13 | transform: XRRigidTransform; 14 | emulatedPosition: boolean; 15 | linearVelocity?: DOMPointReadOnly; 16 | angularVelocity?: DOMPointReadOnly; 17 | }; 18 | 19 | constructor( 20 | transform: XRRigidTransform, 21 | emulatedPosition = false, 22 | linearVelocity: DOMPointReadOnly | undefined = undefined, 23 | angularVelocity: DOMPointReadOnly | undefined = undefined, 24 | ) { 25 | this[P_POSE] = { 26 | transform, 27 | emulatedPosition, 28 | linearVelocity, 29 | angularVelocity, 30 | }; 31 | } 32 | 33 | get transform() { 34 | return this[P_POSE].transform; 35 | } 36 | 37 | get emulatedPosition() { 38 | return this[P_POSE].emulatedPosition; 39 | } 40 | 41 | get linearVelocity() { 42 | return this[P_POSE].linearVelocity; 43 | } 44 | 45 | get angularVelocity() { 46 | return this[P_POSE].angularVelocity; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/pose/XRViewerPose.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { P_VIEWER_POSE } from '../private.js'; 9 | import { XRPose } from './XRPose.js'; 10 | import { XRRigidTransform } from '../primitives/XRRigidTransform.js'; 11 | import { XRView } from '../views/XRView.js'; 12 | 13 | export class XRViewerPose extends XRPose { 14 | [P_VIEWER_POSE]: { 15 | views: readonly XRView[]; 16 | }; 17 | 18 | constructor( 19 | transform: XRRigidTransform, 20 | views: XRView[], 21 | emulatedPosition: boolean = false, 22 | linearVelocity: DOMPointReadOnly | undefined = undefined, 23 | angularVelocity: DOMPointReadOnly | undefined = undefined, 24 | ) { 25 | super(transform, emulatedPosition, linearVelocity, angularVelocity); 26 | this[P_VIEWER_POSE] = { 27 | views: Object.freeze(views), 28 | }; 29 | } 30 | 31 | get views() { 32 | return this[P_VIEWER_POSE].views; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/primitives/XRRigidTransform.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { mat4, quat, vec3 } from 'gl-matrix'; 9 | 10 | import { P_RIGID_TRANSFORM } from '../private.js'; 11 | 12 | export class XRRigidTransform { 13 | [P_RIGID_TRANSFORM]: { 14 | matrix: mat4; 15 | position: vec3; 16 | orientation: quat; 17 | inverse: XRRigidTransform | null; 18 | }; 19 | 20 | constructor(position?: DOMPointInit, orientation?: DOMPointInit) { 21 | // Default values 22 | const defaultPosition = vec3.fromValues(0, 0, 0); 23 | const defaultOrientation = quat.create(); 24 | 25 | this[P_RIGID_TRANSFORM] = { 26 | matrix: mat4.create(), 27 | position: position 28 | ? vec3.fromValues(position.x!, position.y!, position.z!) 29 | : defaultPosition, 30 | orientation: orientation 31 | ? quat.normalize( 32 | quat.create(), 33 | quat.fromValues( 34 | orientation.x!, 35 | orientation.y!, 36 | orientation.z!, 37 | orientation.w!, 38 | ), 39 | ) 40 | : defaultOrientation, 41 | inverse: null, 42 | }; 43 | 44 | this.updateMatrix(); 45 | } 46 | 47 | private updateMatrix(): void { 48 | mat4.fromRotationTranslation( 49 | this[P_RIGID_TRANSFORM].matrix, 50 | this[P_RIGID_TRANSFORM].orientation, 51 | this[P_RIGID_TRANSFORM].position, 52 | ); 53 | } 54 | 55 | get matrix(): Float32Array { 56 | return this[P_RIGID_TRANSFORM].matrix as Float32Array; 57 | } 58 | 59 | get position(): DOMPointReadOnly { 60 | const pos = this[P_RIGID_TRANSFORM].position; 61 | return new DOMPointReadOnly(pos[0], pos[1], pos[2], 1); 62 | } 63 | 64 | get orientation(): DOMPointReadOnly { 65 | const ori = this[P_RIGID_TRANSFORM].orientation; 66 | return new DOMPointReadOnly(ori[0], ori[1], ori[2], ori[3]); 67 | } 68 | 69 | get inverse(): XRRigidTransform { 70 | if (!this[P_RIGID_TRANSFORM].inverse) { 71 | const invMatrix = mat4.create(); 72 | if (!mat4.invert(invMatrix, this[P_RIGID_TRANSFORM].matrix)) { 73 | throw new Error('Matrix is not invertible.'); 74 | } 75 | 76 | // Decomposing the inverse matrix into position and orientation 77 | let invPosition = vec3.create(); 78 | mat4.getTranslation(invPosition, invMatrix); 79 | 80 | let invOrientation = quat.create(); 81 | mat4.getRotation(invOrientation, invMatrix); 82 | 83 | // Creating a new XRRigidTransform for the inverse 84 | this[P_RIGID_TRANSFORM].inverse = new XRRigidTransform( 85 | new DOMPointReadOnly(invPosition[0], invPosition[1], invPosition[2], 1), 86 | new DOMPointReadOnly( 87 | invOrientation[0], 88 | invOrientation[1], 89 | invOrientation[2], 90 | invOrientation[3], 91 | ), 92 | ); 93 | 94 | // Setting the inverse of the inverse to be this transform 95 | this[P_RIGID_TRANSFORM].inverse[P_RIGID_TRANSFORM].inverse = this; 96 | } 97 | 98 | return this[P_RIGID_TRANSFORM].inverse; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/private.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | export const P_ACTION_PLAYER = Symbol('@iwer/action-player'); 9 | export const P_ACTION_RECORDER = Symbol('@iwer/action-recorder'); 10 | export const P_ANCHOR = Symbol('@iwer/xr-anchor'); 11 | export const P_CONTROLLER = Symbol('@iwer/xr-controller'); 12 | export const P_DEVICE = Symbol('@iwer/xr-device'); 13 | export const P_HAND_INPUT = Symbol('@iwer/xr-hand-input'); 14 | export const P_TRACKED_INPUT = Symbol('@iwer/xr-tracked-input'); 15 | export const P_FRAME = Symbol('@iwer/xr-frame'); 16 | export const P_GAMEPAD = Symbol('@iwer/gamepad'); 17 | export const P_SYSTEM = Symbol('@iwer/xr-system'); 18 | export const P_INPUT_SOURCE = Symbol('@iwer/xr-input-source'); 19 | export const P_WEBGL_LAYER = Symbol('@iwer/xr-webgl-layer'); 20 | export const P_MESH = Symbol('@iwer/xr-mesh'); 21 | export const P_PLANE = Symbol('@iwer/xr-plane'); 22 | export const P_JOINT_POSE = Symbol('@iwer/xr-joint-pose'); 23 | export const P_POSE = Symbol('@iwer/xr-pose'); 24 | export const P_VIEWER_POSE = Symbol('@iwer/xr-viewer-pose'); 25 | export const P_RIGID_TRANSFORM = Symbol('@iwer/xr-rigid-transform'); 26 | export const P_RENDER_STATE = Symbol('@iwer/xr-render-state'); 27 | export const P_SESSION = Symbol('@iwer/xr-session'); 28 | export const P_JOINT_SPACE = Symbol('@iwer/xr-joint-space'); 29 | export const P_REF_SPACE = Symbol('@iwer/xr-reference-space'); 30 | export const P_SPACE = Symbol('@iwer/xr-space'); 31 | export const P_VIEW = Symbol('@iwer/xr-view'); 32 | export const P_VIEWPORT = Symbol('@iwer/xr-viewport'); 33 | export const P_RAY = Symbol('@iwer/xr-ray'); 34 | export const P_HIT_TEST = Symbol('@iwer/xr-hit-test'); 35 | -------------------------------------------------------------------------------- /src/session/XRRenderState.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { P_RENDER_STATE } from '../private.js'; 9 | import { XRWebGLLayer } from '../layers/XRWebGLLayer.js'; 10 | 11 | export class XRRenderState { 12 | [P_RENDER_STATE]: { 13 | depthNear: number; 14 | depthFar: number; 15 | inlineVerticalFieldOfView: number | null; 16 | baseLayer: XRWebGLLayer | null; 17 | }; 18 | 19 | constructor(init: Partial = {}, oldState?: XRRenderState) { 20 | this[P_RENDER_STATE] = { 21 | depthNear: init.depthNear || oldState?.depthNear || 0.1, 22 | depthFar: init.depthFar || oldState?.depthFar || 1000.0, 23 | inlineVerticalFieldOfView: 24 | init.inlineVerticalFieldOfView || 25 | oldState?.inlineVerticalFieldOfView || 26 | null, 27 | baseLayer: init.baseLayer || oldState?.baseLayer || null, 28 | }; 29 | } 30 | 31 | get depthNear(): number { 32 | return this[P_RENDER_STATE].depthNear; 33 | } 34 | 35 | get depthFar(): number { 36 | return this[P_RENDER_STATE].depthFar; 37 | } 38 | 39 | get inlineVerticalFieldOfView(): number | null { 40 | return this[P_RENDER_STATE].inlineVerticalFieldOfView; 41 | } 42 | 43 | get baseLayer(): XRWebGLLayer | null { 44 | return this[P_RENDER_STATE].baseLayer; 45 | } 46 | } 47 | 48 | // XRRenderStateInit interface definition for TypeScript 49 | export interface XRRenderStateInit { 50 | depthNear?: number; 51 | depthFar?: number; 52 | inlineVerticalFieldOfView?: number; 53 | baseLayer?: XRWebGLLayer; 54 | } 55 | -------------------------------------------------------------------------------- /src/spaces/XRJointSpace.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { P_JOINT_SPACE } from '../private.js'; 9 | import { XRHandJoint } from '../input/XRHand.js'; 10 | import { XRSpace } from './XRSpace.js'; 11 | import { mat4 } from 'gl-matrix'; 12 | 13 | export class XRJointSpace extends XRSpace { 14 | [P_JOINT_SPACE]: { 15 | jointName: XRHandJoint; 16 | radius: number; 17 | }; 18 | 19 | constructor( 20 | jointName: XRHandJoint, 21 | parentSpace?: XRSpace, 22 | offsetMatrix?: mat4, 23 | ) { 24 | super(parentSpace, offsetMatrix); 25 | this[P_JOINT_SPACE] = { jointName, radius: 0 }; 26 | } 27 | 28 | get jointName() { 29 | return this[P_JOINT_SPACE].jointName; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/spaces/XRReferenceSpace.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { P_REF_SPACE } from '../private.js'; 9 | import { XRReferenceSpaceEventHandler } from '../events/XRReferenceSpaceEvent.js'; 10 | import { XRSpace } from './XRSpace.js'; 11 | import { mat4 } from 'gl-matrix'; 12 | 13 | export enum XRReferenceSpaceType { 14 | Viewer = 'viewer', 15 | Local = 'local', 16 | LocalFloor = 'local-floor', 17 | BoundedFloor = 'bounded-floor', 18 | Unbounded = 'unbounded', 19 | } 20 | 21 | export class XRReferenceSpace extends XRSpace { 22 | [P_REF_SPACE]: { 23 | type: XRReferenceSpaceType; 24 | onreset: XRReferenceSpaceEventHandler | null; 25 | } = { 26 | type: null as any, 27 | onreset: () => {}, 28 | }; 29 | 30 | constructor( 31 | type: XRReferenceSpaceType, 32 | parentSpace: XRSpace, 33 | offsetMatrix?: mat4, 34 | ) { 35 | super(parentSpace, offsetMatrix); 36 | this[P_REF_SPACE].type = type; 37 | } 38 | 39 | get onreset(): XRReferenceSpaceEventHandler { 40 | return this[P_REF_SPACE].onreset ?? (() => {}); 41 | } 42 | 43 | set onreset(callback: XRReferenceSpaceEventHandler) { 44 | if (this[P_REF_SPACE].onreset) { 45 | this.removeEventListener( 46 | 'reset', 47 | this[P_REF_SPACE].onreset as EventListener, 48 | ); 49 | } 50 | this[P_REF_SPACE].onreset = callback; 51 | if (callback) { 52 | this.addEventListener('reset', callback as EventListener); 53 | } 54 | } 55 | 56 | // Create a new XRReferenceSpace with an offset from the current space 57 | getOffsetReferenceSpace(originOffset: mat4): XRReferenceSpace { 58 | // Create a new XRReferenceSpace with the originOffset as its offsetMatrix 59 | // The new space's parent is set to 'this' (the current XRReferenceSpace) 60 | return new XRReferenceSpace(this[P_REF_SPACE].type, this, originOffset); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/spaces/XRSpace.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { mat4, quat, vec3 } from 'gl-matrix'; 9 | 10 | import { P_SPACE } from '../private.js'; 11 | 12 | export class XRSpace extends EventTarget { 13 | [P_SPACE]: { 14 | parentSpace: XRSpace | undefined; 15 | offsetMatrix: mat4; 16 | emulated: boolean; 17 | }; 18 | 19 | constructor(parentSpace?: XRSpace, offsetMatrix?: mat4) { 20 | super(); 21 | this[P_SPACE] = { 22 | parentSpace, 23 | offsetMatrix: offsetMatrix ? mat4.clone(offsetMatrix) : mat4.create(), 24 | emulated: true, 25 | }; 26 | } 27 | } 28 | 29 | export class GlobalSpace extends XRSpace { 30 | constructor() { 31 | super(undefined, mat4.create()); // GlobalSpace has no parent 32 | } 33 | } 34 | 35 | export class XRSpaceUtils { 36 | // Update the position component of the offsetMatrix of a given XRSpace 37 | static updateOffsetPosition(space: XRSpace, position: vec3): void { 38 | const offsetMatrix = space[P_SPACE].offsetMatrix; 39 | mat4.fromTranslation(offsetMatrix, position); 40 | } 41 | 42 | // Update the rotation component of the offsetMatrix of a given XRSpace using a quaternion 43 | static updateOffsetQuaternion(space: XRSpace, quaternion: quat): void { 44 | const offsetMatrix = space[P_SPACE].offsetMatrix; 45 | const translation = vec3.create(); 46 | mat4.getTranslation(translation, offsetMatrix); 47 | mat4.fromRotationTranslation(offsetMatrix, quaternion, translation); 48 | } 49 | 50 | // Update the offsetMatrix of a given XRSpace directly 51 | static updateOffsetMatrix(space: XRSpace, matrix: mat4): void { 52 | const offsetMatrix = space[P_SPACE].offsetMatrix; 53 | mat4.copy(offsetMatrix, matrix); 54 | } 55 | 56 | // Calculate the global offset matrix for a given XRSpace 57 | static calculateGlobalOffsetMatrix( 58 | space: XRSpace, 59 | globalOffset: mat4 = mat4.create(), 60 | ): mat4 { 61 | const parentOffset = space[P_SPACE].parentSpace 62 | ? XRSpaceUtils.calculateGlobalOffsetMatrix(space[P_SPACE].parentSpace) 63 | : mat4.create(); // Identity matrix for GlobalSpace 64 | 65 | mat4.multiply(globalOffset, parentOffset, space[P_SPACE].offsetMatrix); 66 | return globalOffset; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/utils/Math.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { quat, vec3 } from 'gl-matrix'; 9 | 10 | /** 11 | * Wrapper class for gl-matrix vec3 12 | * Minimal interoperable interface to Vector3 in Three.js and Babylon.js 13 | */ 14 | export class Vector3 { 15 | vec3: vec3; 16 | private tempVec3: vec3; 17 | 18 | constructor(x: number = 0, y: number = 0, z: number = 0) { 19 | this.vec3 = vec3.fromValues(x, y, z); 20 | this.tempVec3 = vec3.create(); 21 | } 22 | 23 | get x() { 24 | return this.vec3[0]; 25 | } 26 | 27 | set x(value: number) { 28 | this.vec3[0] = value; 29 | } 30 | 31 | get y() { 32 | return this.vec3[1]; 33 | } 34 | 35 | set y(value: number) { 36 | this.vec3[1] = value; 37 | } 38 | 39 | get z() { 40 | return this.vec3[2]; 41 | } 42 | 43 | set z(value: number) { 44 | this.vec3[2] = value; 45 | } 46 | 47 | set(x: number, y: number, z: number): this { 48 | vec3.set(this.vec3, x, y, z); 49 | return this; 50 | } 51 | 52 | clone(): Vector3 { 53 | return new Vector3(this.x, this.y, this.z); 54 | } 55 | 56 | copy(v: Vector3): this { 57 | this.x = v.x; 58 | this.y = v.y; 59 | this.z = v.z; 60 | return this; 61 | } 62 | 63 | round(): this { 64 | this.x = Math.round(this.x); 65 | this.y = Math.round(this.y); 66 | this.z = Math.round(this.z); 67 | return this; 68 | } 69 | 70 | normalize(): this { 71 | vec3.copy(this.tempVec3, this.vec3); 72 | vec3.normalize(this.vec3, this.tempVec3); 73 | return this; 74 | } 75 | 76 | add(v: Vector3): this { 77 | vec3.copy(this.tempVec3, this.vec3); 78 | vec3.add(this.vec3, this.tempVec3, v.vec3); 79 | return this; 80 | } 81 | 82 | applyQuaternion(q: Quaternion): this { 83 | vec3.copy(this.tempVec3, this.vec3); 84 | vec3.transformQuat(this.vec3, this.tempVec3, q.quat); 85 | return this; 86 | } 87 | } 88 | 89 | /** 90 | * Wrapper class for gl-matrix quat4 91 | * Minimal interoperable interface to Vector3 in Three.js and Babylon.js 92 | */ 93 | export class Quaternion { 94 | quat: quat; 95 | private tempQuat: quat; 96 | 97 | constructor(x: number = 0, y: number = 0, z: number = 0, w: number = 1) { 98 | this.quat = quat.fromValues(x, y, z, w); 99 | this.tempQuat = quat.create(); 100 | } 101 | 102 | get x() { 103 | return this.quat[0]; 104 | } 105 | 106 | set x(value: number) { 107 | this.quat[0] = value; 108 | } 109 | 110 | get y() { 111 | return this.quat[1]; 112 | } 113 | 114 | set y(value: number) { 115 | this.quat[1] = value; 116 | } 117 | 118 | get z() { 119 | return this.quat[2]; 120 | } 121 | 122 | set z(value: number) { 123 | this.quat[2] = value; 124 | } 125 | 126 | get w() { 127 | return this.quat[3]; 128 | } 129 | 130 | set w(value: number) { 131 | this.quat[3] = value; 132 | } 133 | 134 | set(x: number, y: number, z: number, w: number): this { 135 | quat.set(this.quat, x, y, z, w); 136 | return this; 137 | } 138 | 139 | clone(): Quaternion { 140 | return new Quaternion(this.x, this.y, this.z, this.w); 141 | } 142 | 143 | copy(q: Quaternion): this { 144 | quat.set(this.quat, q.x, q.y, q.z, q.w); 145 | return this; 146 | } 147 | 148 | normalize(): this { 149 | quat.copy(this.tempQuat, this.quat); 150 | quat.normalize(this.quat, this.tempQuat); 151 | return this; 152 | } 153 | 154 | invert(): this { 155 | quat.copy(this.tempQuat, this.quat); 156 | quat.conjugate(this.quat, this.tempQuat); 157 | return this; 158 | } 159 | 160 | multiply(q: Quaternion): this { 161 | quat.copy(this.tempQuat, this.quat); 162 | quat.multiply(this.quat, this.tempQuat, q.quat); 163 | return this; 164 | } 165 | 166 | setFromAxisAngle(axis: Vector3, angle: number): this { 167 | quat.setAxisAngle(this.quat, axis.vec3, angle); 168 | return this; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/views/XRView.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { P_VIEW } from '../private.js'; 9 | import { XRRigidTransform } from '../primitives/XRRigidTransform.js'; 10 | import { XRSession } from '../session/XRSession.js'; 11 | 12 | export enum XREye { 13 | None = 'none', 14 | Left = 'left', 15 | Right = 'right', 16 | } 17 | 18 | export class XRView { 19 | [P_VIEW]: { 20 | eye: XREye; 21 | projectionMatrix: Float32Array; 22 | transform: XRRigidTransform; 23 | recommendedViewportScale: number | null; 24 | requestedViewportScale: number; 25 | session: XRSession; 26 | }; 27 | 28 | constructor( 29 | eye: XREye, 30 | projectionMatrix: Float32Array, 31 | transform: XRRigidTransform, 32 | session: XRSession, 33 | ) { 34 | this[P_VIEW] = { 35 | eye, 36 | projectionMatrix, 37 | transform, 38 | recommendedViewportScale: null, 39 | requestedViewportScale: 1.0, 40 | session, 41 | }; 42 | } 43 | 44 | get eye(): XREye { 45 | return this[P_VIEW].eye; 46 | } 47 | 48 | get projectionMatrix(): Float32Array { 49 | return this[P_VIEW].projectionMatrix; 50 | } 51 | 52 | get transform(): XRRigidTransform { 53 | return this[P_VIEW].transform; 54 | } 55 | 56 | get recommendedViewportScale(): number | null { 57 | return this[P_VIEW].recommendedViewportScale; 58 | } 59 | 60 | requestViewportScale(scale: number | null): void { 61 | if (scale === null || scale <= 0 || scale > 1) { 62 | console.warn('Invalid scale value. Scale must be > 0 and <= 1.'); 63 | return; 64 | } 65 | this[P_VIEW].requestedViewportScale = scale; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/views/XRViewport.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { P_VIEWPORT } from '../private.js'; 9 | 10 | export class XRViewport { 11 | [P_VIEWPORT]: { 12 | x: number; 13 | y: number; 14 | width: number; 15 | height: number; 16 | }; 17 | 18 | constructor(x: number, y: number, width: number, height: number) { 19 | this[P_VIEWPORT] = { x, y, width, height }; 20 | } 21 | 22 | get x(): number { 23 | return this[P_VIEWPORT].x; 24 | } 25 | 26 | get y(): number { 27 | return this[P_VIEWPORT].y; 28 | } 29 | 30 | get width(): number { 31 | return this[P_VIEWPORT].width; 32 | } 33 | 34 | get height(): number { 35 | return this[P_VIEWPORT].height; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/events/XRInputSourceChangeEvent.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { XRInputSourcesChangeEvent } from '../../src/events/XRInputSourcesChangeEvent.js'; 9 | import { XRInputSource } from '../../src/input/XRInputSource.js'; 10 | import { XRSession } from '../../src/session/XRSession.js'; 11 | 12 | // Mock dependencies 13 | jest.mock('../../src/input/XRInputSource.js'); 14 | jest.mock('../../src/session/XRSession.js'); 15 | 16 | describe('XRInputSourcesChangeEvent', () => { 17 | let mockInputSource1: jest.Mocked; 18 | let mockInputSource2: jest.Mocked; 19 | let mockSession: jest.Mocked; 20 | 21 | beforeEach(() => { 22 | mockInputSource1 = {} as any; 23 | mockInputSource2 = {} as any; 24 | mockSession = {} as any; 25 | }); 26 | 27 | describe('constructor', () => { 28 | it('should create event with type and event init', () => { 29 | const eventInit = { 30 | session: mockSession, 31 | added: [mockInputSource1], 32 | removed: [mockInputSource2], 33 | }; 34 | 35 | const event = new XRInputSourcesChangeEvent( 36 | 'inputsourceschange', 37 | eventInit, 38 | ); 39 | 40 | expect(event.type).toBe('inputsourceschange'); 41 | expect(event.session).toBe(mockSession); 42 | expect(event.added).toEqual([mockInputSource1]); 43 | expect(event.removed).toEqual([mockInputSource2]); 44 | }); 45 | 46 | it('should handle empty added and removed arrays', () => { 47 | const eventInit = { 48 | session: mockSession, 49 | added: [], 50 | removed: [], 51 | }; 52 | 53 | const event = new XRInputSourcesChangeEvent( 54 | 'inputsourceschange', 55 | eventInit, 56 | ); 57 | 58 | expect(event.added).toEqual([]); 59 | expect(event.removed).toEqual([]); 60 | }); 61 | 62 | it('should handle multiple input sources', () => { 63 | const mockInputSource3 = {} as any; 64 | const eventInit = { 65 | session: mockSession, 66 | added: [mockInputSource1, mockInputSource2], 67 | removed: [mockInputSource3], 68 | }; 69 | 70 | const event = new XRInputSourcesChangeEvent( 71 | 'inputsourceschange', 72 | eventInit, 73 | ); 74 | 75 | expect(event.added).toHaveLength(2); 76 | expect(event.removed).toHaveLength(1); 77 | expect(event.added).toContain(mockInputSource1); 78 | expect(event.added).toContain(mockInputSource2); 79 | }); 80 | 81 | it('should inherit from Event', () => { 82 | const eventInit = { 83 | session: mockSession, 84 | added: [], 85 | removed: [], 86 | }; 87 | 88 | const event = new XRInputSourcesChangeEvent( 89 | 'inputsourceschange', 90 | eventInit, 91 | ); 92 | 93 | expect(event).toBeInstanceOf(Event); 94 | }); 95 | 96 | it('should handle event options', () => { 97 | const eventInit = { 98 | session: mockSession, 99 | added: [], 100 | removed: [], 101 | bubbles: true, 102 | cancelable: true, 103 | }; 104 | 105 | const event = new XRInputSourcesChangeEvent( 106 | 'inputsourceschange', 107 | eventInit, 108 | ); 109 | 110 | expect(event.bubbles).toBe(true); 111 | expect(event.cancelable).toBe(true); 112 | }); 113 | 114 | it('should throw error when session is missing', () => { 115 | const eventInit = { 116 | added: [], 117 | removed: [], 118 | } as any; 119 | 120 | expect(() => { 121 | new XRInputSourcesChangeEvent('inputsourceschange', eventInit); 122 | }).toThrow('XRInputSourcesChangeEventInit.session is required'); 123 | }); 124 | 125 | it('should throw error when added is missing', () => { 126 | const eventInit = { 127 | session: mockSession, 128 | removed: [], 129 | } as any; 130 | 131 | expect(() => { 132 | new XRInputSourcesChangeEvent('inputsourceschange', eventInit); 133 | }).toThrow('XRInputSourcesChangeEventInit.added is required'); 134 | }); 135 | 136 | it('should throw error when removed is missing', () => { 137 | const eventInit = { 138 | session: mockSession, 139 | added: [], 140 | } as any; 141 | 142 | expect(() => { 143 | new XRInputSourcesChangeEvent('inputsourceschange', eventInit); 144 | }).toThrow('XRInputSourcesChangeEventInit.removed is required'); 145 | }); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /tests/events/XRInputSourceEvent.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { XRInputSourceEvent } from '../../src/events/XRInputSourceEvent.js'; 9 | import { XRInputSource } from '../../src/input/XRInputSource.js'; 10 | import { XRFrame } from '../../src/frameloop/XRFrame.js'; 11 | 12 | // Mock dependencies 13 | jest.mock('../../src/input/XRInputSource.js'); 14 | jest.mock('../../src/frameloop/XRFrame.js'); 15 | 16 | describe('XRInputSourceEvent', () => { 17 | let mockInputSource: jest.Mocked; 18 | let mockFrame: jest.Mocked; 19 | 20 | beforeEach(() => { 21 | mockInputSource = {} as any; 22 | mockFrame = {} as any; 23 | }); 24 | 25 | describe('constructor', () => { 26 | it('should create event with type and event init', () => { 27 | const eventInit = { 28 | frame: mockFrame, 29 | inputSource: mockInputSource, 30 | }; 31 | 32 | const event = new XRInputSourceEvent('select', eventInit); 33 | 34 | expect(event.type).toBe('select'); 35 | expect(event.frame).toBe(mockFrame); 36 | expect(event.inputSource).toBe(mockInputSource); 37 | }); 38 | 39 | it('should inherit from Event', () => { 40 | const eventInit = { 41 | frame: mockFrame, 42 | inputSource: mockInputSource, 43 | }; 44 | 45 | const event = new XRInputSourceEvent('select', eventInit); 46 | 47 | expect(event).toBeInstanceOf(Event); 48 | }); 49 | 50 | it('should handle event options', () => { 51 | const eventInit = { 52 | frame: mockFrame, 53 | inputSource: mockInputSource, 54 | bubbles: true, 55 | cancelable: true, 56 | }; 57 | 58 | const event = new XRInputSourceEvent('select', eventInit); 59 | 60 | expect(event.bubbles).toBe(true); 61 | expect(event.cancelable).toBe(true); 62 | }); 63 | 64 | it('should throw error when frame is missing', () => { 65 | const eventInit = { 66 | inputSource: mockInputSource, 67 | } as any; 68 | 69 | expect(() => { 70 | new XRInputSourceEvent('select', eventInit); 71 | }).toThrow('XRInputSourceEventInit.frame is required'); 72 | }); 73 | 74 | it('should throw error when inputSource is missing', () => { 75 | const eventInit = { 76 | frame: mockFrame, 77 | } as any; 78 | 79 | expect(() => { 80 | new XRInputSourceEvent('select', eventInit); 81 | }).toThrow('XRInputSourceEventInit.inputSource is required'); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /tests/events/XRReferenceSpaceEvent.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { XRReferenceSpaceEvent } from '../../src/events/XRReferenceSpaceEvent.js'; 9 | import { XRReferenceSpace } from '../../src/spaces/XRReferenceSpace.js'; 10 | import { XRRigidTransform } from '../../src/primitives/XRRigidTransform.js'; 11 | 12 | // Mock dependencies 13 | jest.mock('../../src/spaces/XRReferenceSpace.js'); 14 | jest.mock('../../src/primitives/XRRigidTransform.js'); 15 | 16 | describe('XRReferenceSpaceEvent', () => { 17 | let mockReferenceSpace: jest.Mocked; 18 | let mockTransform: jest.Mocked; 19 | 20 | beforeEach(() => { 21 | mockReferenceSpace = {} as any; 22 | mockTransform = {} as any; 23 | }); 24 | 25 | describe('constructor', () => { 26 | it('should create event with type and event init', () => { 27 | const eventInit = { 28 | referenceSpace: mockReferenceSpace, 29 | transform: mockTransform, 30 | }; 31 | 32 | const event = new XRReferenceSpaceEvent('reset', eventInit); 33 | 34 | expect(event.type).toBe('reset'); 35 | expect(event.referenceSpace).toBe(mockReferenceSpace); 36 | expect(event.transform).toBe(mockTransform); 37 | }); 38 | 39 | it('should inherit from Event', () => { 40 | const eventInit = { 41 | referenceSpace: mockReferenceSpace, 42 | transform: mockTransform, 43 | }; 44 | 45 | const event = new XRReferenceSpaceEvent('reset', eventInit); 46 | 47 | expect(event).toBeInstanceOf(Event); 48 | }); 49 | 50 | it('should handle event options', () => { 51 | const eventInit = { 52 | referenceSpace: mockReferenceSpace, 53 | transform: mockTransform, 54 | bubbles: true, 55 | cancelable: true, 56 | }; 57 | 58 | const event = new XRReferenceSpaceEvent('reset', eventInit); 59 | 60 | expect(event.bubbles).toBe(true); 61 | expect(event.cancelable).toBe(true); 62 | }); 63 | 64 | it('should throw error when referenceSpace is missing', () => { 65 | const eventInit = { 66 | transform: mockTransform, 67 | } as any; 68 | 69 | expect(() => { 70 | new XRReferenceSpaceEvent('reset', eventInit); 71 | }).toThrow('XRReferenceSpaceEventInit.referenceSpace is required'); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /tests/events/XRSessionEvent.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { XRSessionEvent } from '../../src/events/XRSessionEvent.js'; 9 | import { XRSession } from '../../src/session/XRSession.js'; 10 | 11 | // Mock dependencies 12 | jest.mock('../../src/session/XRSession.js'); 13 | 14 | describe('XRSessionEvent', () => { 15 | let mockSession: jest.Mocked; 16 | 17 | beforeEach(() => { 18 | mockSession = {} as any; 19 | }); 20 | 21 | describe('constructor', () => { 22 | it('should create event with type and event init', () => { 23 | const eventInit = { 24 | session: mockSession, 25 | }; 26 | 27 | const event = new XRSessionEvent('end', eventInit); 28 | 29 | expect(event.type).toBe('end'); 30 | expect(event.session).toBe(mockSession); 31 | }); 32 | 33 | it('should inherit from Event', () => { 34 | const eventInit = { 35 | session: mockSession, 36 | }; 37 | 38 | const event = new XRSessionEvent('end', eventInit); 39 | 40 | expect(event).toBeInstanceOf(Event); 41 | }); 42 | 43 | it('should handle event options', () => { 44 | const eventInit = { 45 | session: mockSession, 46 | bubbles: true, 47 | cancelable: true, 48 | }; 49 | 50 | const event = new XRSessionEvent('end', eventInit); 51 | 52 | expect(event.bubbles).toBe(true); 53 | expect(event.cancelable).toBe(true); 54 | }); 55 | 56 | it('should throw error when session is missing', () => { 57 | const eventInit = {} as any; 58 | 59 | expect(() => { 60 | new XRSessionEvent('end', eventInit); 61 | }).toThrow('XRSessionEventInit.session is required'); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /tests/hittest/XRHitTest.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { XRHitTestSource, XRHitTestResult } from '../../src/hittest/XRHitTest.js'; 9 | import { XRRay } from '../../src/hittest/XRRay.js'; 10 | import { XRSession } from '../../src/session/XRSession.js'; 11 | import { XRFrame } from '../../src/frameloop/XRFrame.js'; 12 | import { XRSpace } from '../../src/spaces/XRSpace.js'; 13 | import { P_SESSION } from '../../src/private.js'; 14 | 15 | // Mock dependencies 16 | jest.mock('../../src/session/XRSession.js'); 17 | jest.mock('../../src/frameloop/XRFrame.js'); 18 | jest.mock('../../src/spaces/XRSpace.js'); 19 | 20 | describe('XRHitTestSource', () => { 21 | let mockSession: jest.Mocked; 22 | let mockSpace: jest.Mocked; 23 | 24 | beforeEach(() => { 25 | // Create a proper session mock with the private symbol 26 | mockSession = { 27 | [P_SESSION]: { 28 | hitTestSources: new Set() 29 | } 30 | } as any; 31 | mockSpace = {} as any; 32 | }); 33 | 34 | describe('constructor', () => { 35 | it('should create a hit test source with required options', () => { 36 | const options = { 37 | space: mockSpace, 38 | offsetRay: new XRRay() 39 | }; 40 | 41 | const hitTestSource = new XRHitTestSource(mockSession, options); 42 | 43 | expect(hitTestSource).toBeDefined(); 44 | }); 45 | 46 | it('should use default ray when offsetRay not provided', () => { 47 | const options = { 48 | space: mockSpace, 49 | offsetRay: undefined as any 50 | }; 51 | 52 | const hitTestSource = new XRHitTestSource(mockSession, options); 53 | 54 | expect(hitTestSource).toBeDefined(); 55 | }); 56 | }); 57 | 58 | describe('cancel method', () => { 59 | it('should remove itself from session hit test sources', () => { 60 | const options = { 61 | space: mockSpace, 62 | offsetRay: new XRRay() 63 | }; 64 | const hitTestSource = new XRHitTestSource(mockSession, options); 65 | 66 | // Add to session first 67 | (mockSession as any)[P_SESSION].hitTestSources.add(hitTestSource); 68 | expect((mockSession as any)[P_SESSION].hitTestSources.has(hitTestSource)).toBe(true); 69 | 70 | hitTestSource.cancel(); 71 | 72 | expect((mockSession as any)[P_SESSION].hitTestSources.has(hitTestSource)).toBe(false); 73 | }); 74 | }); 75 | }); 76 | 77 | describe('XRHitTestResult', () => { 78 | let mockFrame: jest.Mocked; 79 | let mockOffsetSpace: jest.Mocked; 80 | let mockBaseSpace: jest.Mocked; 81 | 82 | beforeEach(() => { 83 | mockFrame = { 84 | getPose: jest.fn(), 85 | createAnchor: jest.fn() 86 | } as any; 87 | mockOffsetSpace = {} as any; 88 | mockBaseSpace = {} as any; 89 | }); 90 | 91 | describe('constructor', () => { 92 | it('should create a hit test result', () => { 93 | const result = new XRHitTestResult(mockFrame, mockOffsetSpace); 94 | 95 | expect(result).toBeDefined(); 96 | }); 97 | }); 98 | 99 | describe('getPose method', () => { 100 | it('should call frame.getPose with correct spaces', () => { 101 | const result = new XRHitTestResult(mockFrame, mockOffsetSpace); 102 | const mockPose = {} as any; 103 | mockFrame.getPose.mockReturnValue(mockPose); 104 | 105 | const pose = result.getPose(mockBaseSpace); 106 | 107 | expect(mockFrame.getPose).toHaveBeenCalledWith(mockOffsetSpace, mockBaseSpace); 108 | expect(pose).toBe(mockPose); 109 | }); 110 | 111 | it('should return undefined when frame.getPose returns undefined', () => { 112 | const result = new XRHitTestResult(mockFrame, mockOffsetSpace); 113 | mockFrame.getPose.mockReturnValue(undefined as any); 114 | 115 | const pose = result.getPose(mockBaseSpace); 116 | 117 | expect(pose).toBeUndefined(); 118 | }); 119 | }); 120 | 121 | describe('createAnchor method', () => { 122 | it('should call frame.createAnchor with rigid transform and offset space', () => { 123 | const result = new XRHitTestResult(mockFrame, mockOffsetSpace); 124 | const mockAnchor = {} as any; 125 | mockFrame.createAnchor.mockReturnValue(mockAnchor); 126 | 127 | const anchor = result.createAnchor(); 128 | 129 | expect(mockFrame.createAnchor).toHaveBeenCalledWith( 130 | expect.any(Object), // XRRigidTransform 131 | mockOffsetSpace 132 | ); 133 | expect(anchor).toBe(mockAnchor); 134 | }); 135 | }); 136 | }); -------------------------------------------------------------------------------- /tests/input/XRInputSource.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { 9 | XRHandedness, 10 | XRInputSource, 11 | XRTargetRayMode, 12 | } from '../../src/input/XRInputSource'; 13 | 14 | import { Gamepad } from '../../src/gamepad/Gamepad'; 15 | import { XRSpace } from '../../src/spaces/XRSpace'; 16 | 17 | describe('XRInputSource', () => { 18 | let targetRaySpace: XRSpace; 19 | let gripSpace: XRSpace; 20 | let gamepad: Gamepad; 21 | 22 | beforeEach(() => { 23 | targetRaySpace = new XRSpace(); 24 | gripSpace = new XRSpace(); 25 | gamepad = {} as Gamepad; 26 | }); 27 | 28 | test('should properly initialize with given parameters', () => { 29 | const handedness = XRHandedness.Left; 30 | const targetRayMode = XRTargetRayMode.TrackedPointer; 31 | const profiles = ['profile1', 'profile2']; 32 | 33 | const inputSource = new XRInputSource( 34 | handedness, 35 | targetRayMode, 36 | profiles, 37 | targetRaySpace, 38 | gamepad, 39 | gripSpace, 40 | ); 41 | 42 | expect(inputSource.handedness).toBe(handedness); 43 | expect(inputSource.targetRayMode).toBe(targetRayMode); 44 | expect(inputSource.targetRaySpace).toBe(targetRaySpace); 45 | expect(inputSource.gripSpace).toBe(gripSpace); 46 | expect(inputSource.profiles).toEqual(profiles); 47 | expect(inputSource.gamepad).toBe(gamepad); 48 | }); 49 | 50 | // Test for different handedness 51 | test.each([[XRHandedness.None], [XRHandedness.Left], [XRHandedness.Right]])( 52 | 'should handle handedness: %s', 53 | (handedness) => { 54 | const inputSource = new XRInputSource( 55 | handedness, 56 | XRTargetRayMode.TrackedPointer, 57 | [], 58 | targetRaySpace, 59 | gamepad, 60 | gripSpace, 61 | ); 62 | expect(inputSource.handedness).toBe(handedness); 63 | }, 64 | ); 65 | 66 | // Test for different target ray modes 67 | test.each([ 68 | [XRTargetRayMode.Gaze], 69 | [XRTargetRayMode.TrackedPointer], 70 | [XRTargetRayMode.Screen], 71 | [XRTargetRayMode.TransientPointer], 72 | ])('should handle targetRayMode: %s', (targetRayMode) => { 73 | const inputSource = new XRInputSource( 74 | XRHandedness.None, 75 | targetRayMode, 76 | [], 77 | targetRaySpace, 78 | gamepad, 79 | gripSpace, 80 | ); 81 | expect(inputSource.targetRayMode).toBe(targetRayMode); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /tests/jsconfig.json: -------------------------------------------------------------------------------- 1 | { "typeAcquisition": { "include": [ "jest" ] } } -------------------------------------------------------------------------------- /tests/polyfill.ts: -------------------------------------------------------------------------------- 1 | class PolyfillDOMPointReadOnly { 2 | readonly x: number; 3 | readonly y: number; 4 | readonly z: number; 5 | readonly w: number; 6 | constructor(x = 0, y = 0, z = 0, w = 1) { 7 | this.x = x; 8 | this.y = y; 9 | this.z = z; 10 | this.w = w; 11 | Object.freeze(this); 12 | } 13 | static fromPoint(other: DOMPointInit): DOMPointReadOnly { 14 | return new PolyfillDOMPointReadOnly(other.x, other.y, other.z, other.w); 15 | } 16 | 17 | matrixTransform(matrix: DOMMatrixReadOnly): DOMPointReadOnly { 18 | const x = 19 | matrix.m11 * this.x + 20 | matrix.m21 * this.y + 21 | matrix.m31 * this.z + 22 | matrix.m41 * this.w; 23 | const y = 24 | matrix.m12 * this.x + 25 | matrix.m22 * this.y + 26 | matrix.m32 * this.z + 27 | matrix.m42 * this.w; 28 | const z = 29 | matrix.m13 * this.x + 30 | matrix.m23 * this.y + 31 | matrix.m33 * this.z + 32 | matrix.m43 * this.w; 33 | const w = 34 | matrix.m14 * this.x + 35 | matrix.m24 * this.y + 36 | matrix.m34 * this.z + 37 | matrix.m44 * this.w; 38 | return new PolyfillDOMPointReadOnly(x, y, z, w); 39 | } 40 | 41 | toJSON(): any { 42 | return { x: this.x, y: this.y, z: this.z, w: this.w }; 43 | } 44 | } 45 | 46 | if (typeof globalThis.DOMPointReadOnly === 'undefined') { 47 | // @ts-ignore 48 | globalThis.DOMPointReadOnly = PolyfillDOMPointReadOnly as any; 49 | } 50 | -------------------------------------------------------------------------------- /tests/primitives/XRRigidTransform.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { quat, vec3 } from 'gl-matrix'; 9 | 10 | import { XRRigidTransform } from '../../src/primitives/XRRigidTransform'; 11 | 12 | describe('XRRigidTransform', () => { 13 | it('should create an identity transform with no arguments', () => { 14 | const transform = new XRRigidTransform(); 15 | expect(transform.position.x).toBe(0); 16 | expect(transform.position.y).toBe(0); 17 | expect(transform.position.z).toBe(0); 18 | expect(transform.orientation.x).toBe(0); 19 | expect(transform.orientation.y).toBe(0); 20 | expect(transform.orientation.z).toBe(0); 21 | expect(transform.orientation.w).toBe(1); 22 | expect(transform.matrix).toEqual(expect.any(Float32Array)); 23 | expect(transform.matrix.length).toBe(16); 24 | }); 25 | 26 | it('should create a transform with given position and default orientation', () => { 27 | const position = { x: 1, y: 2, z: 3, w: 1 }; 28 | const transform = new XRRigidTransform(position); 29 | expect(transform.position.x).toBe(1); 30 | expect(transform.position.y).toBe(2); 31 | expect(transform.position.z).toBe(3); 32 | expect(transform.orientation.x).toBe(0); 33 | expect(transform.orientation.y).toBe(0); 34 | expect(transform.orientation.z).toBe(0); 35 | expect(transform.orientation.w).toBe(1); 36 | }); 37 | 38 | it('should create a transform with given position and orientation', () => { 39 | const position = { x: 1, y: 2, z: 3, w: 1 }; 40 | const orientation = { x: 0, y: 0, z: 1, w: 0 }; 41 | const transform = new XRRigidTransform(position, orientation); 42 | expect(transform.position.x).toBe(1); 43 | expect(transform.position.y).toBe(2); 44 | expect(transform.position.z).toBe(3); 45 | expect(transform.orientation.x).toBeCloseTo(0); 46 | expect(transform.orientation.y).toBeCloseTo(0); 47 | expect(transform.orientation.z).toBeCloseTo(1); 48 | expect(transform.orientation.w).toBeCloseTo(0); 49 | }); 50 | 51 | it('should correctly calculate the inverse transform', () => { 52 | const position = { x: 1, y: 2, z: 3, w: 1 }; 53 | const orientation = { 54 | x: 0.11109410971403122, 55 | y: 0.21208874881267548, 56 | z: 0.3130834102630615, 57 | w: 0.9190512895584106, 58 | }; // (normalized) 59 | const transform = new XRRigidTransform(position, orientation); 60 | const inverse = transform.inverse; 61 | 62 | // Test applying the transform and then the inverse transform to a point 63 | const point = vec3.fromValues(5, 5, 5); // Arbitrary point 64 | const transformedPoint = vec3.transformMat4( 65 | vec3.create(), 66 | point, 67 | transform.matrix, 68 | ); 69 | const retransformedPoint = vec3.transformMat4( 70 | vec3.create(), 71 | transformedPoint, 72 | inverse.matrix, 73 | ); 74 | 75 | // The retransformedPoint should be close to the original point 76 | expect(retransformedPoint[0]).toBeCloseTo(point[0]); 77 | expect(retransformedPoint[1]).toBeCloseTo(point[1]); 78 | expect(retransformedPoint[2]).toBeCloseTo(point[2]); 79 | 80 | const originalQuat = quat.fromValues( 81 | transform.orientation.x, 82 | transform.orientation.y, 83 | transform.orientation.z, 84 | transform.orientation.w, 85 | ); 86 | 87 | const inverseQuat = quat.fromValues( 88 | inverse.orientation.x, 89 | inverse.orientation.y, 90 | inverse.orientation.z, 91 | inverse.orientation.w, 92 | ); 93 | 94 | // Normalizing the quaternions (if not already normalized) 95 | quat.normalize(originalQuat, originalQuat); 96 | quat.normalize(inverseQuat, inverseQuat); 97 | 98 | // Combining the original and inverse quaternion 99 | const combinedQuat = quat.create(); 100 | quat.multiply(combinedQuat, inverseQuat, originalQuat); 101 | 102 | // The combined quaternion should be close to a neutral quaternion (0, 0, 0, 1) 103 | expect(combinedQuat[0]).toBeCloseTo(0); 104 | expect(combinedQuat[1]).toBeCloseTo(0); 105 | expect(combinedQuat[2]).toBeCloseTo(0); 106 | expect(combinedQuat[3]).toBeCloseTo(1); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /tests/spaces/XRReferenceSpace.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { GlobalSpace, XRSpaceUtils } from '../../src/spaces/XRSpace'; 9 | import { P_REF_SPACE, P_SPACE } from '../../src/private'; 10 | import { 11 | XRReferenceSpace, 12 | XRReferenceSpaceType, 13 | } from '../../src/spaces/XRReferenceSpace'; 14 | import { mat4, vec3 } from 'gl-matrix'; 15 | 16 | describe('XRReferenceSpace', () => { 17 | let globalSpace: GlobalSpace; 18 | let xrReferenceSpace: XRReferenceSpace; 19 | 20 | beforeEach(() => { 21 | globalSpace = new GlobalSpace(); 22 | xrReferenceSpace = new XRReferenceSpace( 23 | XRReferenceSpaceType.Local, 24 | globalSpace, 25 | ); 26 | }); 27 | 28 | test('XRReferenceSpace should initialize correctly', () => { 29 | expect(xrReferenceSpace).toBeDefined(); 30 | expect(xrReferenceSpace[P_REF_SPACE].type).toBe(XRReferenceSpaceType.Local); 31 | expect(xrReferenceSpace[P_SPACE].parentSpace).toBe(globalSpace); 32 | }); 33 | 34 | test('getOffsetReferenceSpace should create a new XRReferenceSpace with correct offset', () => { 35 | const offsetMatrix = mat4.create(); 36 | mat4.translate(offsetMatrix, offsetMatrix, vec3.fromValues(1, 2, 3)); // Some arbitrary offset 37 | 38 | const offsetSpace = xrReferenceSpace.getOffsetReferenceSpace(offsetMatrix); 39 | 40 | expect(offsetSpace).toBeInstanceOf(XRReferenceSpace); 41 | expect(offsetSpace[P_SPACE].parentSpace).toBe(xrReferenceSpace); 42 | expect( 43 | mat4.equals(offsetSpace[P_SPACE].offsetMatrix, offsetMatrix), 44 | ).toBeTruthy(); 45 | }); 46 | 47 | test('calculateGlobalOffsetMatrix should return correct global offset for nested XRReferenceSpaces created with getOffsetReferenceSpace', () => { 48 | // Create a parent XRReferenceSpace 49 | const parentSpace = new XRReferenceSpace( 50 | XRReferenceSpaceType.Local, 51 | globalSpace, 52 | ); 53 | XRSpaceUtils.updateOffsetPosition(parentSpace, vec3.fromValues(0, 1, 0)); // 1 unit up 54 | 55 | // Create an offset XRReferenceSpace from parentSpace 56 | const offsetMatrix = mat4.create(); 57 | mat4.fromTranslation(offsetMatrix, vec3.fromValues(1, 0, 0)); // 1 unit right 58 | const offsetSpace = parentSpace.getOffsetReferenceSpace(offsetMatrix); 59 | 60 | // Calculate the global offset matrix for offsetSpace 61 | const globalMatrix = XRSpaceUtils.calculateGlobalOffsetMatrix(offsetSpace); 62 | 63 | // Expected global matrix combines parent's and offsetSpace's transformations 64 | const expectedMatrix = mat4.create(); 65 | mat4.translate(expectedMatrix, expectedMatrix, vec3.fromValues(1, 1, 0)); // Combined translation 66 | 67 | expect(mat4.equals(globalMatrix, expectedMatrix)).toBeTruthy(); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /tests/spaces/XRSpace.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { GlobalSpace, XRSpace, XRSpaceUtils } from '../../src/spaces/XRSpace'; 9 | import { mat4, quat, vec3 } from 'gl-matrix'; 10 | 11 | import { P_SPACE } from '../../src/private'; 12 | 13 | describe('XRSpace and XRSpaceUtils', () => { 14 | let globalSpace: GlobalSpace; 15 | let xrSpace: XRSpace; 16 | 17 | beforeEach(() => { 18 | globalSpace = new GlobalSpace(); 19 | xrSpace = new XRSpace(); 20 | }); 21 | 22 | test('XRSpace should initialize with default values', () => { 23 | expect( 24 | mat4.equals(xrSpace[P_SPACE].offsetMatrix, mat4.create()), 25 | ).toBeTruthy(); 26 | expect(xrSpace[P_SPACE].parentSpace).toBeUndefined(); 27 | }); 28 | 29 | test('GlobalSpace should initialize as an XRSpace with no parent', () => { 30 | expect(globalSpace[P_SPACE].parentSpace).toBeUndefined(); 31 | expect( 32 | mat4.equals(globalSpace[P_SPACE].offsetMatrix, mat4.create()), 33 | ).toBeTruthy(); 34 | }); 35 | 36 | test('updateOffsetPosition should update XRSpace offset matrix', () => { 37 | const position = vec3.fromValues(1, 2, 3); 38 | XRSpaceUtils.updateOffsetPosition(xrSpace, position); 39 | 40 | const expectedMatrix = mat4.create(); 41 | mat4.fromTranslation(expectedMatrix, position); 42 | expect( 43 | mat4.equals(xrSpace[P_SPACE].offsetMatrix, expectedMatrix), 44 | ).toBeTruthy(); 45 | }); 46 | 47 | test('updateOffsetQuaternion should update XRSpace offset matrix with rotation', () => { 48 | const quaternion = quat.create(); 49 | quat.rotateX(quaternion, quaternion, Math.PI / 2); // Rotate 90 degrees around X-axis 50 | XRSpaceUtils.updateOffsetQuaternion(xrSpace, quaternion); 51 | 52 | const expectedMatrix = mat4.create(); 53 | mat4.fromQuat(expectedMatrix, quaternion); 54 | expect( 55 | mat4.equals(xrSpace[P_SPACE].offsetMatrix, expectedMatrix), 56 | ).toBeTruthy(); 57 | }); 58 | 59 | test('calculateGlobalOffsetMatrix should return global offset for XRSpace', () => { 60 | const position = vec3.fromValues(1, 2, 3); 61 | XRSpaceUtils.updateOffsetPosition(xrSpace, position); 62 | 63 | const globalMatrix = XRSpaceUtils.calculateGlobalOffsetMatrix(xrSpace); 64 | 65 | const expectedMatrix = mat4.create(); 66 | mat4.fromTranslation(expectedMatrix, position); 67 | expect(mat4.equals(globalMatrix, expectedMatrix)).toBeTruthy(); 68 | }); 69 | 70 | test('calculateGlobalOffsetMatrix with nested XRSpaces', () => { 71 | // Create child and grandchild spaces with globalSpace as the root 72 | const childSpace = new XRSpace(globalSpace); 73 | const grandchildSpace = new XRSpace(childSpace); 74 | 75 | // Set position and orientation for each space 76 | const parentPosition = vec3.fromValues(1, 0, 0); 77 | const parentQuaternion = quat.create(); 78 | quat.rotateY(parentQuaternion, parentQuaternion, Math.PI / 4); // Rotate 45 degrees around Y-axis 79 | 80 | const childPosition = vec3.fromValues(0, 2, 0); 81 | const childQuaternion = quat.create(); 82 | quat.rotateX(childQuaternion, childQuaternion, Math.PI / 2); // Rotate 90 degrees around X-axis 83 | 84 | XRSpaceUtils.updateOffsetPosition(globalSpace, parentPosition); 85 | XRSpaceUtils.updateOffsetQuaternion(globalSpace, parentQuaternion); 86 | XRSpaceUtils.updateOffsetPosition(childSpace, childPosition); 87 | XRSpaceUtils.updateOffsetQuaternion(childSpace, childQuaternion); 88 | 89 | // Calculate the global offset matrix for the grandchild space 90 | const globalMatrix = 91 | XRSpaceUtils.calculateGlobalOffsetMatrix(grandchildSpace); 92 | 93 | // Expected global offset matrix 94 | const expectedMatrix = mat4.create(); 95 | const parentMatrix = mat4.create(); 96 | mat4.fromRotationTranslation( 97 | parentMatrix, 98 | parentQuaternion, 99 | parentPosition, 100 | ); 101 | const childMatrix = mat4.create(); 102 | mat4.fromRotationTranslation(childMatrix, childQuaternion, childPosition); 103 | mat4.multiply(expectedMatrix, parentMatrix, childMatrix); 104 | 105 | expect(mat4.equals(globalMatrix, expectedMatrix)).toBeTruthy(); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES2018", /* Good balance for modern browsers */ 5 | "module": "esnext", /* Suitable for bundlers like Webpack or Rollup */ 6 | "lib": ["dom", "es2018"], /* Standard library files for web development */ 7 | "declaration": true, /* Generates corresponding '.d.ts' file */ 8 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file */ 9 | "sourceMap": true, /* Generates corresponding '.map' file */ 10 | "outDir": "lib", /* Redirect output structure to the directory */ 11 | 12 | /* Strict Type-Checking Options */ 13 | "strict": true, /* Enable all strict type-checking options */ 14 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type */ 15 | "strictNullChecks": true, /* Enable strict null checks */ 16 | "strictFunctionTypes": true, /* Enable strict checking of function types */ 17 | "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions */ 18 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes */ 19 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type */ 20 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file */ 21 | 22 | /* Additional Checks */ 23 | "noUnusedLocals": true, /* Report errors on unused locals */ 24 | "noUnusedParameters": true, /* Report errors on unused parameters */ 25 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value */ 26 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement */ 27 | 28 | /* Module Resolution Options */ 29 | "moduleResolution": "node", /* Specify module resolution strategy */ 30 | "resolveJsonModule": true, /* allows for importing JSON modules */ 31 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules */ 32 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators */ 33 | "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators */ 34 | 35 | /* Advanced Options */ 36 | "skipLibCheck": true, /* Skip type checking of declaration files */ 37 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file */ 38 | }, 39 | "include": ["src/**/*"] 40 | } 41 | --------------------------------------------------------------------------------