Repository: pmndrs/react-three-fiber Branch: master Commit: 9525ea0d63c8 Files: 173 Total size: 792.4 KB Directory structure: gitextract_tr77z8gq/ ├── .changeset/ │ ├── README.md │ └── config.json ├── .codesandbox/ │ └── ci.json ├── .eslintignore ├── .eslintrc.json ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── issue_template.md │ └── workflows/ │ ├── canary.yml │ ├── docs.yml │ └── test.yml ├── .gitignore ├── .husky/ │ ├── .gitignore │ └── pre-commit ├── .prettierignore ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE ├── babel.config.js ├── docs/ │ ├── API/ │ │ ├── additional-exports.mdx │ │ ├── canvas.mdx │ │ ├── events.mdx │ │ ├── hooks.mdx │ │ ├── objects.mdx │ │ ├── testing.mdx │ │ └── typescript.mdx │ ├── advanced/ │ │ ├── pitfalls.mdx │ │ └── scaling-performance.mdx │ ├── getting-started/ │ │ ├── basic-example-sandpack/ │ │ │ ├── index.jsx │ │ │ └── styles.css │ │ ├── community-r3f-components.mdx │ │ ├── examples.mdx │ │ ├── installation.mdx │ │ ├── introduction.mdx │ │ └── your-first-scene.mdx │ └── tutorials/ │ ├── basic-animations.mdx │ ├── events-and-interaction.mdx │ ├── how-it-works.mdx │ ├── loading-models.mdx │ ├── loading-textures.mdx │ └── v9-migration-guide.mdx ├── example/ │ ├── .gitignore │ ├── CHANGELOG.md │ ├── index.html │ ├── package.json │ ├── public/ │ │ ├── Parrot.glb │ │ ├── Stork.glb │ │ ├── apple.gltf │ │ ├── bottle.gltf │ │ ├── farm.gltf │ │ ├── lightning.gltf │ │ └── ramen.gltf │ ├── src/ │ │ ├── App.tsx │ │ ├── components.tsx │ │ ├── demos/ │ │ │ ├── Activity.tsx │ │ │ ├── AutoDispose.tsx │ │ │ ├── ChangeTexture.tsx │ │ │ ├── ClickAndHover.tsx │ │ │ ├── ContextMenuOverride.tsx │ │ │ ├── FlushSync.tsx │ │ │ ├── Gestures.tsx │ │ │ ├── Gltf.tsx │ │ │ ├── Inject.tsx │ │ │ ├── Layers.tsx │ │ │ ├── Lines.tsx │ │ │ ├── MultiMaterial.tsx │ │ │ ├── MultiRender.tsx │ │ │ ├── MultiView.tsx │ │ │ ├── Pointcloud.tsx │ │ │ ├── Portals.tsx │ │ │ ├── Reparenting.tsx │ │ │ ├── ResetProps.tsx │ │ │ ├── SVGRenderer.tsx │ │ │ ├── Selection.tsx │ │ │ ├── StopPropagation.tsx │ │ │ ├── SuspenseAndErrors.tsx │ │ │ ├── SuspenseMaterial.tsx │ │ │ ├── Test.tsx │ │ │ ├── ViewTracking.tsx │ │ │ ├── Viewcube.tsx │ │ │ ├── WebGPU.tsx │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── styles.css │ ├── tsconfig.json │ └── vite.config.ts ├── jest.config.js ├── package.json ├── packages/ │ ├── eslint-plugin/ │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── docs/ │ │ │ └── rules/ │ │ │ ├── no-clone-in-loop.md │ │ │ └── no-new-in-loop.md │ │ ├── package.json │ │ ├── scripts/ │ │ │ └── codegen.ts │ │ ├── src/ │ │ │ ├── configs/ │ │ │ │ ├── all.ts │ │ │ │ └── recommended.ts │ │ │ ├── index.ts │ │ │ ├── lib/ │ │ │ │ └── url.ts │ │ │ └── rules/ │ │ │ ├── index.ts │ │ │ ├── no-clone-in-loop.ts │ │ │ └── no-new-in-loop.ts │ │ └── tests/ │ │ └── rules/ │ │ ├── no-clone-in-loop.test.ts │ │ └── no-new-in-loop.test.ts │ ├── fiber/ │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── __mocks__/ │ │ │ ├── expo-asset.ts │ │ │ ├── expo-file-system.ts │ │ │ ├── expo-gl.ts │ │ │ ├── react-native.ts │ │ │ └── react-use-measure.ts │ │ ├── native/ │ │ │ └── package.json │ │ ├── package.json │ │ ├── readme.md │ │ ├── src/ │ │ │ ├── core/ │ │ │ │ ├── events.ts │ │ │ │ ├── hooks.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── loop.ts │ │ │ │ ├── reconciler.tsx │ │ │ │ ├── renderer.tsx │ │ │ │ ├── store.ts │ │ │ │ └── utils.tsx │ │ │ ├── index.tsx │ │ │ ├── native/ │ │ │ │ ├── Canvas.tsx │ │ │ │ ├── events.ts │ │ │ │ └── polyfills.ts │ │ │ ├── native.tsx │ │ │ ├── three-types.ts │ │ │ └── web/ │ │ │ ├── Canvas.tsx │ │ │ └── events.ts │ │ └── tests/ │ │ ├── __snapshots__/ │ │ │ ├── canvas.native.test.tsx.snap │ │ │ ├── canvas.test.tsx.snap │ │ │ ├── index.test.tsx.snap │ │ │ └── utils.test.ts.snap │ │ ├── canvas.native.test.tsx │ │ ├── canvas.test.tsx │ │ ├── events.test.tsx │ │ ├── hooks.test.tsx │ │ ├── index.test.tsx │ │ ├── polyfills.test.ts │ │ ├── reconciler.test.ts │ │ ├── renderer.test.tsx │ │ └── utils.test.ts │ ├── shared/ │ │ └── setupTests.ts │ └── test-renderer/ │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── markdown/ │ │ ├── rttr-instance.md │ │ └── rttr.md │ ├── package.json │ └── src/ │ ├── WebGL2RenderingContext.ts │ ├── __tests__/ │ │ ├── RTTR.core.test.tsx │ │ ├── RTTR.events.test.tsx │ │ ├── RTTR.hooks.test.tsx │ │ ├── RTTR.methods.test.tsx │ │ └── __snapshots__/ │ │ └── RTTR.core.test.tsx.snap │ ├── createTestCanvas.ts │ ├── createTestInstance.ts │ ├── fireEvent.ts │ ├── helpers/ │ │ ├── events.ts │ │ ├── graph.ts │ │ ├── strings.ts │ │ ├── testInstance.ts │ │ ├── tree.ts │ │ └── waitFor.ts │ ├── index.tsx │ └── types/ │ ├── index.ts │ ├── internal.ts │ └── public.ts ├── readme.md ├── tsconfig.json └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changeset/README.md ================================================ # Changesets Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json", "changelog": "@changesets/changelog-git", "commit": true, "linked": [], "access": "public", "baseBranch": "master", "updateInternalDependencies": "minor", "ignore": [], "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { "onlyUpdatePeerDependentsWhenOutOfRange": true } } ================================================ FILE: .codesandbox/ci.json ================================================ { "sandboxes": ["/example"], "node": "18" } ================================================ FILE: .eslintignore ================================================ dist/ node_modules/ .yarn/ ================================================ FILE: .eslintrc.json ================================================ { "env": { "browser": true, "es6": true, "node": true }, "extends": [ "prettier", "plugin:prettier/recommended", "plugin:react-hooks/recommended", "plugin:import/recommended", "plugin:@react-three/recommended" ], "plugins": ["@typescript-eslint", "react", "react-hooks", "import", "jest", "prettier", "@react-three"], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaFeatures": { "jsx": true }, "ecmaVersion": 2018, "sourceType": "module", "rules": { "curly": ["warn", "multi-line", "consistent"], "no-console": "off", "no-empty-pattern": "warn", "no-duplicate-imports": "error", "import/no-unresolved": "off", "import/export": "error", // https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/FAQ.md#eslint-plugin-import // We recommend you do not use the following import/* rules, as TypeScript provides the same checks as part of standard type checking: "import/named": "off", "import/namespace": "off", "import/default": "off", "no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-explicit-any": "off", "jest/consistent-test-it": ["error", { "fn": "it", "withinDescribe": "it" }] } }, "settings": { "react": { "version": "detect" }, "import/extensions": [".js", ".jsx", ".ts", ".tsx"], "import/parsers": { "@typescript-eslint/parser": [".js", ".jsx", ".ts", ".tsx"] }, "import/resolver": { "node": { "extensions": [".js", ".jsx", ".ts", ".tsx", ".json"], "paths": ["src"] }, "alias": { "extensions": [".js", ".jsx", ".ts", ".tsx", ".json"], "map": [["@react-three/fiber", "./packages/fiber/src/web"]] } } }, "overrides": [ { "files": ["src"], "parserOptions": { "project": "./tsconfig.json" } } ], "rules": { "import/no-unresolved": "off", "import/named": "off", "import/namespace": "off", "import/no-named-as-default-member": "off" } } ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: drcmda open_collective: react-three-fiber ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- 👋 hi there, for issues that aren't that pressing, that could be related to threejs etc, please consider [github discussions](https://github.com/pmndrs/react-three-fiber/discussions). ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- ================================================ FILE: .github/issue_template.md ================================================ Hi, 👋 if this is about a bug, before you go ahead, please do us a favour and make sure to check the [threejs issue tracker](https://github.com/mrdoob/three.js/issues) for the problem you are experiencing. This library is just a soft wrap around threejs without direct dependencies. So if something is flipped upside down, or doesn't project the way you intent to, there's a good chance others will have experienced the same issue with plain threejs. ================================================ FILE: .github/workflows/canary.yml ================================================ name: Canary Release on: push: branches: [v10] workflow_dispatch: permissions: id-token: write # Required for npm OIDC contents: read jobs: canary: name: Publish canary runs-on: ubuntu-latest steps: - name: Checkout repo uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v4 with: run_install: false - name: Use Node 22 uses: actions/setup-node@v4 with: node-version: 22 cache: 'pnpm' registry-url: 'https://registry.npmjs.org' - name: Update npm for OIDC run: npm install -g npm@latest - name: Install deps run: pnpm install - name: Build run: pnpm build - name: Set canary versions run: | CANARY_VERSION="10.0.0-canary.$(git rev-parse --short HEAD)" echo "Publishing canary version: $CANARY_VERSION" cd packages/fiber && npm version $CANARY_VERSION --no-git-tag-version cd ../eslint-plugin && npm version $CANARY_VERSION --no-git-tag-version cd ../test-renderer && npm version $CANARY_VERSION --no-git-tag-version - name: Publish to npm run: | pnpm --filter @react-three/fiber publish --tag canary --no-git-checks --provenance pnpm --filter @react-three/eslint-plugin publish --tag canary --no-git-checks --provenance pnpm --filter @react-three/test-renderer publish --tag canary --no-git-checks --provenance ================================================ FILE: .github/workflows/docs.yml ================================================ name: Build documentation and deploy to GitHub Pages on: push: branches: ['master'] workflow_dispatch: # Cancel previous run (see: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency) concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: uses: pmndrs/docs/.github/workflows/build.yml@v3 with: mdx: 'docs' libname: 'React Three Fiber' libname_short: 'r3f' home_redirect: '/getting-started/introduction' icon: '🇨🇭' logo: '/logo.jpg' github: 'https://github.com/pmndrs/react-three-fiber' discord: 'https://discord.com/channels/740090768164651008/740093168770613279' deploy: needs: build runs-on: ubuntu-latest # Grant GITHUB_TOKEN the permissions required to make a Pages deployment permissions: pages: write # to deploy to Pages id-token: write # to verify the deployment originates from an appropriate source # Deploy to the github-pages environment environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: push: branches: - 'master' pull_request: {} jobs: build: name: Build, lint, and test (React ${{ matrix.react-version }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: react-version: - 19.0.0 - latest steps: - name: Checkout repo uses: actions/checkout@v4 - name: Use Node 20 uses: actions/setup-node@v4 with: node-version: 20 - name: Cache node_modules and Yarn cache uses: actions/cache@v4 with: path: | **/node_modules .yarn/cache key: > ${{ runner.os }}-node20-react-${{ matrix.react-version }}-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-node20-react-${{ matrix.react-version }}- - name: Install deps and build (with cache) uses: bahmutov/npm-install@v1 with: install-command: yarn --immutable --silent - name: Override React version (${{ matrix.react-version }}) run: | yarn add @types/react@${{ matrix.react-version }} react@${{ matrix.react-version }} @types/react-dom@${{ matrix.react-version }} react-dom@${{ matrix.react-version }} --dev -W - name: Check types run: yarn run typecheck - name: Check lint run: yarn run eslint - name: Build run: yarn run build - name: Rsbuild (strict) run: yarn add @rsbuild/core -D -W && yarn rsbuild build --root ./packages/fiber - name: Jest run run: yarn run dev && yarn run test - name: Report Fiber size run: yarn run analyze-fiber - name: Report Test Renderer size run: yarn run analyze-test - name: Check formatting run: yarn run format ================================================ FILE: .gitignore ================================================ node_modules/ coverage/ dist/ build/ types/ packages/fiber/react-reconciler/ # commit types in src !packages/*/src/types/ Thumbs.db ehthumbs.db Desktop.ini $RECYCLE.BIN/ .DS_Store .vscode .docz/ package-lock.json coverage/ .idea yarn-error.log .size-snapshot.json __tests__/__image_snapshots__/__diff_output__ ================================================ FILE: .husky/.gitignore ================================================ _ ================================================ FILE: .husky/pre-commit ================================================ #!/bin/sh . "$(dirname "$0")/_/husky.sh" npx pretty-quick --staged ================================================ FILE: .prettierignore ================================================ dist/ coverage/ node_modules/ packages/fiber/react-reconciler/ .yarn/ *.gltf *.mdx ================================================ FILE: .prettierrc ================================================ { "semi": false, "trailingComma": "all", "singleQuote": true, "tabWidth": 2, "printWidth": 120, "bracketSameLine": true, "endOfLine": "auto" } ================================================ FILE: CONTRIBUTING.md ================================================ ## Contributing This project uses [semantic commits](https://conventionalcommits.org) and [semver](https://semver.org). To get started, make sure you have [Node](https://nodejs.org) and [Yarn 1](https://classic.yarnpkg.com) (newer versions of Yarn do not work) installed. Install dependencies with: ```bash yarn ``` [Preconstruct](https://github.com/preconstruct/preconstruct) will automatically build and link packages for local development via symlinks. If you ever need to do this manually, try running: ```bash yarn dev ``` > **Note**: Some Windows users may need to [enable developer mode](https://howtogeek.com/292914/what-is-developer-mode-in-windows-10) if experiencing `EPERM: operation not permitted, symlink` with Preconstruct. If this persists, you might be running on an unsupported drive/format. In which case, consider using [Docker](https://docs.docker.com/docker-for-windows). ### Development Locally run examples against the library with: ```bash yarn examples ``` ### Testing Run test suites against the library with: ```bash yarn test # or, to test live against changes yarn test:watch ``` If your code invalidates a snapshot, you can update it with: ```bash yarn test -u ``` > **Note**: Use discretion when updating snapshots, as they represent the integrity of the package. > > If the difference is complex or you're unsure of the changes, leave it for review and we'll unblock it. ### Publishing We use [atlassian/changesets](https://github.com/atlassian/changesets) to publish our packages, which will automatically document and version changes. To publish a release on NPM, run the following and complete the dialog (see [FAQ](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)): ```bash # Describe the changes you've made as you would semantic commits for CHANGELOG.md yarn changeset:add # Tag which packages should receive an update and be published. yarn vers # Commit and publish changes to NPM. yarn release ``` We don't have automatic CI deployments yet, so make sure to [create a release](https://github.com/pmndrs/react-three-fiber/releases/new) on GitHub to notify people when it's ready. Choose or create the version generated by your changeset, and you can leave the rest to auto-fill via the "Generate release notes" button to describe PRs since the last release. ### Prerelease Follow the same steps as before, but specify a tag for [prerelease mode](https://github.com/changesets/changesets/blob/main/docs/prereleases.md) with: ```bash yarn changeset pre enter ``` To cancel or leave prerelease mode, try running: ```bash yarn changeset pre exit ``` ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019-2025 Poimandres Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: babel.config.js ================================================ module.exports = { plugins: [], presets: [ [ '@babel/preset-env', { include: [ '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-nullish-coalescing-operator', '@babel/plugin-proposal-numeric-separator', '@babel/plugin-proposal-logical-assignment-operators', ], bugfixes: true, loose: true, modules: false, targets: '> 1%, not dead, not ie 11, not op_mini all', }, ], ['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript', ], } ================================================ FILE: docs/API/additional-exports.mdx ================================================ --- title: Additional Exports nav: 8 --- | export | usage | | -------------------- | -------------------------------------------------------------- | | `addEffect` | Adds a global render callback which is called each frame | | `addAfterEffect` | Adds a global after-render callback which is called each frame | | `addTail` | Adds a global callback which is called when rendering stops | | `buildGraph` | Collects nodes and materials from a THREE.Object3D | | `flushGlobalEffects` | Flushes global render-effects for when manually driving a loop | | `flushSync` | Force React to flush any updates synchronously and immediately | | `invalidate` | Forces view global invalidation | | `advance` | Advances the frameloop (given that it's set to 'never') | | `extend` | Extends the native-object catalogue | | `createPortal` | Creates a portal (it's a React feature for re-parenting) | | `createRoot` | Creates a root that can render three JSX into a canvas | | `events` | Dom pointer-event system | | `applyProps` | `applyProps(element, props)` sets element properties, | | `act` | usage with react-testing | | `useInstanceHandle` | Exposes react-internal local state from `instance.__r3f` | | | | ================================================ FILE: docs/API/canvas.mdx ================================================ --- title: Canvas description: The Canvas object is your portal into three.js nav: 4 --- The `Canvas` object is where you start to define your React Three Fiber Scene. ```jsx import React from 'react' import { Canvas } from '@react-three/fiber' const App = () => ( ) ``` ## Properties | Prop | Description | Default | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | | children | three.js JSX elements or regular components | | | fallback | optional DOM JSX elements or regular components in case GL is not supported | | | gl | Props that go into the default renderer. Accepts sync/async callback with default props `gl={defaults => new Renderer({ ...defaults })}` | `{}` | | camera | Props that go into the default camera, or your own `THREE.Camera` | `{ fov: 75, near: 0.1, far: 1000, position: [0, 0, 5] }` | | scene | Props that go into the default scene, or your own `THREE.Scene` | `{}` | | shadows | Props that go into `gl.shadowMap`, can be set true for `PCFsoft` or one of the following: 'basic', 'percentage', 'soft', 'variance' | `false` | | raycaster | Props that go into the default raycaster | `{}` | | frameloop | Render mode: always, demand, never | `always` | | resize | Resize config, see react-use-measure's options | `{ scroll: true, debounce: { scroll: 50, resize: 0 } }` | | orthographic | Creates an orthographic camera | `false` | | dpr | Pixel-ratio, use `window.devicePixelRatio`, or automatic: [min, max] | `[1, 2]` | | legacy | Enables THREE.ColorManagement in three r139 or later | `false` | | linear | Switch off automatic sRGB color space and gamma correction | `false` | | events | Configuration for the event manager, as a function of state | `import { events } from "@react-three/fiber"` | | eventSource | The source where events are being subscribed to, HTMLElement | `React.RefObject`, `gl.domElement.parentNode` | | eventPrefix | The event prefix that is cast into canvas pointer x/y events | `offset` | | flat | Use `THREE.NoToneMapping` instead of `THREE.ACESFilmicToneMapping` | `false` | | onCreated | Callback after the canvas has rendered (but not yet committed) | `(state) => {}` | | onPointerMissed | Response for pointer clicks that have missed any target | `(event) => {}` | ## Defaults Canvas uses [createRoot](#createroot) which will create a translucent `THREE.WebGLRenderer` with the following constructor args: - antialias=true - alpha=true - powerPreference="high-performance" and with the following properties: - outputColorSpace = THREE.SRGBColorSpace - toneMapping = THREE.ACESFilmicToneMapping It will also create the following scene internals: - A `THREE.Perspective` camera - A `THREE.Orthographic` cam if `orthographic` is true - A `THREE.PCFSoftShadowMap` if `shadows` is true - A `THREE.Scene` (into which all the JSX is rendered) and a `THREE.Raycaster` In recent versions of threejs, `THREE.ColorManagement.enabled` will be set to `true` to enable automatic conversion of colors according to the renderer's configured color space. R3F will handle texture color space conversion. For more on this topic, see [https://threejs.org/docs/#manual/en/introduction/Color-management](https://threejs.org/docs/#manual/en/introduction/Color-management). ## Errors and fallbacks On some systems WebGL may not be supported, you can provide a fallback component that will be rendered instead of the canvas: ```jsx Sorry no WebGL supported!}> ``` You should also safeguard the canvas against WebGL context crashes, for instance if users have the GPU disabled or GPU drivers are faulty. ```jsx import { useErrorBoundary } from 'use-error-boundary' function App() { const { ErrorBoundary, didCatch, error } = useErrorBoundary() return didCatch ? (
{error.message}
) : ( ) } ``` > [!NOTE] > Ideally, and if possible, your fallback is a seamless, visual replacement for what the canvas would have otherwise rendered. ## WebGPU Recent Three.js now includes a WebGPU renderer. While still a work in progress and not fully backward-compatible with all of Three's features, the renderer requires an async initialization method. R3F streamlines this by allowing the gl prop to return a promise. ```tsx import * as THREE from 'three/webgpu' import * as TSL from 'three/tsl' import { Canvas, extend, useFrame, useThree } from '@react-three/fiber' declare module '@react-three/fiber' { interface ThreeElements extends ThreeToJSXElements {} } extend(THREE as any) export default () => ( { const renderer = new THREE.WebGPURenderer(props as any) await renderer.init() return renderer }}> ) ``` ## Custom Canvas R3F can render to a root, similar to how `react-dom` and all the other React renderers work. This allows you to shave off `react-dom` (~40kb), `react-use-measure` (~3kb) and, if you don't need them, `pointer-events` (~7kb) (you need to explicitly import `events` and add them to the config otherwise). Roots have the same options and properties as `Canvas`, but you are responsible for resizing it. It requires an existing DOM `` object into which it renders. ### CreateRoot Creates a root targeting a canvas, rendering JSX. ```jsx import * as THREE from 'three' import { extend, createRoot, events } from '@react-three/fiber' // Register the THREE namespace as native JSX elements. // See below for notes on tree-shaking extend(THREE) // Create a react root const root = createRoot(document.querySelector('canvas')) async function app() { // Configure the root, inject events optionally, set camera, etc // This *must* be called before render, and it must be awaited await root.configure({ events, camera: { position: [0, 0, 50] } }) // createRoot by design is not responsive, you have to take care of resize yourself window.addEventListener('resize', () => { root.configure({ size: { width: window.innerWidth, height: window.innerHeight } }) }) // Trigger resize window.dispatchEvent(new Event('resize')) // Render entry point root.render() // Unmount and dispose of memory // root.unmount() } app() ``` ## Tree-shaking New with v8, the underlying reconciler no longer pulls in the THREE namespace automatically. This enables a granular catalogue which also enables tree-shaking via the `extend` API: ```jsx import { extend, createRoot } from '@react-three/fiber' import { Mesh, BoxGeometry, MeshStandardMaterial } from 'three' extend({ Mesh, BoxGeometry, MeshStandardMaterial }) createRoot(canvas).render( <> , ) ``` There's an [official babel plugin](https://github.com/pmndrs/react-three-babel) which will do this for you automatically: ```jsx // In: import { createRoot } from '@react-three/fiber' createRoot(canvasNode).render( , ) // Out: import { createRoot, extend } from '@react-three/fiber' import { Mesh as _Mesh, BoxGeometry as _BoxGeometry, MeshStandardMaterial as _MeshStandardMaterial } from 'three' extend({ Mesh: _Mesh, BoxGeometry: _BoxGeometry, MeshStandardMaterial: _MeshStandardMaterial, }) createRoot(canvasNode).render( , ) ``` ================================================ FILE: docs/API/events.mdx ================================================ --- title: Events description: All the events you can hook up to nav: 7 --- `three.js` objects that implement their own `raycast` method (meshes, lines, etc) can be interacted with by declaring events on them. We support pointer events, clicks and wheel-scroll. Events contain the browser event as well as the `three.js` event data (object, point, distance, etc). You may want to [polyfill](https://github.com/jquery/PEP) them, if that's a concern. Additionally, there's a special `onUpdate` that is called every time the object gets fresh props, which is good for things like `self => (self.verticesNeedUpdate = true)`. Also notice the `onPointerMissed` on the canvas element, which fires on clicks that haven't hit _any_ meshes. ```jsx console.log('click')} onContextMenu={(e) => console.log('context menu')} onDoubleClick={(e) => console.log('double click')} onWheel={(e) => console.log('wheel spins')} onPointerUp={(e) => console.log('up')} onPointerDown={(e) => console.log('down')} onPointerOver={(e) => console.log('over')} onPointerOut={(e) => console.log('out')} onPointerEnter={(e) => console.log('enter')} // see note 1 onPointerLeave={(e) => console.log('leave')} // see note 1 onPointerMove={(e) => console.log('move')} onPointerMissed={() => console.log('missed')} onUpdate={(self) => console.log('props have been updated')} /> ``` ### Event data ```jsx ({ ...DomEvent // All the original event data ...Intersection // All of Three's intersection data - see note 2 intersections: Intersection[] // The first intersection of each intersected object object: Object3D // The object that was actually hit eventObject: Object3D // The object that registered the event unprojectedPoint: Vector3 // Camera-unprojected point ray: Ray // The ray that was used to strike the object camera: Camera // The camera that was used in the raycaster sourceEvent: DomEvent // A reference to the host event delta: number // Distance between mouse down and mouse up event in pixels }) => ... ``` ### How the event-system works, bubbling and capture > [!NOTE] > - `pointerenter` and `pointerleave` events work exactly the same as pointerover and pointerout. > - `pointerenter` and `pointerleave` semantics are not implemented. > [!NOTE] > Some events (such as `pointerout`) happen when there is no intersection between `eventObject` and the ray. When this happens, the event will contain intersection data from a previous event with this object. ### Event propagation (bubbling) Propagation works a bit differently to the DOM because objects can occlude each other in 3D. The `intersections` array in the event includes all objects intersecting the ray, not just the nearest. Only the first intersection with each object is included. The event is first delivered to the object nearest the camera, and then bubbles up through its ancestors like in the DOM. After that, it is delivered to the next nearest object, and then its ancestors, and so on. This means objects are transparent to pointer events by default, even if the object handles the event. `event.stopPropagation()` doesn't just stop this event from bubbling up, it also stops it from being delivered to farther objects (objects behind this one). All other objects, nearer or farther, no longer count as being hit while the pointer is over this object. If they were previously delivered pointerover events, they will immediately be delivered pointerout events. If you want an object to block pointer events from objects behind it, it needs to have an event handler as follows: ```jsx onPointerOver={e => { e.stopPropagation() // ... }} ``` even if you don't want this object to respond to the pointer event. If you do want to handle the event as well as using `stopPropagation()`, remember that the pointerout events will happen **during** the `stopPropagation()` call. You probably want your other event handling to happen after this. ### Pointer capture Because events go to all intersected objects, capturing the pointer also works differently. In the DOM, the capturing object **replaces** the hit test, but in React Three Fiber, the capturing object is **added** to the hit test result: if the capturing object was not hit, then all of the hit objects (and their ancestors) get the event first, followed by the capturing object and its ancestors. The capturing object can also use `event.stopPropagation()` so that objects that really were hit get pointerout events. Note that you can access the `setPointerCapture` and `releasePointerCapture` methods **only** via `event.target`: they don't get added to the `Object3D` instances in the scene graph. `setPointerCapture` and `releasePointerCapture` take a `pointerId` parameter like in the DOM, but for now they don't have support for multiple active pointers. PRs are welcome! ```jsx onPointerDown={e => { // Only the mesh closest to the camera will be processed e.stopPropagation() // You may optionally capture the target e.target.setPointerCapture(e.pointerId) }} onPointerUp={e => { e.stopPropagation() // Optionally release capture e.target.releasePointerCapture(e.pointerId) }} ``` ### Customizing the event settings For some advanced usage it's possible to customize the setting of the event manager globally with the `events` prop on ``: ```tsx import { Canvas, events } from '@react-three/fiber' const eventManagerFactory: Parameters[0]['events'] = (state) => ({ // Default configuration ...events(state), // Determines if the event layer is active enabled: true, // Event layer priority, higher prioritized layers come first and may stop(-propagate) lower layer priority: 1, // The filter can re-order or re-structure the intersections filter: (items: THREE.Intersection[], state: RootState) => items, // The compute defines how pointer events are translated into the raycaster and pointer vector2 compute: (event: DomEvent, state: RootState, previous?: RootState) => { state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1) state.raycaster.setFromCamera(state.pointer, state.camera) }, // Find more configuration default on ./packages/fiber/src/web/events.ts // And type definitions in ./packages/fiber/src/core/events.ts }) function App() { return ( } ``` ### Using a different target element There are cases in which you may want to connect the event handlers to another DOM element instead of the canvas. This is usually done to have events on a shared parent, which allows both the canvas, and dom overlays to receive events. You can either use the event manager: ```jsx const events => useThree(state => state.events) useEffect(() => { state.events.connect(domNode) ``` Or, the `eventSource` shortcut on the canvas (DOM only), which accepts dom-nodes and React.RefObjects to dom-nodes. ```jsx function App() { const target = useRef() return (
``` ### Using a different prefix (DOM only) By default Fiber will use offsetX/offsetY to set up the raycaster. You can change this with the `eventPrefix` shortcut. ```jsx function App() { return ( ``` ### Allow raycast without user interaction By default Fiber will only raycast when the user is interacting with the canvas. If, for instance, the camera moves a hoverable object underneath the cursor it will not trigger a hover event. If this is wanted behaviour you can force a raycast by executing `update()`, call it whenever necessary. ```jsx const events => useThree(state => state.events) useEffect(() => { // Will trigger a onPointerMove with the last-known pointer event state.events.update() ``` You can abstract this into more complex logic. ```jsx function RaycastWhenCameraMoves() { const matrix = new THREE.Matrix4() useFrame((state) => { // Act only when the camera has moved if (!matrix.equals(state.camera.matrixWorld)) { state.events.update() matrix.copy(state.camera.matrixWorld) } }) } ``` ================================================ FILE: docs/API/hooks.mdx ================================================ --- title: Hooks description: Hooks are the heart of react-three-fiber nav: 6 --- Hooks allow you to tie or request specific information to your component. For instance, components that want to participate in the renderloop can use `useFrame`, components that need to be informed of three.js specifics can use `useThree` and so on. All hooks clean up after themselves once the component unmounts. > [!NOTE] > Hooks can only be used inside the Canvas element because they rely on context! ❌ You cannot expect something like this to work: ```jsx import { useThree } from '@react-three/fiber' function App() { const { size } = useThree() // This will just crash return ( ``` ✅ Do this instead: ```jsx function Foo() { const { size } = useThree() ... } function App() { return ( ``` ## `useThree` This hook gives you access to the state model which contains the default renderer, the scene, your camera, and so on. It also gives you the current size of the canvas in screen and viewport coordinates. ```jsx import { useThree } from '@react-three/fiber' function Foo() { const state = useThree() ``` The hook is reactive, if you resize the browser for instance, you get fresh measurements, same applies to any of the state objects that may change. ### `state` properties | Prop | Description | Type | | ----------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `gl` | Renderer | `THREE.WebGLRenderer` | | `scene` | Scene | `THREE.Scene` | | `camera` | Camera | `THREE.PerspectiveCamera` | | `raycaster` | Default raycaster | `THREE.Raycaster` | | `pointer` | Contains updated, normalized, centric pointer coordinates | `THREE.Vector2` | | `mouse` | Note: this is deprecated, use `pointer` instead! Normalized event coordinates | `THREE.Vector2` | | `clock` | Running system clock | `THREE.Clock` | | `linear` | True when the colorspace is linear | `boolean` | | `flat` | True when no tonemapping is used | `boolean` | | `legacy` | Disables global color management via `THREE.ColorManagement` | `boolean` | | `frameloop` | Render mode: always, demand, never | `always`, `demand`, `never` | | `performance` | System regression | `{ current: number, min: number, max: number, debounce: number, regress: () => void }` | | `size` | Canvas size in pixels | `{ width: number, height: number, top: number, left: number }` | | `viewport` | Canvas viewport size in three.js units. Note: This is different from [`gl.getViewport`](https://threejs.org/docs/#api/en/renderers/WebGLRenderer.getViewport) which returns the drawbuffer size | `{ width: number, height: number, initialDpr: number, dpr: number, factor: number, distance: number, aspect: number, getCurrentViewport: (camera?: Camera, target?: THREE.Vector3, size?: Size) => Viewport }` | | `xr` | XR interface, manages WebXR rendering | `{ connect: () => void, disconnect: () => void }` | | `set` | Allows you to set any state property | `(state: SetState) => void` | | `get` | Allows you to retrieve any state property non-reactively | `() => GetState` | | `invalidate` | Request a new render, given that `frameloop === 'demand'` | `() => void` | | `advance` | Advance one tick, given that `frameloop === 'never'` | `(timestamp: number, runGlobalEffects?: boolean) => void` | | `setSize` | Resize the canvas | `(width: number, height: number, top?: number, left?: number) => void` | | `setDpr` | Set the pixel-ratio | `(dpr: number) => void` | | `setFrameloop` | Shortcut to set the current render mode | `(frameloop?: 'always', 'demand', 'never') => void` | | `setEvents` | Shortcut to setting the event layer | `(events: Partial>) => void` | | `onPointerMissed` | Response for pointer clicks that have missed a target | `() => void` | | `events` | Pointer-event handling | `{ connected: TargetNode, handlers: Events, connect: (target: TargetNode) => void, disconnect: () => void }` | ### Selector You can also select properties, this allows you to avoid needless re-render for components that are interested only in particulars. Reactivity does not include deeper three.js internals! ```jsx // Will only trigger re-render when the default camera is exchanged const camera = useThree((state) => state.camera) // Will only re-render on resize changes const viewport = useThree((state) => state.viewport) // ❌ You cannot expect reactivity from three.js internals! const zoom = useThree((state) => state.camera.zoom) ``` ### Reading state from outside of the component cycle ```jsx function Foo() { const get = useThree((state) => state.get) ... get() // Get fresh state from anywhere you want ``` ### Exchanging defaults ```jsx function Foo() { const set = useThree((state) => state.set) ... useEffect(() => { set({ camera: new THREE.OrthographicCamera(...) }) }, []) ``` ## `useFrame` This hook allows you to execute code on every rendered frame, like running effects, updating controls, and so on. You receive the state (same as `useThree`) and a clock delta. Your callback function will be invoked just before a frame is rendered. When the component unmounts it is unsubscribed automatically from the render-loop. ```jsx import { useFrame } from '@react-three/fiber' function Foo() { useFrame((state, delta, xrFrame) => { // This function runs at the native refresh rate inside of a shared render-loop }) ``` > [!CAUTION] > Be careful about what you do inside useFrame! You should never setState in there! Your calculations should be slim and > you should mind all the commonly known pitfalls when dealing with loops in general, like re-use of variables, etc. ### Taking over the render-loop If you need more control you may pass a numerical `renderPriority` value. This will cause React Three Fiber to disable automatic rendering altogether. It will now be your responsibility to render, which is useful when you're working with effect composers, heads-up displays, etc. ```jsx function Render() { // Takes over the render-loop, the user has the responsibility to render useFrame(({ gl, scene, camera }) => { gl.render(scene, camera) }, 1) function RenderOnTop() { // This will execute *after* Render's useframe useFrame(({ gl, ... }) => { gl.render(...) }, 2) ``` > [!NOTE] > Callbacks will be executed in order of ascending priority values (lowest first, highest last.), similar to the DOM's z-order. ### Negative indices Using negative indices will **not take over the render loop**, but it can be useful if you really must order the sequence of useFrames across the component tree. ```jsx function A() { // This will execute first useFrame(() => ..., -2) function B() { // This useFrame will execute *after* A's useFrame(() => ..., -1) ``` ## `useLoader` This hook loads assets and suspends for easier fallback- and error-handling. It can take any three.js loader as its first argument: GLTFLoader, OBJLoader, TextureLoader, FontLoader, etc. It is based on [React.Suspense](https://react.dev/reference/react/Suspense), so fallback-handling and [error-handling](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) happen at the parental level. ```jsx import { Suspense } from 'react' import { useLoader } from '@react-three/fiber' import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' function Model() { const result = useLoader(GLTFLoader, '/model.glb') // You don't need to check for the presence of the result, when we're here // the result is guaranteed to be present since useLoader suspends the component return } function App() { return ( /* or null */}> ) } ``` > [!TIP] > Internally, `useLoader` relies on [`suspend-react`](https://github.com/pmndrs/suspend-react). > [!NOTE] > Assets loaded with useLoader are cached by default. The urls given serve as cache-keys. This allows you to re-use loaded data everywhere in the component tree. > [!WARNING] > Be very careful with mutating or disposing of loaded assets, especially when you plan to re-use them. Refer to the automatic disposal section in the API. ### Loader extensions You can provide a callback as the third argument if you need to configure your loader: ```jsx import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader' useLoader(GLTFLoader, url, (loader) => { const dracoLoader = new DRACOLoader() dracoLoader.setDecoderPath('/draco-gltf/') loader.setDRACOLoader(dracoLoader) }) ``` ### Loading multiple assets at once It can also make multiple requests in parallel: ```jsx const [bumpMap, specMap, normalMap] = useLoader(TextureLoader, [url1, url2, url2]) ``` ### Loading status You can get the loading status from a callback you provide as the fourth argument. Though consider alternatives like THREE.DefaultLoadingManager or better yet, [Drei's](https://github.com/pmndrs/drei) loading helpers. ```jsx useLoader(loader, url, extensions, (xhr) => { console.log((xhr.loaded / xhr.total) * 100 + '% loaded') }) ``` ### Special treatment of GLTFLoaders and all loaders that return a scene prop If a `result.scene` prop is found the hook will automatically create a object & material collection: `{ nodes, materials }`. This lets you build immutable scene graphs selectively. You can also specifically alter the data without having to traverse it. [GLTFJSX](https://github.com/pmndrs/gltfjsx) specifically relies on this data. ```jsx const { nodes, materials } = useLoader(GLTFLoader, url) ``` ### Pre-loading assets You can pre-load assets in global space so that models can be loaded in anticipation before they're mounted in the component tree. ```jsx useLoader.preload(GLTFLoader, '/model.glb' /* extensions */) ``` ## `useGraph` Convenience hook which creates a memoized, named object/material collection from any [`Object3D`](https://threejs.org/docs/#api/en/core/Object3D). ```jsx import { useLoader, useGraph } from '@react-three/fiber' function Model(url) { const scene = useLoader(OBJLoader, url) const { nodes, materials } = useGraph(scene) return } ``` ================================================ FILE: docs/API/objects.mdx ================================================ --- title: Objects, properties and constructor arguments description: All the effective ways of using React Three Fiber nav: 5 --- ## Declaring objects You can use [three.js's entire object catalogue and all properties](https://threejs.org/docs). When in doubt, always consult the docs. ❌ You could lay out an object like this: ```jsx ``` ✅ The problem is that all of these properties will always be re-created. Instead, you should define properties declaratively. ```jsx ``` ## Constructor arguments In three.js objects are classes that are instantiated. These classes can receive one-time constructor arguments (`new THREE.SphereGeometry(1, 32)`), and properties (`someObject.visible = true`). In React Three Fiber, constructor arguments are always passed as an array via `args`. If args change later on, the object must naturally get reconstructed from scratch! ```jsx ``` ## Shortcuts ### Set All properties whose underlying object has a `.set()` method can directly receive the same arguments that `set` would otherwise take. For example [`THREE.Color.set`](https://threejs.org/docs/#api/en/math/Color.set) can take a color string, so instead of `color={new THREE.Color('hotpink')}` you can simply write `color="hotpink"`. Some `set` methods take multiple arguments, for instance [`THREE.Vector3`](https://threejs.org/docs/#api/en/math/Vector3.set), give it an array in that case `position={[100, 0, 0]}`. ```jsx ``` > [!NOTE] > If you do link up an existing object to a property, for instance a THREE.Vector3() to a position, be aware that this will end up copying the object in most cases as it calls .copy() on the target. This only applies to objects that expose both .set() and .copy() (Vectors, Eulers, Matrix, ...). If you link up an existing material or geometry on the other hand, it will overwrite, because more these objects do not have a .set() method. ### SetScalar Properties that have a `setScalar` method (for instance `Vector3`) can be set like so: ```jsx // Translates to ``` ## Piercing into nested properties If you want to reach into nested attributes (for instance: `mesh.rotation.x`), just use dash-case. ```jsx ``` ## Dealing with non-scene objects You can put non-Object3D primitives (geometries, materials, etc) into the render tree as well. They take the same properties and constructor arguments they normally would. You might be wondering why you would want to put something in the "scene" that normally would not be part of it, in a vanilla three.js app at least. For the same reason you declare any object: it becomes managed, reactive and auto-disposes. These objects are not technically part of the scene, but they "attach" to a parent which is. ### Attach Use `attach` to bind objects to their parent. If you unmount the attached object it will be taken off its parent automatically. The following attaches a material to the `material` property of a mesh and a geometry to the `geometry` property: ```jsx ``` > [!NOTE] > All objects extending `THREE.Material` receive `attach="material"`, and all objects extending `THREE.BufferGeometry` receive `attach="geometry"`. You do not have to type it out! ```jsx ``` You can also deeply nest attach through piercing. The following adds a buffer-attribute to `geometry.attributes.position` and then adds the buffer geometry to `mesh.geometry`. ```jsx ``` #### More examples ```jsx // Attach bar to foo.a // Attach bar to foo.a.b and foo.a.b.c (nested object attach) // Attach bar to foo.a[0] and foo.a[1] (array attach is just object attach) // Attach bar to foo via explicit add/remove functions { parent.add(self) return () => parent.remove(self) }} /> // The same as a one liner (parent.add(self), () => parent.remove(self))} /> ``` #### Real-world use-cases: Attaching to nested objects, for instance a shadow-camera: ```diff - + + + ``` Arrays must have explicit order, for instance multi-materials: ```jsx {colors.map((color, index) => } ``` ## Putting already existing objects into the scene-graph You can use the `primitive` placeholder for that. You can still give it properties or attach nodes to it. Never add the same object multiple times, this is not allowed in three.js! Primitives will not dispose of the object they carry on unmount, you are responsible for disposing of it! ```jsx const mesh = new THREE.Mesh(geometry, material) function Component() { return ``` > [!NOTE] > Scene objects can only ever be added once in Threejs. If you attempt to add one and the same object in two places Threejs will remove the first instance automatically. This will also happen with primitive! If you want to re-use an existing object, you must clone it first. ## Using 3rd-party objects declaratively The `extend` function extends React Three Fiber's catalogue of JSX elements. Components added this way can then be referenced in the scene-graph using camel casing similar to other primitives. ```jsx import { extend } from '@react-three/fiber' import { OrbitControls, TransformControls } from 'three-stdlib' extend({ OrbitControls, TransformControls }) // ... return ( <> ``` If you're using TypeScript, you'll also need to [extend the JSX namespace](/tutorials/v9-migration-guide#threeelements). ## Disposal Freeing resources is a [manual chore in `three.js`](https://threejs.org/docs/#manual/en/introduction/How-to-dispose-of-objects), but React is aware of object-lifecycles, hence React Three Fiber will attempt to free resources for you by calling `object.dispose()`, if present, on all unmounted objects. If you manage assets by yourself, globally or in a cache, this may _not_ be what you want. You can switch it off by placing `dispose={null}` onto meshes, materials, etc, or even on parent containers like groups, it is now valid for the entire tree. ```jsx const globalGeometry = new THREE.BoxGeometry() const globalMaterial = new THREE.MeshBasicMaterial() function Mesh() { return ( ``` ================================================ FILE: docs/API/testing.mdx ================================================ --- title: 'Testing' description: How to handle unit tests nav: 10 --- Like with every other application testing is an important factor when it comes to releasing an application into the wild and when it comes to React Three Fiber we can use React Three Test Renderer to achieve this. We will be testing the [sandbox](https://codesandbox.io/s/98ppy) we created in [events and interactions](events-and-interaction). ## How to test React Three Fiber Let's start by installing the React Three Test Renderer: ```bash npm install @react-three/test-renderer --save-dev ``` Afterwards, if you are using Create React App you can just add a file that ends in `.test.js` and start writing your code, because React Three Test Renderer is testing library agnostic, so it works with libraries such as `jest`, `jasmine` etc. Let's create an `App.test.js` and set up all our test cases: ```jsx import ReactThreeTestRenderer from '@react-three/test-renderer' import { MyRotatingBox } from './App' test('mesh to have two children', async () => { const renderer = await ReactThreeTestRenderer.create() }) test('click event makes box bigger', async () => { const renderer = await ReactThreeTestRenderer.create() }) ``` In here we created three tests and in each we made sure we created the renderer by using the `create` function. Let's start with the first test and make sure our mesh has two children, the material and cube. We can start by getting the scene and it's children from the test instance we just created like so: ```js const meshChildren = renderer.scene.children ``` If you log this mesh out you can see that it returns an array of one element since that's all we have in the scene. Using this we can make sure to get that first child and use the `allChildren` property on it like so: ```js const meshChildren = renderer.scene.children[0].allChildren ``` There is also one property called `children` but this one is meant to be used for things like groups as this one does not return the geometry and the materials, for that we need `allChildren`. Now to create our assertion: ```js expect(meshChildren.length).toBe(2) ``` Our first test case looks like this: ```js test('mesh to have two children', async () => { const renderer = await ReactThreeTestRenderer.create() const mesh = renderer.scene.children[0].allChildren expect(mesh.length).toBe(2) }) ``` ## Testing interactions Now that we have gotten the first test out of the way we can test our interaction and make sure that when we click on the mesh it does indeed update the scale. We can do that by utilizing the `fireEvent` method existing in a test instance. We know we can get the mesh with: ```js const mesh = renderer.scene.children[0] ``` Since we already have that we can fire an event in it like so: ```js await renderer.fireEvent(mesh, 'click') ``` With that done, all that's left to do is the tree demonstration of our scene and make sure the scale prop on our mesh has updated: ```js expect(mesh.props.scale).toBe(1.5) ``` In the end our test looks something like this: ```js test('click event makes box bigger', async () => { const renderer = await ReactThreeTestRenderer.create() const mesh = renderer.scene.children[0] expect(mesh.props.scale).toBe(1) await renderer.fireEvent(mesh, 'click') expect(mesh.props.scale).toBe(1.5) }) ``` If you want to learn more about React Three Test Renderer you can checkout the repo and their docs: - [Repo](https://github.com/pmndrs/react-three-fiber/blob/master/packages/test-renderer) - [React Three Test Renderer API](https://github.com/pmndrs/react-three-fiber/blob/master/packages/test-renderer/markdown/rttr.md#create) - [React Three Test Instance API](https://github.com/pmndrs/react-three-fiber/blob/master/packages/test-renderer/markdown/rttr-instance.md) ## Exercises - Check the color of the Box we created - Check the rotation using the `advanceFrames` method. ================================================ FILE: docs/API/typescript.mdx ================================================ --- title: TypeScript description: Common scenarios and how to approach them with TypeScript nav: 9 --- ## Typing with `useRef` React's `useRef` won't automatically infer types despite pointing it to a typed ref. You can type the ref yourself by passing a type through `useRef`'s generics: ```tsx import { useRef, useEffect } from 'react' import { Mesh } from 'three' function Box(props) { const meshRef = useRef(null!) useEffect(() => { console.log(Boolean(meshRef.current)) }, []) return ( ) } ``` The exclamation mark is a non-null assertion that will let TS know that `ref.current` is defined when we access it in effects, useFrame et al. You do not need to check against null, the element is assumed to exist. ## Accessing typed three-elements Whenever you want to spread props or type components that rely on three elements, you can use the `ThreeElements` interface to extract the mesh, group, or any other three element, including custom elements. ```tsx import { ThreeElements } from '@react-three/fiber' type FooProps = ThreeElements['mesh'] & { bar: boolean } function Foo({ bar, ...props}: FooProps) { useEffect(() => { console.log(bar) }, [bar]) return } ``` ## Extend usage react-three-fiber can also accept third-party elements and extend them into its internal catalogue. ```tsx import { useRef, useEffect } from 'react' import { GridHelper } from 'three' import { extend } from '@react-three/fiber' // Create our custom element class CustomElement extends GridHelper {} // Extend so the reconciler will learn about it extend({ CustomElement }) ``` The catalogue teaches the underlying reconciler how to create fibers for these elements and treat them within the scene. You can then declaratively create custom elements with primitives, but TypeScript won't know about them nor their props. ```html // error: 'customElement' does not exist on type 'JSX.IntrinsicElements' ``` ### Extending ThreeElements To define our element in JSX, we'll use the `ThreeElement` interface to extend `ThreeElements`. This interface describes three.js classes that are available in the R3F catalog and can be used as native elements. ```tsx import { useRef, useEffect } from 'react' import { GridHelper } from 'three' import { extend, ThreeElement } from '@react-three/fiber' // Create our custom element class CustomElement extends GridHelper {} // Extend so the reconciler will learn about it extend({ CustomElement }) // Add types to ThreeElements elements so primitives pick up on it declare module '@react-three/fiber' { interface ThreeElements { customElement: ThreeElement } } // react-three-fiber will create your custom component and TypeScript will understand it ``` You can shorten element definition by using the `extend` factory signature, which will automatically extend the element locally. This will also prevent namespace bleeding. ```tsx // Create our custom element class CustomElement extends GridHelper {} // Extend so the reconciler will learn about it, types will be inferred const Element = extend(CustomElement) // react-three-fiber will create your custom component and TypeScript will understand it ``` ## Extending three default elements If you open your own root instead of using ``, you can extend the default elements with `extend`. But keep in mind that the `* as THREE` namespace contains classes, functions, numbers, strings. At the moment we suggest you use `any` or `@ts-ignore` unless you extract the exact classes you need (`extend({ Mesh, Group, ... })`). ```tsx import * as THREE from 'three' import { extend, createRoot, events } from '@react-three/fiber' // Register the THREE namespace as native JSX elements. extend(THREE as any) // Create a react root const root = createRoot(document.querySelector('canvas')) ``` ## Exported types react-three-fiber is extensible and exports types for its internals, such as render props, canvas props, and events: ```tsx // Event raycaster intersection Intersection // `useFrame` internal subscription and render callback Subscription RenderCallback // `useThree`'s returned internal state RootState Performance Dpr Size Viewport Camera // Canvas props CanvasProps // Supported events Events // Event manager signature (is completely modular) EventManager // Wraps a platform event as it's passed through the event manager ThreeEvent ``` ================================================ FILE: docs/advanced/pitfalls.mdx ================================================ --- title: Performance pitfalls description: Performance 1x1 nav: 12 --- ## Tips and Tricks This is a good overview: https://discoverthreejs.com/tips-and-tricks The most important gotcha in three.js is that creating objects can be expensive, think twice before you mount/unmount things! Every material or light that you put into the scene has to compile, every geometry you create will be processed. Share materials and geometries if you can, either in global scope or locally: ```jsx const geom = useMemo(() => new BoxGeometry(), []) const mat = useMemo(() => new MeshBasicMaterial(), []) return items.map(i => { const interval = setInterval(() => setX((x) => x + 0.1), 1) return () => clearInterval(interval) }, []) ``` ### ❌ `setState` in useFrame is bad ```jsx const [x, setX] = useState(0) useFrame(() => setX((x) => x + 0.1)) return ``` ### ❌ `setState` in fast events is bad ```jsx setX((x) => e.point.x)} /> ``` ### ✅ Instead, just mutate, use deltas In general you should prefer useFrame. Consider mutating props safe as long as the component is the only entity that mutates. Use deltas instead of fixed values so that your app is refresh-rate independent and runs at the same speed everywhere! ```jsx const meshRef = useRef() useFrame((state, delta) => (meshRef.current.position.x += delta)) return ``` Same goes for events, use references. ```jsx (ref.current.position.x = e.point.x)} /> ``` If you must use intervals, use references as well, but keep in mind that this is not refresh-rate independent. ```jsx useEffect(() => { const interval = setInterval(() => ref.current.position.x += 0.1, 1) return () => clearInterval(interval) }, []) ``` ## Handle animations in loops The frame loop is where you should place your animations. For instance using lerp, or damp. ### ✅ Use `lerp` + `useFrame` ```jsx function Signal({ active }) { const meshRef = useRef() useFrame((state, delta) => { meshRef.current.position.x = THREE.MathUtils.lerp(meshRef.current.position.x, active ? 100 : 0, 0.1) }) return ``` ### ✅ Or react-spring Or, use animation libraries. React-spring has its own frame-loop and animates outside of React. Framer-motion is another popular alternative. ```jsx import { a, useSpring } from '@react-spring/three' function Signal({ active }) { const { x } = useSpring({ x: active ? 100 : 0 }) return ``` ## Do not bind to fast state reactively Using state-managers and selective state is fine, but not for updates that happen rapidly for the same reason as above. ### ❌ Don't bind reactive fast-state ```jsx import { useSelector } from 'react-redux' // Assuming that x gets animated inside the store 60fps const x = useSelector((state) => state.x) return ``` ### ✅ Fetch state directly For instance using [Zustand](https://github.com/pmndrs/zustand) (same in Redux et al). ```jsx useFrame(() => (ref.current.position.x = api.getState().x)) return ``` ## Don't mount indiscriminately In threejs it is very common to not re-mount at all, see the ["disposing of things"](https://discoverthreejs.com/tips-and-tricks/) section in discover-three. This is because buffers and materials get re-initialized/compiled, which can be expensive. ### ❌ Avoid mounting runtime ```jsx { stage === 1 && } { stage === 2 && } { stage === 3 && } ``` ### ✅ Consider using visibility instead ```jsx function Stage1(props) { return ( ... ``` ### ✅ Use `startTransition` for expensive ops React 18 introduces the `startTransition` and `useTransition` APIs to defer and schedule work and state updates. Use these to de-prioritize expensive operations. Since version 8 of Fiber canvases use concurrent mode by default, which means React will schedule and defer expensive operations. You don't need to do anything, but you can play around with the [experimental scheduler](https://github.com/drcmda/scheduler-test) and see if marking ops with a lesser priority makes a difference. ```jsx import { useTransition } from 'react' import { Points } from '@react-three/drei' const [isPending, startTransition] = useTransition() const [radius, setRadius] = useState(1) const positions = calculatePositions(radius) const colors = calculateColors(radius) const sizes = calculateSizes(radius) { startTransition(() => { setRadius(prev => prev + 1) }) }} > ``` ## Don't re-create objects in loops Try to avoid creating too much effort for the garbage collector, re-pool objects when you can! ### ❌ Bad news for the GC This creates a new vector 60 times a second, which allocates memory and forces the GC to eventually kick in. ```jsx useFrame(() => { ref.current.position.lerp(new THREE.Vector3(x, y, z), 0.1) }) ``` ### ✅ Better re-use object Set up re-used objects in global or local space, now the GC will be silent. ```jsx function Foo(props) const vec = new THREE.Vector() useFrame(() => { ref.current.position.lerp(vec.set(x, y, z), 0.1) }) ``` ## `useLoader` instead of plain loaders Threejs loaders give you the ability to load async assets (models, textures, etc), but if you do not re-use assets it can quickly become problematic. ### ❌ No re-use is bad for perf This re-fetches, re-parses for every component instance. ```jsx function Component() { const [texture, set] = useState() useEffect(() => void new TextureLoader().load(url, set), []) return texture ? ( ) : null } ``` Instead use useLoader, which caches assets and makes them available throughout the scene. ### ✅ Cache and re-use objects ```jsx function Component() { const texture = useLoader(TextureLoader, url) return ( ) } ``` Regarding GLTF's try to use [GLTFJSX](https://github.com/pmndrs/gltfjsx) as much as you can, this will create immutable JSX graphs which allow you to even re-use full models. ================================================ FILE: docs/advanced/scaling-performance.mdx ================================================ --- title: Scaling performance description: This is a short primer on how to scale performance. nav: 11 --- Running WebGL can be quite expensive depending on how powerful your devices are. In order to mitigate this, especially if you want to make your application available to a broad variety of devices, including weaker options, you should look into performance optimizations. This article goes through a couple of them. ## On-demand rendering three.js apps usually run in a game-loop that executes 60 times a second, React Three Fiber is no different. This is perfectly fine when your scene has _constantly_ moving parts in it. This is what generally drains batteries the most and makes fans spin up. But if the moving parts in your scene are allowed to come to rest, then it would be wasteful to keep rendering. In such cases you can opt into on-demand rendering, which will only render when necessary. This saves battery and keeps noisy fans in check. Open the sandbox below in a full screen and look into dev tools, you will see that it is completely idle when nothing is going on. It renders only when you move the model. All you need to do is set the canvas `frameloop` prop to `demand`. It will render frames whenever it detects prop changes throughout the component tree. ```jsx ``` ### Triggering manual frames One major caveat is that if anything in the tree _mutates_ props, then React cannot be aware of it and the display would be stale. For instance, camera controls just grab into the camera and mutate its values. Here you can use React Three Fiber's `invalidate` function to trigger frames manually. ```jsx function Controls() { const orbitControlsRef = useRef() const { invalidate, camera, gl } = useThree() useEffect(() => { orbitControlsRef.current.addEventListener('change', invalidate) return () => orbitControlsRef.current.removeEventListener('change', invalidate) }, []) return ``` > [!NOTE] > Drei's controls do this automatically for you. Generally you can call invalidate whenever you need to render: ```jsx invalidate() ``` > [!IMPORTANT] > Calling `invalidate()` will not render immediately, it merely requests a new frame to be rendered out. Calling invalidate multiple times will not render multiple times. Think of it as a flag to tell the system that something has changed. ### Sync animations with on-demand-rendering and invalidate Since `invalidate()` is only a flag that schedules render, you might bump into syncing issues when you run animations that are synchronous (as in, they start immediately). By the time fiber renders the first frame the animation has already progressed which leads to a visible jump. In such cases you should pre-emptively schedule a render and then start the animation in the next frame. ```jsx { // Pre-emptively schedule a render invalidate() // Wait for the next frame to start the animation requestAnimationFrame(() => controls.dolly(1, true)) }} ``` ## Re-using geometries and materials Each geometry and material means additional overhead for the GPU. You should try to re-use resources if you know they will repeat. You could do this globally: ```jsx const red = new THREE.MeshLambertMaterial({ color: "red" }) const sphere = new THREE.SphereGeometry(1, 28, 28) function Scene() { return ( <> ``` If you create a material or color in global space - outside of React Three Fiber's `Canvas` context - you should enable [ColorManagement](https://threejs.org/docs/#manual/en/introduction/Color-management) in three.js. This will allow certain conversions (for hexadecimal and CSS colors in sRGB) to be made automatically, producing correct colors in all cases. ```jsx import * as THREE from 'three' // r150 THREE.ColorManagement.enabled = true // r139-r149 THREE.ColorManagement.legacyMode = false ``` ### Caching with `useLoader` > [!NOTE] > Every resource that is loaded with useLoader is cached automatically! If you access a resource via useLoader with the same URL, throughout the component tree, then you will always refer to the same asset and thereby re-use it. This is especially useful if you run your GLTF assets through [GLTFJSX](https://github.com/pmndrs/gltfjsx) because it links up geometries and materials and thereby creates re-usable models. ```jsx function Shoe(props) { const { nodes, materials } = useLoader(GLTFLoader, "/shoe.glb") return ( ) } ``` ## Instancing Each mesh is a draw call, you should be mindful of how many of these you employ: no more than 1000 as the very maximum, and optimally a few hundred or less. You can win performance back by reducing draw calls, for example by instancing repeating objects. This way you can have hundreds of thousands of objects in a single draw call. Setting up instancing is not so hard, consult [the three.js docs](https://threejs.org/docs/#api/en/objects/InstancedMesh) if you need help. ```jsx function Instances({ count = 100000, temp = new THREE.Object3D() }) { const instancedMeshRef = useRef() useEffect(() => { // Set positions for (let i = 0; i < count; i++) { temp.position.set(Math.random(), Math.random(), Math.random()) temp.updateMatrix() instancedMeshRef.current.setMatrixAt(i, temp.matrix) } // Update the instance instancedMeshRef.current.instanceMatrix.needsUpdate = true }, []) return ( ) } ``` ## Level of detail Sometimes it can be beneficial to reduce the quality of an object the further it is away from the camera. Why would you display it full resolution if it is barely visible. This can be a good strategy to reduce the overall vertex-count which means less work for the GPU. Scroll in and out to see the effect: There is a small component in Drei called `` which sets up LOD without boilerplate. You load or prepare a couple of resolution stages, as many as you like, and then give them the same amount of distances from the camera, starting from highest quality to lowest. ```jsx import { Detailed, useGLTF } from '@react-three/drei' function Model() { const [low, mid, high] = useGLTF(["/low.glb", "/mid.glb", "/high.glb"]) return ( ) } ``` ## Nested loading Nested loading means that lesser textures and models are loaded first, higher-resolution later. The following sandbox goes through three loading stages: - A loading indicator - Low quality - High quality And this is how easy it is to achieve it, you can nest suspense and even use it as a fallback: ```jsx function App() { return ( loading...}> }> ) } function Model({ url }) { const { scene } = useGLTF(url) return } ``` ## Performance monitoring Drei has a new component [PerformanceMonitor](https://github.com/pmndrs/drei#performancemonitor) that allows you to monitor, and adapt to, device performance. This component will collect the average fps (frames per second) over time. If after a couple of iterations the averages are below or above a threshold it will trigger onIncline and onDecline callbacks that allow you to respond. Typically you would reduce the quality of your scene, the resolution, effects, the amount of stuff to render, or, increase it if you have enough framerate to fill. Since this would normally cause ping-ponging between the two callbacks you define upper and lower framerate bounds, as long as you stay within that margin nothing will trigger. Ideally your app should find its way into that margin by gradually altering quality. A simple example for regulating the resolution. It starts out with 1.5, if the system falls below the bounds it goes to 1, if it's fast enough it goes to 2. ```jsx function App() { const [dpr, setDpr] = useState(1.5) return ( setDpr(2)} onDecline={() => setDpr(1)} > ``` You can also use the onChange callback to get notified when the average changes in whichever direction. This allows you to make gradual changes. It gives you a factor between 0 and 1, which is increased by incline and decreased by decline. The factor is initially 0.5 by default. ```jsx import round from 'lodash/round' const [dpr, setDpr] = useState(1) return ( setDpr(round(0.5 + 1.5 * factor, 1))}> ``` If you still experience flip flops despite the bounds you can define a limit of flipflops. If it is met onFallback will be triggered which typically sets a lowest possible baseline for the app. After the fallback has been called PerformanceMonitor will shut down. ```jsx setDpr(1)}> ``` PerformanceMonitor can also have children, if you wrap your app in it you get to use usePerformanceMonitor which allows individual components down the nested tree to respond to performance changes on their own. ```jsx ; function Effects() { usePerformanceMonitor({ onIncline, onDecline, onFallback, onChange }) // ... } ``` ## Movement regression Websites like Sketchfab make sure the scene is always fluid, running at 60 fps, and responsive, no matter which device is being used or how expensive a loaded model is. They do this by regressing movement, where effects, textures, shadows will slightly reduce quality until still-stand The following sandbox uses expensive lights and post-processing. In order for it to run relatively smooth it will scale the pixel ratio on movement and also skip heavy post-processing effects like ambient occlusion. When you inspect the state model you will notice an object called `performance`. ```jsx performance: { current: 1, min: 0.1, max: 1, debounce: 200, regress: () => void, }, ``` - `current`: Performance factor alternates between min and max - `min`: Performance lower bound (should be less than 1) - `max`: Performance upper bound (no higher than 1) - `debounce`: Debounce timeout until it goes to upper bound (1) again - `regress()`: Function that temporarily regresses performance You can define defaults like so: ```jsx ... ``` ### This is how you can put the system into regression The only thing you have to do is call `regress()`. When exactly you do that, that is up to you, but it could be when the mouse moves, or the scene is moving, for instance when controls fire their change-event. Say you are using controls, then the following code puts the system in regress when they are active: ```jsx const regress = useThree((state) => state.performance.regress) useEffect(() => { controls.current?.addEventListener('change', regress) ``` ### This is how you can respond to it > [!NOTE] > Mere calls to `regress()` will not change or affect anything! Your app has to opt into performance scaling by listening to the performance `current`! The number itself will tell you what to do. 1 (max) means everything is ok, the default. Less than 1 (min) means a regression is requested and the number itself tells you how far you should go when scaling down. For instance, you could simply multiply `current` with the pixel ratio to cut down on resolution. If you have defined `min: 0.5` that would mean it will half the resolution for at least 200ms (delay) when regress is called. It can be used for anything else, too: switching off lights when `current < 1`, using lower-res textures, skip post-processing effects, etc. You could of course also animate/lerp these changes. Here is a small prototype component that scales the pixel ratio: ```jsx function AdaptivePixelRatio() { const current = useThree((state) => state.performance.current) const setPixelRatio = useThree((state) => state.setDpr) useEffect(() => { setPixelRatio(window.devicePixelRatio * current) }, [current]) return null } ``` Drop this component into the scene, combine it with the code above that calls `regress()`, and you have adaptive resolution: ```jsx ``` There are pre-made components for this already in the [Drei library](https://github.com/pmndrs/drei/#performance). ## Enable concurrency React 18 introduces concurrent scheduling, specifically time slicing via `startTransition` and `useTransition`. This will virtualize the component graph, which then allows you to prioritise components and actions. Think of how a virtual list avoids scaling issues because it only renders as many items as the screen can take, it is not affected by the amount of items it has to render, be it 10 or 100.000.000. React 18 functions very similar to this, it can potentially defer load and heavy tasks in ways that would be hard or impossible to achieve in a vanilla application. It thereby holds on to a stable framerate even in the most demanding situations. The following benchmark shows how powerful concurrency can be: https://github.com/drcmda/scheduler-test It simulates heavy load by creating hundreds of THREE.TextGeometry instances (510 to be exact). This class, like many others in three.js, is expensive and takes a while to construct. If all 510 instances are created the same time **it will cause approximately 1.5 seconds of pure jank** (Apple M1), the tab would normally freeze. It runs in an interval and **will execute every 2 seconds**. | | Distributed | At-once | | -------- | ----------- | ------- | | three.js | ~20fps | ~5fps | | React | ~60fps | ~60fps |

For more on how to use this API, see [use startTransition for expensive ops](/docs/advanced/pitfalls#use-starttransition-for-expensive-ops). ================================================ FILE: docs/getting-started/basic-example-sandpack/index.jsx ================================================ import { createRoot } from 'react-dom/client' import React, { useRef, useState } from 'react' import { Canvas, useFrame } from '@react-three/fiber' import './styles.css' function Box(props) { // This reference will give us direct access to the mesh const meshRef = useRef() // Set up state for the hovered and active state const [hovered, setHover] = useState(false) const [active, setActive] = useState(false) // Subscribe this component to the render-loop, rotate the mesh every frame useFrame((state, delta) => (meshRef.current.rotation.x += delta)) // Return view, these are regular three.js elements expressed in JSX return ( setActive(!active)} onPointerOver={(event) => setHover(true)} onPointerOut={(event) => setHover(false)}> ) } createRoot(document.getElementById('root')).render( , ) ================================================ FILE: docs/getting-started/basic-example-sandpack/styles.css ================================================ html, body, #root { height: 100%; margin: unset; } ================================================ FILE: docs/getting-started/community-r3f-components.mdx ================================================ --- title: Community R3F Components description: This page showcases some React Three Fiber/r3f community components that have not been merged to the drei components collection or to another pmndrs project. nav: 4 --- If you'd like to list new community components, please make a PR to [this doc](https://github.com/pmndrs/react-three-fiber/tree/master/docs/getting-started/community-r3f-components.mdx) file. It showcases some r3f community components that have not been merged to to the [drei](http://drei.docs.pmnd.rs) components collection or to another [pmndrs](https://github.com/pmndrs) project. ## R3F community components ### Repos and docs This page showcases some React Three Fiber/r3f community components that have not been merged to the [drei](http://drei.docs.pmnd.rs) components collection or to another [pmndrs](https://github.com/pmndrs) project. If you'd like to list new community components, please make a PR to [this doc](https://github.com/pmndrs/react-three-fiber/tree/master/docs/getting-started/community-r3f-components.mdx) file. #### Data sources - [NASA-AMMOS/3DTilesRendererJS](https://github.com/NASA-AMMOS/3DTilesRendererJS/) (r3f [readme](https://github.com/NASA-AMMOS/3DTilesRendererJS/blob/master/src/r3f/README.md)) repo with doc - Luma Labs [Gaussian Splats](https://cdn-luma.com/public/lumalabs.ai/luma-web-library/0.2/fefe154/index.html#react-three-fiber) renderer doc with demos - [NYTimes/three-loader-3dtiles](https://nytimes.github.io/three-loader-3dtiles) #### Renderers & frameworks - Takram three-geospatial [clouds doc](https://github.com/takram-design-engineering/three-geospatial/tree/main/packages/clouds) and [demos](https://takram-design-engineering.github.io/three-geospatial/?path=/story/clouds-3d-tiles-renderer-integration--tokyo), and [atmosphere doc](https://github.com/takram-design-engineering/three-geospatial/tree/main/packages/atmosphere) - [Looking Glass](https://docs.lookingglassfactory.com/developer-tools/webxr/react-three-fiber) doc and demos - [Theatre-js](https://github.com/theatre-js/theatre) repo and [doc](https://www.theatrejs.com/docs/latest) #### Materials - [FarazzShaikh/CustomShaderMaterial](https://github.com/FarazzShaikh/THREE-CustomShaderMaterial) - [pmndrs/THREE.MeshLine](https://github.com/pmndrs/meshline) - [ektogamat/R3F-Ultimate-Lens-Flare](https://github.com/ektogamat/R3F-Ultimate-Lens-Flare) - [troika-three-text](https://github.com/protectwise/troika/tree/main/packages/troika-three-text) #### Utilities - [utsuboco/r3f-perf](https://github.com/utsuboco/r3f-perf) ### Codesandboxes and demos
  • [NASA-AMMOS/3DTilesRendererJS](https://github.com/NASA-AMMOS/3DTilesRendererJS) [3DTilesRendererJS r3f demo](https://nasa-ammos.github.io/3DTilesRendererJS/example/bundle/r3f/basic.html)
  • [Takram three-geospatial clouds](https://github.com/takram-design-engineering/three-geospatial/tree/main/packages/clouds) [Takram three-geospatial clouds](https://takram-design-engineering.github.io/three-geospatial/?path=/story/clouds-3d-tiles-renderer-integration--tokyo)
  • [Luma Gaussian Splats](https://cdn-luma.com/public/lumalabs.ai/luma-web-library/0.2/fefe154/index.html#react-three-fiber)
  • [NYTimes/three-loader-3dtiles](https://github.com/nytimes/three-loader-3dtiles/blob/dev/examples/r3f/src/index.tsx) [three-loader-3dtiles r3f demo](https://nytimes.github.io/three-loader-3dtiles/dist/web/examples/demos/realitycapture/)
  • [Looking Glass](https://docs.lookingglassfactory.com/developer-tools/webxr/react-three-fiber)
  • [Theatre-js](https://github.com/theatre-js/theatre)
  • [Farazz/CustomShaderMaterial](https://github.com/FarazzShaikh/THREE-CustomShaderMaterial) [THREE-CustomShaderMaterial r3f demo](https://farazzshaikh.github.io/THREE-CustomShaderMaterial/#/caustics)
  • [Takram three-geospatial atmosphere](https://github.com/takram-design-engineering/three-geospatial/tree/main/packages/atmosphere) [Takram three-geospatial atmosphere](https://takram-design-engineering.github.io/three-geospatial/?path=/story/atmosphere-3d-tiles-renderer-integration--manhattan)
  • [utsuboco/r3f-perf](https://github.com/utsuboco/r3f-perf)
  • [pmndrs/THREE.MeshLine](https://github.com/pmndrs/meshline)
  • [ektogamat/R3F-Ultimate-Lens-Flare](https://github.com/ektogamat/R3F-Ultimate-Lens-Flare) [R3F-Ultimate-Lens-Flare demo](https://ultimate-lens-flare.vercel.app/)
  • ================================================ FILE: docs/getting-started/examples.mdx ================================================ --- title: Examples description: A few examples that demonstrate what you can do with React Three Fiber nav: 3 --- ## Showcase
  • ## Game prototypes
  • ## Basic examples
  • ================================================ FILE: docs/getting-started/installation.mdx ================================================ --- title: Installation description: Learn how to install react-three-fiber nav: 1 --- ```bash npm install three @react-three/fiber ``` > [!WARNING] > Fiber is compatible with React v18 and v19 and works with ReactDOM and React Native. Fiber is a React renderer, it must pair with a major version of React, just like react-dom, react-native, etc. @react-three/fiber@8 pairs with react@18, @react-three/fiber@9 pairs with react@19. Getting started with React Three Fiber is not nearly as hard as you might have thought, but various frameworks may require particular attention. We've put together guides for getting started with each popular framework: - Vite.js - Next.js - CDN w/o build tools - React Native If you just want to give it a try, fork this [example on codesandbox](https://codesandbox.io/s/rrppl0y8l4?file=/src/App.js)! ## Vite.js `vite` will also work out of the box. ```bash # Create app npm create vite my-app # Select react as framework # Install dependencies cd my-app npm install three @react-three/fiber # Start development server npm run dev ``` ## Next.js It should work out of the box but you will encounter untranspiled add-ons in the three.js ecosystem, in that case, ### Next.js 13.1 or latest version You need to add three to `transpilePackages` property in `next.config.js`: ```js transpilePackages: ['three'], ``` ### Next.js 13.0 or oldest version You can install the `next-transpile-modules` module: ```bash npm install next-transpile-modules --save-dev ``` then, add this to your `next.config.js` ```js const withTM = require('next-transpile-modules')(['three']) module.exports = withTM() ``` Make sure to check out our [official next.js starter](https://github.com/pmndrs/react-three-next), too! ## Without build tools You can use React Three Fiber with browser-ready ES Modules from [esm.sh](https://esm.sh) and a JSX-like syntax powered by [htm](https://github.com/developit/htm). ```jsx import ReactDOM from 'https://esm.sh/react-dom' import React, { useRef, useState } from 'https://esm.sh/react' import { Canvas, useFrame } from 'https://esm.sh/@react-three/fiber' import htm from 'https://esm.sh/htm' const html = htm.bind(React.createElement) ReactDOM.render(html`<${Canvas}>...`, document.getElementById('root')) ```
    Full example ```jsx import ReactDOM from 'https://esm.sh/react-dom' import React, { useRef, useState } from 'https://esm.sh/react' import { Canvas, useFrame } from 'https://esm.sh/@react-three/fiber' import htm from 'https://esm.sh/htm' const html = htm.bind(React.createElement) function Box(props) { const meshRef = useRef() const [hovered, setHover] = useState(false) const [active, setActive] = useState(false) useFrame(() => (meshRef.current.rotation.x = meshRef.current.rotation.y += 0.01)) return html` setActive(!active)} onPointerOver=${() => setHover(true)} onPointerOut=${() => setHover(false)} > ` } ReactDOM.render( html` <${Canvas}> <${Box} position=${[-1.2, 0, 0]} /> <${Box} position=${[1.2, 0, 0]} /> `, document.getElementById('root'), ) ```
    ## React Native R3F v8 adds support for react-native and can be imported from `@react-three/fiber/native`. We use `expo-gl` and `expo-asset` under the hood for WebGL2 bindings and ensuring interplay between Metro and three.js loaders. To get started, create an app via `expo` or `react-native`: ```bash # Create a managed/bare app npx create-expo-app cd my-app # or # Create and link bare app npx react-native init my-app npx install-expo-modules@latest cd my-app ``` Then install dependencies (for manual installation or migration, see [expo modules installation](https://docs.expo.dev/bare/installing-expo-modules)): ```bash # Automatically install expo install expo-gl # Install NPM dependencies npm install three @react-three/fiber ``` Some configuration may be required to tell the Metro bundler about your assets if you use `useLoader` or Drei abstractions like `useGLTF` and `useTexture`: ```js // metro.config.js module.exports = { resolver: { sourceExts: ['js', 'jsx', 'json', 'ts', 'tsx', 'cjs', 'mjs'], assetExts: ['glb', 'gltf', 'png', 'jpg'], }, } ``` R3F's API is completely x-platform, so you can use [events](/api/events) and [hooks](/api/hooks) just as you would on the web. > [!NOTE] > Make sure to import from `@react-three/fiber/native` or `@react-three/drei/native` for correct IntelliSense and platform-specific abstractions. > [!NOTE] > iOS simulators often have incomplete or unreliable OpenGL ES support, which can cause EXC_BAD_ACCESS crashes when rendering 3D content. Always test on a physical iOS device (iPhone/iPad running iOS 16 or later) to ensure stable WebGL rendering. ```jsx import { Suspense } from 'react' import { Canvas } from '@react-three/fiber/native' import { useGLTF } from '@react-three/drei/native' import modelPath from './path/to/model.glb' function Model(props) { const gltf = useGLTF(modelPath) return } export default function App() { return ( ) } ``` ================================================ FILE: docs/getting-started/introduction.mdx ================================================ --- title: Introduction description: React-three-fiber is a React renderer for three.js. nav: 0 --- Build your scene declaratively with re-usable, self-contained components that react to state, are readily interactive and can participate in [React](https://react.dev)'s ecosystem. [![React Three Fiber banner](../banner-r3f.jpg)](/getting-started/examples) ```bash npm install three @types/three @react-three/fiber ``` > [!WARNING] > Three-fiber is a React renderer, it must pair with a major version of React, just like react-dom, react-native, etc. @react-three/fiber@8 pairs with react@18, @react-three/fiber@9 pairs with react@19. ## Does it have limitations? None. Everything that works in Threejs will work here without exception. ## Is it slower than plain Threejs? No. There is no overhead. Components render outside of React. It outperforms Threejs in scale due to React's scheduling abilities. ## Can it keep up with frequent feature updates to Threejs? Yes. It merely expresses Threejs in JSX, `` dynamically turns into `new THREE.Mesh()`. If a new Threejs version adds, removes or changes features, it will be available to you instantly without depending on updates to this library. ## What does it look like? Let's make a re-usable component that has its own state, reacts to user-input and participates in the render-loop:
    Show TypeScript example ```bash npm install @types/three ``` (null!) const [hovered, setHover] = useState(false) const [active, setActive] = useState(false) useFrame((state, delta) => (meshRef.current.rotation.x += delta)) return ( setActive(!active)} onPointerOver={(event) => setHover(true)} onPointerOut={(event) => setHover(false)}> ) } createRoot(document.getElementById('root')).render( , )`, }} />
    Show React Native example For installation instructions see [react native installation instructions](/getting-started/installation#react-native). ```jsx import React, { useRef, useState } from 'react' import { Canvas, useFrame } from '@react-three/fiber/native' function Box(props) { const meshRef = useRef(null) const [hovered, setHover] = useState(false) const [active, setActive] = useState(false) useFrame((state, delta) => (meshRef.current.rotation.x += delta)) return ( setActive(!active)} onPointerOver={(event) => setHover(true)} onPointerOut={(event) => setHover(false)}> ) } export default function App() { return ( ) } ```
    ## First steps You need to be versed in both React and Threejs before rushing into this. If you are unsure about React consult the official [React docs](https://react.dev/learn), especially [the section about hooks](https://react.dev/reference/react). As for Threejs, make sure you at least glance over the following links: 1. Make sure you have a [basic grasp of Threejs](https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene). Keep that site open. 2. When you know what a scene is, a camera, mesh, geometry, material, fork the [demo above](https://github.com/pmndrs/react-three-fiber#what-does-it-look-like). 3. [Look up](https://threejs.org/docs/index.html#api/en/objects/Mesh) the JSX elements that you see (mesh, ambientLight, etc), _all_ threejs exports are native to three-fiber. 4. Try changing some values, scroll through our [API](/api/canvas) to see what the various settings and hooks do. Some helpful material: - [Threejs-docs](https://threejs.org/docs) and [examples](https://threejs.org/examples) - [Discover Threejs](https://discoverthreejs.com), especially the [Tips and Tricks](https://discoverthreejs.com/tips-and-tricks) chapter for best practices - [Bruno Simons Threejs Journey](https://threejs-journey.com), arguably the best learning resource, now includes a full [R3F chapter](https://threejs-journey.com/lessons/what-are-react-and-react-three-fiber) ## Eco system There is a vibrant and extensive eco system around three-fiber, full of libraries, helpers and abstractions. - [`@react-three/drei`](https://github.com/pmndrs/drei) – useful helpers, this is an eco system in itself - [`@react-three/gltfjsx`](https://github.com/pmndrs/gltfjsx) – turns GLTFs into JSX components - [`@react-three/postprocessing`](https://github.com/pmndrs/react-postprocessing) – post-processing effects - [`@react-three/test-renderer`](https://github.com/pmndrs/react-three-fiber/tree/master/packages/test-renderer) – for unit tests in node - [`@react-three/flex`](https://github.com/pmndrs/react-three-flex) – flexbox for react-three-fiber - [`@react-three/xr`](https://github.com/pmndrs/react-xr) – VR/AR controllers and events - [`@react-three/csg`](https://github.com/pmndrs/react-three-csg) – constructive solid geometry - [`@react-three/rapier`](https://github.com/pmndrs/react-three-rapier) – 3D physics using Rapier - [`@react-three/cannon`](https://github.com/pmndrs/use-cannon) – 3D physics using Cannon - [`@react-three/p2`](https://github.com/pmndrs/use-p2) – 2D physics using P2 - [`@react-three/a11y`](https://github.com/pmndrs/react-three-a11y) – real a11y for your scene - [`@react-three/gpu-pathtracer`](https://github.com/pmndrs/react-three-gpu-pathtracer) – realistic path tracing - [`create-r3f-app next`](https://github.com/pmndrs/react-three-next) – nextjs starter - [`lamina`](https://github.com/pmndrs/lamina) – layer based shader materials - [`zustand`](https://github.com/pmndrs/zustand) – flux based state management - [`jotai`](https://github.com/pmndrs/jotai) – atoms based state management - [`valtio`](https://github.com/pmndrs/valtio) – proxy based state management - [`react-spring`](https://github.com/pmndrs/react-spring) – a spring-physics-based animation library - [`framer-motion-3d`](https://www.framer.com/docs/three-introduction/) – framer motion, a popular animation library - [`use-gesture`](https://github.com/pmndrs/react-use-gesture) – mouse/touch gestures - [`leva`](https://github.com/pmndrs/leva) – create GUI controls in seconds - [`maath`](https://github.com/pmndrs/maath) – a kitchen sink for math helpers - [`miniplex`](https://github.com/hmans/miniplex) – ECS (entity management system) - [`composer-suite`](https://github.com/hmans/composer-suite) – composing shaders, particles, effects and game mechanics ## How to contribute If you like this project, please consider helping out. All contributions are welcome as well as donations to [Opencollective](https://opencollective.com/react-three-fiber), or in crypto `BTC: 36fuguTPxGCNnYZSRdgdh6Ea94brCAjMbH`, `ETH: 0x6E3f79Ea1d0dcedeb33D3fC6c34d2B1f156F2682`. ## Backers Thank you to all our backers! 🙏 ## Contributors This project exists thanks to all the people who contribute. ================================================ FILE: docs/getting-started/your-first-scene.mdx ================================================ --- title: Your first scene description: This guide will help you setup your first React Three Fiber scene and introduce you to its core concepts. nav: 2 --- This tutorial will assume some React knowledge. ## Setting up the Canvas We'll start by importing the `` component from `@react-three/fiber` and putting it in our React tree. ```jsx import { createRoot } from 'react-dom/client' import { Canvas } from '@react-three/fiber' function App() { return (
    ) } createRoot(document.getElementById('root')).render() ``` The Canvas component does some important setup work behind the scenes: - It sets up a **Scene** and a **Camera**, the basic building blocks necessary for rendering - It renders our scene every frame, you do not need a traditional render-loop > [!NOTE] > Canvas is responsive to fit the parent node, so you can control how big it is by changing the parents width and height, in this case #canvas-container. ## Adding a Mesh Component To actually see something in our scene, we'll add a lowercase `` native element, which is the direct equivalent to new THREE.Mesh(). ```js ``` > [!NOTE] > Note that we don't need to import anything, All three.js objects will be treated as native JSX elements, just like you can just write <div /> or <span /> in regular ReactDOM. The general rule is that Fiber components are available under the camel-case version of their name in three.js. A [`Mesh`](https://threejs.org/docs/#api/en/objects/Mesh) is a basic scene object in three.js, and it's used to hold the geometry and the material needed to represent a shape in 3D space. We'll create a new mesh using a **BoxGeometry** and a **MeshStandardMaterial** which [automatically attach](/api/objects#attach) to their parent. ```jsx ``` Let's pause for a moment to understand exactly what is happening here. The code we just wrote is the equivalent to this three.js code: ```jsx const scene = new THREE.Scene() const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000) const renderer = new THREE.WebGLRenderer() renderer.setSize(width, height) document.querySelector('#canvas-container').appendChild(renderer.domElement) const mesh = new THREE.Mesh() mesh.geometry = new THREE.BoxGeometry() mesh.material = new THREE.MeshStandardMaterial() scene.add(mesh) function animate() { requestAnimationFrame(animate) renderer.render(scene, camera) } animate() ``` ### Constructor arguments According to the [docs for `BoxGeometry`](https://threejs.org/docs/#api/en/geometries/BoxGeometry) we can optionally pass three arguments for: width, length and depth: ```js new THREE.BoxGeometry(2, 2, 2) ``` In order to do this in Fiber we use the `args` prop, which _always_ takes an array whose items represent the constructor arguments. ```jsx ``` > [!NOTE] > Note that every time you change args, the object must be re-constructed! ## Adding lights Next, we will add some lights to our scene, by putting these components into our canvas. ```jsx ``` ### Props This introduces us to the last fundamental concept of Fiber, how React `props` work on three.js objects. When you set any prop on a Fiber component, it will set the property of the same name on the three.js instance. Let's focus on our `ambientLight`, whose [documentation](https://threejs.org/docs/#api/en/lights/AmbientLight) tells us that we can optionally construct it with a color, but it can also receive props. ```jsx ``` Which is the equivalent to: ```jsx const light = new THREE.AmbientLight() light.intensity = 0.1 ``` ### Shortcuts There are a few shortcuts for props that have a `.set()` method (colors, vectors, etc). ```jsx const light = new THREE.DirectionalLight() light.position.set(0, 0, 5) light.color.set('red') ``` Which is the same as the following in JSX: ```jsx ``` Please refer to the API for [a deeper explanation](/api/objects). ## The result ); }`, }} /> > [!TIP] > You can live-edit the code above: > - try different materials, like [`MeshNormalMaterial`](https://threejs.org/docs/#api/en/materials/MeshNormalMaterial) or [`MeshBasicMaterial`](https://threejs.org/docs/#api/en/materials/MeshBasicMaterial), give them a color > - try different geometries, like [`SphereGeometry`](https://threejs.org/docs/#api/en/geometries/SphereGeometry) or [`OctahedronGeometry`](https://threejs.org/docs/#api/en/geometries/OctahedronGeometry) > - try changing the `position` on our `mesh` component, by setting the prop with the same name > - try extracting our mesh to a new component ================================================ FILE: docs/tutorials/basic-animations.mdx ================================================ --- title: Basic Animations description: This guide will help you understand refs, useFrame and how to make basic animations with Fiber nav: 17 --- This tutorial will assume some React knowledge, and will be based on [this starter codesandbox](https://codesandbox.io/s/getting-started-01-12q81?from-embed), so just fork it and follow along! We will build a really small, continuous animation loop, that will be the basic building block of more advanced animations later on. ## `useFrame` `useFrame` is a Fiber hook that lets you execute code on every frame of Fiber's render loop. This can have a lot of uses, but we will focus on building an animation with it. It's important to remember that **Fiber hooks can only be called inside a `` parent**! ```jsx import { useFrame } from '@react-three/fiber' function MyAnimatedBox() { useFrame(() => { console.log("Hey, I'm executing every frame!") }) return ( ) } ``` This loop is the basic building block of our animation, the callback we pass to `useFrame` will be executed every frame and it will be passed an object containing the state of our Fiber scene: For example, we can extract time information from the `clock` parameter, to know how much time has elapsed in our application, and use that time to animate a value: ```jsx useFrame(({ clock }) => { const a = clock.elapsedTime console.log(a) // the value will be 0 at scene initialization and grow each frame }) ``` `clock` is a [three.js Clock](https://threejs.org/docs/#api/en/core/Clock) object, from which we are getting the total elapsed time, which will be key for our animations. ## Animating with Refs It would be tempting to just update the state of our component via `setState` and let it change the `mesh` via props, but going through state isn't ideal, when dealing with continuous updates, commonly know as [transient updates](). Instead, we want to **directly mutate our mesh each frame**. First, we'll have to get a `reference` to it, via the `useRef` React hook: ```jsx import React from 'react' function MyAnimatedBox() { const myMesh = React.useRef() return ( ) } ``` `myMesh` will now hold a reference to the actual three.js object, which we can now freely mutate in `useFrame`, without having to worry about React: ```jsx useFrame(({ clock }) => { myMesh.current.rotation.x = clock.elapsedTime }) ``` Let's have a closer look: - We are destructuring `clock` from the argument passed to `useFrame`, which we know is the state of our Fiber scene. - We are accessing the `rotation.x` property of `myMesh.current` object, which is a reference to our mesh object - We are assigning our time-dependent value `a` to the `rotation` on the `x` axis, meaning our object will now infinitely rotate between -1 and 1 radians around the x axis! **Exercises** - Try `Math.sin(clock.elapsedTime)` and see how your animation changes ## Next steps Now that you understand the basic technique for animating in Fiber, [learn how event works](/tutorials/events-and-interaction)! If you want to go deeper into animations, check these out: - [Animating with React Spring](/tutorials/using-with-react-spring) ================================================ FILE: docs/tutorials/events-and-interaction.mdx ================================================ --- title: 'Events and Interaction' description: Let's make our meshes react to user input. nav: 14 --- This tutorial will assume some React knowledge, and will be based on [this starter codesandbox](https://codesandbox.io/s/getting-started-01-12q81?from-embed), so just fork it and follow along! After we have our continuous loop running the next step would be to allow our mesh to react to user interaction, so in this part let's attach a click handler to the cube and make it bigger on click. ## User Interaction Any Object3D that has a raycast method can receive a large number of events, for instance a mesh: ```jsx console.log('click')} onContextMenu={(e) => console.log('context menu')} onDoubleClick={(e) => console.log('double click')} onWheel={(e) => console.log('wheel spins')} onPointerUp={(e) => console.log('up')} onPointerDown={(e) => console.log('down')} onPointerOver={(e) => console.log('over')} onPointerOut={(e) => console.log('out')} onPointerEnter={(e) => console.log('enter')} onPointerLeave={(e) => console.log('leave')} onPointerMove={(e) => console.log('move')} onPointerMissed={() => console.log('missed')} onUpdate={(self) => console.log('props have been updated')} /> ``` From this we can see that what we need to do is use the old `onClick` event we use on any DOM element to react to a user clicking the mesh. Let's add it then: ```jsx alert('Hellooo')}> ``` We did it! We created the most boring interaction in the story of 3D and we made an alert show up. Now let's make it actually animate our mesh. Let's start by setting some state to check if the mesh is active: ```jsx const [active, setActive] = useState(false) ``` After we have this we can set the scale with a ternary operator like so: ```jsx setActive(!active)}> ``` If you try to click on your mesh now, it scales up and down. We just made our first interactive 3D mesh! What we did in this chapter was: - Attached a click handler to our mesh - Added some state to track if the mesh is currently active - Changed the scale based on that state **Exercises** - Change other props of the mesh like the `position` or even the `color` of the material. - Use `onPointerOver` and `onPointerOut` to change the props of the mesh on hover events. ## Next steps We just made our mesh react to user interaction but it looks pretty bland without any transition, right? In the next chapter let's integrate `react-spring` into our project to make this into an actual animation. ================================================ FILE: docs/tutorials/how-it-works.mdx ================================================ --- title: How does it work? description: This is an advanced guide on the inner workings of Fiber, if you are just getting started, take a look at our introduction! nav: 18 --- React Three Fiber is a React renderer for **three.js**. This means that each Fiber component will effectively create a new THREE object that will be added to a `scene`. Understanding how this works is not necessarily needed to use Fiber, but it will better arm you to deal with anything that you might need in your projects, reading other people's Fiber code and even help you contribute. Let's take a small React example: ```jsx import { Canvas } from '@react-three/fiber' function MyApp() { return ( ) } ``` In three.js, this is equivalent to: ```js import * as THREE from 'three' const scene = new THREE.Scene() // const group = new THREE.Group() // const mesh = new THREE.Mesh() // const material = new THREE.MeshNormalMaterial() // const geometry = new THREE.BoxGeometry(2, 2, 2) // mesh.material = material mesh.geometry = geometry group.add(mesh) scene.add(group) ``` Our `Canvas` element will create a new scene, and Fiber will instantiate new objects for each component and correctly compose them together in a scene graph! Additionally, Fiber will: - Setup a new perspective camera at [0, 0, 0] and set it as default - Setup a **render loop** with automatic render to screen - Setup pointer events via raycasting on all meshes with `onPointer` props - Setup tone mapping - Automatically handle window resize **Let's break this down!** ## Creating THREE objects In three.js, we can create new object using the classic JS API: ```js const myBox = new THREE.BoxGeometry(1, 2, 3) ``` Object creation is handled transparently by the Fiber renderer, the name of the constructor `BoxGeometry` is equivalent to the camel case component ``, while the constructor arguments - in our example `[1, 2, 3]` - are passed via the `args` prop: ```jsx ``` > [!NOTE] > Note that the object will be created only when first adding the component to the React tree! ## The `attach` props Fiber always tries to correctly infer the relationship between components and their parents, for example: ```jsx ``` Here, we always know that a group can only have children, so Fiber just calls the `add` method on the group: ```js group.add(mesh) ``` For meshes and other three.js objects, rules can be different. Looking at the three.js documentation, we can see how a `THREE.Mesh` object is constructed using a `material` and a `geometry`. With the `attach` prop, we can precisely tell the renderer what property to attach each component to: ```jsx ``` This will _explicitly_ tell Fiber to render like this: ```js mesh.material = new THREE.MeshNormalMaterial() mesh.geometry = new THREE.BoxGeometry() ``` As you can see, the `attach` prop is telling Fiber to set the parent's `material` property to a reference to our `` object. Note that while we used geometry and material for this example, Fiber also infers the attach property from the constructor name, so anything with `material` or `geometry` will automatically get attached to the correct property of its parent. ## Props With Fiber, you can pass any three.js property as a React property, and it will be assigned to the constructed object: ```jsx ``` is equivalent to: ```jsx const material = new THREE.MeshBasicMaterial() material.color = 'red' ``` Fiber will check the type of the property value and either: - assign the new value directly - if the value is an object with a `set` method, call that - construct a new object if needed. - convert between formats ```jsx ``` is equivalent to: ```jsx const mesh = new THREE.Mesh() mesh.scale = new THREE.Vector3(1, 2, 3) // on update, it will instead `set()` the vector mesh.scale.set(3, 4, 5) ``` ## Pointer Events [Pointer Events](/api/events) are transparently handled by Fiber. On startup, it will create a [raycaster](https://threejs.org/docs/#api/en/core/Raycaster) for mouse picking. Every object with `onPointer` props will be added to the array of objects checked every frame by the raycaster: ```jsx ... ``` The ray's `origin` and `direction` are updated every time the mouse moves on the `` element or the window is resized. Fiber also handles camera switching, meaning that the raycaster will always use the currently active camera. When using the `raycast` prop, the object will instead be picked using a custom ray: ```jsx import { useCamera } from '@react-three/drei' return ``` ## Render Loop By default, Fiber will setup a render loop that renders the default `scene` from the default `camera` to a [WebGLRenderer](https://threejs.org/docs/#api/en/renderers/WebGLRenderer). The loop is setup using [setAnimationLoop](https://threejs.org/docs/#api/en/renderers/WebGLRenderer.setAnimationLoop), which will execute its callback every time a new frame is renderable. This is what will happen every render: 1. All global before effects are executed 2. Clock delta is saved - implying all `useFrame` calls will share the same `delta` 3. `useFrame` callbacks are executed in order 4. `renderer.render(scene, camera)` is called, effectively rendering the scene to screen 5. All global after effects are executed ================================================ FILE: docs/tutorials/loading-models.mdx ================================================ --- title: 'Loading Models' description: 3D Software to the web! nav: 15 --- > All the models in this page were created by Sara Vieira and are freely available to download from any of the sandboxes. There are many types of 3D model extensions, in this page we will focus on loading the three most common ones: `GLTF`, `FBX` and `OBJ`. All of these will use the `useLoader` function but in slightly different ways. This whole section will assume you have placed your models in the public folder or in a place in your application where you can import them easily. ## Loading GLTF models Starting with the open standard and the one that has more support in React Three Fiber we will load a `.gltf` model. Let's start by importing the two things we need: ```js import { useLoader } from '@react-three/fiber' import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js' ``` With this we can create a Model component and place it in our scene like so: ```jsx function Scene() { const gltf = useLoader(GLTFLoader, '/Poimandres.gltf') return } ``` You can play with the sandbox and see how it looks here after I added an HDRI background: ### Loading GLTF models as JSX Components Here comes the really fancy part, you can transform these models into React components and then use them as you would any React component. To do this, grab your `GLTF` model and head over to [https://gltf.pmnd.rs/](https://gltf.pmnd.rs/) and drop your `GLTF`, after that you should see something like: ![gltfjsx](gltfjsx.png) Let's now copy the code and move it over to `Model.js`: ```jsx /* Auto-generated by: https://github.com/pmndrs/gltfjsx */ import React, { useRef } from 'react' import { useGLTF } from '@react-three/drei' export default function Model(props) { const groupRef = useRef() const { nodes, materials } = useGLTF('/Poimandres.gltf') return ( ) } useGLTF.preload('/Poimandres.gltf') ``` Now we can import our model like we would import any React component and use it in our app: ```jsx import { Suspense } from 'react' import { Canvas } from '@react-three/fiber' import { Environment } from '@react-three/drei' import Model from './Model' export default function App() { return (
    ) } ``` You can play with the sandbox here: ## Loading OBJ models In this case, we will use the trusted `useLoader` hook but in combination with `three.js` `OBJLoader`. ```js import { OBJLoader } from 'three/addons/loaders/OBJLoader.js' import { useLoader } from '@react-three/fiber' ``` With these imported let's get the mesh into our scene: ```jsx function Scene() { const obj = useLoader(OBJLoader, '/Poimandres.obj') return } ``` And here we go, we have an OBJ model showing on the web! Pretty cool ah? You can play with the sandbox here: ## Loading FBX models Let's again use the trusted `useLoader` but this time with the `FBXLoader` that comes from `three.js` ```js import { useLoader } from '@react-three/fiber' import { FBXLoader } from 'three/addons/loaders/FBXLoader.js' ``` To create our scene we can get the FBX as a return value of the useLoader by passing the `FBXloader` and the location of our file like so: ```jsx function Scene() { const fbx = useLoader(FBXLoader, '/Poimandres.fbx') return } ``` You can play with the sandbox here: ### Loading FBX models using `useFBX` [@react-three/drei](https://github.com/pmndrs/drei) exports a very useful helper when it comes to loading FBX models and it's called `useFBX`, in this case there is no need to import anything from `three.js` as it is all done behind the scenes and we can just pass the location of the file to `useFBX` like so: ```jsx function Scene() { const fbx = useFBX('/Poimandres.fbx') return } ``` You can play with the sandbox here: ## Showing a loader If your model is big and takes a while to load, it's always good to show a small loader of how much is already is loaded and again [@react-three/drei](https://github.com/pmndrs/drei) is here to help with `Html` and `useProgress`. - `Html` allows you place plain ol' HTML in your canvas and render it like you would a normal DOM element. - `useProgress` is a hook that gives you a bunch of information about the loading status of your model. With these two things, we can create a very bare-bones loading component like so: ```jsx import { Html, useProgress } from '@react-three/drei' function Loader() { const { progress } = useProgress() return {progress} % loaded } ``` We can then wrap our model in it using `Suspense` like so: ```jsx export default function App() { return ( }> ) } ``` The hook returns much more than just the progress so there is a lot you can do there to give the user more information about the loading status of the application. You can play with all of them in this sandbox: ================================================ FILE: docs/tutorials/loading-textures.mdx ================================================ --- title: 'Loading Textures' description: Let's load some fancy textures. nav: 16 --- > All textures used in this chapter were downloaded from [cc0textures](https://cc0textures.com/). ## Using `TextureLoader` and `useLoader` To load the textures we will use the `TextureLoader` from three.js in combination with `useLoader` that will allow us to pass the location of the texture and get the map back. It's better to explain with code, let's say you downloaded [this texture](https://cc0textures.com/view?id=PavingStones092) and placed it in the public folder of your site, to get the color map from it you could do: ```js const colorMap = useLoader(TextureLoader, 'PavingStones092_1K_Color.jpg') ``` Let's then with this information create a small scene where we can use this texture: ```jsx import { Suspense } from 'react' import { Canvas, useLoader } from '@react-three/fiber' import { TextureLoader } from 'three' function Scene() { const colorMap = useLoader(TextureLoader, 'PavingStones092_1K_Color.jpg') return ( <> ) } export default function App() { return ( ) } ``` If everything went according to plan, you should now be able to apply this texture to the sphere like so: ```jsx ``` Awesome! That works but we have a lot more textures to import and do we have to create a different useLoader for each of them? That's the great part! You don't, the second argument is an array where you can pass all the textures you have and the maps will be returned and ready to use: ```js const [colorMap, displacementMap, normalMap, roughnessMap, aoMap] = useLoader(TextureLoader, [ 'PavingStones092_1K_Color.jpg', 'PavingStones092_1K_Displacement.jpg', 'PavingStones092_1K_Normal.jpg', 'PavingStones092_1K_Roughness.jpg', 'PavingStones092_1K_AmbientOcclusion.jpg', ]) ``` Now we can place them in our mesh like so: ```jsx ``` The displacement will probably be too much, usually setting it to 0.2 will make it look good. Our final code would look something like: ```jsx function Scene() { const [colorMap, displacementMap, normalMap, roughnessMap, aoMap] = useLoader(TextureLoader, [ 'PavingStones092_1K_Color.jpg', 'PavingStones092_1K_Displacement.jpg', 'PavingStones092_1K_Normal.jpg', 'PavingStones092_1K_Roughness.jpg', 'PavingStones092_1K_AmbientOcclusion.jpg', ]) return ( {/* Width and height segments for displacementMap */} ) } ``` ## Using `useTexture` Another way to import these is using `useTexture` from [`@react-three/drei`](https://github.com/pmndrs/drei), that will make it slightly easier and there is no need to import the `TextureLoader`, our code would look like: ```js import { useTexture } from "@react-three/drei" ... const [colorMap, displacementMap, normalMap, roughnessMap, aoMap] = useTexture([ 'PavingStones092_1K_Color.jpg', 'PavingStones092_1K_Displacement.jpg', 'PavingStones092_1K_Normal.jpg', 'PavingStones092_1K_Roughness.jpg', 'PavingStones092_1K_AmbientOcclusion.jpg', ]) ``` You can also use object-notation which is the most convenient: ```jsx const props = useTexture({ map: 'PavingStones092_1K_Color.jpg', displacementMap: 'PavingStones092_1K_Displacement.jpg', normalMap: 'PavingStones092_1K_Normal.jpg', roughnessMap: 'PavingStones092_1K_Roughness.jpg', aoMap: 'PavingStones092_1K_AmbientOcclusion.jpg', }) return ( ) ``` You can play with the sandbox and see how it looks: ================================================ FILE: docs/tutorials/v9-migration-guide.mdx ================================================ --- title: 'v9 Migration Guide' description: Changes and new features with v9 and React 19 nav: 13 --- This is a compatibility release for React 19, which brings further performance, stability, and type improvements. You can check out the React 19 changelog [here](https://react.dev/blog/2024/04/25/react-19). We would like to express our gratitude to the community for their continued support, as well as to all our contributors, including the React team at Meta and Vercel, for ensuring this upgrade went smoothly. 🎉 > [!WARNING] > This release contains breaking changes when using Strict Mode, which can highlight bugs during development. See [StrictMode](#strictmode). ## Features ### useLoader Accepts Loader Instance `useLoader` now supports re-use of external loader instances for more controlled pooling and setup. ```jsx import { GLTFLoader } from 'three/addons' import { useLoader } from '@react-three/fiber' function Model() { const gltf = useLoader(GLTFLoader, '/path/to/model.glb') // ... } // or, const loader = new GLTFLoader() function Model() { const gltf = useLoader(loader, '/path/to/model.glb') // ... } ``` ### Factory extend Signature `extend` can now produce a component when a three.js class is passed to it individually instead of a catalog of named classes. This is backwards compatible and reduces TypeScript boilerplate and JSX collisions. We recommend libraries migrate to this signature so internal components don't clash with user-land declarations. ```tsx import { OrbitControls } from 'three/addons' import { type ThreeElement, type ThreeElements } from '@react-three/fiber' declare module '@react-three/fiber' { interface ThreeElements { orbitControls: ThreeElement } } extend({ OrbitControls }) // or, const Controls = extend(OrbitControls) ``` ### Async GL prop The Canvas GL prop accepts constructor parameters, properties, or a renderer instance via a callback. The callback now passes constructor parameters instead of just a canvas reference. ```diff new WebGLRenderer({ canvas })} + gl={(props) => new WebGLRenderer(props)} > ``` Further, a callback passed to GL can now return a promise for async constructors like `WebGPURenderer` (see [WebGPU](#webgpu)). ```tsx { // ... return renderer }} > ```` ## WebGPU Recent Three.js now includes a WebGPU renderer. While still a work in progress and not fully backward-compatible with all of Three's features, the renderer requires an async initialization method. R3F streamlines this by allowing the gl prop to return a promise. ```tsx import * as THREE from 'three/webgpu' import * as TSL from 'three/tsl' import { Canvas, extend, useFrame, useThree } from '@react-three/fiber' declare module '@react-three/fiber' { interface ThreeElements extends ThreeToJSXElements {} } extend(THREE as any) export default () => ( { const renderer = new THREE.WebGPURenderer(props as any) await renderer.init() return renderer }}> ) ``` ## Fixes ### Color Management of Textures Automatic sRGB conversion of texture props has been removed. Color textures are now handled automatically for built-in materials, aligning with vanilla Three.js behavior. This prevents issues where data textures (e.g., normals or displacement) become corrupted or non-linear. For custom materials or shaders, annotate color textures with `texture.colorSpace = THREE.SRGBColorSpace` or `texture-colorSpace={THREE.SRGBColorSpace}` in JSX. For more details, see https://threejs.org/docs/#manual/en/introduction/Color-management. ### Suspense and Side-Effects The handling of Suspense and fallback content has improved in React and R3F. Side-effects like attach and constructor effects (e.g., controls adding event listeners) no longer fire repeatedly without proper cleanup during suspension. ```jsx import { ThreeElement, useThree } from '@react-three/fiber' import { OrbitControls } from 'three/addons' declare module '@react-three/fiber' { interface ThreeElements { OrbitControls: ThreeElement } } extend({ OrbitControls }) function Controls() { const camera = useThree((state) => state.camera) const gl = useThree((state) => state.gl) // Will only initialize when tree is connected to screen return } ``` ### Swapping with args and primitives Swapping elements when changing the `args` or primitive `object` prop has been improved for structured children like arrays or iterators (React supports both, including async iterators). Previously, primitives sharing an object could update out of order or be removed from the scene along with their children. See: https://github.com/pmndrs/react-three-fiber/pull/3272 ## TypeScript Changes ### Props renamed to CanvasProps Canvas `Props` is now called `CanvasProps` for clarity. These were aliased in v8 for forward compatibility, but `Props` is removed with v9. ```diff -function Canvas(props: Props) +function Canvas(props: CanvasProps) ``` ### Dynamic JSX Types Since R3F's creation, JSX types had to be maintained in accordance with additions to three core API. Although missing or future types could be ignored or resilient for forward and backwards compatibility, this was a major maintenance burden for us and those extending three. Furthermore, libraries which wrap or bind to the known catalog of elements (e.g. React Spring ``) had no way of knowing which elements belonged to a renderer. Since v8, we've added a catalog of known elements to a `ThreeElements` interface, and with v9 automatically map three API to JSX types. As types are now dynamically mapped, hardcoded exports like `MeshProps` have been removed, and can be accessed as `ThreeElements['mesh']`. Helper types like `Color` or `Vector3` remain to reflect the JSX `MathType` API for shorthand expression. ```diff -import { MeshProps } from '@react-three/fiber' -type Props = MeshProps +import { ThreeElements } from '@react-three/fiber' +type Props = ThreeElements['mesh'] ``` ### Node Helpers Specialized `Node` type helpers for extending JSX (`Node`, `Object3DNode`, `BufferGeometryNode`, `MaterialNode`, `LightNode`) are removed and combined into 'ThreeElement', which accepts a single type representing the extended element instance. ```tsx import { type ThreeElement } from '@react-three/fiber' declare module '@react-three/fiber' { interface ThreeElements { customElement: ThreeElement } } extend({ CustomElement }) ``` ### ThreeElements `ThreeElements` was added as an interface since v8.1.0 and is the current way of declaring or accessing JSX within R3F since React's depreciation of `global` JSX (see https://react.dev/blog/2024/04/25/react-19-upgrade-guide#the-jsx-namespace-in-typescript). All JSX types belonging to R3F are accessible from `ThreeElements`. ```diff -import { type Node } from '@react-three/fiber' - -declare global { - namespace JSX { - interface IntrinsicElements { - customElement: Node - } - } -} - -extend({ CustomElement }) +import { type ThreeElement } from '@react-three/fiber' + +declare module '@react-three/fiber' { + interface ThreeElements { + customElement: ThreeElement + } +} + +extend({ CustomElement }) ``` ## Testing ### StrictMode `StrictMode` is now correctly inherited from a parent renderer like react-dom. Previously, `` mounted in another root such as react-dom would not affect the R3F canvas, so it had to be redeclared within the canvas as well. ```diff - - // ... - + // ... ``` Keep in mind, this change may affect the behavior of your application. If you encounter anything that worked before and fails now, profile it first in dev and then production. If it works in prod then strict mode has flushed out a side-effect in your code. ### Act `act` is now exported from React itself and can be used for all renderers. It will return the contents of a passed async callback like before and recursively flush async effects to synchronously test React output. ```tsx import { act } from 'react' import { createRoot } from '@react-three/fiber' const store = await act(async () => createRoot(canvas).render()) console.log(store.getState()) ``` ================================================ FILE: example/.gitignore ================================================ node_modules .DS_Store dist dist-ssr *.local ================================================ FILE: example/CHANGELOG.md ================================================ # example ## 1.1.0 ### Minor Changes - 85c80e70: eventsource and eventprefix on the canvas component ## 1.0.0 ### Major Changes - 385ba9c: v8 major, react-18 compat - 04c07b8: v8 major, react-18 compat ## 1.0.0-beta.0 ### Major Changes - 385ba9c: v8 major, react-18 compat ================================================ FILE: example/index.html ================================================ react-three-fiber example
    ================================================ FILE: example/package.json ================================================ { "name": "example", "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "serve": "vite preview" }, "dependencies": { "@react-three/drei": "^9.105.5", "@use-gesture/react": "latest", "react": "19.2.0", "react-dom": "19.2.0", "react-use-refs": "^1.0.1", "three": "^0.172.0", "three-stdlib": "^2.35.16", "use-error-boundary": "^2.0.6", "wouter": "^2.12.1", "zustand": "^4.4.7" }, "devDependencies": { "@types/three": "^0.172.0", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react-refresh": "^1.3.6", "@vitejs/plugin-react": "^4.2.1", "typescript": "^5.3.3", "vite": "^5.2.10" } } ================================================ FILE: example/public/apple.gltf ================================================ { "extensionsUsed": ["KHR_materials_unlit", "KHR_draco_mesh_compression"], "asset": { "generator": "UniGLTF-1.27", "version": "2.0" }, "buffers": [ { "name": "buffer", "byteLength": 1696, "uri": "data:application/octet-stream;base64,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" } ], "bufferViews": [ { "buffer": 0, "byteOffset": 0, "byteLength": 733 }, { "buffer": 0, "byteOffset": 736, "byteLength": 249 }, { "buffer": 0, "byteOffset": 988, "byteLength": 303 }, { "buffer": 0, "byteOffset": 1292, "byteLength": 216 }, { "buffer": 0, "byteOffset": 1508, "byteLength": 188 } ], "accessors": [ { "type": "SCALAR", "componentType": 5125, "count": 270, "min": [0], "max": [135] }, { "type": "VEC3", "componentType": 5126, "count": 136, "max": [0.09829112117938082, 0.15707400633348856, 0.09824317429100063], "min": [-0.00009589377676037153, -0.00009589377676037153, -0.09824317429100064] }, { "type": "VEC3", "componentType": 5126, "count": 136, "min": [-1.007843137254902, -1.007843137254902, -0.9083239440824471], "max": [0.9058823529411764, 1.007843137254902, 0.9112838990548078] }, { "type": "VEC2", "componentType": 5126, "count": 136, "min": [-3.872480570978666, -5.190146624750639], "max": [3.868810683984095, 3.4346615669198286] }, { "type": "SCALAR", "componentType": 5125, "count": 30, "min": [0], "max": [19] }, { "type": "VEC3", "componentType": 5126, "count": 20, "max": [0.00963633247975553, 0.19077478181780055, 0.009620332180030559], "min": [-0.000021366590864202948, 0.1469946371370487, -0.009630966188616297] }, { "type": "VEC3", "componentType": 5126, "count": 20, "min": [-1.0078249400737238, -0.10404930024170408, -1.0031846588733149], "max": [1.003184658873315, 1.007092190227088, 1.0000000000000002] }, { "type": "VEC2", "componentType": 5126, "count": 20, "min": [-0.38604318408089766, -6.51924526003915], "max": [0.3852044690039851, 1.386043184080898] }, { "type": "SCALAR", "componentType": 5125, "count": 78, "min": [0], "max": [23] }, { "type": "VEC3", "componentType": 5126, "count": 24, "max": [0.00007905138808456816, 0.1404482741236439, 0.08098814709264007], "min": [-0.00007905138808456816, 0.016574748995125614, -0.08098814709264009] }, { "type": "VEC3", "componentType": 5126, "count": 24, "min": [-1.003921568627451, -0.00392156862745098, -0.00392156862745098], "max": [-0.996078431372549, 0.00392156862745098, 0.00392156862745098] }, { "type": "VEC2", "componentType": 5126, "count": 24, "min": [-3.191625186308621, -4.532298156126736], "max": [3.191625186308621, 0.35010993375806176] }, { "type": "SCALAR", "componentType": 5125, "count": 12, "min": [0], "max": [7] }, { "type": "VEC3", "componentType": 5126, "count": 8, "max": [0.00002426540176054298, 0.09019649050616613, 0.024859904103676285], "min": [-0.00002426540176054298, 0.06954663360794405, -0.024859904103676285] }, { "type": "VEC3", "componentType": 5126, "count": 8, "min": [-1.003921568627451, -0.00392156862745098, -0.00392156862745098], "max": [-0.996078431372549, 0.00392156862745098, 0.00392156862745098] }, { "type": "VEC2", "componentType": 5126, "count": 8, "min": [-0.9796926527778425, -2.552443459586314], "max": [0.9796926527778425, -1.7361922152231166] }, { "type": "SCALAR", "componentType": 5125, "count": 6, "min": [0], "max": [5] }, { "type": "VEC3", "componentType": 5126, "count": 6, "max": [0.005384004925540091, 0.147021261545223, 0.00538663383419514], "min": [-0.000005257817310097745, 0.1470107459106028, -0.00538663383419514] }, { "type": "VEC3", "componentType": 5126, "count": 6, "min": [-0.00392156862745098, 0.996078431372549, -0.00392156862745098], "max": [0.00392156862745098, 1.003921568627451, 0.00392156862745098] }, { "type": "VEC2", "componentType": 5126, "count": 6, "min": [-0.38001393852345755, -6.511660094834492], "max": [0.21416659508982017, -4.786348347090556] } ], "materials": [ { "name": "red", "pbrMetallicRoughness": { "baseColorFactor": [0.9098039, 0.333333343, 0.3254902, 1], "metallicFactor": 1, "roughnessFactor": 1 }, "doubleSided": false, "alphaMode": "OPAQUE", "emissiveFactor": [0, 0, 0] }, { "name": "brown", "pbrMetallicRoughness": { "baseColorFactor": [0.827451, 0.5647059, 0.403921574, 1], "metallicFactor": 1, "roughnessFactor": 1 }, "doubleSided": false, "alphaMode": "OPAQUE", "emissiveFactor": [0, 0, 0] }, { "name": "brownLight", "pbrMetallicRoughness": { "baseColorFactor": [0.9764706, 0.772549033, 0.549019635, 1], "metallicFactor": 1, "roughnessFactor": 1 }, "doubleSided": false, "alphaMode": "OPAQUE", "emissiveFactor": [0, 0, 0] }, { "name": "brownDark", "pbrMetallicRoughness": { "baseColorFactor": [0.6392157, 0.3882353, 0.2784314, 1], "metallicFactor": 1, "roughnessFactor": 1 }, "doubleSided": false, "alphaMode": "OPAQUE", "emissiveFactor": [0, 0, 0] }, { "name": "_defaultMat", "pbrMetallicRoughness": { "baseColorFactor": [1, 1, 1, 1], "metallicFactor": 1, "roughnessFactor": 1 }, "doubleSided": false, "alphaMode": "OPAQUE", "emissiveFactor": [0, 0, 0] } ], "meshes": [ { "name": "Mesh appleHalf", "primitives": [ { "mode": 4, "indices": 0, "attributes": { "POSITION": 1, "NORMAL": 2, "TEXCOORD_0": 3 }, "material": 0, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 0, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } }, { "mode": 4, "indices": 4, "attributes": { "POSITION": 5, "NORMAL": 6, "TEXCOORD_0": 7 }, "material": 1, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 1, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } }, { "mode": 4, "indices": 8, "attributes": { "POSITION": 9, "NORMAL": 10, "TEXCOORD_0": 11 }, "material": 2, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 2, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } }, { "mode": 4, "indices": 12, "attributes": { "POSITION": 13, "NORMAL": 14, "TEXCOORD_0": 15 }, "material": 3, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 3, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } }, { "mode": 4, "indices": 16, "attributes": { "POSITION": 17, "NORMAL": 18, "TEXCOORD_0": 19 }, "material": 4, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 4, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } } ] } ], "nodes": [ { "children": [1], "name": "tmpParent", "translation": [0, 0, 0], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1] }, { "name": "appleHalf", "translation": [0, 0, 0], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1], "mesh": 0 } ], "scenes": [{ "nodes": [1] }], "scene": 0, "extensionsRequired": ["KHR_draco_mesh_compression"] } ================================================ FILE: example/public/bottle.gltf ================================================ { "extensionsUsed": ["KHR_materials_unlit", "KHR_draco_mesh_compression"], "asset": { "generator": "UniGLTF-1.27", "version": "2.0" }, "buffers": [ { "name": "buffer", "byteLength": 1004, "uri": "data:application/octet-stream;base64,RFJBQ08CAgEBAAAAJDQCMwMCGQ8ZCwMQ79auW9NWVfEva0vXddGiAIACAHFkC4gpS5BuMmCDL1WAZAuIKUuQbjJggy9VgAP/AAEAAQABAQABAAkDAAACAQEJAwABAwEDCQIAAgIEAQEADAPJARPlCFUVHQeRE1UFDCNY15ilc8hSzX+8Xw48sQv+QBEc5E7EKhDg+xP/AYACUHwHAPgEAPgHADgKABwFAb5B8R8AngAOQQEoqgCQFRQAQBAAHFIAAPALsHPUBgQCYCUAELwBwMUCAIAbAAAAgDNtALBYAF1+ucTMJ4DFJwBGEgEAABcBNBEFAKgBwG0AAA63BNHgA4EaWQWKsQfEQgarA7/KggRAA0CIgAAAAAD/BwAA5dK4vQAAAADnD6C9uDkFPwsGAwEBBP8B4Sy5Cf///+9pCSHsJJGNFX1HBAI97/XOnNfjTSqhdGtLEH5V8IWKRsmHS4D/AAAAfwAAAP8CGEgIBQEBAAsDKQi9GUEBC+EBJQM5F6Ac4zcnvBmY1hE3kfWoH7ZVdyCtMA/1+1Uf6eVPgI6JnaMhYg6H8Bqi8K6h6DuH8BoiKQHAa4jCu4aigP+j6Ugk6pKmi9QCSJoOUQsgabpILYCk6SI8mo6oGXjhOhIYrl/ArpjyukrsIqSzxy6qdSXxYSOKe5EduyBeiJgSOyIRQErsiEQAKbGDDOB67CJK6nb0IiIUQEcvqnj0ggA0AAAAAQKCQwAAAAD/AwAAXsAvwPpQmcGLV7RBCgBEUkFDTwICAQEAAAAeKAIoAQEnCwEMX7tu7fK1bakWVS0t/wEiiAnoNmQvEM0Sa4CICeg2ZC8QzRJrgAP/AAEAAQABAQABAAkDAAACAQEJAwABAwEDCQIAAgIEAQEADAOJCA/NDEUEAzUTaQbNDAuvfqTVRnKEpXCJbjoGgLSQIGoADCfUlBoBPisAnj0CAJ8B8CwSQg0A4oQaACTQawXgAwkApGPUlgEAADxtAdgJ4DGAtQAA3AKwE7gA8LT1mCfwANgJ3AHAswPqHwA6wP4BAAqzA5uFTBxlBg5hgZ55gAAAAAAAAP8HAAAA1aq9mwUaPuPxk72E69E+CwYDAQEEAkE4wQcL0OIw5NWXSnUq+5b/AAAAfwAAAP8C3UQIBQEBAAsDhQ9VFekCA/gD+K0K8QG1CRbt1EYh/80XV9wKBm+OKvgrGjszv/+VFP3JQJIgRUpOlCQ6kZIUJYlyEABFSaIwUZLoREpSlCTKENIA2oD44ICKAmDin6IAoKIAkAioKAAm/ikKACoKvvkVEIguxiIrJgAAAAECf0IAAAAA/wMAAF0t0r/6gajBJXy6QQo=" } ], "bufferViews": [ { "buffer": 0, "byteOffset": 0, "byteLength": 571 }, { "buffer": 0, "byteOffset": 572, "byteLength": 432 } ], "accessors": [ { "type": "SCALAR", "componentType": 5125, "count": 156, "min": [0], "max": [101] }, { "type": "VEC3", "componentType": 5126, "count": 102, "max": [0.09051262757649467, 0.5206661997740596, 0.07845129908718129], "min": [-0.09050023093870575, -0.00025423154285842753, -0.07840956285646851] }, { "type": "VEC3", "componentType": 5126, "count": 102, "min": [-0.8701851585332085, -1.0078100779477288, -1.0035950162831475], "max": [0.8714622238103082, 0.38238379674799305, 1.0035950162831475] }, { "type": "VEC2", "componentType": 5126, "count": 102, "min": [-2.768152080789456, -19.186575256601223], "max": [2.762863699408687, 3.400242172494778] }, { "type": "SCALAR", "componentType": 5125, "count": 120, "min": [0], "max": [65] }, { "type": "VEC3", "componentType": 5126, "count": 66, "max": [0.0836303700134704, 0.5606122827917177, 0.07237291420137934], "min": [-0.0836143708616335, 0.15021171506346873, -0.07243899915514775] }, { "type": "VEC3", "componentType": 5126, "count": 66, "min": [-0.8738685851003609, -1.007843137254902, -1.007843137254902], "max": [0.8751510227427763, 1.007843137254902, 1.007843137254902] }, { "type": "VEC2", "componentType": 5126, "count": 66, "min": [-1.6647959047981256, -21.086251645726776], "max": [1.6620370837134235, 2.2699388558097837] } ], "materials": [ { "name": "brownDark", "pbrMetallicRoughness": { "baseColorFactor": [0.6392157, 0.3882353, 0.2784314, 1], "metallicFactor": 1, "roughnessFactor": 1 }, "doubleSided": false, "alphaMode": "OPAQUE", "emissiveFactor": [0, 0, 0] }, { "name": "red", "pbrMetallicRoughness": { "baseColorFactor": [0.9098039, 0.333333343, 0.3254902, 1], "metallicFactor": 1, "roughnessFactor": 1 }, "doubleSided": false, "alphaMode": "OPAQUE", "emissiveFactor": [0, 0, 0] } ], "meshes": [ { "name": "Mesh sodaBottle", "primitives": [ { "mode": 4, "indices": 0, "attributes": { "POSITION": 1, "NORMAL": 2, "TEXCOORD_0": 3 }, "material": 0, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 0, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } }, { "mode": 4, "indices": 4, "attributes": { "POSITION": 5, "NORMAL": 6, "TEXCOORD_0": 7 }, "material": 1, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 1, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } } ] } ], "nodes": [ { "children": [1], "name": "tmpParent", "translation": [0, 0, 0], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1] }, { "name": "sodaBottle", "translation": [0, 0, 0], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1], "mesh": 0 } ], "scenes": [{ "nodes": [1] }], "scene": 0, "extensionsRequired": ["KHR_draco_mesh_compression"] } ================================================ FILE: example/public/farm.gltf ================================================ { "asset": { "generator": "Khronos glTF Blender I/O v1.5.17", "version": "2.0" }, "scene": 0, "scenes": [{ "name": "Scene", "nodes": [0, 1, 2, 3, 4, 5, 6, 7, 8] }], "nodes": [ { "mesh": 0, "name": "Circle", "scale": [3.7607693672180176, 3.7607693672180176, 3.7607693672180176], "translation": [0.47165822982788086, 0, -0.6066201329231262], "rotation": [0, 0, 0, 1] }, { "mesh": 1, "name": "WindMill.001", "scale": [0.4821113348007202, 0.4821113348007202, 0.4821113348007202], "translation": [1.263749599456787, 1.900821566581726, 0.1711728870868683], "rotation": [0, 0, 0, 1] }, { "mesh": 2, "name": "Fence_White.013", "rotation": [0, 0.7071068286895752, 0, 0.7071068286895752], "scale": [0.09888678789138794, 0.09888678789138794, 0.09888678789138794], "translation": [1.3952089548110962, -0.005745698232203722, -2.4713940620422363] }, { "mesh": 3, "name": "Wooden Box", "scale": [0.4007795751094818, 0.4007795751094818, 0.4007795751094818], "translation": [0.19056254625320435, 0.05061200261116028, 0.21082305908203125], "rotation": [0, 0, 0, 1] }, { "mesh": 4, "name": "Plane", "scale": [0.0304397102445364, 0.0304397102445364, 0.0304397102445364], "translation": [-0.16933752596378326, 1.0484503507614136, 0.31743738055229187], "rotation": [0, 0, 0, 1] }, { "mesh": 5, "name": "Well", "scale": [0.1421276479959488, 0.12818977236747742, 0.1421276479959488], "translation": [2.077780246734619, 0.22757045924663544, -0.05814981460571289], "rotation": [0, 0, 0, 1] }, { "mesh": 6, "name": "Trees", "scale": [62.94956588745117, 62.94956588745117, 62.94956588745117], "translation": [0.37176066637039185, 0.14777842164039612, -1.3625603914260864], "rotation": [0, 0, 0, 1] }, { "mesh": 7, "name": "Barn_01", "scale": [1.1511201858520508, 1.1511201858520508, 1.1511201858520508], "translation": [-0.17597436904907227, 0.4125245213508606, -0.17850732803344727], "rotation": [0, 0, 0, 1] }, { "mesh": 8, "name": "WindMill", "scale": [0.4821113348007202, 0.4821113348007202, 0.4821113348007202], "translation": [1.2636184692382812, 1.564571499824524, 0.07810831815004349], "rotation": [0, 0, 0, 1] } ], "materials": [ { "doubleSided": true, "name": "Farm_Pack_001", "pbrMetallicRoughness": { "baseColorTexture": { "index": 0, "texCoord": 0 }, "metallicFactor": 0, "roughnessFactor": 0.9863636493682861, "baseColorFactor": [1, 1, 1, 1] }, "emissiveFactor": [0, 0, 0], "alphaMode": "OPAQUE" }, { "name": "default", "emissiveFactor": [0, 0, 0], "alphaMode": "OPAQUE", "doubleSided": false } ], "meshes": [ { "name": "Circle", "primitives": [ { "attributes": { "POSITION": 1, "NORMAL": 2 }, "indices": 0, "mode": 4, "material": 1, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 0, "attributes": { "POSITION": 0, "NORMAL": 1 } } } } ] }, { "name": "Cube.029", "primitives": [ { "attributes": { "POSITION": 4, "NORMAL": 5, "TEXCOORD_0": 6 }, "indices": 3, "material": 0, "mode": 4, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 1, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } } ] }, { "name": "Cube.027", "primitives": [ { "attributes": { "POSITION": 8, "NORMAL": 9, "TEXCOORD_0": 10 }, "indices": 7, "material": 0, "mode": 4, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 2, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } } ] }, { "name": "Cube.005", "primitives": [ { "attributes": { "POSITION": 12, "NORMAL": 13, "TEXCOORD_0": 14 }, "indices": 11, "material": 0, "mode": 4, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 3, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } } ] }, { "name": "Plane.003", "primitives": [ { "attributes": { "POSITION": 16, "NORMAL": 17, "TEXCOORD_0": 18 }, "indices": 15, "material": 0, "mode": 4, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 4, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } } ] }, { "name": "Cylinder.001", "primitives": [ { "attributes": { "POSITION": 20, "NORMAL": 21, "TEXCOORD_0": 22 }, "indices": 19, "material": 0, "mode": 4, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 5, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } } ] }, { "name": "Plane.002", "primitives": [ { "attributes": { "POSITION": 24, "NORMAL": 25, "TEXCOORD_0": 26 }, "indices": 23, "material": 0, "mode": 4, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 6, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } } ] }, { "name": "Cube.002", "primitives": [ { "attributes": { "POSITION": 28, "NORMAL": 29, "TEXCOORD_0": 30 }, "indices": 27, "material": 0, "mode": 4, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 7, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } } ] }, { "name": "Cube", "primitives": [ { "attributes": { "POSITION": 32, "NORMAL": 33, "TEXCOORD_0": 34 }, "indices": 31, "material": 0, "mode": 4, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 8, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } } ] } ], "textures": [{ "sampler": 0, "source": 0 }], "images": [{ "bufferView": 9, "mimeType": "image/jpeg", "name": "Diffuse_palette_noise" }], "accessors": [ { "componentType": 5123, "count": 90, "type": "SCALAR" }, { "componentType": 5126, "count": 32, "max": [0.8007816435886583, 0.0007816316677292907, 0.8007816435886583], "min": [-0.8007816435886582, -0.0007816316677292907, -0.8007816435886582], "type": "VEC3" }, { "componentType": 5126, "count": 32, "type": "VEC3" }, { "componentType": 5123, "count": 2076, "type": "SCALAR" }, { "componentType": 5126, "count": 1322, "max": [0.8385503353814003, 0.8385502161721108, 0.05095415724360425], "min": [-0.8385503353814002, -0.8385504545906898, -0.1684030803183433], "type": "VEC3" }, { "componentType": 5126, "count": 1322, "type": "VEC3" }, { "componentType": 5126, "count": 1322, "type": "VEC2" }, { "componentType": 5123, "count": 3060, "type": "SCALAR" }, { "componentType": 5126, "count": 2000, "max": [0.10322994759563642, 1.6502795766030693, 2.2154936462014696], "min": [-39.21565765908245, -0.019190028951344522, -23.70925850441937], "type": "VEC3" }, { "componentType": 5126, "count": 2000, "type": "VEC3" }, { "componentType": 5126, "count": 2000, "type": "VEC2" }, { "componentType": 5123, "count": 2292, "type": "SCALAR" }, { "componentType": 5126, "count": 1694, "max": [0.18579399166423982, 0.1461265524262385, 0.14491311928442063], "min": [-0.14491329809835496, -0.1464015217702681, -0.9569067778270537], "type": "VEC3" }, { "componentType": 5126, "count": 1694, "type": "VEC3" }, { "componentType": 5126, "count": 1694, "type": "VEC2" }, { "componentType": 5123, "count": 822, "type": "SCALAR" }, { "componentType": 5126, "count": 624, "max": [4.812053583748002, 0.0031038604833231176, 0.24871478110741096], "min": [-5.050960444099564, -8.329193495399613, -0.3048462795087286], "type": "VEC3" }, { "componentType": 5126, "count": 624, "type": "VEC3" }, { "componentType": 5126, "count": 624, "type": "VEC2" }, { "componentType": 5123, "count": 2370, "type": "SCALAR" }, { "componentType": 5126, "count": 1493, "max": [1.5453098778265772, 1.1136803437164486, 0.867781436321033], "min": [-1.307572226612872, -1.810559492199725, -1.2458345700195492], "type": "VEC3" }, { "componentType": 5126, "count": 1493, "type": "VEC3" }, { "componentType": 5126, "count": 1493, "type": "VEC2" }, { "componentType": 5123, "count": 768, "type": "SCALAR" }, { "componentType": 5126, "count": 526, "max": [0.016896564860131663, 0.017585191424480177, 0.004240338889400612], "min": [-0.013106122959639949, -0.0036612214727812227, -0.01328685355582468], "type": "VEC3" }, { "componentType": 5126, "count": 526, "type": "VEC3" }, { "componentType": 5126, "count": 526, "type": "VEC2" }, { "componentType": 5123, "count": 2226, "type": "SCALAR" }, { "componentType": 5126, "count": 1338, "max": [0.32661417775988055, 0.3417208139262784, 0.47399327198237284], "min": [-0.3267274502870451, -0.36584189573496684, -0.44418025056094035], "type": "VEC3" }, { "componentType": 5126, "count": 1338, "type": "VEC3" }, { "componentType": 5126, "count": 1338, "type": "VEC2" }, { "componentType": 5123, "count": 984, "type": "SCALAR" }, { "componentType": 5126, "count": 638, "max": [0.7299346994293803, 0.958772202963706, 0.4743475615832653], "min": [-0.7322520222614961, -3.2909023966740327, -1.4544944968174653], "type": "VEC3" }, { "componentType": 5126, "count": 638, "type": "VEC3" }, { "componentType": 5126, "count": 638, "type": "VEC2" } ], "bufferViews": [ { "buffer": 0, "byteOffset": 0, "byteLength": 251 }, { "buffer": 0, "byteOffset": 252, "byteLength": 3939 }, { "buffer": 0, "byteOffset": 4192, "byteLength": 2986 }, { "buffer": 0, "byteOffset": 7180, "byteLength": 2912 }, { "buffer": 0, "byteOffset": 10092, "byteLength": 1908 }, { "buffer": 0, "byteOffset": 12000, "byteLength": 4237 }, { "buffer": 0, "byteOffset": 16240, "byteLength": 1432 }, { "buffer": 0, "byteOffset": 17672, "byteLength": 2925 }, { "buffer": 0, "byteOffset": 20600, "byteLength": 1771 }, { "buffer": 0, "byteOffset": 22372, "byteLength": 85348 } ], "samplers": [{ "magFilter": 9729, "minFilter": 9987, "wrapS": 10497, "wrapT": 10497 }], "buffers": [ { "name": "buffer", "byteLength": 107720, "uri": "data:application/octet-stream;base64,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dG9tbG9uZwAAAAAAAAAMY3JvcFJlY3RMZWZ0bG9uZwAAAAAAAAANY3JvcFJlY3RSaWdodGxvbmcAAAAAAAAAC2Nyb3BSZWN0VG9wbG9uZwAAAAAAOEJJTQPtAAAAAAAQAEgAAAABAAEASAAAAAEAAThCSU0EJgAAAAAADgAAAAAAAAAAAAA/gAAAOEJJTQQNAAAAAAAEAAAAHjhCSU0EGQAAAAAABAAAAB44QklNA/MAAAAAAAkAAAAAAAAAAAEAOEJJTScQAAAAAAAKAAEAAAAAAAAAAThCSU0D9QAAAAAASAAvZmYAAQBsZmYABgAAAAAAAQAvZmYAAQChmZoABgAAAAAAAQAyAAAAAQBaAAAABgAAAAAAAQA1AAAAAQAtAAAABgAAAAAAAThCSU0D+AAAAAAAcAAA/////////////////////////////wPoAAAAAP////////////////////////////8D6AAAAAD/////////////////////////////A+gAAAAA/////////////////////////////wPoAAA4QklNBAAAAAAAAAIAAjhCSU0EAgAAAAAABgAAAAAAADhCSU0EMAAAAAAAAwEBAQA4QklNBC0AAAAAAAYAAQAAAAU4QklNBAgAAAAAABAAAAABAAACQAAAAkAAAAAAOEJJTQQeAAAAAAAEAAAAADhCSU0EGgAAAAADcQAAAAYAAAAAAAAAAAAAAgAAAAIAAAAAHgBEAGkAZgBmAHUAcwBlAF8AcABhAGwAZQB0AHQAZQBfAG4AbwBpAHMAZQBfAGcAcgBhAGQAaQBlAG4AdAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAACAAAAAgAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAQAAAAAAAG51bGwAAAACAAAABmJvdW5kc09iamMAAAABAAAAAAAAUmN0MQAAAAQAAAAAVG9wIGxvbmcAAAAAAAAAAExlZnRsb25nAAAAAAAAAABCdG9tbG9uZwAAAgAAAAAAUmdodGxvbmcAAAIAAAAABnNsaWNlc1ZsTHMAAAABT2JqYwAAAAEAAAAAAAVzbGljZQAAABIAAAAHc2xpY2VJRGxvbmcAAAAAAAAAB2dyb3VwSURsb25nAAAAAAAA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IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS4zLWMwMTEgNjYuMTQ1NjYxLCAyMDEyLzAyLzA2LTE0OjU2OjI3ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE4LTA1LTE5VDE5OjMxOjQzKzAxOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOC0wNi0xNFQyMzoyNjowNiswMTowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOC0wNi0xNFQyMzoyNjowNiswMTowMCIgeG1wTU06RG9jdW1lbnRJRD0iRDIyRjk4N0IyQjFDNUE5MEI4MUFDNzhBNkZDMjAxQjkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RkY2RDcxMTkyMTcwRTgxMUFBRDVGQjRFNTZDOUNFOTQiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0iRDIyRjk4N0IyQjFDNUE5MEI4MUFDNzhBNkZDMjAxQjkiIGRjOmZvcm1hdD0iaW1hZ2UvanBlZyIgcGhvdG9zaG9wOkxlZ2FjeUlQVENEaWdlc3Q9IkQzQzk5RjRCM0Q4REVBMjg4NTQ4MzU2QTg0MTU5QzJCIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MEJDRjQ1REM2QjVDRTgxMUI3Q0RFOUIwMEI2NjFFNTAiIHN0RXZ0OndoZW49IjIwMTgtMDUtMjBUMjE6MjQ6NTYrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDozNzc3RjMxNDExNkZFODExQUNENjk5OEE5QzcwNzk5NSIgc3RFdnQ6d2hlbj0iMjAxOC0wNi0xM1QxNDo1NjoyNSswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNvbnZlcnRlZCIgc3RFdnQ6cGFyYW1ldGVycz0iZnJvbSBpbWFnZS9qcGVnIHRvIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3AiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImRlcml2ZWQiIHN0RXZ0OnBhcmFtZXRlcnM9ImNvbnZlcnRlZCBmcm9tIGltYWdlL2pwZWcgdG8gYXBwbGljYXRpb24vdm5kLmFkb2JlLnBob3Rvc2hvcCIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6Mzg3N0YzMTQxMTZGRTgxMUFDRDY5OThBOUM3MDc5OTUiIHN0RXZ0OndoZW49IjIwMTgtMDYtMTNUMTQ6NTY6MjUrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpGRTZENzExOTIxNzBFODExQUFENUZCNEU1NkM5Q0U5NCIgc3RFdnQ6d2hlbj0iMjAxOC0wNi0xNFQyMzoyNjowNiswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNvbnZlcnRlZCIgc3RFdnQ6cGFyYW1ldGVycz0iZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL2pwZWciLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImRlcml2ZWQiIHN0RXZ0OnBhcmFtZXRlcnM9ImNvbnZlcnRlZCBmcm9tIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3AgdG8gaW1hZ2UvanBlZyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6RkY2RDcxMTkyMTcwRTgxMUFBRDVGQjRFNTZDOUNFOTQiIHN0RXZ0OndoZW49IjIwMTgtMDYtMTRUMjM6MjY6MDYrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpGRTZENzExOTIxNzBFODExQUFENUZCNEU1NkM5Q0U5NCIgc3RSZWY6ZG9jdW1lbnRJRD0iRDIyRjk4N0IyQjFDNUE5MEI4MUFDNzhBNkZDMjAxQjkiIHN0UmVmOm9yaWdpbmFsRG9jdW1lbnRJRD0iRDIyRjk4N0IyQjFDNUE5MEI4MUFDNzhBNkZDMjAxQjkiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0idyI/Pv/uACFBZG9iZQBkQAAAAAEDABADAgMGAAAAAAAAAAAAAAAA/9sAhAACAgICAgICAgICAwICAgMEAwICAwQFBAQEBAQFBgUFBQUFBQYGBwcIBwcGCQkKCgkJDAwMDAwMDAwMDAwMDAwMAQMDAwUEBQkGBgkNCgkKDQ8ODg4ODw8MDAwMDA8PDAwMDAwMDwwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wgARCAIAAgADAREAAhEBAxEB/8QBHgAAAgIDAQEBAQAAAAAAAAAAAQIABwMGCAkFBAoBAQACAwEBAQAAAAAAAAAAAAABBQIEBgMHCBAAAQMBBQYEBAYBBAMBAQAAAQARAgMSBAUGBxAgEzM0CCEyFBUxJRYXMCMkNRgJIiY2JzhBNyhAGREAAQIDBAQCDRIPDAkFAQEAAQARIQIDMUEEBVFhEgZxBxCBkaGx0SIy0hMzsxTBQpKyI3ODk9MkNHSEtBUlNRYg8OFSckNjZJSFlbUmNjdigqLCo0RU1GV1dhfxU6RFVUZWlnfDpWYnOMQYEgABAgEIBggEBAUEAQUAAAABAAIgMXGBscEycgMRkaHRsjMwQCFBgpJzBFFhEjTwIsKzEEJSg5NQ4dITYmCiI1Nj/9oADAMBAQIRAxEAAADViiSCiCjnoCevDH8xCECFP0Dzi5/oqS1dpBjIlh4dH79f0hh56trbBIQgTZ9rX3n11/l6vsEggSI+nseXlLzv3DQda0AgwUw60u+Cq901O6XTRBmSNBjq22+fWJbc9Wnl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" } ], "extensionsRequired": ["KHR_draco_mesh_compression"], "extensionsUsed": ["KHR_draco_mesh_compression"] } ================================================ FILE: example/public/lightning.gltf ================================================ { "asset": { "generator": "Khronos glTF Blender I/O v1.5.17", "version": "2.0" }, "scene": 0, "scenes": [ { "name": "Scene", "nodes": [0] } ], "nodes": [ { "mesh": 0, "name": "lightning", "rotation": [0.7071068286895752, 0, 0, 0.7071067094802856] } ], "materials": [ { "doubleSided": true, "name": "Yellow.026", "pbrMetallicRoughness": { "baseColorFactor": [1, 0.5052202939987183, 0.017695415765047073, 1], "metallicFactor": 0, "roughnessFactor": 0.20000000298023224 } } ], "meshes": [ { "name": "Cube.1372", "primitives": [ { "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 }, "indices": 3, "material": 0 } ] } ], "accessors": [ { "bufferView": 0, "componentType": 5126, "count": 206, "max": [0.3984317481517792, 0.1398487091064453, 0.6621757745742798], "min": [-0.4041382372379303, -0.1398487091064453, -0.5749139785766602], "type": "VEC3" }, { "bufferView": 1, "componentType": 5126, "count": 206, "type": "VEC3" }, { "bufferView": 2, "componentType": 5126, "count": 206, "type": "VEC2" }, { "bufferView": 3, "componentType": 5123, "count": 684, "type": "SCALAR" } ], "bufferViews": [ { "buffer": 0, "byteLength": 2472, "byteOffset": 0 }, { "buffer": 0, "byteLength": 2472, "byteOffset": 2472 }, { "buffer": 0, "byteLength": 1648, "byteOffset": 4944 }, { "buffer": 0, "byteLength": 1368, "byteOffset": 6592 } ], "buffers": [ { "byteLength": 7960, "uri": "data:application/octet-stream;base64,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mQCdAKQAnQCoAJwAowCnAKkArgCxAKkAsQCvAK4AswC0AK4AtACxAKoAsgC0AKoAtACzALIAuAC7ALIAuwC0AK0AtwC7AK0AuwC6ALcAsACxALcAsQC7ALEAtAC7AL4AwgDGAL4AxgDFAMEAyADJAMEAyQDGAL8AxwDJAL8AyQDIAMcAzADNAMcAzQDJAMAAygDNAMAAzQDMAMsAxQDGAMsAxgDNAMYAyQDNAIYAdAB6AIYAegCJAIkAegCAAIkAgACIAIgAgAB3AIgAdwCFAFEAhgCJAFEAiQBUAFQAiQCHAFQAhwBSAFIAhwCEAFIAhABPAGcASQBMAGcATABsAGwATABNAGwATQBoAGkATgBKAGkASgBlAFAAbQBwAFAAcABTAFMAcABzAFMAcwBVAFQAcgBvAFQAbwBRAJUAigCNAJUAjQClAKUAjQCRAKUAkQCbAJoAkACMAJoAjACTADoAlACfADoAnwA/AD8AnwCWAD8AlgA7ADwAlwCSADwAkgA4ADkAVwBbADkAWwA9AD0AWwBcAD0AXAA+AD8AXQBYAD8AWAA6AF8AQQBFAF8ARQBjAGMARQBHAGMARwBiAGEARgBCAGEAQgBeAIoAQgBGAIoARgCNAI0ARgBEAI0ARACOAI8AQwBAAI8AQACLAHUASgBOAHUATgB8AHwATgBLAHwASwCBAIEASwBIAIEASAB2AKsAZgBrAKsAawC2ALUAagBpALUAaQCvAK8AaQBlAK8AZQCpAMAAWABdAMAAXQDKAMoAXQBZAMoAWQDDAMQAWgBWAMQAVgC8AF4AvwDIAF4AyABhAGEAyADBAGEAwQBkAGQAwQC9AGQAvQBgAG4ArAC5AG4AuQBxAHEAuQCyAHEAsgByAHIAsgCqAHIAqgBvAL8AlQCmAL8ApgDHAMcApgCeAMcAngDMAMwAngCUAMwAlADAAHQAqgCzAHQAswB4AHgAswCuAHgArgB9AH0ArgCpAH0AqQB1ADgAkgAqADgAKgACAA==" } ] } ================================================ FILE: example/public/ramen.gltf ================================================ { "extensionsUsed": ["KHR_materials_unlit", "KHR_draco_mesh_compression"], "asset": { "generator": "UniGLTF-1.27", "version": "2.0" }, "buffers": [ { "name": "buffer", "byteLength": 2576, "uri": "data:application/octet-stream;base64,RFJBQ08CAgEBAAAAKkACPwcCFwsYEAMT/570XVN9itKiUpR33dp1eUpLAYACgHBzDcRkrjM1kxmEiIlDtYQ+C9bd46FKdLSV44yCA/8AAQABAAEBAAEACQMAAAIBAQkDAAEDAQMJAgACAgQBAQAMA4kBE4kBdR6hBxkGxRANvMvnD/xWzI9aW23PgAUzUSD/6pMAYY9ZAwBQds3R9XUAdgcAABQBAIAhojsAiQD2lIIAQDIBAAAidgcQEcB+EgAATuwDAAAJAED5DU8BDhAIDiAIAADAbwBBAAAAokUcLV/Ej2LEPWANAED6dP/MbgAAYBsAcBEASsL48BY3AOCIAAAOALC/eB8FAEBaWHz0gxHgaRsAsA3wAQ3ZA3CuQCp6B0TlBINdNm4GVQMSxIIAAAAAAP8HAAAhOoq+AAAAAGOcn75jnB8/CwYDAQEF/wGVF/kR/Qj////reQ05tYJTxjkdgfigt6FbKpX+oUjG4l7lBUqa6qvE+S0LT+dqJLn7J8bLvXKoWSZrqs+Oz0fMhy2k9lmF/wAAAH8AAAD/AkdJCAUBAQEI2QfhGzjcGzhLODhTOAc4BzgrpCs4Kzg4DzgjEQEr3Dc4NzhLIQIDwQIxAzeBAREBKzg/EQFsODj/LzgPbA+kAzgvpAtsB9wRAQM4jzgXOB9sODtsFzhLMQNlAwN9ARM41zj//5s43zi7OP/nOKekJzg4iziSAbCMlgbqzW4v83sTv08IvPlbVHUmwQzI7/oQE3Tc8aRz9pDLoc2llItEw3eJXqRVhkacIe1+c/wrzPkPbclPLFkBVeO7NAqWAKgd5mnTnPWy4kYdWFsFxT5LnA2oUjGlpnfd2Cp5+qr4xpnaGF5uIJChs9zOVWIH1HvrN9eOPa6nQcEI3QP4PyWrH8VfwEF9zM+AKgAAAAECx0IAAAAA/wMAACVfxMC51ADByoNWQQpEUkFDTwICAQEAAAAMDAIMAQELCwEF79auWwP/ARGABAAV+4CABAAV+4AD/wABAAEAAQEAAQAJAwAAAgEBCQMAAQMBAwkCAAICAQEBAAwDrRobVQWtClUVBQQvP+yK7NZOAABkB+Bu7fwPI2bXP9qtHQAAAAAAIADIzn8AAAAAAP8HAAAhOoq+GochPmOcn75jnB8/CwYDAQEDAQFAAQD/AAAAfwAAAP8CoUEIBQEBAQXbA1UhVQWtDv////////9XrQIBCA1mXjhLDJPHbJJK7AxVDAAAAAECwEAAAAAA/wMAACVfxMC51ADBJV9EQQpEUkFDTwICAQEAAAAWGAIYAQENDQAIb7XU163dti7/ASK0BZj2rTuMtAWY9q07jAP/AAEAAQABAQABAAkDAAACAQEJAwABAwEDCQIAAgIBAQEADAPRBRu9CL0IuSgHfvE6oHdsgObliIozBwBbag4AAADYSC2ZxJFaAmAjtgT6AQCKNgUAPQcAAPzvWQAAoCmAYFAB8P8FAeClTAKAMe8CIEOdADBpAAD+fhYAAHAKQJgAAABMAAAAAP8HAADBW1y+GcpEPq5yfr6ucv4+CwYDAQEDAQFAAQD/AAAAfwAAAP8CWkIIBQEBAAsDTQuVFokHD8UDxQMRDww0bFpucnchWn4bX5PyWryclgq1uwI4gr83oqUjguyNyx2hmAWgpSOC4yV9SueEDIqshIQWAAAAAQJnQQAAAAD/AwAAHY4HwYyGDMGMhpxBCgAAAERSQUNPAgIBAQAAABgWAhYAAAfv+75rq7QK/wJEQIAFABXY00CABQAV2NNAA/8AAQABAAEBAAEACQMAAAIBAQkDAAEDAQMJAgACAgQBAQAMAwEIF60CrRKpGgEICHRQc2xUqK6scAWAsGIAgFatALrjZwDCUAgAQq0BSEkSAaCsvwDAKVgAYUAzPjXYPACQBgCAFz3as5SOAEZ5BAC1GwGQkhAAOgAAAEYAAJRyBGAKEkAKB/8Cd0AIwAOAoIUDVQNCW4AAAAAAAP8HAADWoqs8GcpEPjUb9L2aQYo+CwYDAQEEAsUwPQ8Lj0PPfzN68dYgaYH/AAAAfwAAAP8C8kIIBQEBAAsDGQZtGxOhBz0PoQcNZMxkXjFygqEjA1wvl3ECH2EvejKEF7lALH2RuyBJhrNSiYqA0ebgoy5bVEB8yElhAfEhoiAKxCdK+SkB7QniGkxAe4IWAAAAAQJnQQAAAAD/AwAABgO/wGk1DsG+im9BCkRSQUNPAgIBAQAAABwiAiIIBBcSBBEBEQUhBw1ft79r12VftibrmicL/wER/wKTQv8Ck0ID/wABAAAAAQAAAQAJAwAAAgEBCQMAAQMBAwkCAAICAQEBAAwfSQLdBpUUSSIIwHet2NwSSYE6AUAdcwKAxAMAAP6XLQAA0CcA/7cRAAKwAQBGTyAAA44CAOKuBgACCxQAPBMDQIRzAZDpCoDkFQBiRQKI0xNApoMA4KuEAEBOYwQAj34LwBcCBgDijgGAJo4AgJBBAOAyAAC0DAAA+Da2APAtAAAAAP8HAAA20DK+GcpEPtJ5Tr7Sec4+CwYDAQEDAQFAAQD/AAAAfwAAAP8C6UEIBQEBAAsDARAlKUkCF5UEB/94Yj/1b4+c7LjJ8GiroICAhioiCIoKCBoAAAABAqtBAAAAAP8DAAAp/9vAxAfewMQHfkEKRFJBQ08CAgEBAAAAFiQCIwMBGRABCl/7NX0opa0tqhQBARCICdCaOrV/L8xogLoIjm8qqinckIAD/wABAAEAAQEAAQAJAwAAAgEBCQMAAQMBAwkCAAICBAEBAAwDdREb6QKNDhkdBx7D+wFAH4y2EiUAXK5KALQ1xgCoNWMnfyMGACKLznwAAADz8YcLEu/hvwE4THAQuJS8+BWDTGkZ2hpjAADYN20Rkn05lYf/BgAFZgMOxIEaTwVjdDzSiAAEQAPAhoAAAAAA/wcAAAAAAAAAAAAA4UMJvqfFDD4LBgMBAQT/AQEghRb////vfQkW9f+2fJXebuZwtpi6GEVJbYfOAgdAg/8AAAB/AAAA/wLiQwgFAQEACwM1IxvNBAEYCQCMP8N2kHQZggFWAgAAAADA64a5Il3zPbpSZisAAJCsK2V1VKnr0bkiXXo9ACMDQDKElOWKNAAk82oZBgAAAAECYEAAAAAA/wMAAPgUgMBVvBrA+BSAQAoAAABEUkFDTwICAQEAAAAGBAIEAAAC7wr/ARH/ATP/ATMD/wABAAAAAQAAAQAJAwAAAgEBCQMAAQMBAwkCAAICAQEBAAwHrQofVTUD4GqDAADA/7892f9XON4AoLC8AYBMOLIAAAAAAAD/BwAAZ142PCoU1Dy/NdC9vOONPQsGAwEAAgMBQAEAaQn/AAAAfwAAAP8CZkAIBQEBAAsDrQoBIBtVFQPYGm92B1C3gIgoBAAAAAECQEAAAAAA/wMAAPgUgMAeIU3AJpNJQAoAAAA=" } ], "bufferViews": [ { "buffer": 0, "byteOffset": 0, "byteLength": 716 }, { "buffer": 0, "byteOffset": 716, "byteLength": 240 }, { "buffer": 0, "byteOffset": 956, "byteLength": 329 }, { "buffer": 0, "byteOffset": 1288, "byteLength": 372 }, { "buffer": 0, "byteOffset": 1660, "byteLength": 332 }, { "buffer": 0, "byteOffset": 1992, "byteLength": 377 }, { "buffer": 0, "byteOffset": 2372, "byteLength": 201 } ], "accessors": [ { "type": "SCALAR", "componentType": 5125, "count": 192, "min": [0], "max": [157] }, { "type": "VEC3", "componentType": 5126, "count": 158, "max": [0.2703543085118999, 0.23026423868925677, 0.31204459330309997], "min": [-0.2702793206857598, -0.0003045823263085407, -0.3120445933030999] }, { "type": "VEC3", "componentType": 5126, "count": 158, "min": [-0.9165827636625252, -1.007843137254902, -0.794834673872181], "max": [0.9187113539845335, 1.007843137254902, 0.797322188872917] }, { "type": "VEC2", "componentType": 5126, "count": 158, "min": [-6.149720065638, -8.065039985224182], "max": [6.143466845518915, 5.368346564814026] }, { "type": "SCALAR", "componentType": 5125, "count": 36, "min": [0], "max": [23] }, { "type": "VEC3", "componentType": 5126, "count": 24, "max": [0.2703543085118999, 0.2302325277224831, 0.31204459330309997], "min": [-0.2702793206857598, 0.15743735173474188, -0.3120445933030999] }, { "type": "VEC3", "componentType": 5126, "count": 24, "min": [-1.007843137254902, -0.00784313725490196, -0.8738685851003609], "max": [1.007843137254902, 0.00784313725490196, 0.8751510227427763] }, { "type": "VEC2", "componentType": 5126, "count": 24, "min": [-6.14861161361575, -8.063931533201933], "max": [6.148611613615751, -5.196578995340148] }, { "type": "SCALAR", "componentType": 5125, "count": 72, "min": [0], "max": [33] }, { "type": "VEC3", "componentType": 5126, "count": 34, "max": [0.21549624639222492, 0.22689459351690794, 0.24872712232627459], "min": [-0.2154365424112279, 0.1919344123407405, -0.24872712232627459] }, { "type": "VEC3", "componentType": 5126, "count": 34, "min": [-1.007843137254902, -0.00784313725490196, -1.007843137254902], "max": [1.007843137254902, 1.011764705882353, 1.007843137254902] }, { "type": "VEC2", "componentType": 5126, "count": 34, "min": [-8.491321428546923, -8.801974161396044], "max": [8.492391850125639, 10.801974161396046] }, { "type": "SCALAR", "componentType": 5125, "count": 66, "min": [0], "max": [41] }, { "type": "VEC3", "componentType": 5126, "count": 42, "max": [0.13901636096715753, 0.23425834950625635, 0.1509711552151075], "min": [0.02081975380698529, 0.19204527552048054, -0.11932443415006312] }, { "type": "VEC3", "componentType": 5126, "count": 42, "min": [-0.9195546440049713, 0.4021469720438415, -0.7973163656159943], "max": [0.9195546440049713, 1.0104126898681416, 0.7984866352642284] }, { "type": "VEC2", "componentType": 5126, "count": 42, "min": [-5.983753844789746, -8.90267436175752], "max": [5.98749032957463, 6.097967788271191] }, { "type": "SCALAR", "componentType": 5125, "count": 102, "min": [0], "max": [27] }, { "type": "VEC3", "componentType": 5126, "count": 28, "max": [0.17486788937389763, 0.19237419829667926, 0.20183358953298447], "min": [-0.17481939361394763, 0.19198018445669293, -0.2018335895329845] }, { "type": "VEC3", "componentType": 5126, "count": 28, "min": [-0.00392156862745098, 0.996078431372549, -0.00392156862745098], "max": [0.00392156862745098, 1.003921568627451, 0.00392156862745098] }, { "type": "VEC2", "componentType": 5126, "count": 28, "min": [-6.890417417356579, -6.9539678896161], "max": [6.891286945529464, 8.9539678896161] }, { "type": "SCALAR", "componentType": 5125, "count": 108, "min": [0], "max": [59] }, { "type": "VEC3", "componentType": 5126, "count": 60, "max": [0.12390678747578487, 0.13753989200564087, 0.000066826357513039], "min": [-0.00006715815039337933, -0.00006715815039337933, -0.13411515812845887] }, { "type": "VEC3", "componentType": 5126, "count": 60, "min": [-0.7602580790426217, -0.7602580790426217, -1.007843137254902], "max": [0.7613105484083587, 0.7613105484083587, 1.007843137254902] }, { "type": "VEC2", "componentType": 5126, "count": 60, "min": [-4.006472232404692, -2.4216574454936812], "max": [0.003912570539458038, 1.0057543470712704] }, { "type": "SCALAR", "componentType": 5125, "count": 12, "min": [0], "max": [5] }, { "type": "VEC3", "componentType": 5126, "count": 6, "max": [0.06386241567984013, 0.08609991803816254, -0.03234913807251305], "min": [0.011097060068331669, 0.025854675582431267, -0.10169885818026214] }, { "type": "VEC3", "componentType": 5126, "count": 6, "min": [-0.7563365104151707, 0.6547679302739162, -0.00392156862745098], "max": [-0.7484933731602688, 0.6626110675288182, 0.00392156862745098] }, { "type": "VEC2", "componentType": 5126, "count": 6, "min": [-4.0056384558318765, -3.208225106680265], "max": [-1.2716694134537891, -0.052461290872225064] } ], "materials": [ { "name": "_defaultMat", "pbrMetallicRoughness": { "baseColorFactor": [1, 1, 1, 1], "metallicFactor": 1, "roughnessFactor": 1 }, "doubleSided": false, "alphaMode": "OPAQUE", "emissiveFactor": [0, 0, 0] }, { "name": "purple", "pbrMetallicRoughness": { "baseColorFactor": [0.623529434, 0.5568628, 0.9098039, 1], "metallicFactor": 1, "roughnessFactor": 1 }, "doubleSided": false, "alphaMode": "OPAQUE", "emissiveFactor": [0, 0, 0] }, { "name": "brown", "pbrMetallicRoughness": { "baseColorFactor": [0.827451, 0.5647059, 0.403921574, 1], "metallicFactor": 1, "roughnessFactor": 1 }, "doubleSided": false, "alphaMode": "OPAQUE", "emissiveFactor": [0, 0, 0] }, { "name": "green", "pbrMetallicRoughness": { "baseColorFactor": [0.34117648, 0.7372549, 0.5921569, 1], "metallicFactor": 1, "roughnessFactor": 1 }, "doubleSided": false, "alphaMode": "OPAQUE", "emissiveFactor": [0, 0, 0] }, { "name": "brownLight", "pbrMetallicRoughness": { "baseColorFactor": [0.9764706, 0.772549033, 0.549019635, 1], "metallicFactor": 1, "roughnessFactor": 1 }, "doubleSided": false, "alphaMode": "OPAQUE", "emissiveFactor": [0, 0, 0] }, { "name": "yellow", "pbrMetallicRoughness": { "baseColorFactor": [0.9607843, 0.7254902, 0.258823544, 1], "metallicFactor": 1, "roughnessFactor": 1 }, "doubleSided": false, "alphaMode": "OPAQUE", "emissiveFactor": [0, 0, 0] } ], "meshes": [ { "name": "Mesh bowlBroth", "primitives": [ { "mode": 4, "indices": 0, "attributes": { "POSITION": 1, "NORMAL": 2, "TEXCOORD_0": 3 }, "material": 0, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 0, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } }, { "mode": 4, "indices": 4, "attributes": { "POSITION": 5, "NORMAL": 6, "TEXCOORD_0": 7 }, "material": 1, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 1, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } }, { "mode": 4, "indices": 8, "attributes": { "POSITION": 9, "NORMAL": 10, "TEXCOORD_0": 11 }, "material": 2, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 2, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } }, { "mode": 4, "indices": 12, "attributes": { "POSITION": 13, "NORMAL": 14, "TEXCOORD_0": 15 }, "material": 3, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 3, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } }, { "mode": 4, "indices": 16, "attributes": { "POSITION": 17, "NORMAL": 18, "TEXCOORD_0": 19 }, "material": 4, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 4, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } } ] }, { "name": "Mesh Group 495", "primitives": [ { "mode": 4, "indices": 20, "attributes": { "POSITION": 21, "NORMAL": 22, "TEXCOORD_0": 23 }, "material": 0, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 5, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } }, { "mode": 4, "indices": 24, "attributes": { "POSITION": 25, "NORMAL": 26, "TEXCOORD_0": 27 }, "material": 5, "extensions": { "KHR_draco_mesh_compression": { "bufferView": 6, "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 } } } } ] } ], "nodes": [ { "children": [1], "name": "tmpParent", "translation": [0, 0, 0], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1] }, { "children": [2, 3], "name": "bowlBroth", "translation": [0, 0, 0], "rotation": [0, 0, 0, 1], "scale": [0.93, 0.93, 0.93], "mesh": 0 }, { "name": "Group", "translation": [0.143740281, 0.164340109, -0.104827814], "rotation": [0, 0.49999997, 0, 0.866025448], "scale": [1.07526886, 1.07526886, 1.07526886], "mesh": 1 }, { "name": "Group", "translation": [0.1626537, 0.164340109, 0.07206881], "rotation": [0, 0, 0, 1], "scale": [1.07526886, 1.07526886, 1.07526886], "mesh": 1 } ], "scenes": [{ "nodes": [1] }], "scene": 0, "extensionsRequired": ["KHR_draco_mesh_compression"] } ================================================ FILE: example/src/App.tsx ================================================ import * as React from 'react' import { useErrorBoundary } from 'use-error-boundary' import { Redirect, Route, useRoute } from 'wouter' import { DemoPanel, Dot, Error, Loading, Page } from './components' import './styles.css' import * as demos from './demos' const DEFAULT_COMPONENT_NAME = 'Portals' const visibleComponents: any = Object.entries(demos).reduce((acc, [name, item]) => ({ ...acc, [name]: item }), {}) function ErrorBoundary({ children, fallback, name }: any) { const { ErrorBoundary, didCatch, error } = useErrorBoundary() return didCatch ? fallback(error) : {children} } function Demo() { const [match, params] = useRoute('/demo/:name') const compName = match ? params.name : DEFAULT_COMPONENT_NAME const { Component } = visibleComponents[compName] return ( {e.message}}> ) } function Dots() { const [match, params] = useRoute('/demo/:name') if (!match) return null return ( <> {Object.entries(visibleComponents).map(function mapper([name, item]) { const background = params.name === name ? 'salmon' : '#fff' return })} {params.name} ) } export default function App() { const dev = new URLSearchParams(location.search).get('dev') return ( }> } /> {dev === null && } ) } ================================================ FILE: example/src/components.tsx ================================================ import * as React from 'react' import { type LinkProps, Link } from 'wouter' export const Page = (props: { children?: React.ReactNode }) =>
    export const DemoPanel = (props: { children?: React.ReactNode }) =>
    export const Dot = (props: React.PropsWithChildren) => export const Loading = () => { return (
    Loading.
    ) } export const Error = ({ children }: { children?: React.ReactNode }) => { return
    {children}
    } ================================================ FILE: example/src/demos/Activity.tsx ================================================ import { useRef, useEffectEvent, Suspense, use, useState, Activity } from 'react' import { useFrame, Canvas } from '@react-three/fiber' import { Mesh, Group } from 'three' import { DRACOLoader, GLTFLoader } from 'three-stdlib' import { Environment } from '@react-three/drei' const colors = ['orange', 'hotpink', 'cyan', 'lime', 'yellow', 'red', 'blue', 'purple', 'green', 'coral'] function SceneA({ onSelect }: { onSelect: Function }) { const ref = useRef(null) const [scale, setScale] = useState(1) const [color, setColor] = useState(colors[0]) // Stable event handler using the new React 19.2 API const handleSelect = useEffectEvent(() => onSelect()) useFrame((_, dt) => { if (ref.current) ref.current.rotation.y += dt * 1.2 }) return ( { setScale(1.2) setColor(colors[Math.floor(Math.random() * colors.length)]) }} onPointerOut={() => setScale(1)}> ) } const dracoLoader = new DRACOLoader() dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.5/') const gltfLoader = new GLTFLoader() gltfLoader.setDRACOLoader(dracoLoader) const modelPromise = gltfLoader.loadAsync('/apple.gltf') function SceneB({ onSelect }: { onSelect: Function }) { const gltf = use(modelPromise) const ref = useRef(null) const [scale, setScale] = useState(5) const handleSelect = useEffectEvent(() => onSelect()) useFrame((_, dt) => { if (ref.current) ref.current.rotation.y -= dt * 0.8 }) return ( setScale(6)} onPointerOut={() => setScale(5)} /> ) } export default function App() { const [active, setActive] = useState('A') return ( setActive('B')} /> setActive('A')} /> ) } ================================================ FILE: example/src/demos/AutoDispose.tsx ================================================ import { Canvas, type ThreeElements, useFrame } from '@react-three/fiber' import { useRef, useState } from 'react' import * as THREE from 'three' type BoxProps = ThreeElements['object3D'] & { setActive: (active: boolean) => void active: boolean } function Box1(props: BoxProps) { const mesh = useRef(null!) const [hovered, setHover] = useState(false) useFrame((state) => (mesh.current.position.y = Math.sin(state.clock.elapsedTime))) return ( props.setActive(!props.active)} onPointerOver={(e) => setHover(true)} onPointerOut={(e) => setHover(false)}> ) } function Box2(props: BoxProps) { const mesh = useRef(null!) const [hovered, setHover] = useState(false) useFrame((state) => (mesh.current.position.y = Math.sin(state.clock.elapsedTime))) return ( props.setActive(!props.active)} onPointerOver={(e) => setHover(true)} onPointerOut={(e) => setHover(false)}> ) } function Switcher() { const [active, setActive] = useState(false) return ( <> {active && } {!active && } ) } export default function App() { return ( ) } ================================================ FILE: example/src/demos/ChangeTexture.tsx ================================================ import { useTexture } from '@react-three/drei' import { Canvas } from '@react-three/fiber' import { useDeferredValue, useEffect, useState } from 'react' export default function App() { return ( ) } function Plane() { const textures = ['/pmndrs.png', '/react.png', '/three.png'] const [index, setIndex] = useState(0) const deferred = useDeferredValue(index) const texture = useTexture(textures[deferred]) useEffect(() => { const interval = setInterval(() => setIndex((i) => (i + 1) % textures.length), 1000) return () => clearInterval(interval) }, []) return ( ) } ================================================ FILE: example/src/demos/ClickAndHover.tsx ================================================ import { Canvas, type ThreeElements, useFrame } from '@react-three/fiber' import { useRef, useState } from 'react' import * as THREE from 'three' const mesh = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial({ color: 'red' })) const group = new THREE.Group() group.add(mesh) function Box(props: ThreeElements['mesh']) { const ref = useRef(null!) const [hovered, setHovered] = useState(false) const [clicked, setClicked] = useState(false) useFrame((state) => { ref.current.position.y = Math.sin(state.clock.elapsedTime) / 3 }) return ( setHovered(true)} onPointerOut={(e) => setHovered(false)} onClick={() => setClicked(!clicked)} scale={clicked ? [1.5, 1.5, 1.5] : [1, 1, 1]} {...props}> ) } function Box2(props: ThreeElements['group']) { return console.log('hi')} /> } export default function App() { return ( ) } ================================================ FILE: example/src/demos/ContextMenuOverride.tsx ================================================ import { Canvas } from '@react-three/fiber' import { useState } from 'react' export default function App() { const [state, setState] = useState(false) return ( console.log('canvas.missed')}> { ev.nativeEvent.preventDefault() setState((value) => !value) }} onPointerMissed={() => console.log('mesh.missed')}> ) } ================================================ FILE: example/src/demos/FlushSync.tsx ================================================ import { Canvas, flushSync, useThree } from '@react-three/fiber' import { useCallback, useRef, useState } from 'react' const colors = ['orange', 'hotpink', 'cyan', 'lime', 'yellow', 'red', 'blue', 'purple', 'green', 'coral'] function Capture() { const [color, setColor] = useState(colors[0]) const { gl } = useThree() const wantToCapture = useRef(false) const handleClick = useCallback(() => { // Use flushSync to ensure the color is updated immediately flushSync(() => setColor(colors[Math.floor(Math.random() * colors.length)])) wantToCapture.current = true }, []) const captureScreenshot = useCallback(() => { if (wantToCapture.current) { wantToCapture.current = false // Takes a screenshot of the canvas and downloads it const link = document.createElement('a') link.href = gl.domElement.toDataURL() link.download = 'screenshot.png' link.click() } }, [gl]) return ( ) } export default function App() { return ( ) } ================================================ FILE: example/src/demos/Gestures.tsx ================================================ import { Canvas, useFrame, useThree } from '@react-three/fiber' import { useDrag } from '@use-gesture/react' import { useRef, useState } from 'react' import type * as THREE from 'three' function Object({ scale = 1, z = 0, opacity = 1 }) { const { viewport } = useThree() const [hovered, hover] = useState(false) const [position, set] = useState<[number, number, number]>([0, 0, z]) const bind = useDrag(({ event, offset: [x, y] }) => { event.stopPropagation() const aspect = viewport.getCurrentViewport().factor set([x / aspect, -y / aspect, z]) }) const mesh = useRef(null!) useFrame(() => { mesh.current!.rotation.x = mesh.current!.rotation.y += 0.01 }) return ( { e.stopPropagation() hover(true) }} onPointerOut={(e) => { e.stopPropagation() hover(false) }} onClick={(e) => { e.stopPropagation() console.log('clicked', { z }) }} castShadow scale={scale}> ) } export default function App() { return ( ) } ================================================ FILE: example/src/demos/Gltf.tsx ================================================ import { Canvas, useLoader } from '@react-three/fiber' import { Suspense, useEffect, useReducer } from 'react' import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' function Test() { const [flag, toggle] = useReducer((state) => !state, true) const { scene } = useLoader(GLTFLoader, flag ? '/Stork.glb' : '/Parrot.glb') useEffect(() => { const interval = setInterval(toggle, 1000) return () => clearInterval(interval) }, []) return } export default function App() { return ( ) } ================================================ FILE: example/src/demos/Inject.tsx ================================================ import { Canvas, createPortal, getRootState, type ThreeElements, useThree } from '@react-three/fiber' import { useEffect, useReducer, useRef, useState } from 'react' import * as THREE from 'three' type CubeProps = ThreeElements['mesh'] & { color: string } const customCamera1 = new THREE.PerspectiveCamera() const customCamera2 = new THREE.PerspectiveCamera() export default function App() { const [scene1] = useState(() => new THREE.Scene()) const [scene2] = useState(() => new THREE.Scene()) const [mounted, mount] = useReducer(() => true, false) useEffect(() => { const timeout = setTimeout(mount, 1000) return () => clearTimeout(timeout) }, []) return ( {createPortal( {mounted && } {createPortal(, scene2, { camera: customCamera2 })} , scene1, { camera: customCamera1 }, )} ) } function Cube({ color, ...props }: CubeProps) { const camera = useThree((state) => state.camera) const ref = useRef(null!) useEffect(() => { console.log(`from within ${color}.useEffect`, getRootState(ref.current)?.camera, 'camera', camera.uuid) }, []) return ( ) } ================================================ FILE: example/src/demos/Layers.tsx ================================================ import { Canvas, type ThreeElements } from '@react-three/fiber' import { useEffect, useReducer } from 'react' import * as THREE from 'three' const invisibleLayer = new THREE.Layers() invisibleLayer.set(4) const visibleLayers = new THREE.Layers() visibleLayers.enableAll() visibleLayers.disable(4) function Box(props: ThreeElements['mesh']) { return ( ) } function Sphere(props: ThreeElements['mesh']) { return ( ) } export default function App() { const [visible, toggle] = useReducer((state) => !state, false) useEffect(() => { const interval = setInterval(toggle, 1000) return () => clearInterval(interval) }) return ( ) } ================================================ FILE: example/src/demos/Lines.tsx ================================================ import { Line } from '@react-three/drei' import { Canvas, type ThreeElements, type ThreeEvent, type Vector3 } from '@react-three/fiber' import { useState } from 'react' function PolyLine({ defaultStart, defaultEnd, }: { defaultStart: [number, number, number] defaultEnd: [number, number, number] }) { const [start, setStart] = useState(defaultStart) const [end, setEnd] = useState(defaultEnd) return ( <> ) } function EndPoint({ position, onDrag, }: ThreeElements['mesh'] & { onDrag: (position: [number, number, number]) => void }) { const [active, setActive] = useState(false) const [hovered, setHover] = useState(false) const down = (event: ThreeEvent) => { event.stopPropagation() ;(event.target as HTMLElement).setPointerCapture(event.pointerId) setActive(true) } const up = (event: ThreeEvent) => { setActive(false) } const move = (event: ThreeEvent) => { if (active && onDrag) onDrag(event.unprojectedPoint.toArray()) } return ( setHover(true)} onPointerOut={() => setHover(false)} onPointerDown={down} onLostPointerCapture={up} onPointerUp={up} onPointerMove={move}> ) } export default function App() { return ( ) } ================================================ FILE: example/src/demos/MultiMaterial.tsx ================================================ import { Canvas, type ThreeElements } from '@react-three/fiber' import { useEffect, useMemo, useRef, useState } from 'react' import * as THREE from 'three' const redMaterial = new THREE.MeshBasicMaterial({ color: 'aquamarine', toneMapped: false }) function ReuseMaterial(props: ThreeElements['mesh']) { return ( ) } function TestReuse() { const [okay, setOkay] = useState(true) useEffect(() => { const interval = setInterval(() => setOkay((okay) => !okay), 1000) return () => clearInterval(interval) }, []) return ( <> {okay && } ) } function TestMultiMaterial(props: ThreeElements['mesh']) { const ref = useRef(null!) const [okay, setOkay] = useState(true) useEffect(() => { const interval = setInterval(() => setOkay((okay) => !okay), 1000) return () => clearInterval(interval) }, []) useEffect(() => { console.log(ref.current.material) }, [okay]) return ( {okay ? ( ) : ( )} ) } function TestMultiDelete(props: ThreeElements['mesh']) { const ref = useRef(null!) const [okay, setOkay] = useState(true) useEffect(() => { const interval = setInterval(() => setOkay((okay) => !okay), 1000) return () => clearInterval(interval) }, []) useEffect(() => { console.log(ref.current.material) }, [okay]) return ( {okay && } ) } function TestMix(props: ThreeElements['mesh']) { const [size, setSize] = useState(0.1) const geometry = useMemo(() => new THREE.SphereGeometry(size, 64, 64), [size]) useEffect(() => { const timeout = setInterval( () => setSize((s) => { return s < 0.4 ? s + 0.025 : 0 }), 1000, ) return () => clearTimeout(timeout) }, []) return ( ) } export default function Test() { return ( ) } ================================================ FILE: example/src/demos/MultiRender.tsx ================================================ import { Canvas, useFrame } from '@react-three/fiber' import { useEffect, useRef, useState } from 'react' import * as THREE from 'three' const CanvasStyle = { width: '100%', height: '50%', } const Object = () => { const meshRef = useRef(null!) useFrame(() => { if (meshRef.current) { meshRef.current.rotation.y += 0.03 } }) return ( ) } const SpinningScene = () => (
    ) const StaticScene = () => (
    ) export default function App() { const [secondScene, setSecondScene] = useState(false) useEffect(() => { setTimeout(() => setSecondScene(true), 500) }, []) return (
    {secondScene && }
    ) } ================================================ FILE: example/src/demos/MultiView.tsx ================================================ import { ArcballControls, Bounds, CameraShake, Environment, OrbitControls, OrthographicCamera, PerspectiveCamera, TransformControls, useGLTF, } from '@react-three/drei' import { Canvas, type ComputeFunction, createPortal, type ThreeElements, type ThreeEvent, useFrame, useThree, } from '@react-three/fiber' import { ComponentProps, useCallback, useEffect, useState } from 'react' import * as THREE from 'three' function useHover() { const [hovered, setHovered] = useState(false) const props = { onPointerOver: (e: ThreeEvent) => { e.stopPropagation() setHovered(true) }, onPointerOut: () => setHovered(false), } return [hovered, props] as const } export function Soda(props: ThreeElements['group']) { const [hovered, hoverProps] = useHover() const { meshes, materials } = useGLTF('/bottle.gltf') return ( ) } function View({ index = 1, children, clearColor, placement, }: { index: number children: React.ReactNode clearColor: THREE.Color placement: string }) { const { events, size } = useThree() const [scene] = useState(() => new THREE.Scene()) const [position] = useState(() => new THREE.Vector2()) const [el] = useState(() => { const div = document.createElement('div') div.style.zIndex = index.toString() div.style.position = 'absolute' div.style.width = div.style.height = '50%' return div }) useEffect(() => { switch (placement) { case 'topright': position.set(1, 1) el.style.top = el.style.right = '0px' break case 'topleft': position.set(0, 1) el.style.top = el.style.left = '0px' break case 'bottomright': position.set(1, 0) el.style.bottom = el.style.right = '0px' break case 'bottomleft': default: position.set(0, 0) el.style.bottom = el.style.left = '0px' break } }, [placement]) useEffect(() => { if (events.connected) { const target = events.connected target.appendChild(el) return () => void target.removeChild(el) } }, [events, el]) const compute = useCallback((event, state) => { if (event.target === el) { const width = state.size.width const height = state.size.height state.pointer.set((event.offsetX / width) * 2 - 1, -(event.offsetY / height) * 2 + 1) state.raycaster.setFromCamera(state.pointer, state.camera) } }, []) return ( <> {createPortal( {children} , scene, { events: { compute, priority: events.priority + index, connected: el }, size: { width: size.width / 2, height: size.height / 2, top: 0, left: 0 }, }, )} ) } function Container({ children, index, clearColor, position, }: { children: React.ReactNode index: number clearColor: THREE.Color position: THREE.Vector2 }) { const { size, camera, scene } = useThree() useFrame((state) => { const left = Math.floor(size.width * position.x) const bottom = Math.floor(size.height * position.y) const width = Math.floor(size.width) const height = Math.floor(size.height) state.gl.setViewport(left, bottom, width, height) state.gl.setScissor(left, bottom, width, height) state.gl.setScissorTest(true) if (clearColor) state.gl.setClearColor(clearColor) state.gl.render(scene, camera) }, index) return <>{children} } const App = () => ( ) function Scene({ children, preset, }: { children?: React.ReactNode preset: ComponentProps['preset'] }) { return ( {children} ) } export default App ================================================ FILE: example/src/demos/Pointcloud.tsx ================================================ import { Canvas, ThreeEvent, extend } from '@react-three/fiber' import { useCallback, useMemo, useRef } from 'react' import * as THREE from 'three' class DotMaterialImpl extends THREE.ShaderMaterial { constructor() { super({ transparent: true, uniforms: { size: { value: 15 }, scale: { value: 1 } }, vertexShader: THREE.ShaderLib.points.vertexShader, fragmentShader: ` varying vec3 vColor; void main() { gl_FragColor = vec4(vColor, step(length(gl_PointCoord.xy - vec2(0.5)), 0.5)); }`, }) } } const DotMaterial = extend(DotMaterialImpl) const white = new THREE.Color('white') const hotpink = new THREE.Color('hotpink') function Particles({ pointCount }: { pointCount: number }) { const [positions, colors] = useMemo(() => { const positions = [...new Array(pointCount * 3)].map(() => 5 - Math.random() * 10) const colors = [...new Array(pointCount)].flatMap(() => hotpink.toArray()) return [new Float32Array(positions), new Float32Array(colors)] }, [pointCount]) const points = useRef(null!) const hover = useCallback((e: ThreeEvent) => { e.stopPropagation() white.toArray(points.current.geometry.attributes.color.array, e.index! * 3) points.current.geometry.attributes.color.needsUpdate = true }, []) const unhover = useCallback((e: ThreeEvent) => { hotpink.toArray(points.current.geometry.attributes.color.array, e.index! * 3) points.current.geometry.attributes.color.needsUpdate = true }, []) return ( ) } export default function App() { return ( ) } ================================================ FILE: example/src/demos/Portals.tsx ================================================ import { Environment, OrbitControls, useFBO, useGLTF } from '@react-three/drei' import { Canvas, ComputeFunction, createPortal, type ThreeElements, useFrame, useThree } from '@react-three/fiber' import { useCallback, useLayoutEffect, useRef, useState } from 'react' import * as THREE from 'three' export function Lights() { return ( <> ) } export function Farm(props: ThreeElements['group']) { const { scene } = useGLTF('/farm.gltf') return } export function Ramen(props: ThreeElements['group']) { const { scene } = useGLTF('/ramen.gltf') return } export function Soda(props: ThreeElements['group']) { const [hovered, spread] = useHover() const { meshes, materials } = useGLTF('/bottle.gltf') return ( ) } function useHover() { const [hovered, hover] = useState(false) return [hovered, { onPointerOver: (e: any) => (e.stopPropagation(), hover(true)), onPointerOut: () => hover(false) }] } function Portal({ children, scale = [1, 1, 1], clearColor = 'white', ...props }: { children: React.ReactNode clearColor?: string } & ThreeElements['mesh']) { const ref = useRef(null!) const fbo = useFBO() const { events } = useThree() // The portal will render into this scene const [scene] = useState(() => new THREE.Scene()) // We have our own camera in here, separate from the default const [camera] = useState(() => new THREE.PerspectiveCamera(50, 1, 0.1, 1000)) useLayoutEffect(() => { camera.aspect = ref.current.scale.x / ref.current.scale.y camera.updateProjectionMatrix() }, [scale]) useFrame((state) => { // Copy the default cameras whereabouts camera.position.copy(state.camera.position) camera.rotation.copy(state.camera.rotation) camera.scale.copy(state.camera.scale) // Render into a WebGLRenderTarget as a texture (the FBO above) state.gl.clearColor() state.gl.setRenderTarget(fbo) state.gl.render(scene, camera) state.gl.setRenderTarget(null) }) // This is a custom raycast-compute function, it controls how the raycaster functions. const compute = useCallback((event, state, previous) => { if (!previous) return false // First we call the previous state-onion-layers compute, this is what makes it possible to nest portals if (!previous.raycaster.camera) previous.events.compute!(event, previous, previous?.previousRoot?.getState()) // We run a quick check against the textured plane itself, if it isn't hit there's no need to raycast at all const [intersection] = previous?.raycaster.intersectObject(ref.current) if (!intersection) return false // We take that hits uv coords, set up this layers raycaster, et voilà, we have raycasting with perspective shift const uv = intersection.uv! state.raycaster.setFromCamera(state.pointer.set(uv.x * 2 - 1, uv.y * 2 - 1), camera) }, []) return ( <> {/* This mesh receives the render-targets texture and draws it onto a plane */} {/* A portal by default now has its own state, separate from the root state. The third argument to createPortal allows you to override parts of it, in here for example we place our own camera and override the events definition with a lower priority than the previous layer, and our custom compute function. */} {createPortal(children, scene, { camera, events: { compute, priority: events.priority - 1 } })} ) } function Test() { const controls = useThree((state) => state.controls) console.log(controls) return null } export default function App() { return ( {/* First layer, a portal */} ) } ================================================ FILE: example/src/demos/Reparenting.tsx ================================================ import { Canvas, createPortal } from '@react-three/fiber' import { useCallback, useEffect, useReducer, useState } from 'react' import * as THREE from 'three' function Icosahedron() { const [active, setActive] = useState(false) const handleClick = useCallback(() => setActive((state) => !state), []) return ( ) } function RenderToPortal({ targets }: { targets: THREE.Group[] }) { const [target, toggle] = useReducer((state) => (state + 1) % targets.length, 0) useEffect(() => { const interval = setInterval(toggle, 1000) return () => clearInterval(interval) }, [targets]) return <>{createPortal(, targets[target])} } export default function Group() { const [ref1, set1] = useState(null!) const [ref2, set2] = useState(null!) return ( console.log('onCreated')}> {ref1 && ref2 && } ) } ================================================ FILE: example/src/demos/ResetProps.tsx ================================================ import { OrbitControls } from '@react-three/drei' import { Canvas, useFrame, useThree } from '@react-three/fiber' import { useEffect, useRef, useState } from 'react' import * as THREE from 'three' function AdaptivePixelRatio() { const gl = useThree((state) => state.gl) const current = useThree((state) => state.performance.current) const initialDpr = useThree((state) => state.viewport.initialDpr) const setDpr = useThree((state) => state.setDpr) // Restore initial pixelratio on unmount useEffect(() => { const domElement = gl.domElement return () => { setDpr(initialDpr) domElement.style.imageRendering = 'auto' } }, []) // Set adaptive pixelratio useEffect(() => { setDpr(current * initialDpr) gl.domElement.style.imageRendering = current === 1 ? 'auto' : 'pixelated' }, [current]) return null } function AdaptiveEvents() { const get = useThree((state) => state.get) const current = useThree((state) => state.performance.current) useEffect(() => { const enabled = get().events.enabled return () => void (get().events.enabled = enabled) }, []) useEffect(() => void (get().events.enabled = current === 1), [current]) return null } function Scene() { const group = useRef(null!) const [showCube, setShowCube] = useState(false) const [hovered, setHovered] = useState(false) useEffect(() => { const interval = setInterval(() => setShowCube((showCube) => !showCube), 1000) return () => clearInterval(interval) }, []) useFrame(({ clock }) => group.current?.rotation.set(Math.sin(clock.elapsedTime), 0, 0)) return ( <> setHovered(true)} onPointerOut={() => setHovered(false)}> {showCube ? ( ) : ( )} {showCube ? : } ) } export default function App() { return ( ) } ================================================ FILE: example/src/demos/SVGRenderer.tsx ================================================ import { Canvas, useFrame } from '@react-three/fiber' import { useEffect, useRef, useState } from 'react' import * as THREE from 'three' import { SVGRenderer } from 'three-stdlib' function TorusKnot() { const [hovered, setHovered] = useState(false) const ref = useRef(null!) useFrame((state) => { const t = state.clock.elapsedTime / 2 ref.current.rotation.set(t, t, t) }) return ( setHovered(true)} onPointerOut={() => setHovered(false)}> ) } const gl = new SVGRenderer() gl.domElement.style.position = 'absolute' gl.domElement.style.top = '0' gl.domElement.style.left = '0' export default function () { useEffect(() => { document.body.appendChild(gl.domElement) return () => void document.body.removeChild(gl.domElement) }, []) return ( ) } ================================================ FILE: example/src/demos/Selection.tsx ================================================ import { Canvas } from '@react-three/fiber' import { useState } from 'react' function Sphere() { const [hovered, setHovered] = useState(false) console.log('sphere', hovered) return ( (e.stopPropagation(), setHovered(true))} onPointerOut={(e) => setHovered(false)}> ) } function Circle() { const [hovered, setHovered] = useState(false) console.log('circle', hovered) return ( (e.stopPropagation(), setHovered(true))} onPointerOut={(e) => setHovered(false)}> ) } export default function App() { return ( console.log('group1 over')} onPointerOut={(e) => console.log('group1 out')}> console.log(' group2 over')} onPointerOut={(e) => console.log(' group2 out')}> console.log(' white mesh over')} onPointerOut={(e) => console.log(' white mesh out')}> console.log(' black mesh over')} onPointerOut={(e) => console.log(' black mesh out')}> ) } ================================================ FILE: example/src/demos/StopPropagation.tsx ================================================ import { Environment, OrbitControls, useGLTF } from '@react-three/drei' import { Canvas, createPortal, type ThreeElements, type ThreeEvent, useFrame, useThree } from '@react-three/fiber' import { Suspense, useCallback, useState } from 'react' import * as THREE from 'three' function useHover() { const [hovered, setHovered] = useState(false) const props = { onPointerOver: (e: ThreeEvent) => { e.stopPropagation() setHovered(true) }, onPointerOut: () => setHovered(false), } return [hovered, props] as const } function Soda(props: ThreeElements['group']) { const [hovered, spread] = useHover() const { meshes, materials } = useGLTF('/bottle.gltf') return ( ) } function Hud({ priority = 1, children }: { priority?: number; children: React.ReactNode }) { const { gl, scene: defaultScene, camera: defaultCamera } = useThree() const [scene] = useState(() => new THREE.Scene()) useFrame(() => { if (priority === 1) { gl.autoClear = true gl.render(defaultScene, defaultCamera) gl.autoClear = false } gl.clearDepth() gl.render(scene, defaultCamera) }, priority) return <>{createPortal(children, scene, { events: { priority: priority + 1 } })} } function Plane({ stop = false, color, position, }: { stop?: boolean color: string position: [number, number, number] }) { const [hovered, setHovered] = useState(false) const onPointerOver = useCallback((e: ThreeEvent) => { if (stop) e.stopPropagation() setHovered(true) }, []) const onPointerOut = useCallback((e: ThreeEvent) => { if (stop) e.stopPropagation() setHovered(false) }, []) return ( ) } export default function App() { return ( ) } ================================================ FILE: example/src/demos/SuspenseAndErrors.tsx ================================================ import * as React from 'react' import { useState, useEffect } from 'react' import { Canvas, useLoader } from '@react-three/fiber' import { suspend } from 'suspend-react' import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' // Suspends the scene for 2 seconds, simulating loading an async asset function AsyncComponent({ cacheKey }: { cacheKey: string }) { suspend(() => new Promise((res) => setTimeout(res, 2000)), [cacheKey]) return null } // Loads a file that does not exist function SimulateError() { useLoader(GLTFLoader, '/doesnotexist.glb') return null } export default function App() { const [load, setLoad] = useState(false) useEffect(() => { const timeout = setTimeout(() => setLoad(true), 3000) return () => clearTimeout(timeout) }, []) return ( {load && } ) } ================================================ FILE: example/src/demos/SuspenseMaterial.tsx ================================================ import { Canvas } from '@react-three/fiber' import { Suspense, useReducer } from 'react' import { suspend } from 'suspend-react' function SlowMaterial({ arg = 0 }) { suspend(() => new Promise((res) => setTimeout(res, 1000)), [arg]) return } function FallbackMaterial() { return } export default function App() { const [arg, inc] = useReducer((x) => x + 1, 0) return ( }> ) } ================================================ FILE: example/src/demos/Test.tsx ================================================ import * as THREE from 'three' import { Canvas, useFrame } from '@react-three/fiber' import { OrbitControls, Hud } from '@react-three/drei' import { useEffect, useRef, useState } from 'react' function Box({ color = 'orange', ...props }) { const ref = useRef(null!) const [hovered, setHovered] = useState(false) const [clicked, setClicked] = useState(false) useFrame((state, delta) => (ref.current.rotation.x += delta)) return ( setClicked(!clicked)} onPointerOver={(event) => (event.stopPropagation(), setHovered(true))} onPointerOut={() => setHovered(false)}> ) } export default function App() { const [visible, setVisible] = useState(true) useEffect(() => { setTimeout(() => setVisible(false), 2000) setTimeout(() => setVisible(true), 4000) }, []) return ( {visible && } ) } ================================================ FILE: example/src/demos/ViewTracking.tsx ================================================ import { Environment, OrbitControls, PerspectiveCamera, Preload, TransformControls, useGLTF } from '@react-three/drei' import { Canvas, type ComputeFunction, createPortal, type ThreeElements, type ThreeEvent, useFrame, useThree, } from '@react-three/fiber' import { useCallback, useEffect, useReducer, useRef, useState } from 'react' import useRefs from 'react-use-refs' import * as THREE from 'three' function useHover() { const [hovered, setHovered] = useState(false) const props = { onPointerOver: (e: ThreeEvent) => { e.stopPropagation() setHovered(true) }, onPointerOut: () => setHovered(false), } return [hovered, props] as const } function Soda(props: ThreeElements['group']) { const ref = useRef(null!) const [hovered, spread] = useHover() const { meshes, materials } = useGLTF('/bottle.gltf') useFrame((state, delta) => (ref.current.rotation.y += delta)) return ( ) } function Duck(props: ThreeElements['group']) { const { scene } = useGLTF('https://vazxmixjsiawhamofees.supabase.co/storage/v1/object/public/models/duck/model.gltf') useFrame((state, delta) => (scene.rotation.x = scene.rotation.y += delta)) return } function Candy(props: ThreeElements['group']) { const { scene } = useGLTF( 'https://vazxmixjsiawhamofees.supabase.co/storage/v1/object/public/models/candy-bucket/model.gltf', ) useFrame((state, delta) => (scene.rotation.z = scene.rotation.y += delta)) return } function Flash(props: ThreeElements['group']) { const { scene } = useGLTF('/lightning.gltf') useFrame((state, delta) => (scene.rotation.y += delta)) return } function Apple(props: ThreeElements['group']) { const { scene } = useGLTF('/apple.gltf') useFrame((state, delta) => (scene.rotation.x = scene.rotation.y += delta)) return } const isOrthographicCamera = (def: THREE.Camera): def is THREE.OrthographicCamera => def && (def as THREE.OrthographicCamera).isOrthographicCamera const col = new THREE.Color() function Container({ scene, index, children, frames, rect, track, }: { scene: THREE.Scene index: number children: React.ReactNode frames: number rect: React.RefObject track: React.RefObject }) { const { camera } = useThree() let frameCount = 0 useFrame((state) => { if (frames === Infinity || frameCount <= frames) { rect.current = track.current?.getBoundingClientRect() frameCount++ } const { left = 0, right = 0, top = 0, bottom = 0, width = 0, height = 0 } = rect.current || {} const isOffscreen = bottom < 0 || top > state.size.height || right < 0 || left > state.size.width const positiveYUpBottom = state.size.height - bottom const aspect = width / height if (isOrthographicCamera(camera)) { camera.left = width / -2 camera.right = width / 2 camera.top = height / 2 camera.bottom = height / -2 } else { camera.aspect = aspect } camera.updateProjectionMatrix() state.gl.setViewport(left, positiveYUpBottom, width, height) state.gl.setScissor(left, positiveYUpBottom, width, height) state.gl.setScissorTest(true) if (isOffscreen) { state.gl.getClearColor(col) state.gl.setClearColor(col, state.gl.getClearAlpha()) state.gl.clear(true, true) return } state.gl.render(scene, camera) }, index) const get = useThree((state) => state.get) const setEvents = useThree((state) => state.setEvents) const [ready, toggle] = useReducer(() => true, false) useEffect(() => { const old = get().events.connected setEvents({ connected: track.current }) toggle() return () => setEvents({ connected: old }) }, []) return ready && children } export const View = ({ track, index = 1, frames = Infinity, children, ...props }: any) => { const rect = useRef(null!) const [scene] = useState(() => new THREE.Scene()) const compute = useCallback( (event, state) => { if (track.current && event.target === track.current) { if (!rect.current) return const { width, height, left, top } = rect.current const x = event.clientX - left const y = event.clientY - top state.pointer.set((x / width) * 2 - 1, -(y / height) * 2 + 1) state.raycaster.setFromCamera(state.pointer, state.camera) } }, [rect], ) return ( <> {createPortal( {children} , scene, { events: { compute } }, )} ) } function Scene() { return ( <> ) } export default function App() { const ref = useRef(null!) const [view1, view2, view3, view4, view5] = useRefs() as any return (
    Work on version 8 has begun 3 Sep 2021.
    This is perhaps the biggest update to Fiber yet.
    We've tried our best to keep breaking-changes to a minimum,
    they mostly affect rarely used api's like attach.
    This release brings a ton of performance related fixes,
    but also includes some new and ground-breaking features.
    state.events.connect?.(ref.current)} style={{ pointerEvents: 'none', position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh', }}>
    ) } ================================================ FILE: example/src/demos/Viewcube.tsx ================================================ import { OrbitControls } from '@react-three/drei' import { Canvas, createPortal, useFrame, useThree } from '@react-three/fiber' import { useLayoutEffect, useMemo, useRef, useState } from 'react' import * as THREE from 'three' function Viewcube() { const { gl, scene: defaultScene, camera: defaultCamera, size, events } = useThree() const scene = useMemo(() => new THREE.Scene(), []) const camera = useMemo(() => new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 1000), []) useLayoutEffect(() => { camera.left = -size.width / 2 camera.right = size.width / 2 camera.top = size.height / 2 camera.bottom = -size.height / 2 camera.position.set(0, 0, 100) camera.updateProjectionMatrix() }, [size]) const ref = useRef(null!) const [hover, setHover] = useState(null) const matrix = new THREE.Matrix4() useFrame(() => { matrix.copy(defaultCamera.matrix).invert() ref.current.quaternion.setFromRotationMatrix(matrix) gl.autoClear = true gl.render(defaultScene, defaultCamera) gl.autoClear = false gl.clearDepth() gl.render(scene, camera) }, 1) return ( <> {createPortal( setHover(null)} onPointerMove={(e) => setHover(Math.floor((e.faceIndex || 0) / 2))}> {[...Array(6)].map((_, index) => ( ))} , scene, { camera, events: { priority: events.priority + 1 } }, )} ) } export default function App() { return ( ) } ================================================ FILE: example/src/demos/WebGPU.tsx ================================================ import { Canvas, extend, type ThreeToJSXElements, useFrame, type ThreeElements } from '@react-three/fiber' import { easing } from 'maath' import { useMemo, useState } from 'react' import { color, mix, positionLocal, sin, time, uniform, vec3 } from 'three/tsl' import * as THREE from 'three/webgpu' declare module '@react-three/fiber' { interface ThreeElements extends ThreeToJSXElements {} } extend(THREE as any) function Plane(props: ThreeElements['mesh']) { const [hovered, hover] = useState(false) const { key, uHovered, colorNode, positionNode } = useMemo(() => { const uHovered = uniform(0.0) const col1 = color('orange') const col2 = color('hotpink') const col3 = color('aquamarine') const currentTime = time.mul(2) const colorNode = mix(mix(col1, col2, sin(currentTime).add(1).div(2)), col3, uHovered) const positionNode = positionLocal.add(vec3(0, sin(currentTime).mul(0.05), 0)) return { key: uHovered.uuid, uHovered, colorNode, positionNode } }, []) useFrame((state, delta) => { easing.damp(uHovered, 'value', hovered ? 1 : 0, 0.1, delta) }) return ( hover(true)} onPointerOut={() => hover(false)} {...props}> ) } export default function App() { return ( { const renderer = new THREE.WebGPURenderer(props as any) await renderer.init() return renderer }}> ) } ================================================ FILE: example/src/demos/index.tsx ================================================ import { lazy } from 'react' const Activity = { Component: lazy(() => import('./Activity')) } const AutoDispose = { Component: lazy(() => import('./AutoDispose')) } const ClickAndHover = { Component: lazy(() => import('./ClickAndHover')) } const ContextMenuOverride = { Component: lazy(() => import('./ContextMenuOverride')) } const Gestures = { Component: lazy(() => import('./Gestures')) } const Gltf = { Component: lazy(() => import('./Gltf')) } const Inject = { Component: lazy(() => import('./Inject')) } const Layers = { Component: lazy(() => import('./Layers')) } const Lines = { Component: lazy(() => import('./Lines')) } const MultiMaterial = { Component: lazy(() => import('./MultiMaterial')) } const MultiRender = { Component: lazy(() => import('./MultiRender')) } const MultiView = { Component: lazy(() => import('./MultiView')) } const Pointcloud = { Component: lazy(() => import('./Pointcloud')) } const Reparenting = { Component: lazy(() => import('./Reparenting')) } const ResetProps = { Component: lazy(() => import('./ResetProps')) } const Selection = { Component: lazy(() => import('./Selection')) } const StopPropagation = { Component: lazy(() => import('./StopPropagation')) } const SuspenseAndErrors = { Component: lazy(() => import('./SuspenseAndErrors')) } const SuspenseMaterial = { Component: lazy(() => import('./SuspenseMaterial')) } const SVGRenderer = { Component: lazy(() => import('./SVGRenderer')) } const Test = { Component: lazy(() => import('./Test')) } const Viewcube = { Component: lazy(() => import('./Viewcube')) } const Portals = { Component: lazy(() => import('./Portals')) } const ViewTracking = { Component: lazy(() => import('./ViewTracking')) } const ChangeTexture = { Component: lazy(() => import('./ChangeTexture')) } const WebGPU = { Component: lazy(() => import('./WebGPU')) } const FlushSync = { Component: lazy(() => import('./FlushSync')) } export { Activity, AutoDispose, ClickAndHover, ContextMenuOverride, Gestures, Gltf, Inject, Layers, Lines, MultiMaterial, MultiRender, Pointcloud, Reparenting, ResetProps, Selection, StopPropagation, SuspenseAndErrors, SuspenseMaterial, SVGRenderer, Test, Viewcube, MultiView, Portals, ViewTracking, ChangeTexture, WebGPU, FlushSync, } ================================================ FILE: example/src/index.tsx ================================================ import * as ReactDOM from 'react-dom/client' import App from './App' ReactDOM.createRoot(document.getElementById('root')!).render() ================================================ FILE: example/src/styles.css ================================================ /* @import url('@pmndrs/branding/styles.css'); */ @import url('https://rsms.me/inter/inter.css'); .pmndrs-menu { font-family: 'Inter var', sans-serif; position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; color: #808080; padding: 40px; pointer-events: none; justify-content: flex-start; align-items: flex-end; flex-direction: row; font-size: 10px; line-height: 1.5em; } .pmndrs-menu > div { word-wrap: none; word-break: none; white-space: pre; padding-left: 25px; padding-right: 25px; display: flex; justify-content: flex-start; align-items: flex-start; flex-direction: column; } .pmndrs-menu > div b { font-weight: 600; color: #b0b0b0; } .pmndrs-menu a { pointer-events: all; cursor: pointer; color: inherit; text-decoration: none; } .pmndrs-menu a:hover { text-decoration: underline; color: inherit; } * { box-sizing: border-box; } html, body, #root { width: 100%; height: 100%; margin: 0; padding: 0; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; overflow: hidden; } #root { overflow: auto; } body { position: fixed; overflow: hidden; overscroll-behavior-y: none; font-family: 'Inter var', sans-serif; color: black; background: #dedddf !important; } canvas { touch-action: none; } .container { position: relative; width: 100%; height: 100%; } .text { line-height: 1em; text-align: left; font-size: 8em; word-break: break-word; position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .Page { position: relative; width: 100%; height: 100vh; & > h1 { font-family: 'Roboto', sans-serif; font-weight: 900; font-size: 8em; margin: 0; color: white; line-height: 0.59em; letter-spacing: -2px; } & > h1 { position: absolute; top: 70px; left: 60px; } & > span { position: absolute; bottom: 60px; right: 60px; } @media only screen and (max-width: 1000px) { & > h1 { font-size: 5em; letter-spacing: -1px; } } & > a { margin: 0; color: white; text-decoration: none; } } .DemoPanel { z-index: 1000; position: absolute; bottom: 50px; left: 50px; max-width: 250px; } .Dot { display: inline-block; width: 20px; height: 20px; border-radius: 50%; margin: 8px; } .LoadingContainer { position: fixed; inset: 0; z-index: 100; display: flex; align-items: center; justify-content: center; background-color: #dedddf; color: white; } .LoadingMessage { font-family: 'Inter', Helvetica, sans-serif; } .Error { position: absolute; padding: 10px 20px; bottom: unset; right: unset; top: 60px; left: 60px; max-width: 380px; border: 2px solid #ff5050; color: #ff5050; } ================================================ FILE: example/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "lib": ["DOM", "DOM.Iterable", "ESNext"], "allowJs": false, "skipLibCheck": false, "esModuleInterop": false, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, "include": ["./src", "./typings/*.d.ts", "../packages/*"] } ================================================ FILE: example/vite.config.ts ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ optimizeDeps: { exclude: ['@react-three/fiber'], }, plugins: [react()], }) ================================================ FILE: jest.config.js ================================================ /** @type {import('jest').Config} */ module.exports = { preset: 'ts-jest', testEnvironment: 'jsdom', testPathIgnorePatterns: ['/node_modules/'], globals: { 'ts-jest': { tsconfig: { allowJs: true, }, }, }, transform: { '^.+\\.jsx?$': ['ts-jest', { useESM: true }], }, moduleNameMapper: { '^three$': '/node_modules/three/build/three.cjs', }, coveragePathIgnorePatterns: [ '/node_modules/', '/packages/fiber/dist', '/packages/fiber/src/index', '/packages/test-renderer/dist', ], coverageDirectory: './coverage/', collectCoverage: false, moduleFileExtensions: ['js', 'ts', 'tsx'], verbose: false, testTimeout: 30000, setupFilesAfterEnv: ['/packages/shared/setupTests.ts'], } ================================================ FILE: package.json ================================================ { "name": "react-three-fiber--root", "version": "0.0.0", "license": "MIT", "private": true, "workspaces": [ "packages/*", "example" ], "husky": { "hooks": { "pre-commit": "lint-staged" } }, "preconstruct": { "packages": [ "packages/*" ] }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ "eslint --fix" ] }, "scripts": { "changeset:add": "changeset add", "postinstall": "preconstruct dev && yarn patch-react-reconciler", "patch-react-reconciler": "vite build", "build": "preconstruct build", "examples": "yarn workspace example dev", "dev": "preconstruct dev", "prepare": "husky install", "eslint": "eslint packages/**/src/**/*.{ts,tsx}", "eslint:fix": "yarn run eslint --fix", "test": "jest --coverage", "test:watch": "jest --watchAll", "typecheck": "tsc --noEmit --emitDeclarationOnly false --strict", "validate": "preconstruct validate", "release": "yarn build && yarn changeset publish", "vers": "yarn changeset version", "codegen:eslint": "cd packages/eslint-plugin && yarn codegen", "analyze-fiber": "cd packages/fiber && npm publish --dry-run", "analyze-test": "cd packages/test-renderer && npm publish --dry-run", "format": "prettier --check .", "format:fix": "prettier --write ." }, "devDependencies": { "@babel/core": "7.17.8", "@babel/preset-env": "7.16.11", "@babel/preset-react": "7.16.7", "@babel/preset-typescript": "^7.16.7", "@changesets/changelog-git": "^0.1.11", "@changesets/cli": "^2.22.0", "@preconstruct/cli": "^2.1.5", "@testing-library/react": "^15.0.2", "@types/jest": "^29.2.5", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@types/react-native": "0.69.5", "@types/scheduler": "0.23.0", "@types/three": "^0.172.0", "@typescript-eslint/eslint-plugin": "^5.17.0", "@typescript-eslint/parser": "^5.17.0", "eslint": "^8.12.0", "eslint-config-prettier": "^8.5.0", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-import": "^2.25.4", "eslint-plugin-jest": "^27.2.1", "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-react": "^7.29.4", "eslint-plugin-react-hooks": "^4.4.0", "expo-asset": "^8.6.0", "expo-file-system": "^15.4.3", "expo-gl": "^11.4.0", "husky": "^7.0.4", "jest": "^29.7.0", "jest-cli": "^27.5.1", "lint-staged": "^12.3.7", "prettier": "^2.6.1", "pretty-quick": "^3.1.3", "react": "^19.2.0", "react-dom": "^19.2.0", "react-native": "0.69.3", "react-nil": "^2.0.0", "three": "^0.172.0", "three-stdlib": "^2.35.16", "ts-jest": "^29.1.2", "typescript": "^4.6.3", "vite": "^6.4.1" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } ================================================ FILE: packages/eslint-plugin/.npmignore ================================================ scripts/ src/ index.js ================================================ FILE: packages/eslint-plugin/CHANGELOG.md ================================================ # @react-three/eslint-plugin ## 0.1.2 ### Patch Changes - 6c907263: fix(eslint-plugin): include type declare files ## 0.1.0 ### Minor Changes - 75521d21: Initial release of the eslint plugin containing two rules, `no-clone-in-loop` and `no-new-in-loop`. ================================================ FILE: packages/eslint-plugin/README.md ================================================ # @react-three/eslint-plugin [![Version](https://img.shields.io/npm/v/@react-three/eslint-plugin?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/@react-three/eslint-plugin) [![Twitter](https://img.shields.io/twitter/follow/pmndrs?label=%40pmndrs&style=flat&colorA=000000&colorB=000000&logo=twitter&logoColor=000000)](https://twitter.com/pmndrs) [![Discord](https://img.shields.io/discord/740090768164651008?style=flat&colorA=000000&colorB=000000&label=discord&logo=discord&logoColor=000000)](https://discord.gg/ZZjjNvJ) [![Open Collective](https://img.shields.io/opencollective/all/react-three-fiber?style=flat&colorA=000000&colorB=000000)](https://opencollective.com/react-three-fiber) [![ETH](https://img.shields.io/badge/ETH-f5f5f5?style=flat&colorA=000000&colorB=000000)](https://blockchain.com/eth/address/0x6E3f79Ea1d0dcedeb33D3fC6c34d2B1f156F2682) [![BTC](https://img.shields.io/badge/BTC-f5f5f5?style=flat&colorA=000000&colorB=000000)](https://blockchain.com/btc/address/36fuguTPxGCNnYZSRdgdh6Ea94brCAjMbH) An ESLint plugin which provides lint rules for [@react-three/fiber](https://github.com/pmndrs/react-three-fiber). ## Installation ```bash npm install @react-three/eslint-plugin --save-dev ``` ## Configuration Use the recommended [config](#recommended) to get reasonable defaults: ```json "extends": [ "plugin:@react-three/recommended" ] ``` If you do not use a config you will need to specify individual rules and add extra configuration. Add "@react-three" to the plugins section. ```json "plugins": [ "@react-three" ] ``` Enable the rules that you would like to use. ```json "rules": { "@react-three/no-clone-in-frame-loop": "error" } ``` ## Rules ✅ Enabled in the `recommended` [configuration](#recommended).
    🔧 Automatically fixable by the `--fix` [CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
    💡 Manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). | Rule | Description | ✅ | 🔧 | 💡 | | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | --- | --- | --- | | no-clone-in-loop | Disallow cloning vectors in the frame loop which can cause performance problems. | ✅ | | | | no-new-in-loop | Disallow instantiating new objects in the frame loop which can cause performance problems. | ✅ | | | ## Shareable configs ### Recommended This plugin exports a `recommended` configuration that enforces rules appropriate for everyone using React Three Fiber. ```json "extends": [ "plugin:@react-three/recommended" ] ``` ### All This plugin also exports an `all` configuration that includes every available rule. ```json "extends": [ "plugin:@react-three/all" ] ``` ================================================ FILE: packages/eslint-plugin/docs/rules/no-clone-in-loop.md ================================================ Cloning vectors in the frame loop instantiates new objects wasting large amounts of memory, which is especially bad for Three.js classes. Instead create once in a `useMemo` or a single shared reference outside of the component. #### ❌ Incorrect This creates a new vector 60+ times a second allocating large amounts of memory. ```js function Direction({ targetPosition }) { const ref = useRef() useFrame(() => { const direction = ref.current.position.clone().sub(targetPosition).normalize() }) return } ``` #### ✅ Correct This creates a vector outside of the frame loop to be reused each frame. ```js const tempVec = new THREE.Vector3() function Direction({ targetPosition }) { const ref = useRef() useFrame(() => { const direction = tempVec.copy(ref.current.position).sub(targetPosition).normalize() }) return } ``` This creates a vector once outside of the frame loop inside a `useMemo` to be reused each frame. ```js function Direction({ targetPosition }) { const ref = useRef() const tempVec = useMemo(() => new THREE.Vector3()) useFrame(() => { const direction = tempVec.copy(ref.current.position).sub(targetPosition).normalize() }) return } ``` ================================================ FILE: packages/eslint-plugin/docs/rules/no-new-in-loop.md ================================================ Instantiating new objects in the frame loop can waste large amounts of memory, which is especially bad for large CPU containers such as Three.js classes and any GPU resource as there is no reliable garbage collection. Instead create once in a `useMemo` or a single shared reference outside of the component. #### ❌ Incorrect This creates a new vector 60+ times a second allocating large amounts of memory. ```js function MoveTowards({ x, y, z }) { const ref = useRef() useFrame(() => { ref.current.position.lerp(new THREE.Vector3(x, y, z), 0.1) }) return } ``` #### ✅ Correct This creates a vector outside of the frame loop to be reused each frame. ```js const tempVec = new THREE.Vector3() function MoveTowards({ x, y, z }) { const ref = useRef() useFrame(() => { ref.current.position.lerp(tempVec.set(x, y, z), 0.1) }) return } ``` This creates a vector once outside of the frame loop inside a `useMemo` to be reused each frame. ```js function MoveTowards({ x, y, z }) { const ref = useRef() const tempVec = useMemo(() => new THREE.Vector3()) useFrame(() => { ref.current.position.lerp(tempVec.set(x, y, z), 0.1) }) return } ``` ================================================ FILE: packages/eslint-plugin/package.json ================================================ { "name": "@react-three/eslint-plugin", "version": "0.1.2", "description": "An ESLint plugin which provides lint rules for @react-three/fiber.", "keywords": [ "react", "renderer", "fiber", "three", "threejs", "eslint" ], "author": "Michael Dougall (https://github.com/itsdouges)", "license": "MIT", "bugs": { "url": "https://github.com/pmndrs/react-three-fiber/issues" }, "homepage": "https://github.com/pmndrs/react-three-fiber/packages/eslint-plugin#readme", "repository": { "type": "git", "url": "git+https://github.com/pmndrs/react-three-fiber.git" }, "collective": { "type": "opencollective", "url": "https://opencollective.com/react-three-fiber" }, "main": "dist/react-three-eslint-plugin.cjs.js", "module": "dist/react-three-eslint-plugin.esm.js", "types": "dist/react-three-eslint-plugin.cjs.d.ts", "files": [ "dist" ], "sideEffects": false, "preconstruct": { "entrypoints": [ "index.ts" ] }, "dependencies": { "@babel/runtime": "^7.17.8", "eslint": "^8.12.0" }, "devDependencies": { "@types/eslint": "^8.4.10", "@types/lodash": "^4.14.191", "lodash": "^4.17.19", "prettier": "^2.6.1", "ts-node": "^10.9.1" }, "scripts": { "codegen": "ts-node scripts/codegen.ts" } } ================================================ FILE: packages/eslint-plugin/scripts/codegen.ts ================================================ import type { Rule } from 'eslint' import fs from 'fs/promises' import { join, extname, relative } from 'path' import { camelCase } from 'lodash' import { format, resolveConfig } from 'prettier' const jsHeader = (file: string) => `// THIS FILE WAS GENERATED DO NOT MODIFY BY HAND // @command yarn codegen:eslint ` + file interface FoundRule { module: Rule.RuleModule moduleName: string } interface GeneratedConfig { name: string path: string } const ignore = ['index.ts'] const srcDir = join(__dirname, '../src') const docsDir = join(__dirname, '../docs/rules') const rulesDir = join(srcDir, 'rules') const configsDir = join(srcDir, 'configs') const generatedConfigs: GeneratedConfig[] = [] async function ruleDocsPath(name: string): Promise { const absolutePath = join(docsDir, name + '.md') const relativePath = '.' + absolutePath.replace(process.cwd(), '') try { await fs.readFile(absolutePath) return relativePath } catch (_) { throw new Error(`invariant: rule ${name} should have docs at ${absolutePath}`) } } async function generateConfig(name: string, rules: FoundRule[]) { const code = ` export default { plugins: ['@react-three'], rules: { ${rules.map((rule) => `'@react-three/${rule.moduleName}': 'error'`).join(',')} }, } ` const filepath = join(configsDir, `${name}.ts`) await writeFile(filepath, code) generatedConfigs.push({ name: camelCase(name), path: './' + relative(srcDir, join(configsDir, name)) }) } async function writeFile(filepath: string, code: string) { const config = await resolveConfig(filepath) await fs.writeFile(filepath, format(extname(filepath) === '.md' ? code : jsHeader(code), { ...config, filepath })) } async function generateRuleIndex(rules: FoundRule[]) { const code = ` ${rules.map((rule) => `import ${camelCase(rule.moduleName)} from './${rule.moduleName}'`).join('\n')} export default { ${rules.map((rule) => `'${rule.moduleName}': ${camelCase(rule.moduleName)}`).join(',')} } ` const filepath = join(rulesDir, 'index.ts') await writeFile(filepath, code) } async function generatePluginIndex() { const code = ` ${generatedConfigs.map((config) => `import ${config.name} from '${config.path}'`).join('\n')} export { default as rules } from './rules/index' export const configs = { ${generatedConfigs.map((config) => `${config.name}`).join(',')} } ` const filepath = join(srcDir, 'index.ts') await writeFile(filepath, code) } const conditional = (cond: string, content?: boolean | string) => (content ? cond : '') const link = (content: string, url?: string) => (url ? `${content}` : content) async function generateReadme(rules: FoundRule[]) { const filepath = join(srcDir, '../', 'README.md') const readme = await fs.readFile(filepath, 'utf-8') const rows: string[] = [] for (const rule of rules) { const docsPath = await ruleDocsPath(rule.moduleName) const row = `| ${link(rule.moduleName, docsPath)} | ${rule.module.meta?.docs?.description} | ${conditional( '✅', rule.module.meta?.docs?.recommended, )} | ${conditional('🔧', rule.module.meta?.fixable)} | ${conditional('💡', rule.module.meta?.hasSuggestions)} |` rows.push(row) } const code = ` | Rule | Description | ✅ | 🔧 | 💡 | | ---- | -- | -- | -- | -- | ${rows.join('\n')} ` const found = /(.|\n)*/.exec(readme) if (!found) { throw new Error('invariant') } const newReadme = readme.replace( found[0], '' + '\n' + code + '\n', ) await writeFile(filepath, newReadme) } async function generate() { const rulePaths = await fs.readdir(rulesDir) const recommended: FoundRule[] = [] const rules: FoundRule[] = [] for (const moduleName of rulePaths) { if (ignore.includes(moduleName)) { continue } const rule: Rule.RuleModule = (await import(join(rulesDir, moduleName))).default const foundRule = { module: rule, moduleName: moduleName.replace(extname(moduleName), '') } rules.push(foundRule) if (rule.meta?.docs?.recommended) { recommended.push(foundRule) } } await generateRuleIndex(rules) await generateConfig('all', rules) await generateConfig('recommended', recommended) await generatePluginIndex() await generateReadme(rules) } generate() ================================================ FILE: packages/eslint-plugin/src/configs/all.ts ================================================ // THIS FILE WAS GENERATED DO NOT MODIFY BY HAND // @command yarn codegen:eslint export default { plugins: ['@react-three'], rules: { '@react-three/no-clone-in-loop': 'error', '@react-three/no-new-in-loop': 'error', }, } ================================================ FILE: packages/eslint-plugin/src/configs/recommended.ts ================================================ // THIS FILE WAS GENERATED DO NOT MODIFY BY HAND // @command yarn codegen:eslint export default { plugins: ['@react-three'], rules: { '@react-three/no-clone-in-loop': 'error', '@react-three/no-new-in-loop': 'error', }, } ================================================ FILE: packages/eslint-plugin/src/index.ts ================================================ // THIS FILE WAS GENERATED DO NOT MODIFY BY HAND // @command yarn codegen:eslint import all from './configs/all' import recommended from './configs/recommended' export { default as rules } from './rules/index' export const configs = { all, recommended, } ================================================ FILE: packages/eslint-plugin/src/lib/url.ts ================================================ export function gitHubUrl(name: string) { return `https://github.com/pmndrs/react-three-fiber/blob/master/packages/eslint-plugin/docs/rules/${name}.md` } ================================================ FILE: packages/eslint-plugin/src/rules/index.ts ================================================ // THIS FILE WAS GENERATED DO NOT MODIFY BY HAND // @command yarn codegen:eslint import noCloneInLoop from './no-clone-in-loop' import noNewInLoop from './no-new-in-loop' export default { 'no-clone-in-loop': noCloneInLoop, 'no-new-in-loop': noNewInLoop, } ================================================ FILE: packages/eslint-plugin/src/rules/no-clone-in-loop.ts ================================================ import type { Rule } from 'eslint' import * as ESTree from 'estree' import { gitHubUrl } from '../lib/url' const rule: Rule.RuleModule = { meta: { messages: { noClone: 'Cloning vectors in the frame loop can cause performance problems. Instead, create once in a useMemo or a single, shared reference outside of the component.', }, docs: { url: gitHubUrl('no-clone-in-loop'), recommended: true, description: 'Disallow cloning vectors in the frame loop which can cause performance problems.', }, }, create(ctx) { return { ['CallExpression[callee.name=useFrame] CallExpression MemberExpression Identifier[name=clone]']( node: ESTree.NewExpression, ) { ctx.report({ messageId: 'noClone', node: node, }) }, } }, } export default rule ================================================ FILE: packages/eslint-plugin/src/rules/no-new-in-loop.ts ================================================ import type { Rule } from 'eslint' import * as ESTree from 'estree' import { gitHubUrl } from '../lib/url' const rule: Rule.RuleModule = { meta: { messages: { noNew: 'Instantiating new objects in the frame loop can cause performance problems. Instead, create once in a useMemo or a single, shared reference outside of the component.', }, docs: { url: gitHubUrl('no-new-in-loop'), recommended: true, description: 'Disallow instantiating new objects in the frame loop which can cause performance problems.', }, }, create(ctx) { return { ['CallExpression[callee.name=useFrame] NewExpression'](node: ESTree.NewExpression) { ctx.report({ messageId: 'noNew', node: node, }) }, } }, } export default rule ================================================ FILE: packages/eslint-plugin/tests/rules/no-clone-in-loop.test.ts ================================================ import { RuleTester } from 'eslint' import rule from '../../src/rules/no-clone-in-loop' const tester = new RuleTester({ parserOptions: { ecmaVersion: 2015 }, }) tester.run('no-new-in-loop', rule, { valid: [ ` const vec = new THREE.Vector3() useFrame(() => { ref.current.position.copy(vec) }) `, ` useFrame(() => { clone() }) `, ` useFrame(() => { const clone = vec.copy(); }) `, ], invalid: [ { code: ` useFrame(() => { ref.current.position.clone() }) `, errors: [{ messageId: 'noClone' }], }, ], }) ================================================ FILE: packages/eslint-plugin/tests/rules/no-new-in-loop.test.ts ================================================ import { RuleTester } from 'eslint' import rule from '../../src/rules/no-new-in-loop' const tester = new RuleTester({ parserOptions: { ecmaVersion: 2015 }, }) tester.run('no-new-in-loop', rule, { valid: [ ` const vec = new THREE.Vector3() useFrame(() => { ref.current.position.copy(vec) }) `, ` const vec = new THREE.Vector3() useFrame(() => { ref.current.position.lerp(vec.set(x, y, z), 0.1) }) `, ` const vec = new Vector3() useFrame(() => { ref.current.position.copy(vec) }) `, ` const vec = new Vector3() useFrame(() => { ref.current.position.lerp(vec.set(x, y, z), 0.1) }) `, ], invalid: [ { code: ` useFrame(() => { ref.current.position.lerp(new THREE.Vector3(x, y, z), 0.1) }) `, errors: [{ messageId: 'noNew' }], }, { code: ` useFrame(() => { ref.current.position.lerp(new Vector3(x, y, z), 0.1) }) `, errors: [{ messageId: 'noNew' }], }, ], }) ================================================ FILE: packages/fiber/.npmignore ================================================ examples/ example/ .codesandbox/ .github/ .husky/ markdown/ /src/ tests/ __mocks__ ================================================ FILE: packages/fiber/CHANGELOG.md ================================================ # @react-three/fiber ## 9.5.0 ### Minor Changes - 1050a62bd4bc15e60ab3a65deea08dedfcf24989: Support React 19.2 ## 9.4.2 ### Patch Changes - 3d445fd158b25eb380cd27bd2e01304016aa23e3: fix: Expo SDK 54 compatibility through workaround ## 9.4.1 ### Patch Changes - 4f8cec0d79003a9ef6d1ca1e56de94aec4158714: fix: pass DevTools config through createReconciler to fix React DevTools ## 9.4.0 ### Minor Changes - f0976dc14a2d3203af267d0e3524d45a07f3248a: feat: improve applyProps errors, harden pierced props setting ## 9.3.0 ### Minor Changes - 7579c2d79ed60e5c93a259637b65c01971f39d82: feat: add flushSync example - 26e5d6e8b8b00b817ebb5242432000abe38bfc2c: fix: update flushSync for new reconciler - 0281b6bc4fcf041ed801e64f1876f70c214aa117: fix(native): update react-native deep imports for 0.79 compatibility ## 9.2.0 ### Minor Changes - 94ece53e17a586465b10ec627cf4799cefa72b3a: Export flushSync ## 9.1.4 ### Patch Changes - d491e46087508dff50c48768a99f87bd486b6910: Accept readonly arrays for vector props ## 9.1.3 ### Patch Changes - efd28328f5e612b0592be7da316fad990fdf4675: fix(native) fix crash on rerendering GLView with new arch ## 9.1.2 ### Patch Changes - 053757f45d2411f2929975add76a4c979713e616: fix: reference dev-only act with computed key for Webpack ## 9.1.1 ### Patch Changes - 0deae3fb12b6d62ff083891e388ab09de51330d0: Fix failing builds for production when React.act is unavailable. This fixes issues found in React@19.1.0 and up. ## 9.1.0 ### Minor Changes - cbc79507600b81f6a39de9f3f0ecb3aaf811233f: feat: add meshes to loader graph, misc internal fixes ## 9.0.4 ### Patch Changes - 28ebfbf3e7f9e69dc62f5481965b7fd6a3e3a038: fix(types): exclude type conflicts in React runtime types ## 9.0.3 ### Patch Changes - 01f0f1c8855325aae1c337d5443c846cc46007b0: fix(types): remove recursive references in JSX types ## 9.0.2 ### Patch Changes - 14e133278c3cf1550a3b9aeaaadb624a2aae4781: fix(reconciler): prefer to resolve unprefixed instance types ## 9.0.1 ### Patch Changes - 5d711d103b8d3833b93c3ab74707b2b6db5d274a: fix: add use-sync-external-store dep ## 9.0.0 ### Major Changes - 226d2ec: feat: React 19 support ## 8.18.0 ### Minor Changes - 54a3330f: feat: make children optional in Canvas ## 8.17.14 ### Patch Changes - eeeed16b: fix: update use-measure ## 8.17.13 ### Patch Changes - 0a0f2acd: fix: upstream use-measure ## 8.17.12 ### Patch Changes - ff1a16f1: fix: narrow React peer dep range ## 8.17.11 ### Patch Changes - 7461bf0c: fix: loosen instanceof checks for CSB issue ## 8.17.10 ### Patch Changes - d1a072ac: fix: ThreeEvent should not include initMouseEvent ## 8.17.9 ### Patch Changes - f34de655: fix: add orientation handling ## 8.17.8 ### Patch Changes - 3c885807: fix(types): fix React typings, fault tolerant Node type ## 8.17.7 ### Patch Changes - c20a7d73: fix(native): missing pointerId in pointer events ## 8.17.6 ### Patch Changes - 66c3e9fe: fix(native): don't bind events to GLView ## 8.17.5 ### Patch Changes - 162dbbdd: fix: npmignore ignored types" ## 8.17.4 ### Patch Changes - 43866f4e: fix: rebuild with types ## 8.17.3 ### Patch Changes - 8363eb7a: fix: rebuild with types ## 8.17.2 ### Patch Changes - 6aa4eb28: fix: rebuild with types ## 8.17.1 ### Patch Changes - e5f3f4f9: fix: rebuild with types ## 8.17.0 ### Minor Changes - 3c22194d: feat: flushSync, native EventTarget ## 8.16.8 ### Patch Changes - 4748b365: fix: update is.equ to compare booleans ## 8.16.7 ### Patch Changes - 4d6408c7: fix(types): revert usage of future module JSX ## 8.16.6 ### Patch Changes - 03ab82fe: fix(applyProps): null check indeterminate instances ## 8.16.5 ### Patch Changes - cb913e01: fix: use fast JSX, future JSX types ## 8.16.4 ### Patch Changes - 1270d24c: fix: missing dependency on inject function ## 8.16.3 ### Patch Changes - 9c83502c: fix(Canvas): don't override camera frustum props ## 8.16.2 ### Patch Changes - e0900489: fix(useLoader): don't dispose of memoized loader ## 8.16.1 ### Patch Changes - 503efc2e: fix: prevent invalidate from piling up frames ## 8.16.0 ### Minor Changes - 6b0ea6eb: feat: add childadded event dispatch ## 8.15.19 ### Patch Changes - 74926b94: fix(types): avoid emitting THREE.XRFrame ## 8.15.18 ### Patch Changes - 8c01939a: fix: correctly pass frames in invalidate ## 8.15.17 ### Patch Changes - 16c2ee97: fix(types): support @types/three@0.162.0 ## 8.15.16 ### Patch Changes - 71cd8f96: fix: tonemapping config overwrites userland - 0bb12fd1: fix(types): remove usage of THREE.Vector ## 8.15.14 ### Patch Changes - 0afc9c12: fix: portal events, update examples ## 8.15.13 ### Patch Changes - 0a399f6d: fix(native): use MSAA for antialias on iOS ## 8.15.12 ### Patch Changes - 496d6f0d: fix: useLoader and XRFrame type fixes ## 8.15.11 ### Patch Changes - 3d9af04d: fix: update import from three examples ## 8.15.10 ### Patch Changes - 49158164: fix: don't recursively dispose primitives ## 8.15.9 ### Patch Changes - 4cbc5530: fix(native): deopt iOS blob URI path ## 8.15.8 ### Patch Changes - 70680832: fix: revert stable sort ## 8.15.7 ### Patch Changes - 07e39e2e: fix(types): remove use of Object3D generic ## 8.15.6 ### Patch Changes - 7bb2950b: experiment: stable object sort ## 8.15.5 ### Patch Changes - 0e44fd8b: fix(types): preserve deprecated JSX annotations ## 8.15.4 ### Patch Changes - 634e5db5: fix(native): harden Blob URI check for Android ## 8.15.3 ### Patch Changes - beab4344: fix(native): workaround Android content policy for Blob URI ## 8.15.2 ### Patch Changes - 086d3932: fix: size check and raycaster camera ## 8.15.1 ### Patch Changes - 2d39676d: fix: ignore deprecated types, use correct XRFrame definition ## 8.15.0 ### Minor Changes - cca8b6bb: feat: export buildGraph ## 8.14.7 ### Patch Changes - 0f63a287: fix(native): restore polyfill conversions, drop networking ## 8.14.6 ### Patch Changes - 465fa0fb: fix(native): revert usage of networking stack ## 8.14.5 ### Patch Changes - f372a5b5: fix(applyProps): loosen copy identity in dev ## 8.14.4 ### Patch Changes - dc7e5739: fix(native): amend BlobManager over globals ## 8.14.3 ### Patch Changes - d77b0990: fix(native): drop fsstat for react-native-web ## 8.14.2 ### Patch Changes - 33e8baef: fix: native perf, loader types ## 8.14.1 ### Patch Changes - c99907bf: fix(native): prefer local uri for fs ## 8.14.0 ### Minor Changes - 89e96bf4: feat: react-native-web, native globals fixes ## 8.13.9 ### Patch Changes - 44d57b3c: fix(native): TextureLoader should remain consistent with FileLoader ## 8.13.8 ### Patch Changes - 5da26d52: fix(useLoader): dispose loaders ## 8.13.7 ### Patch Changes - 37b9502a: fix(Canvas): pass scene prop ## 8.13.6 ### Patch Changes - 0597495c: fix: harden XR init against Renderer shim ## 8.13.5 ### Patch Changes - 7a3b543b: fix: three type regressions ## 8.13.4 ### Patch Changes - 824ee0f7: fix: safely diff instances ## 8.13.3 ### Patch Changes - ffdb5fc4: revert nested portals, up suspend-react ## 8.13.2 ### Patch Changes - bbabdf07: update suspend-react ## 8.13.1 ### Patch Changes - c9fe03ba: fix: primitive swap and reactive portals ## 8.13.0 ### Minor Changes - ecfc48b7: feat: CanvasProps alias, respect r152 color management ## 8.12.2 ### Patch Changes - c5193468: fix: safeguard window.devicePixelRatio ## 8.12.1 ### Patch Changes - 571f07ac: fix: safeguard window.devicePixelRatio ## 8.12.0 ### Minor Changes - 1928d095: feat: `scene` render prop for custom THREE.Scene ## 8.11.11 ### Patch Changes - f03c6ef8: feat: `scene` render prop for custom THREE.Scene ## 8.11.10 ### Patch Changes - 12907836: fix onpointerlostcapture which fired before pointerup ## 8.11.9 ### Patch Changes - 6b5f572c: fix: treeshake THREE.ColorManagement ## 8.11.8 ### Patch Changes - 350cd3f3: fix(Canvas): inline render-effect ## 8.11.7 ### Patch Changes - 96af62d5: fix: don't overwrite public cameras ## 8.11.6 ### Patch Changes - 7d319c18: Fix is.equ obj:shallow, allow it to test arrays 1 level deep, fix canvas.camera prop being stale ## 8.11.5 ### Patch Changes - c658f763: fix: update canvas prop types ## 8.11.4 ### Patch Changes - 970aa58b: fix: play nice with offscreencanvas ## 8.11.3 ### Patch Changes - 2bce569c: fix: progressively set colormanagement ## 8.11.2 ### Patch Changes - 41d655cd: fix: hmr, srgb encode ## 8.11.1 ### Patch Changes - 58cadeff: fix: skip circular onUpdate calls ## 8.11.0 ### Minor Changes - 2917a47b: events.update, allow raycast w/o user interaction - 521bfb07: events.update, allow raycast w/o user interaction ## 8.10.4 ### Patch Changes - d9e6316d: fix: transpile class properties ## 8.10.3 ### Patch Changes - d06d2879: fix: align useLoader type, public fields from builds ## 8.10.2 ### Patch Changes - 564edbbb: fix port inject layers, it should allow root props to overwrite undefined portal props" ## 8.10.1 ### Patch Changes - bfa0298f: fix memoized identity ## 8.10.0 ### Minor Changes - 24c4dba4: shortcut for shadow type ## 8.9.2 ### Patch Changes - 2aeb6500: fix: primitives are incorrectly swapped on key change in maps ## 8.9.1 ### Patch Changes - 0cf11797: fix(events): type spread event props ## 8.9.0 ### Minor Changes - a458b4dd: fix(loop): export flush methods and types ## 8.8.11 ### Patch Changes - 2068f0cc: fix: events pointerlock, useLoader extension types ## 8.8.10 ### Patch Changes - 00c24718: fix: invalidate pierced props ## 8.8.9 ### Patch Changes - 4254400e: fix(createPortal): use correct JSX type ## 8.8.8 ### Patch Changes - fcb183e3: fix: call onUpdate for attached children prop update ## 8.8.7 ### Patch Changes - bedb16e7: fix: prefer named functions, for loops in hot paths ## 8.8.6 ### Patch Changes - 02a558bb: fix: upgrade deps ## 8.8.5 ### Patch Changes - 530a06d6: fix: upgrade deps to work-around CRA ## 8.8.4 ### Patch Changes - 2f2dc9f9: chore: upgrade bridge to harden suspense behavior ## 8.8.3 ### Patch Changes - 9f571239: fix #2506, events should fall back to rootstate" ## 8.8.2 ### Patch Changes - dc389ed6: fix(Canvas): prevent remount on context update ## 8.8.1 ### Patch Changes - 370d3ae5: refactor: pull context bridge from its-fine ## 8.8.0 ### Minor Changes - 46d8b440: bridge cross-container context ## 8.7.4 ### Patch Changes - 259c8895: fix: use self to get global context before window ## 8.7.3 ### Patch Changes - eb5a3be4: fix: if there is an eventsource pointerevent will be set to none ## 8.7.2 ### Patch Changes - 7f801e60: fix: events in portals carry the wrong raycaster, camera, etc ## 8.7.1 ### Patch Changes - 962cc270: fix: allow canvas eventsource to be a ref ## 8.7.0 ### Minor Changes - f5db1b78: feat: useInstanceHandle, flushGlobalEffects ## 8.6.2 ### Patch Changes - 57c12e9c: fix(types): @react-three/drei declaration files ## 8.6.1 ### Patch Changes - 7a0b5670: fix(core): don't append to unmounted containers ## 8.6.0 ### Minor Changes - 85c80e70: eventsource and eventprefix on the canvas component ## 8.5.1 ### Patch Changes - 87821d9: fix: null-check instance.children on reconstruct ## 8.5.0 ### Minor Changes - edc8252: feat: handle primitive children, auto-attach via instanceof ## 8.3.1 ### Patch Changes - aaeb2b8: fix(types): accept readonly arrays for vector props ## 8.3.0 ### Minor Changes - 9c450ec: feat: improve errors ## 8.2.3 ### Patch Changes - b8d2eab: fix: improve useLoader signature, initial size on createRoot ## 8.2.2 ### Patch Changes - acd6f04: fix: warn on stray text ## 8.2.1 ### Patch Changes - 25e35a1: fix: prefer useLayoutEffect in react-native ## 8.2.0 ### Minor Changes - 9770d7d: feat: expose ThreeElements interface for JSX elements ## 8.1.0 ### Minor Changes - 8d0f708c: Expose position information in state.size ## 8.0.27 ### Patch Changes - 7940995: fix: resume on xrsession end, export internal events ## 8.0.26 ### Patch Changes - 7b6df9df: fix: infinite loop updating cam viewport ## 8.0.25 ### Patch Changes - b7cd0f42: update viewport on camera changes ## 8.0.24 ### Patch Changes - ee8e785: fix: attach timings ## 8.0.23 ### Patch Changes - 29d03c64: revert multi attach ## 8.0.22 ### Patch Changes - 419e854: fix: always prepare primitives ## 8.0.21 ### Patch Changes - 3098b9b: fix: resizing in worker contexts, copy over attachments on reconstruct ## 8.0.20 ### Patch Changes - 4c87bce: fix: attach, devtools, and perf fixes ## 8.0.19 ### Patch Changes - 360b45a: fix: handle attach on reconstruct ## 8.0.18 ### Patch Changes - be567c1: fix: suspense attach and three compat in webpack ## 8.0.17 ### Patch Changes - 9e3369e: fix dom resize, improve native tree shaking ## 8.0.16 ### Patch Changes - 669c45c: correctly type useLoader results ## 8.0.15 ### Patch Changes - c4715d5f: allow invalidate to preempt more than 1 frame ## 8.0.14 ### Patch Changes - 5559a119: Add support for recoverable errors ## 8.0.13 ### Patch Changes - 9d77d8e2: fix: detach attribute removal ## 8.0.12 ### Patch Changes - 3d10413f: fix portal layers ## 8.0.11 ### Patch Changes - 5167b1e4: memoized.args can be undefined ## 8.0.10 ### Patch Changes - eb321afd: fix: remount bug, allow portals to inject custom size ## 8.0.9 ### Patch Changes - 624df949: fix: canvas unmount race condition" ## 8.0.8 ### Patch Changes - 952a566: fix: react SSR ## 8.0.7 ### Patch Changes - f63806b: fix: react SSR ## 8.0.6 ### Patch Changes - d4bafb9: fix re-parenting, useframe not working properly in portals, attach crash ## 8.0.5 ### Patch Changes - 227c328: fix pointer for root and portals ## 8.0.4 ### Patch Changes - e981a72: fix: mock three color management, loosen peer dep ## 8.0.3 ### Patch Changes - 3252aed: setevents needs to spread and be mirrored in portals ## 8.0.2 ### Patch Changes - 8035d1f: fix: legacy mode ## 8.0.1 ### Patch Changes - 26db195: add legacy flag to turn of three.colormanagement ## 8.0.0 ### Major Changes - 385ba9c: v8 major, react-18 compat - 04c07b8: v8 major, react-18 compat ### Patch Changes - 347ea79: new beta for library testing ## 8.0.0-beta.0 ### Major Changes - 385ba9c: v8 major, react-18 compat ## 8.0.0-beta.0 ### Patch Changes - cf6316c: new beta for library testing ## 7.0.25 ### Patch Changes - 8698734: Release latest patches ## 7.0.24 ### Patch Changes - 7f46ddf: cleanup captured pointers when released (#1914) ## 7.0.23 ### Patch Changes - 30d38b1: remove logs ## 7.0.22 ### Patch Changes - 259e1fa: add camera:manual ## 7.0.21 ### Patch Changes - 65e4147: up usemeasure, add last event to internals" ## 7.0.20 ### Patch Changes - 54cb0fd: update react-use-measure, allow it to use the offsetSize ## 7.0.19 ### Patch Changes - 7aa2eab: fix: remove zustand subscribe selector ## 7.0.18 ### Patch Changes - 6780f58: fix unmount pointer capture ## 7.0.17 ### Patch Changes - 894c550: fix: event count ## 7.0.16 ### Patch Changes - c7a4220: patch: applyprops returns the same instance ## 7.0.15 ### Patch Changes - c5645e8: fix primitive leftovers on switch ## 7.0.14 ### Patch Changes - 05af996: fix: revert the is function ## 7.0.13 ### Patch Changes - f256558: fix(core): don't overwrite camera rotation - 51e6fc9: fix(core): safely handle external instances ## 7.0.12 ### Patch Changes - 0df6073: fix: missed events ## 7.0.11 ### Patch Changes - 62b0a3a: fix: event order of missed pointers ## 7.0.10 ### Patch Changes - e019dd4: fixes ## 7.0.9 ### Patch Changes - cd266e4: Fix diffProps dashed keys ## 7.0.8 ### Patch Changes - 6f68406: Allow getCurrentViewport to receive an array ## 7.0.7 ### Patch Changes - 0375896: Simplify useframe, support instanced event cancelation, silence disposal ## 7.0.6 ### Patch Changes - fb052ad: Fix babel-env browserslist transpiling into old code" ## 7.0.5 ### Patch Changes - c97794a: Add useLoader.clear(Loader, input) ## 7.0.4 ### Patch Changes - 974ecfb: Allow elements to define attachFns for specific mount/unmount ## 7.0.2 ### Patch Changes - a97aca3: Add controls state field - 4c703d6: fix rttr didn't work with r130 ## 7.0.0 ### Major Changes - 96ae1ad: fix javascript interpreting negative renderpriority as positive This is a major breaking change that will fix an edge-case. It will only affect you if you used negative useFrame indices, for instance ```jsx useFrame(..., -1) ``` Surprisingly this disabled auto-rendering although the documentation says positive numbers only. As of v7 this will not take over the render loop. ```jsx function Render() { // Takes over the render-loop, the user has the responsibility to render useFrame(({ gl, scene, camera }) => { gl.render(scene, camera) }, 1) function RenderOnTop() { // This will render on top of the previous call useFrame(({ gl, ... }) => { gl.render(...) }, 2) function A() { // Will *not* take over the render-loop, negative indices can still be useful for sorting useFrame(() => ..., -1) function B() { // B's useFrame will execute *after* A's useFrame(() => ..., -2) ``` ## 6.2.3 ### Patch Changes - 26bc7eb: typescript changes ## 6.2.2 ### Patch Changes - 4f44a2c: use more helpful name with event handling in rttr ## 6.2.1 ### Patch Changes - Fix stopPropagation logic ## 6.2.0 ### Minor Changes - Allow object3d instances to be attached ## 6.1.5 ### Patch Changes - fix(rttr): if children is undefined return an array to map with ## 6.1.4 ### Patch Changes - 6faa090: Add shape to types, exclude event functions from event data ## 6.1.3 ### Patch Changes - 71e72c0: Fix constructor args with attached children (#1348) - 015fc03: Only set up pointer/wheel events as passive - a160e08: Fix event setPointerCapture and stopPropagation. ## 6.1.2 ================================================ FILE: packages/fiber/__mocks__/expo-asset.ts ================================================ class Asset { name = 'test asset' type = 'glb' hash = null localUri: string | null = null uri = 'test://null' width = 800 height = 400 static fromURI = (uri: any) => Object.assign(new Asset(), { uri }) static fromModule = (uri: any) => this.fromURI(uri) async downloadAsync() { if (typeof this.uri === 'string' && !this.uri.includes(':')) { this.localUri = 'drawable.png' } else { this.localUri = 'test://null' } return this } } export { Asset } ================================================ FILE: packages/fiber/__mocks__/expo-file-system.ts ================================================ export const EncodingType = { UTF8: 'utf8', Base64: 'base64' } export const cacheDirectory = 'file:///test/' export const readAsStringAsync = async () => '' export const writeAsStringAsync = async () => {} export const copyAsync = async () => {} ================================================ FILE: packages/fiber/__mocks__/expo-gl.ts ================================================ import * as React from 'react' import type { GLViewProps } from 'expo-gl' import { WebGL2RenderingContext } from '@react-three/test-renderer/src/WebGL2RenderingContext' export function GLView({ onContextCreate, ref, ...props }: GLViewProps & any) { React.useLayoutEffect(() => { const gl = new WebGL2RenderingContext({ width: 1280, height: 800 } as HTMLCanvasElement) gl.endFrameEXP = () => {} onContextCreate(gl as any) }, [onContextCreate]) return React.createElement('glview', props) } ================================================ FILE: packages/fiber/__mocks__/react-native.ts ================================================ import * as React from 'react' import { ViewProps, LayoutChangeEvent } from 'react-native' export class View extends React.Component & { children: React.ReactNode }> { componentDidMount() { this.props.onLayout?.({ nativeEvent: { layout: { x: 0, y: 0, width: 1280, height: 800, }, }, } as LayoutChangeEvent) } render() { const { onLayout, ...props } = this.props return React.createElement('view', props) } } export const StyleSheet = { absoluteFill: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, } export const PanResponder = { create: () => ({ panHandlers: {} }), } export const Image = { getSize(_uri: string, res: Function, rej?: Function) { res(1, 1) }, } export const Platform = { OS: 'web', } export const NativeModules = {} export const PixelRatio = { get() { return 1 }, } ================================================ FILE: packages/fiber/__mocks__/react-use-measure.ts ================================================ import * as React from 'react' export default function useMeasure() { const element = React.useRef(null) const [bounds] = React.useState({ left: 0, top: 0, width: 1280, height: 800, bottom: 0, right: 0, x: 0, y: 0, }) const ref = (node: HTMLElement) => { if (!node || element.current) { return } element.current = node } return [ref, bounds] } ================================================ FILE: packages/fiber/native/package.json ================================================ { "main": "dist/react-three-fiber-native.cjs.js", "module": "dist/react-three-fiber-native.esm.js", "types": "dist/react-three-fiber-native.cjs.d.ts" } ================================================ FILE: packages/fiber/package.json ================================================ { "name": "@react-three/fiber", "version": "9.5.0", "description": "A React renderer for Threejs", "keywords": [ "react", "renderer", "fiber", "three", "threejs" ], "author": "Paul Henschel (https://github.com/drcmda)", "license": "MIT", "maintainers": [ "Josh Ellis (https://github.com/joshuaellis)", "Cody Bennett (https://github.com/codyjasonbennett)", "Kris Baumgarter (https://github.com/krispya)" ], "bugs": { "url": "https://github.com/pmndrs/react-three-fiber/issues" }, "homepage": "https://github.com/pmndrs/react-three-fiber#readme", "repository": { "type": "git", "url": "git+https://github.com/pmndrs/react-three-fiber.git" }, "collective": { "type": "opencollective", "url": "https://opencollective.com/react-three-fiber" }, "main": "dist/react-three-fiber.cjs.js", "module": "dist/react-three-fiber.esm.js", "types": "dist/react-three-fiber.cjs.d.ts", "react-native": "native/dist/react-three-fiber-native.cjs.js", "sideEffects": false, "preconstruct": { "entrypoints": [ "index.tsx", "native.tsx" ] }, "scripts": { "prebuild": "cp ../../readme.md readme.md" }, "devDependencies": { "@types/react-reconciler": "^0.32.3", "react-reconciler": "^0.33.0" }, "dependencies": { "@babel/runtime": "^7.17.8", "@types/webxr": "*", "base64-js": "^1.5.1", "buffer": "^6.0.3", "its-fine": "^2.0.0", "react-use-measure": "^2.1.7", "scheduler": "^0.27.0", "suspend-react": "^0.1.3", "use-sync-external-store": "^1.4.0", "zustand": "^5.0.3" }, "peerDependencies": { "expo": ">=43.0", "expo-asset": ">=8.4", "expo-file-system": ">=11.0", "expo-gl": ">=11.0", "react": ">=19 <19.3", "react-dom": ">=19 <19.3", "react-native": ">=0.78", "three": ">=0.156" }, "peerDependenciesMeta": { "react-dom": { "optional": true }, "react-native": { "optional": true }, "expo": { "optional": true }, "expo-asset": { "optional": true }, "expo-file-system": { "optional": true }, "expo-gl": { "optional": true } } } ================================================ FILE: packages/fiber/readme.md ================================================

    react-three-fiber

    [![Version](https://img.shields.io/npm/v/@react-three/fiber?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/@react-three/fiber) [![Downloads](https://img.shields.io/npm/dt/react-three-fiber.svg?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/@react-three/fiber) [![Twitter](https://img.shields.io/twitter/follow/pmndrs?label=%40pmndrs&style=flat&colorA=000000&colorB=000000&logo=twitter&logoColor=000000)](https://twitter.com/pmndrs) [![Discord](https://img.shields.io/discord/740090768164651008?style=flat&colorA=000000&colorB=000000&label=discord&logo=discord&logoColor=000000)](https://discord.gg/ZZjjNvJ) [![Open Collective](https://img.shields.io/opencollective/all/react-three-fiber?style=flat&colorA=000000&colorB=000000)](https://opencollective.com/react-three-fiber) [![ETH](https://img.shields.io/badge/ETH-f5f5f5?style=flat&colorA=000000&colorB=000000)](https://blockchain.com/eth/address/0x6E3f79Ea1d0dcedeb33D3fC6c34d2B1f156F2682) [![BTC](https://img.shields.io/badge/BTC-f5f5f5?style=flat&colorA=000000&colorB=000000)](https://blockchain.com/btc/address/36fuguTPxGCNnYZSRdgdh6Ea94brCAjMbH) react-three-fiber is a React renderer for threejs. Build your scene declaratively with re-usable, self-contained components that react to state, are readily interactive and can participate in React's ecosystem. ```bash npm install three @react-three/fiber ``` #### Does it have limitations? None. Everything that works in Threejs will work here without exception. #### Is it slower than plain Threejs? No. There is no overhead. Components render outside of React. It outperforms Threejs in scale due to React's scheduling abilities. #### Can it keep up with frequent feature updates to Threejs? Yes. It merely expresses Threejs in JSX: `` becomes `new THREE.Mesh()`, and that happens dynamically. If a new Threejs version adds, removes or changes features, it will be available to you instantly without depending on updates to this library. ### What does it look like?
    Let's make a re-usable component that has its own state, reacts to user-input and participates in the render-loop. (live demo).
    ```jsx import { createRoot } from 'react-dom/client' import React, { useRef, useState } from 'react' import { Canvas, useFrame } from '@react-three/fiber' function Box(props) { // This reference gives us direct access to the THREE.Mesh object const ref = useRef() // Hold state for hovered and clicked events const [hovered, hover] = useState(false) const [clicked, click] = useState(false) // Subscribe this component to the render-loop, rotate the mesh every frame useFrame((state, delta) => (ref.current.rotation.x += delta)) // Return the view, these are regular Threejs elements expressed in JSX return ( click(!clicked)} onPointerOver={(event) => hover(true)} onPointerOut={(event) => hover(false)}> ) } createRoot(document.getElementById('root')).render( , ) ```
    Show TypeScript example ```bash npm install @types/three ``` ```tsx import * as THREE from 'three' import { createRoot } from 'react-dom/client' import React, { useRef, useState } from 'react' import { Canvas, useFrame, ThreeElements } from '@react-three/fiber' function Box(props: ThreeElements['mesh']) { const ref = useRef(null!) const [hovered, hover] = useState(false) const [clicked, click] = useState(false) useFrame((state, delta) => (ref.current.rotation.x += delta)) return ( click(!clicked)} onPointerOver={(event) => hover(true)} onPointerOut={(event) => hover(false)}> ) } createRoot(document.getElementById('root') as HTMLElement).render( , ) ``` Live demo: https://codesandbox.io/s/icy-tree-brnsm?file=/src/App.tsx
    Show React Native example This example relies on react 18 and uses `expo-cli`, but you can create a bare project with their template or with the `react-native` CLI. ```bash # Install expo-cli, this will create our app npm install expo-cli -g # Create app and cd into it expo init my-app cd my-app # Install dependencies npm install three @react-three/fiber react # Start expo start ``` Some configuration may be required to tell the Metro bundler about your assets if you use `useLoader` or Drei abstractions like `useGLTF` and `useTexture`: ```js // metro.config.js module.exports = { resolver: { sourceExts: ['js', 'jsx', 'json', 'ts', 'tsx', 'cjs'], assetExts: ['glb', 'png', 'jpg'], }, } ``` ```tsx import React, { useRef, useState } from 'react' import { Canvas, useFrame } from '@react-three/fiber/native' function Box(props) { const mesh = useRef(null) const [hovered, setHover] = useState(false) const [active, setActive] = useState(false) useFrame((state, delta) => (mesh.current.rotation.x += delta)) return ( setActive(!active)} onPointerOver={(event) => setHover(true)} onPointerOut={(event) => setHover(false)}> ) } export default function App() { return ( ) } ```
    --- # Documentation, tutorials, examples Visit [docs.pmnd.rs](https://docs.pmnd.rs/react-three-fiber) # Fundamentals You need to be versed in both React and Threejs before rushing into this. If you are unsure about React consult the official [React docs](https://react.dev/learn), especially [the section about hooks](https://react.dev/reference/react). As for Threejs, make sure you at least glance over the following links: 1. Make sure you have a [basic grasp of Threejs](https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene). Keep that site open. 2. When you know what a scene is, a camera, mesh, geometry, material, fork the [demo above](https://github.com/pmndrs/react-three-fiber#what-does-it-look-like). 3. [Look up](https://threejs.org/docs/index.html#api/en/objects/Mesh) the JSX elements that you see (mesh, ambientLight, etc), _all_ threejs exports are native to three-fiber. 4. Try changing some values, scroll through our [API](https://docs.pmnd.rs/react-three-fiber/API) to see what the various settings and hooks do. Some reading material: - [Threejs-docs](https://threejs.org/docs) - [Threejs-examples](https://threejs.org/examples) - [Threejs-fundamentals](https://threejs.org/manual/#en/fundamentals) - [three.js-journey](https://threejs-journey.com) - [Discover Threejs](https://discoverthreejs.com) - [Do's and don'ts](https://discoverthreejs.com/tips-and-tricks) for performance and best practices - [react-three-fiber alligator.io tutorial](https://alligator.io/react/react-with-threejs) by [@dghez\_](https://twitter.com/dghez_) # Ecosystem - [`@react-three/gltfjsx`](https://github.com/pmndrs/gltfjsx) – turns GLTFs into JSX components - [`@react-three/drei`](https://github.com/pmndrs/drei) – useful helpers for react-three-fiber - [`@react-three/postprocessing`](https://github.com/pmndrs/react-postprocessing) – post-processing effects - [`@react-three/flex`](https://github.com/pmndrs/react-three-flex) – flexbox for react-three-fiber - [`@react-three/xr`](https://github.com/pmndrs/react-xr) – VR/AR controllers and events - [`@react-three/cannon`](https://github.com/pmndrs/use-cannon) – physics based hooks - [`@react-three/a11y`](https://github.com/pmndrs/react-three-a11y) – real a11y for your scene - [`zustand`](https://github.com/pmndrs/zustand) – state management - [`react-spring`](https://github.com/pmndrs/react-spring) – a spring-physics-based animation library - [`react-use-gesture`](https://github.com/pmndrs/react-use-gesture) – mouse/touch gestures - [`leva`](https://github.com/pmndrs/leva) – create GUI controls in seconds # How to contribute If you like this project, please consider helping out. All contributions are welcome as well as donations to [Opencollective](https://opencollective.com/react-three-fiber), or in crypto `BTC: 36fuguTPxGCNnYZSRdgdh6Ea94brCAjMbH`, `ETH: 0x6E3f79Ea1d0dcedeb33D3fC6c34d2B1f156F2682`. #### Backers Thank you to all our backers! 🙏 #### Contributors This project exists thanks to all the people who contribute. ================================================ FILE: packages/fiber/src/core/events.ts ================================================ import * as THREE from 'three' import { type Properties, getRootState } from './utils' import type { Instance } from './reconciler' import type { RootState, RootStore } from './store' export interface Intersection extends THREE.Intersection { /** The event source (the object which registered the handler) */ eventObject: THREE.Object3D } export interface IntersectionEvent extends Intersection { /** The event source (the object which registered the handler) */ eventObject: THREE.Object3D /** An array of intersections */ intersections: Intersection[] /** vec3.set(pointer.x, pointer.y, 0).unproject(camera) */ unprojectedPoint: THREE.Vector3 /** Normalized event coordinates */ pointer: THREE.Vector2 /** Delta between first click and this event */ delta: number /** The ray that pierced it */ ray: THREE.Ray /** The camera that was used by the raycaster */ camera: Camera /** stopPropagation will stop underlying handlers from firing */ stopPropagation: () => void /** The original host event */ nativeEvent: TSourceEvent /** If the event was stopped by calling stopPropagation */ stopped: boolean } export type Camera = THREE.OrthographicCamera | THREE.PerspectiveCamera export type ThreeEvent = IntersectionEvent & Properties export type DomEvent = PointerEvent | MouseEvent | WheelEvent export interface Events { onClick: EventListener onContextMenu: EventListener onDoubleClick: EventListener onWheel: EventListener onPointerDown: EventListener onPointerUp: EventListener onPointerLeave: EventListener onPointerMove: EventListener onPointerCancel: EventListener onLostPointerCapture: EventListener } export interface EventHandlers { onClick?: (event: ThreeEvent) => void onContextMenu?: (event: ThreeEvent) => void onDoubleClick?: (event: ThreeEvent) => void onPointerUp?: (event: ThreeEvent) => void onPointerDown?: (event: ThreeEvent) => void onPointerOver?: (event: ThreeEvent) => void onPointerOut?: (event: ThreeEvent) => void onPointerEnter?: (event: ThreeEvent) => void onPointerLeave?: (event: ThreeEvent) => void onPointerMove?: (event: ThreeEvent) => void onPointerMissed?: (event: MouseEvent) => void onPointerCancel?: (event: ThreeEvent) => void onWheel?: (event: ThreeEvent) => void onLostPointerCapture?: (event: ThreeEvent) => void } export type FilterFunction = (items: THREE.Intersection[], state: RootState) => THREE.Intersection[] export type ComputeFunction = (event: DomEvent, root: RootState, previous?: RootState) => void export interface EventManager { /** Determines if the event layer is active */ enabled: boolean /** Event layer priority, higher prioritized layers come first and may stop(-propagate) lower layer */ priority: number /** The compute function needs to set up the raycaster and an xy- pointer */ compute?: ComputeFunction /** The filter can re-order or re-structure the intersections */ filter?: FilterFunction /** The target node the event layer is tied to */ connected?: TTarget /** All the pointer event handlers through which the host forwards native events */ handlers?: Events /** Allows re-connecting to another target */ connect?: (target: TTarget) => void /** Removes all existing events handlers from the target */ disconnect?: () => void /** Triggers a onPointerMove with the last known event. This can be useful to enable raycasting without * explicit user interaction, for instance when the camera moves a hoverable object underneath the cursor. */ update?: () => void } export interface PointerCaptureTarget { intersection: Intersection target: Element } function makeId(event: Intersection) { return (event.eventObject || event.object).uuid + '/' + event.index + event.instanceId } /** * Release pointer captures. * This is called by releasePointerCapture in the API, and when an object is removed. */ function releaseInternalPointerCapture( capturedMap: Map>, obj: THREE.Object3D, captures: Map, pointerId: number, ): void { const captureData: PointerCaptureTarget | undefined = captures.get(obj) if (captureData) { captures.delete(obj) // If this was the last capturing object for this pointer if (captures.size === 0) { capturedMap.delete(pointerId) captureData.target.releasePointerCapture(pointerId) } } } export function removeInteractivity(store: RootStore, object: THREE.Object3D) { const { internal } = store.getState() // Removes every trace of an object from the data store internal.interaction = internal.interaction.filter((o) => o !== object) internal.initialHits = internal.initialHits.filter((o) => o !== object) internal.hovered.forEach((value, key) => { if (value.eventObject === object || value.object === object) { // Clear out intersects, they are outdated by now internal.hovered.delete(key) } }) internal.capturedMap.forEach((captures, pointerId) => { releaseInternalPointerCapture(internal.capturedMap, object, captures, pointerId) }) } export function createEvents(store: RootStore) { /** Calculates delta */ function calculateDistance(event: DomEvent) { const { internal } = store.getState() const dx = event.offsetX - internal.initialClick[0] const dy = event.offsetY - internal.initialClick[1] return Math.round(Math.sqrt(dx * dx + dy * dy)) } /** Returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc */ function filterPointerEvents(objects: THREE.Object3D[]) { return objects.filter((obj) => ['Move', 'Over', 'Enter', 'Out', 'Leave'].some( (name) => (obj as Instance['object']).__r3f?.handlers[('onPointer' + name) as keyof EventHandlers], ), ) } function intersect(event: DomEvent, filter?: (objects: THREE.Object3D[]) => THREE.Object3D[]) { const state = store.getState() const duplicates = new Set() const intersections: Intersection[] = [] // Allow callers to eliminate event objects const eventsObjects = filter ? filter(state.internal.interaction) : state.internal.interaction // Reset all raycaster cameras to undefined for (let i = 0; i < eventsObjects.length; i++) { const state = getRootState(eventsObjects[i]) if (state) { state.raycaster.camera = undefined! } } if (!state.previousRoot) { // Make sure root-level pointer and ray are set up state.events.compute?.(event, state) } function handleRaycast(obj: THREE.Object3D) { const state = getRootState(obj) // Skip event handling when noEvents is set, or when the raycasters camera is null if (!state || !state.events.enabled || state.raycaster.camera === null) return [] // When the camera is undefined we have to call the event layers update function if (state.raycaster.camera === undefined) { state.events.compute?.(event, state, state.previousRoot?.getState()) // If the camera is still undefined we have to skip this layer entirely if (state.raycaster.camera === undefined) state.raycaster.camera = null! } // Intersect object by object return state.raycaster.camera ? state.raycaster.intersectObject(obj, true) : [] } // Collect events let hits: THREE.Intersection[] = eventsObjects // Intersect objects .flatMap(handleRaycast) // Sort by event priority and distance .sort((a, b) => { const aState = getRootState(a.object) const bState = getRootState(b.object) if (!aState || !bState) return a.distance - b.distance return bState.events.priority - aState.events.priority || a.distance - b.distance }) // Filter out duplicates .filter((item) => { const id = makeId(item as Intersection) if (duplicates.has(id)) return false duplicates.add(id) return true }) // https://github.com/mrdoob/three.js/issues/16031 // Allow custom userland intersect sort order, this likely only makes sense on the root filter if (state.events.filter) hits = state.events.filter(hits, state) // Bubble up the events, find the event source (eventObject) for (const hit of hits) { let eventObject: THREE.Object3D | null = hit.object // Bubble event up while (eventObject) { if ((eventObject as Instance['object']).__r3f?.eventCount) intersections.push({ ...hit, eventObject }) eventObject = eventObject.parent } } // If the interaction is captured, make all capturing targets part of the intersect. if ('pointerId' in event && state.internal.capturedMap.has(event.pointerId)) { for (let captureData of state.internal.capturedMap.get(event.pointerId)!.values()) { if (!duplicates.has(makeId(captureData.intersection))) intersections.push(captureData.intersection) } } return intersections } /** Handles intersections by forwarding them to handlers */ function handleIntersects( intersections: Intersection[], event: DomEvent, delta: number, callback: (event: ThreeEvent) => void, ) { // If anything has been found, forward it to the event listeners if (intersections.length) { const localState = { stopped: false } for (const hit of intersections) { let state = getRootState(hit.object) // If the object is not managed by R3F, it might be parented to an element which is. // Traverse upwards until we find a managed parent and use its state instead. if (!state) { hit.object.traverseAncestors((obj) => { const parentState = getRootState(obj) if (parentState) { state = parentState return false } }) } if (state) { const { raycaster, pointer, camera, internal } = state const unprojectedPoint = new THREE.Vector3(pointer.x, pointer.y, 0).unproject(camera) const hasPointerCapture = (id: number) => internal.capturedMap.get(id)?.has(hit.eventObject) ?? false const setPointerCapture = (id: number) => { const captureData = { intersection: hit, target: event.target as Element } if (internal.capturedMap.has(id)) { // if the pointerId was previously captured, we add the hit to the // event capturedMap. internal.capturedMap.get(id)!.set(hit.eventObject, captureData) } else { // if the pointerId was not previously captured, we create a map // containing the hitObject, and the hit. hitObject is used for // faster access. internal.capturedMap.set(id, new Map([[hit.eventObject, captureData]])) } // Call the original event now ;(event.target as Element).setPointerCapture(id) } const releasePointerCapture = (id: number) => { const captures = internal.capturedMap.get(id) if (captures) { releaseInternalPointerCapture(internal.capturedMap, hit.eventObject, captures, id) } } // Add native event props let extractEventProps: any = {} // This iterates over the event's properties including the inherited ones. Native PointerEvents have most of their props as getters which are inherited, but polyfilled PointerEvents have them all as their own properties (i.e. not inherited). We can't use Object.keys() or Object.entries() as they only return "own" properties; nor Object.getPrototypeOf(event) as that *doesn't* return "own" properties, only inherited ones. for (let prop in event) { let property = event[prop as keyof DomEvent] // Only copy over atomics, leave functions alone as these should be // called as event.nativeEvent.fn() if (typeof property !== 'function') extractEventProps[prop] = property } let raycastEvent: ThreeEvent = { ...hit, ...extractEventProps, pointer, intersections, stopped: localState.stopped, delta, unprojectedPoint, ray: raycaster.ray, camera: camera, // Hijack stopPropagation, which just sets a flag stopPropagation() { // https://github.com/pmndrs/react-three-fiber/issues/596 // Events are not allowed to stop propagation if the pointer has been captured const capturesForPointer = 'pointerId' in event && internal.capturedMap.get(event.pointerId) // We only authorize stopPropagation... if ( // ...if this pointer hasn't been captured !capturesForPointer || // ... or if the hit object is capturing the pointer capturesForPointer.has(hit.eventObject) ) { raycastEvent.stopped = localState.stopped = true // Propagation is stopped, remove all other hover records // An event handler is only allowed to flush other handlers if it is hovered itself if ( internal.hovered.size && Array.from(internal.hovered.values()).find((i) => i.eventObject === hit.eventObject) ) { // Objects cannot flush out higher up objects that have already caught the event const higher = intersections.slice(0, intersections.indexOf(hit)) cancelPointer([...higher, hit]) } } }, // there should be a distinction between target and currentTarget target: { hasPointerCapture, setPointerCapture, releasePointerCapture }, currentTarget: { hasPointerCapture, setPointerCapture, releasePointerCapture }, nativeEvent: event, } // Call subscribers callback(raycastEvent) // Event bubbling may be interrupted by stopPropagation if (localState.stopped === true) break } } } return intersections } function cancelPointer(intersections: Intersection[]) { const { internal } = store.getState() for (const hoveredObj of internal.hovered.values()) { // When no objects were hit or the the hovered object wasn't found underneath the cursor // we call onPointerOut and delete the object from the hovered-elements map if ( !intersections.length || !intersections.find( (hit) => hit.object === hoveredObj.object && hit.index === hoveredObj.index && hit.instanceId === hoveredObj.instanceId, ) ) { const eventObject = hoveredObj.eventObject const instance = (eventObject as Instance['object']).__r3f internal.hovered.delete(makeId(hoveredObj)) if (instance?.eventCount) { const handlers = instance.handlers // Clear out intersects, they are outdated by now const data = { ...hoveredObj, intersections } handlers.onPointerOut?.(data as ThreeEvent) handlers.onPointerLeave?.(data as ThreeEvent) } } } } function pointerMissed(event: MouseEvent, objects: THREE.Object3D[]) { for (let i = 0; i < objects.length; i++) { const instance = (objects[i] as Instance['object']).__r3f instance?.handlers.onPointerMissed?.(event) } } function handlePointer(name: string) { // Deal with cancelation switch (name) { case 'onPointerLeave': case 'onPointerCancel': return () => cancelPointer([]) case 'onLostPointerCapture': return (event: DomEvent) => { const { internal } = store.getState() if ('pointerId' in event && internal.capturedMap.has(event.pointerId)) { // If the object event interface had onLostPointerCapture, we'd call it here on every // object that's getting removed. We call it on the next frame because onLostPointerCapture // fires before onPointerUp. Otherwise pointerUp would never be called if the event didn't // happen in the object it originated from, leaving components in a in-between state. requestAnimationFrame(() => { // Only release if pointer-up didn't do it already if (internal.capturedMap.has(event.pointerId)) { internal.capturedMap.delete(event.pointerId) cancelPointer([]) } }) } } } // Any other pointer goes here ... return function handleEvent(event: DomEvent) { const { onPointerMissed, internal } = store.getState() // prepareRay(event) internal.lastEvent.current = event // Get fresh intersects const isPointerMove = name === 'onPointerMove' const isClickEvent = name === 'onClick' || name === 'onContextMenu' || name === 'onDoubleClick' const filter = isPointerMove ? filterPointerEvents : undefined const hits = intersect(event, filter) const delta = isClickEvent ? calculateDistance(event) : 0 // Save initial coordinates on pointer-down if (name === 'onPointerDown') { internal.initialClick = [event.offsetX, event.offsetY] internal.initialHits = hits.map((hit) => hit.eventObject) } // If a click yields no results, pass it back to the user as a miss // Missed events have to come first in order to establish user-land side-effect clean up if (isClickEvent && !hits.length) { if (delta <= 2) { pointerMissed(event, internal.interaction) if (onPointerMissed) onPointerMissed(event) } } // Take care of unhover if (isPointerMove) cancelPointer(hits) function onIntersect(data: ThreeEvent) { const eventObject = data.eventObject const instance = (eventObject as Instance['object']).__r3f // Check presence of handlers if (!instance?.eventCount) return const handlers = instance.handlers /* MAYBE TODO, DELETE IF NOT: Check if the object is captured, captured events should not have intersects running in parallel But wouldn't it be better to just replace capturedMap with a single entry? Also, are we OK with straight up making picking up multiple objects impossible? const pointerId = (data as ThreeEvent).pointerId if (pointerId !== undefined) { const capturedMeshSet = internal.capturedMap.get(pointerId) if (capturedMeshSet) { const captured = capturedMeshSet.get(eventObject) if (captured && captured.localState.stopped) return } }*/ if (isPointerMove) { // Move event ... if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) { // When enter or out is present take care of hover-state const id = makeId(data) const hoveredItem = internal.hovered.get(id) if (!hoveredItem) { // If the object wasn't previously hovered, book it and call its handler internal.hovered.set(id, data) handlers.onPointerOver?.(data as ThreeEvent) handlers.onPointerEnter?.(data as ThreeEvent) } else if (hoveredItem.stopped) { // If the object was previously hovered and stopped, we shouldn't allow other items to proceed data.stopPropagation() } } // Call mouse move handlers.onPointerMove?.(data as ThreeEvent) } else { // All other events ... const handler = handlers[name as keyof EventHandlers] as (event: ThreeEvent) => void if (handler) { // Forward all events back to their respective handlers with the exception of click events, // which must use the initial target if (!isClickEvent || internal.initialHits.includes(eventObject)) { // Missed events have to come first pointerMissed( event, internal.interaction.filter((object) => !internal.initialHits.includes(object)), ) // Now call the handler handler(data as ThreeEvent) } } else { // Trigger onPointerMissed on all elements that have pointer over/out handlers, but not click and weren't hit if (isClickEvent && internal.initialHits.includes(eventObject)) { pointerMissed( event, internal.interaction.filter((object) => !internal.initialHits.includes(object)), ) } } } } handleIntersects(hits, event, delta, onIntersect) } } return { handlePointer } } ================================================ FILE: packages/fiber/src/core/hooks.tsx ================================================ import * as THREE from 'three' import * as React from 'react' import { suspend, preload, clear } from 'suspend-react' import { context, RootState, RenderCallback, RootStore } from './store' import { buildGraph, ObjectMap, is, useMutableCallback, useIsomorphicLayoutEffect, isObject3D } from './utils' import type { Instance, ConstructorRepresentation } from './reconciler' /** * Exposes an object's {@link Instance}. * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#useInstanceHandle * * **Note**: this is an escape hatch to react-internal fields. Expect this to change significantly between versions. */ export function useInstanceHandle(ref: React.RefObject): React.RefObject> { const instance = React.useRef(null!) React.useImperativeHandle(instance, () => (ref.current as unknown as Instance['object']).__r3f!, [ref]) return instance } /** * Returns the R3F Canvas' Zustand store. Useful for [transient updates](https://github.com/pmndrs/zustand#transient-updates-for-often-occurring-state-changes). * @see https://docs.pmnd.rs/react-three-fiber/api/hooks#usestore */ export function useStore(): RootStore { const store = React.useContext(context) if (!store) throw new Error('R3F: Hooks can only be used within the Canvas component!') return store } /** * Accesses R3F's internal state, containing renderer, canvas, scene, etc. * @see https://docs.pmnd.rs/react-three-fiber/api/hooks#usethree */ export function useThree( selector: (state: RootState) => T = (state) => state as unknown as T, equalityFn?: (state: T, newState: T) => boolean, ): T { return useStore()(selector, equalityFn) } /** * Executes a callback before render in a shared frame loop. * Can order effects with render priority or manually render with a positive priority. * @see https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe */ export function useFrame(callback: RenderCallback, renderPriority: number = 0): null { const store = useStore() const subscribe = store.getState().internal.subscribe // Memoize ref const ref = useMutableCallback(callback) // Subscribe on mount, unsubscribe on unmount useIsomorphicLayoutEffect(() => subscribe(ref, renderPriority, store), [renderPriority, subscribe, store]) return null } /** * Returns a node graph of an object with named nodes & materials. * @see https://docs.pmnd.rs/react-three-fiber/api/hooks#usegraph */ export function useGraph(object: THREE.Object3D): ObjectMap { return React.useMemo(() => buildGraph(object), [object]) } type InputLike = string | string[] | string[][] | Readonly type LoaderLike = THREE.Loader type GLTFLike = { scene: THREE.Object3D } type LoaderInstance> = T extends ConstructorRepresentation ? InstanceType : T export type LoaderResult> = Awaited< ReturnType['loadAsync']> > extends infer R ? R extends GLTFLike ? R & ObjectMap : R : never export type Extensions> = ( loader: LoaderInstance, ) => void const memoizedLoaders = new WeakMap, LoaderLike>() const isConstructor = ( value: unknown, ): value is ConstructorRepresentation> => typeof value === 'function' && value?.prototype?.constructor === value function loadingFn>>( extensions?: Extensions, onProgress?: (event: ProgressEvent) => void, ) { return function (Proto: L, ...input: string[]) { let loader: LoaderLike // Construct and cache loader if constructor was passed if (isConstructor(Proto)) { loader = memoizedLoaders.get(Proto)! if (!loader) { loader = new Proto() memoizedLoaders.set(Proto, loader) } } else { loader = Proto as any } // Apply loader extensions if (extensions) extensions(loader as any) // Go through the urls and load them return Promise.all( input.map( (input) => new Promise>((res, reject) => loader.load( input, (data) => { if (isObject3D(data?.scene)) Object.assign(data, buildGraph(data.scene)) res(data) }, onProgress, (error) => reject(new Error(`Could not load ${input}: ${(error as ErrorEvent)?.message}`)), ), ), ), ) } } /** * Synchronously loads and caches assets with a three loader. * * Note: this hook's caller must be wrapped with `React.Suspense` * @see https://docs.pmnd.rs/react-three-fiber/api/hooks#useloader */ export function useLoader>( loader: L, input: I, extensions?: Extensions, onProgress?: (event: ProgressEvent) => void, ) { // Use suspense to load async assets const keys = (Array.isArray(input) ? input : [input]) as string[] const results = suspend(loadingFn(extensions, onProgress), [loader, ...keys], { equal: is.equ }) // Return the object(s) return (Array.isArray(input) ? results : results[0]) as I extends any[] ? LoaderResult[] : LoaderResult } /** * Preloads an asset into cache as a side-effect. */ useLoader.preload = function >( loader: L, input: I, extensions?: Extensions, ): void { const keys = (Array.isArray(input) ? input : [input]) as string[] return preload(loadingFn(extensions), [loader, ...keys]) } /** * Removes a loaded asset from cache. */ useLoader.clear = function >( loader: L, input: I, ): void { const keys = (Array.isArray(input) ? input : [input]) as string[] return clear([loader, ...keys]) } ================================================ FILE: packages/fiber/src/core/index.tsx ================================================ export type { Intersection, ThreeEvent, DomEvent, Events, EventHandlers, FilterFunction, ComputeFunction, EventManager, } from './events' export { createEvents } from './events' export * from './hooks' export type { GlobalRenderCallback, GlobalEffectType } from './loop' export { flushGlobalEffects, addEffect, addAfterEffect, addTail, invalidate, advance } from './loop' export type { AttachFnType, AttachType, ConstructorRepresentation, Catalogue, Args, InstanceProps, Instance, } from './reconciler' export { extend, reconciler } from './reconciler' export type { ReconcilerRoot, GLProps, CameraProps, RenderProps, InjectState } from './renderer' export { _roots, createRoot, unmountComponentAtNode, createPortal, flushSync } from './renderer' export type { Subscription, Dpr, Size, Viewport, RenderCallback, Frameloop, Performance, Renderer, XRManager, RootState, RootStore, } from './store' export { context } from './store' export type { ObjectMap, Camera, Disposable, Act } from './utils' export { applyProps, getRootState, dispose, act, buildGraph } from './utils' ================================================ FILE: packages/fiber/src/core/loop.ts ================================================ import { _roots } from './renderer' import type { RootState, Subscription } from './store' export type GlobalRenderCallback = (timestamp: number) => void interface SubItem { callback: GlobalRenderCallback } function createSubs(callback: GlobalRenderCallback, subs: Set): () => void { const sub = { callback } subs.add(sub) return () => void subs.delete(sub) } const globalEffects = new Set() const globalAfterEffects = new Set() const globalTailEffects = new Set() /** * Adds a global render callback which is called each frame. * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addEffect */ export const addEffect = (callback: GlobalRenderCallback) => createSubs(callback, globalEffects) /** * Adds a global after-render callback which is called each frame. * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addAfterEffect */ export const addAfterEffect = (callback: GlobalRenderCallback) => createSubs(callback, globalAfterEffects) /** * Adds a global callback which is called when rendering stops. * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addTail */ export const addTail = (callback: GlobalRenderCallback) => createSubs(callback, globalTailEffects) function run(effects: Set, timestamp: number) { if (!effects.size) return for (const { callback } of effects.values()) { callback(timestamp) } } export type GlobalEffectType = 'before' | 'after' | 'tail' export function flushGlobalEffects(type: GlobalEffectType, timestamp: number): void { switch (type) { case 'before': return run(globalEffects, timestamp) case 'after': return run(globalAfterEffects, timestamp) case 'tail': return run(globalTailEffects, timestamp) } } let subscribers: Subscription[] let subscription: Subscription function update(timestamp: number, state: RootState, frame?: XRFrame) { // Run local effects let delta = state.clock.getDelta() // In frameloop='never' mode, clock times are updated using the provided timestamp if (state.frameloop === 'never' && typeof timestamp === 'number') { delta = timestamp - state.clock.elapsedTime state.clock.oldTime = state.clock.elapsedTime state.clock.elapsedTime = timestamp } // Call subscribers (useFrame) subscribers = state.internal.subscribers for (let i = 0; i < subscribers.length; i++) { subscription = subscribers[i] subscription.ref.current(subscription.store.getState(), delta, frame) } // Render content if (!state.internal.priority && state.gl.render) state.gl.render(state.scene, state.camera) // Decrease frame count state.internal.frames = Math.max(0, state.internal.frames - 1) return state.frameloop === 'always' ? 1 : state.internal.frames } let running = false let useFrameInProgress = false let repeat: number let frame: number let state: RootState export function loop(timestamp: number): void { frame = requestAnimationFrame(loop) running = true repeat = 0 // Run effects flushGlobalEffects('before', timestamp) // Render all roots useFrameInProgress = true for (const root of _roots.values()) { state = root.store.getState() // If the frameloop is invalidated, do not run another frame if ( state.internal.active && (state.frameloop === 'always' || state.internal.frames > 0) && !state.gl.xr?.isPresenting ) { repeat += update(timestamp, state) } } useFrameInProgress = false // Run after-effects flushGlobalEffects('after', timestamp) // Stop the loop if nothing invalidates it if (repeat === 0) { // Tail call effects, they are called when rendering stops flushGlobalEffects('tail', timestamp) // Flag end of operation running = false return cancelAnimationFrame(frame) } } /** * Invalidates the view, requesting a frame to be rendered. Will globally invalidate unless passed a root's state. * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#invalidate */ export function invalidate(state?: RootState, frames = 1): void { if (!state) return _roots.forEach((root) => invalidate(root.store.getState(), frames)) if (state.gl.xr?.isPresenting || !state.internal.active || state.frameloop === 'never') return if (frames > 1) { // legacy support for people using frames parameters // Increase frames, do not go higher than 60 state.internal.frames = Math.min(60, state.internal.frames + frames) } else { if (useFrameInProgress) { //called from within a useFrame, it means the user wants an additional frame state.internal.frames = 2 } else { //the user need a new frame, no need to increment further than 1 state.internal.frames = 1 } } // If the render-loop isn't active, start it if (!running) { running = true requestAnimationFrame(loop) } } /** * Advances the frameloop and runs render effects, useful for when manually rendering via `frameloop="never"`. * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#advance */ export function advance(timestamp: number, runGlobalEffects: boolean = true, state?: RootState, frame?: XRFrame): void { if (runGlobalEffects) flushGlobalEffects('before', timestamp) if (!state) for (const root of _roots.values()) update(timestamp, root.store.getState()) else update(timestamp, state, frame) if (runGlobalEffects) flushGlobalEffects('after', timestamp) } ================================================ FILE: packages/fiber/src/core/reconciler.tsx ================================================ import packageData from '../../package.json' import * as THREE from 'three' import * as React from 'react' import Reconciler from '../../react-reconciler/index.js' import { ContinuousEventPriority, DiscreteEventPriority, DefaultEventPriority, } from '../../react-reconciler/constants.js' import { unstable_IdlePriority as idlePriority, unstable_scheduleCallback as scheduleCallback } from 'scheduler' import { diffProps, applyProps, invalidateInstance, attach, detach, prepare, isObject3D, findInitialRoot, IsAllOptional, } from './utils' import type { RootStore } from './store' import { removeInteractivity, type EventHandlers } from './events' import type { ThreeElement } from '../three-types' type Fiber = Omit & { refCleanup: null | (() => void); alternate: Fiber | null } function createReconciler< Type, Props, Container, Instance, TextInstance, SuspenseInstance, HydratableInstance, FormInstance, PublicInstance, HostContext, ChildSet, TimeoutHandle, NoTimeout, TransitionStatus, >( config: Reconciler.HostConfig< Type, Props, Container, Instance, TextInstance, SuspenseInstance, HydratableInstance, FormInstance, PublicInstance, HostContext, ChildSet, TimeoutHandle, NoTimeout, TransitionStatus >, ): Reconciler.Reconciler { const reconciler = Reconciler(config as any) // @ts-ignore DefinitelyTyped is not up to date reconciler.injectIntoDevTools() return reconciler as any } const NoEventPriority = 0 export interface Root { fiber: Reconciler.FiberRoot store: RootStore } export type AttachFnType = (parent: any, self: O) => () => void export type AttachType = string | AttachFnType export type ConstructorRepresentation = new (...args: any[]) => T export interface Catalogue { [name: string]: ConstructorRepresentation } // TODO: handle constructor overloads // https://github.com/pmndrs/react-three-fiber/pull/2931 // https://github.com/microsoft/TypeScript/issues/37079 export type Args = T extends ConstructorRepresentation ? T extends typeof THREE.Color ? [r: number, g: number, b: number] | [color: THREE.ColorRepresentation] : ConstructorParameters : any[] type ArgsProp

    = P extends ConstructorRepresentation ? IsAllOptional> extends true ? { args?: Args

    } : { args: Args

    } : { args: unknown[] } export type InstanceProps = ArgsProp

    & { object?: T dispose?: null attach?: AttachType onUpdate?: (self: T) => void } export interface Instance { root: RootStore type: string parent: Instance | null children: Instance[] props: InstanceProps & Record object: O & { __r3f?: Instance } eventCount: number handlers: Partial attach?: AttachType previousAttach?: any isHidden: boolean } interface HostConfig { type: string props: Instance['props'] container: RootStore instance: Instance textInstance: void suspenseInstance: Instance hydratableInstance: never formInstance: never publicInstance: Instance['object'] hostContext: {} childSet: never timeoutHandle: number | undefined noTimeout: -1 TransitionStatus: null } const catalogue: Catalogue = {} const PREFIX_REGEX = /^three(?=[A-Z])/ const toPascalCase = (type: string): string => `${type[0].toUpperCase()}${type.slice(1)}` let i = 0 const isConstructor = (object: unknown): object is ConstructorRepresentation => typeof object === 'function' export function extend(objects: T): React.ExoticComponent> export function extend(objects: T): void export function extend( objects: T, ): React.ExoticComponent> | void { if (isConstructor(objects)) { const Component = `${i++}` catalogue[Component] = objects return Component as any } else { Object.assign(catalogue, objects) } } function validateInstance(type: string, props: HostConfig['props']): void { // Get target from catalogue const name = toPascalCase(type) const target = catalogue[name] // Validate element target if (type !== 'primitive' && !target) throw new Error( `R3F: ${name} is not part of the THREE namespace! Did you forget to extend? See: https://docs.pmnd.rs/react-three-fiber/api/objects#using-3rd-party-objects-declaratively`, ) // Validate primitives if (type === 'primitive' && !props.object) throw new Error(`R3F: Primitives without 'object' are invalid!`) // Throw if an object or literal was passed for args if (props.args !== undefined && !Array.isArray(props.args)) throw new Error('R3F: The args prop must be an array!') } function createInstance(type: string, props: HostConfig['props'], root: RootStore): HostConfig['instance'] { // Remove three* prefix from elements if native element not present type = toPascalCase(type) in catalogue ? type : type.replace(PREFIX_REGEX, '') validateInstance(type, props) // Regenerate the R3F instance for primitives to simulate a new object if (type === 'primitive' && props.object?.__r3f) delete props.object.__r3f return prepare(props.object, root, type, props) } function hideInstance(instance: HostConfig['instance']): void { if (!instance.isHidden) { if (instance.props.attach && instance.parent?.object) { detach(instance.parent, instance) } else if (isObject3D(instance.object)) { instance.object.visible = false } instance.isHidden = true invalidateInstance(instance) } } function unhideInstance(instance: HostConfig['instance']): void { if (instance.isHidden) { if (instance.props.attach && instance.parent?.object) { attach(instance.parent, instance) } else if (isObject3D(instance.object) && instance.props.visible !== false) { instance.object.visible = true } instance.isHidden = false invalidateInstance(instance) } } // https://github.com/facebook/react/issues/20271 // This will make sure events and attach are only handled once when trees are complete function handleContainerEffects(parent: Instance, child: Instance, beforeChild?: Instance) { // Bail if tree isn't mounted or parent is not a container. // This ensures that the tree is finalized and React won't discard results to Suspense const state = child.root.getState() if (!parent.parent && parent.object !== state.scene) return // Create & link object on first run if (!child.object) { // Get target from catalogue const target = catalogue[toPascalCase(child.type)] // Create object child.object = child.props.object ?? new target(...(child.props.args ?? [])) child.object.__r3f = child } // Set initial props applyProps(child.object, child.props) // Append instance if (child.props.attach) { attach(parent, child) } else if (isObject3D(child.object) && isObject3D(parent.object)) { const childIndex = parent.object.children.indexOf(beforeChild?.object) if (beforeChild && childIndex !== -1) { // If the child is already in the parent's children array, move it to the new position // Otherwise, just insert it at the target position const existingIndex = parent.object.children.indexOf(child.object) if (existingIndex !== -1) { parent.object.children.splice(existingIndex, 1) const adjustedIndex = existingIndex < childIndex ? childIndex - 1 : childIndex parent.object.children.splice(adjustedIndex, 0, child.object) } else { child.object.parent = parent.object parent.object.children.splice(childIndex, 0, child.object) child.object.dispatchEvent({ type: 'added' }) parent.object.dispatchEvent({ type: 'childadded', child: child.object }) } } else { parent.object.add(child.object) } } // Link subtree for (const childInstance of child.children) handleContainerEffects(child, childInstance) // Tree was updated, request a frame invalidateInstance(child) } function appendChild(parent: HostConfig['instance'], child: HostConfig['instance'] | HostConfig['textInstance']) { if (!child) return // Link instances child.parent = parent parent.children.push(child) // Attach tree once complete handleContainerEffects(parent, child) } function insertBefore( parent: HostConfig['instance'], child: HostConfig['instance'] | HostConfig['textInstance'], beforeChild: HostConfig['instance'] | HostConfig['textInstance'], ) { if (!child || !beforeChild) return // Link instances child.parent = parent const childIndex = parent.children.indexOf(beforeChild) if (childIndex !== -1) parent.children.splice(childIndex, 0, child) else parent.children.push(child) // Attach tree once complete handleContainerEffects(parent, child, beforeChild) } function disposeOnIdle(object: any) { if (typeof object.dispose === 'function') { const handleDispose = () => { try { object.dispose() } catch { // no-op } } // In a testing environment, cleanup immediately if (typeof IS_REACT_ACT_ENVIRONMENT !== 'undefined') handleDispose() // Otherwise, using a real GPU so schedule cleanup to prevent stalls else scheduleCallback(idlePriority, handleDispose) } } function removeChild( parent: HostConfig['instance'], child: HostConfig['instance'] | HostConfig['textInstance'], dispose?: boolean, ) { if (!child) return // Unlink instances child.parent = null const childIndex = parent.children.indexOf(child) if (childIndex !== -1) parent.children.splice(childIndex, 1) // Eagerly tear down tree if (child.props.attach) { detach(parent, child) } else if (isObject3D(child.object) && isObject3D(parent.object)) { parent.object.remove(child.object) removeInteractivity(findInitialRoot(child), child.object) } // Allow objects to bail out of unmount disposal with dispose={null} const shouldDispose = child.props.dispose !== null && dispose !== false // Recursively remove instance children for (let i = child.children.length - 1; i >= 0; i--) { const node = child.children[i] removeChild(child, node, shouldDispose) } child.children.length = 0 // Unlink instance object delete child.object.__r3f // Dispose object whenever the reconciler feels like it. // Never dispose of primitives because their state may be kept outside of React! // In order for an object to be able to dispose it // - has a dispose method // - cannot be a // - cannot be a THREE.Scene, because three has broken its own API if (shouldDispose && child.type !== 'primitive' && child.object.type !== 'Scene') { disposeOnIdle(child.object) } // Tree was updated, request a frame for top-level instance if (dispose === undefined) invalidateInstance(child) } function setFiberRef(fiber: Fiber, publicInstance: HostConfig['publicInstance']): void { for (const _fiber of [fiber, fiber.alternate]) { if (_fiber !== null) { if (typeof _fiber.ref === 'function') { _fiber.refCleanup?.() const cleanup = _fiber.ref(publicInstance) if (typeof cleanup === 'function') _fiber.refCleanup = cleanup } else if (_fiber.ref) { _fiber.ref.current = publicInstance } } } } const reconstructed: [oldInstance: HostConfig['instance'], props: HostConfig['props'], fiber: Fiber][] = [] function swapInstances(): void { // Detach instance for (const [instance] of reconstructed) { const parent = instance.parent if (parent) { if (instance.props.attach) { detach(parent, instance) } else if (isObject3D(instance.object) && isObject3D(parent.object)) { parent.object.remove(instance.object) } for (const child of instance.children) { if (child.props.attach) { detach(instance, child) } else if (isObject3D(child.object) && isObject3D(instance.object)) { instance.object.remove(child.object) } } } // If the old instance is hidden, we need to unhide it. // React assumes it can discard instances since they're pure for DOM. // This isn't true for us since our lifetimes are impure and longliving. // So, we manually check if an instance was hidden and unhide it. if (instance.isHidden) unhideInstance(instance) // Dispose of old object if able if (instance.object.__r3f) delete instance.object.__r3f if (instance.type !== 'primitive') disposeOnIdle(instance.object) } // Update instance for (const [instance, props, fiber] of reconstructed) { instance.props = props const parent = instance.parent if (parent) { // Get target from catalogue const target = catalogue[toPascalCase(instance.type)] // Create object instance.object = instance.props.object ?? new target(...(instance.props.args ?? [])) instance.object.__r3f = instance setFiberRef(fiber, instance.object) // Set initial props applyProps(instance.object, instance.props) if (instance.props.attach) { attach(parent, instance) } else if (isObject3D(instance.object) && isObject3D(parent.object)) { parent.object.add(instance.object) } for (const child of instance.children) { if (child.props.attach) { attach(instance, child) } else if (isObject3D(child.object) && isObject3D(instance.object)) { instance.object.add(child.object) } } // Tree was updated, request a frame invalidateInstance(instance) } } reconstructed.length = 0 } // Don't handle text instances, make it no-op const handleTextInstance = () => {} const NO_CONTEXT: HostConfig['hostContext'] = {} let currentUpdatePriority: number = NoEventPriority // https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberFlags.js const NoFlags = 0 const Update = 4 export const reconciler = /* @__PURE__ */ createReconciler< HostConfig['type'], HostConfig['props'], HostConfig['container'], HostConfig['instance'], HostConfig['textInstance'], HostConfig['suspenseInstance'], HostConfig['hydratableInstance'], HostConfig['formInstance'], HostConfig['publicInstance'], HostConfig['hostContext'], HostConfig['childSet'], HostConfig['timeoutHandle'], HostConfig['noTimeout'], HostConfig['TransitionStatus'] >({ isPrimaryRenderer: false, warnsIfNotActing: false, supportsMutation: true, supportsPersistence: false, supportsHydration: false, createInstance, removeChild, appendChild, appendInitialChild: appendChild, insertBefore, appendChildToContainer(container, child) { const scene = (container.getState().scene as unknown as Instance['object']).__r3f if (!child || !scene) return appendChild(scene, child) }, removeChildFromContainer(container, child) { const scene = (container.getState().scene as unknown as Instance['object']).__r3f if (!child || !scene) return removeChild(scene, child) }, insertInContainerBefore(container, child, beforeChild) { const scene = (container.getState().scene as unknown as Instance['object']).__r3f if (!child || !beforeChild || !scene) return insertBefore(scene, child, beforeChild) }, getRootHostContext: () => NO_CONTEXT, getChildHostContext: () => NO_CONTEXT, commitUpdate( instance: HostConfig['instance'], type: HostConfig['type'], oldProps: HostConfig['props'], newProps: HostConfig['props'], fiber: Fiber, ) { validateInstance(type, newProps) let reconstruct = false // Reconstruct primitives if object prop changes if (instance.type === 'primitive' && oldProps.object !== newProps.object) reconstruct = true // Reconstruct instance if args were added or removed else if (newProps.args?.length !== oldProps.args?.length) reconstruct = true // Reconstruct instance if args were changed else if (newProps.args?.some((value, index) => value !== oldProps.args?.[index])) reconstruct = true // Reconstruct when args or false, commitMount() {}, getPublicInstance: (instance) => instance?.object!, prepareForCommit: () => null, preparePortalMount: (container) => prepare(container.getState().scene, container, '', {}), resetAfterCommit: () => {}, shouldSetTextContent: () => false, clearContainer: () => false, hideInstance, unhideInstance, createTextInstance: handleTextInstance, hideTextInstance: handleTextInstance, unhideTextInstance: handleTextInstance, scheduleTimeout: (typeof setTimeout === 'function' ? setTimeout : undefined) as any, cancelTimeout: (typeof clearTimeout === 'function' ? clearTimeout : undefined) as any, noTimeout: -1, getInstanceFromNode: () => null, beforeActiveInstanceBlur() {}, afterActiveInstanceBlur() {}, detachDeletedInstance() {}, prepareScopeUpdate() {}, getInstanceFromScope: () => null, shouldAttemptEagerTransition: () => false, trackSchedulerEvent: () => {}, resolveEventType: () => null, resolveEventTimeStamp: () => -1.1, requestPostPaintCallback() {}, maySuspendCommit: () => false, preloadInstance: () => true, // true indicates already loaded suspendInstance() {}, waitForCommitToBeReady: () => null, NotPendingTransition: null, // The reconciler types use the internal ReactContext with all the hidden properties // so we have to cast from the public React.Context type HostTransitionContext: /* @__PURE__ */ React.createContext( null, ) as unknown as Reconciler.ReactContext, setCurrentUpdatePriority(newPriority: number) { currentUpdatePriority = newPriority }, getCurrentUpdatePriority() { return currentUpdatePriority }, resolveUpdatePriority() { if (currentUpdatePriority !== NoEventPriority) return currentUpdatePriority switch (typeof window !== 'undefined' && window.event?.type) { case 'click': case 'contextmenu': case 'dblclick': case 'pointercancel': case 'pointerdown': case 'pointerup': return DiscreteEventPriority case 'pointermove': case 'pointerout': case 'pointerover': case 'pointerenter': case 'pointerleave': case 'wheel': return ContinuousEventPriority default: return DefaultEventPriority } }, resetFormInstance() {}, // @ts-ignore DefinitelyTyped is not up to date rendererPackageName: '@react-three/fiber', rendererVersion: packageData.version, // https://github.com/facebook/react/pull/31975 // https://github.com/facebook/react/pull/31999 applyViewTransitionName(_instance: any, _name: any, _className: any) {}, restoreViewTransitionName(_instance: any, _props: any) {}, cancelViewTransitionName(_instance: any, _name: any, _props: any) {}, cancelRootViewTransitionName(_rootContainer: any) {}, restoreRootViewTransitionName(_rootContainer: any) {}, InstanceMeasurement: null, measureInstance: (_instance: any) => null, wasInstanceInViewport: (_measurement: any): boolean => true, hasInstanceChanged: (_oldMeasurement: any, _newMeasurement: any): boolean => false, hasInstanceAffectedParent: (_oldMeasurement: any, _newMeasurement: any): boolean => false, // https://github.com/facebook/react/pull/32002 // https://github.com/facebook/react/pull/34486 suspendOnActiveViewTransition(_state: any, _container: any) {}, // https://github.com/facebook/react/pull/32451 // https://github.com/facebook/react/pull/32760 startGestureTransition: () => null, startViewTransition: () => null, stopViewTransition(_transition: null) {}, // https://github.com/facebook/react/pull/32038 createViewTransitionInstance: (_name: string): null => null, // https://github.com/facebook/react/pull/32379 // https://github.com/facebook/react/pull/32786 getCurrentGestureOffset(_provider: null): number { throw new Error('startGestureTransition is not yet supported in react-three-fiber.') }, // https://github.com/facebook/react/pull/32500 cloneMutableInstance(instance: any, _keepChildren: any) { return instance }, cloneMutableTextInstance(textInstance: any) { return textInstance }, cloneRootViewTransitionContainer(_rootContainer: any) { throw new Error('Not implemented.') }, removeRootViewTransitionClone(_rootContainer: any, _clone: any) { throw new Error('Not implemented.') }, // https://github.com/facebook/react/pull/32465 createFragmentInstance: (_fiber: any): null => null, updateFragmentInstanceFiber(_fiber: any, _instance: any): void {}, commitNewChildToFragmentInstance(_child: any, _fragmentInstance: any): void {}, deleteChildFromFragmentInstance(_child: any, _fragmentInstance: any): void {}, // https://github.com/facebook/react/pull/32653 measureClonedInstance: (_instance: any) => null, // https://github.com/facebook/react/pull/32819 maySuspendCommitOnUpdate: (_type: any, _oldProps: any, _newProps: any) => false, maySuspendCommitInSyncRender: (_type: any, _props: any) => false, // https://github.com/facebook/react/pull/34486 startSuspendingCommit: () => null, // https://github.com/facebook/react/pull/34522 getSuspendedCommitReason: (_state: any, _rootContainer: any) => null, }) ================================================ FILE: packages/fiber/src/core/renderer.tsx ================================================ import * as React from 'react' import { ConcurrentRoot } from '../../react-reconciler/constants.js' import * as THREE from 'three' import { createWithEqualityFn } from 'zustand/traditional' import type { ThreeElement } from '../three-types' import { ComputeFunction, EventManager } from './events' import { useStore } from './hooks' import { advance, invalidate } from './loop' import { reconciler, Root } from './reconciler' import { context, createStore, Dpr, Frameloop, isRenderer, Performance, Renderer, RootState, RootStore, Size, } from './store' import { type Properties, applyProps, calculateDpr, Camera, dispose, EquConfig, is, prepare, updateCamera, useIsomorphicLayoutEffect, useMutableCallback, } from './utils' // Shim for OffscreenCanvas since it was removed from DOM types // https://github.com/DefinitelyTyped/DefinitelyTyped/pull/54988 interface OffscreenCanvas extends EventTarget {} export const _roots = new Map() const shallowLoose = { objects: 'shallow', strict: false } as EquConfig export type DefaultGLProps = Omit & { canvas: HTMLCanvasElement | OffscreenCanvas } export type GLProps = | Renderer | ((defaultProps: DefaultGLProps) => Renderer) | ((defaultProps: DefaultGLProps) => Promise) | Partial | THREE.WebGLRendererParameters> export type CameraProps = ( | Camera | Partial< ThreeElement & ThreeElement & ThreeElement > ) & { /** Flags the camera as manual, putting projection into your own hands */ manual?: boolean } export interface RenderProps { /** A threejs renderer instance or props that go into the default renderer */ gl?: GLProps /** Dimensions to fit the renderer to. Will measure canvas dimensions if omitted */ size?: Size /** * Enables shadows (by default PCFsoft). Can accept `gl.shadowMap` options for fine-tuning, * but also strings: 'basic' | 'percentage' | 'soft' | 'variance'. * @see https://threejs.org/docs/#api/en/renderers/WebGLRenderer.shadowMap */ shadows?: boolean | 'basic' | 'percentage' | 'soft' | 'variance' | Partial /** * Disables three r139 color management. * @see https://threejs.org/docs/#manual/en/introduction/Color-management */ legacy?: boolean /** Switch off automatic sRGB encoding and gamma correction */ linear?: boolean /** Use `THREE.NoToneMapping` instead of `THREE.ACESFilmicToneMapping` */ flat?: boolean /** Creates an orthographic camera */ orthographic?: boolean /** * R3F's render mode. Set to `demand` to only render on state change or `never` to take control. * @see https://docs.pmnd.rs/react-three-fiber/advanced/scaling-performance#on-demand-rendering */ frameloop?: Frameloop /** * R3F performance options for adaptive performance. * @see https://docs.pmnd.rs/react-three-fiber/advanced/scaling-performance#movement-regression */ performance?: Partial> /** Target pixel ratio. Can clamp between a range: `[min, max]` */ dpr?: Dpr /** Props that go into the default raycaster */ raycaster?: Partial /** A `THREE.Scene` instance or props that go into the default scene */ scene?: THREE.Scene | Partial /** A `THREE.Camera` instance or props that go into the default camera */ camera?: CameraProps /** An R3F event manager to manage elements' pointer events */ events?: (store: RootStore) => EventManager /** Callback after the canvas has rendered (but not yet committed) */ onCreated?: (state: RootState) => void /** Response for pointer clicks that have missed any target */ onPointerMissed?: (event: MouseEvent) => void } export interface ReconcilerRoot { configure: (config?: RenderProps) => Promise> render: (element: React.ReactNode) => RootStore unmount: () => void } function computeInitialSize(canvas: HTMLCanvasElement | OffscreenCanvas, size?: Size): Size { if ( !size && typeof HTMLCanvasElement !== 'undefined' && canvas instanceof HTMLCanvasElement && canvas.parentElement ) { const { width, height, top, left } = canvas.parentElement.getBoundingClientRect() return { width, height, top, left } } else if (!size && typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas) { return { width: canvas.width, height: canvas.height, top: 0, left: 0, } } return { width: 0, height: 0, top: 0, left: 0, ...size } } export function createRoot( canvas: TCanvas, ): ReconcilerRoot { // Check against mistaken use of createRoot const prevRoot = _roots.get(canvas) const prevFiber = prevRoot?.fiber const prevStore = prevRoot?.store if (prevRoot) console.warn('R3F.createRoot should only be called once!') // Report when an error was detected in a previous render // https://github.com/pmndrs/react-three-fiber/pull/2261 const logRecoverableError = typeof reportError === 'function' ? // In modern browsers, reportError will dispatch an error event, // emulating an uncaught JavaScript error. reportError : // In older browsers and test environments, fallback to console.error. console.error // Create store const store = prevStore || createStore(invalidate, advance) // Create renderer const fiber = prevFiber || (reconciler as any).createContainer( store, // container ConcurrentRoot, // tag null, // hydration callbacks false, // isStrictMode null, // concurrentUpdatesByDefaultOverride '', // identifierPrefix logRecoverableError, // onUncaughtError logRecoverableError, // onCaughtError logRecoverableError, // onRecoverableError null, // transitionCallbacks ) // Map it if (!prevRoot) _roots.set(canvas, { fiber, store }) // Locals let onCreated: ((state: RootState) => void) | undefined let lastCamera: RenderProps['camera'] let configured = false let pending: Promise | null = null return { async configure(props: RenderProps = {}): Promise> { let resolve!: () => void pending = new Promise((_resolve) => (resolve = _resolve)) let { gl: glConfig, size: propsSize, scene: sceneOptions, events, onCreated: onCreatedCallback, shadows = false, linear = false, flat = false, legacy = false, orthographic = false, frameloop = 'always', dpr = [1, 2], performance, raycaster: raycastOptions, camera: cameraOptions, onPointerMissed, } = props let state = store.getState() // Set up renderer (one time only!) let gl = state.gl if (!state.gl) { const defaultProps: DefaultGLProps = { canvas: canvas as HTMLCanvasElement, powerPreference: 'high-performance', antialias: true, alpha: true, } const customRenderer = ( typeof glConfig === 'function' ? await glConfig(defaultProps) : glConfig ) as THREE.WebGLRenderer if (isRenderer(customRenderer)) { gl = customRenderer } else { gl = new THREE.WebGLRenderer({ ...defaultProps, ...glConfig, }) } state.set({ gl }) } // Set up raycaster (one time only!) let raycaster = state.raycaster if (!raycaster) state.set({ raycaster: (raycaster = new THREE.Raycaster()) }) // Set raycaster options const { params, ...options } = raycastOptions || {} if (!is.equ(options, raycaster, shallowLoose)) applyProps(raycaster, { ...options } as any) if (!is.equ(params, raycaster.params, shallowLoose)) applyProps(raycaster, { params: { ...raycaster.params, ...params } } as any) // Create default camera, don't overwrite any user-set state if (!state.camera || (state.camera === lastCamera && !is.equ(lastCamera, cameraOptions, shallowLoose))) { lastCamera = cameraOptions const isCamera = (cameraOptions as unknown as THREE.Camera | undefined)?.isCamera const camera = isCamera ? (cameraOptions as Camera) : orthographic ? new THREE.OrthographicCamera(0, 0, 0, 0, 0.1, 1000) : new THREE.PerspectiveCamera(75, 0, 0.1, 1000) if (!isCamera) { camera.position.z = 5 if (cameraOptions) { applyProps(camera, cameraOptions as any) // Preserve user-defined frustum if possible // https://github.com/pmndrs/react-three-fiber/issues/3160 if (!(camera as any).manual) { if ( 'aspect' in cameraOptions || 'left' in cameraOptions || 'right' in cameraOptions || 'bottom' in cameraOptions || 'top' in cameraOptions ) { ;(camera as any).manual = true camera.updateProjectionMatrix() } } } // Always look at center by default if (!state.camera && !cameraOptions?.rotation) camera.lookAt(0, 0, 0) } state.set({ camera }) // Configure raycaster // https://github.com/pmndrs/react-xr/issues/300 raycaster.camera = camera } // Set up scene (one time only!) if (!state.scene) { let scene: THREE.Scene if ((sceneOptions as unknown as THREE.Scene | undefined)?.isScene) { scene = sceneOptions as THREE.Scene prepare(scene, store, '', {}) } else { scene = new THREE.Scene() prepare(scene, store, '', {}) if (sceneOptions) applyProps(scene as any, sceneOptions as any) } state.set({ scene }) } // Store events internally if (events && !state.events.handlers) state.set({ events: events(store) }) // Check size, allow it to take on container bounds initially const size = computeInitialSize(canvas, propsSize) if (!is.equ(size, state.size, shallowLoose)) { state.setSize(size.width, size.height, size.top, size.left) } // Check pixelratio if (dpr && state.viewport.dpr !== calculateDpr(dpr)) state.setDpr(dpr) // Check frameloop if (state.frameloop !== frameloop) state.setFrameloop(frameloop) // Check pointer missed if (!state.onPointerMissed) state.set({ onPointerMissed }) // Check performance if (performance && !is.equ(performance, state.performance, shallowLoose)) state.set((state) => ({ performance: { ...state.performance, ...performance } })) // Set up XR (one time only!) if (!state.xr) { // Handle frame behavior in WebXR const handleXRFrame: XRFrameRequestCallback = (timestamp: number, frame?: XRFrame) => { const state = store.getState() if (state.frameloop === 'never') return advance(timestamp, true, state, frame) } // Toggle render switching on session const handleSessionChange = () => { const state = store.getState() state.gl.xr.enabled = state.gl.xr.isPresenting state.gl.xr.setAnimationLoop(state.gl.xr.isPresenting ? handleXRFrame : null) if (!state.gl.xr.isPresenting) invalidate(state) } // WebXR session manager const xr = { connect() { const gl = store.getState().gl gl.xr.addEventListener('sessionstart', handleSessionChange) gl.xr.addEventListener('sessionend', handleSessionChange) }, disconnect() { const gl = store.getState().gl gl.xr.removeEventListener('sessionstart', handleSessionChange) gl.xr.removeEventListener('sessionend', handleSessionChange) }, } // Subscribe to WebXR session events if (typeof gl.xr?.addEventListener === 'function') xr.connect() state.set({ xr }) } // Set shadowmap if (gl.shadowMap) { const oldEnabled = gl.shadowMap.enabled const oldType = gl.shadowMap.type gl.shadowMap.enabled = !!shadows if (is.boo(shadows)) { gl.shadowMap.type = THREE.PCFSoftShadowMap } else if (is.str(shadows)) { const types = { basic: THREE.BasicShadowMap, percentage: THREE.PCFShadowMap, soft: THREE.PCFSoftShadowMap, variance: THREE.VSMShadowMap, } gl.shadowMap.type = types[shadows] ?? THREE.PCFSoftShadowMap } else if (is.obj(shadows)) { Object.assign(gl.shadowMap, shadows) } if (oldEnabled !== gl.shadowMap.enabled || oldType !== gl.shadowMap.type) gl.shadowMap.needsUpdate = true } THREE.ColorManagement.enabled = !legacy // Set color space and tonemapping preferences if (!configured) { gl.outputColorSpace = linear ? THREE.LinearSRGBColorSpace : THREE.SRGBColorSpace gl.toneMapping = flat ? THREE.NoToneMapping : THREE.ACESFilmicToneMapping } // Update color management state if (state.legacy !== legacy) state.set(() => ({ legacy })) if (state.linear !== linear) state.set(() => ({ linear })) if (state.flat !== flat) state.set(() => ({ flat })) // Set gl props if (glConfig && !is.fun(glConfig) && !isRenderer(glConfig) && !is.equ(glConfig, gl, shallowLoose)) applyProps(gl, glConfig as any) // Set locals onCreated = onCreatedCallback configured = true resolve() return this }, render(children: React.ReactNode): RootStore { // The root has to be configured before it can be rendered if (!configured && !pending) this.configure() pending!.then(() => { reconciler.updateContainer( , fiber, null, () => undefined, ) }) return store }, unmount(): void { unmountComponentAtNode(canvas) }, } } interface ProviderProps { onCreated?: (state: RootState) => void store: RootStore children: React.ReactNode rootElement: TCanvas } function Provider({ store, children, onCreated, rootElement, }: ProviderProps): React.JSX.Element { useIsomorphicLayoutEffect(() => { const state = store.getState() // Flag the canvas active, rendering will now begin state.set((state) => ({ internal: { ...state.internal, active: true } })) // Notify that init is completed, the scene graph exists, but nothing has yet rendered if (onCreated) onCreated(state) // Connect events to the targets parent, this is done to ensure events are registered on // a shared target, and not on the canvas itself if (!store.getState().events.connected) state.events.connect?.(rootElement) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return {children} } export function unmountComponentAtNode( canvas: TCanvas, callback?: (canvas: TCanvas) => void, ): void { const root = _roots.get(canvas) const fiber = root?.fiber if (fiber) { const state = root?.store.getState() if (state) state.internal.active = false reconciler.updateContainer(null, fiber, null, () => { if (state) { setTimeout(() => { try { state.events.disconnect?.() state.gl?.renderLists?.dispose?.() state.gl?.forceContextLoss?.() if (state.gl?.xr) state.xr.disconnect() dispose(state.scene) _roots.delete(canvas) if (callback) callback(canvas) } catch (e) { /* ... */ } }, 500) } }) } } export type InjectState = Partial< Omit & { events?: { enabled?: boolean priority?: number compute?: ComputeFunction connected?: any } } > export function createPortal( children: React.ReactNode, container: THREE.Object3D, state?: InjectState, ): React.JSX.Element { return } interface PortalProps { children: React.ReactNode state?: InjectState container: THREE.Object3D } function Portal({ state = {}, children, container }: PortalProps): React.JSX.Element { /** This has to be a component because it would not be able to call useThree/useStore otherwise since * if this is our environment, then we are not in r3f's renderer but in react-dom, it would trigger * the "R3F hooks can only be used within the Canvas component!" warning: * * {createPortal(...)} */ const { events, size, ...rest } = state const previousRoot = useStore() const [raycaster] = React.useState(() => new THREE.Raycaster()) const [pointer] = React.useState(() => new THREE.Vector2()) const inject = useMutableCallback((rootState: RootState, injectState: RootState) => { let viewport = undefined if (injectState.camera && size) { const camera = injectState.camera // Calculate the override viewport, if present viewport = rootState.viewport.getCurrentViewport(camera, new THREE.Vector3(), size) // Update the portal camera, if it differs from the previous layer if (camera !== rootState.camera) updateCamera(camera, size) } return { // The intersect consists of the previous root state ...rootState, ...injectState, // Portals have their own scene, which forms the root, a raycaster and a pointer scene: container as THREE.Scene, raycaster, pointer, mouse: pointer, // Their previous root is the layer before it previousRoot, // Events, size and viewport can be overridden by the inject layer events: { ...rootState.events, ...injectState.events, ...events }, size: { ...rootState.size, ...size }, viewport: { ...rootState.viewport, ...viewport }, // Layers are allowed to override events setEvents: (events: Partial>) => injectState.set((state) => ({ ...state, events: { ...state.events, ...events } })), } as RootState }) const usePortalStore = React.useMemo(() => { // Create a mirrored store, based on the previous root with a few overrides ... const store = createWithEqualityFn((set, get) => ({ ...rest, set, get } as RootState)) // Subscribe to previous root-state and copy changes over to the mirrored portal-state const onMutate = (prev: RootState) => store.setState((state) => inject.current(prev, state)) onMutate(previousRoot.getState()) previousRoot.subscribe(onMutate) return store // eslint-disable-next-line react-hooks/exhaustive-deps }, [previousRoot, container]) return ( // @ts-ignore, reconciler types are not maintained <> {reconciler.createPortal( {children}, usePortalStore, null, )} ) } /** * Force React to flush any updates inside the provided callback synchronously and immediately. * All the same caveats documented for react-dom's `flushSync` apply here (see https://react.dev/reference/react-dom/flushSync). * Nevertheless, sometimes one needs to render synchronously, for example to keep DOM and 3D changes in lock-step without * having to revert to a non-React solution. Note: this will only flush updates within the `Canvas` root. */ export function flushSync(fn: () => R): R { // @ts-ignore - reconciler types are not maintained return reconciler.flushSyncFromReconciler(fn) } ================================================ FILE: packages/fiber/src/core/store.ts ================================================ import * as THREE from 'three' import * as React from 'react' import { type StoreApi } from 'zustand' import { createWithEqualityFn, type UseBoundStoreWithEqualityFn } from 'zustand/traditional' import type { DomEvent, EventManager, PointerCaptureTarget, ThreeEvent } from './events' import { calculateDpr, type Camera, isOrthographicCamera, updateCamera } from './utils' export interface Intersection extends THREE.Intersection { eventObject: THREE.Object3D } export type Subscription = { ref: React.RefObject priority: number store: RootStore } export type Dpr = number | [min: number, max: number] export interface Size { width: number height: number top: number left: number } export type Frameloop = 'always' | 'demand' | 'never' export interface Viewport extends Size { /** The initial pixel ratio */ initialDpr: number /** Current pixel ratio */ dpr: number /** size.width / viewport.width */ factor: number /** Camera distance */ distance: number /** Camera aspect ratio: width / height */ aspect: number } export type RenderCallback = (state: RootState, delta: number, frame?: XRFrame) => void export interface Performance { /** Current performance normal, between min and max */ current: number /** How low the performance can go, between 0 and max */ min: number /** How high the performance can go, between min and max */ max: number /** Time until current returns to max in ms */ debounce: number /** Sets current to min, puts the system in regression */ regress: () => void } export interface Renderer { render: (scene: THREE.Scene, camera: THREE.Camera) => any } export const isRenderer = (def: any) => !!def?.render export interface InternalState { interaction: THREE.Object3D[] hovered: Map> subscribers: Subscription[] capturedMap: Map> initialClick: [x: number, y: number] initialHits: THREE.Object3D[] lastEvent: React.RefObject active: boolean priority: number frames: number subscribe: (callback: React.RefObject, priority: number, store: RootStore) => () => void } export interface XRManager { connect: () => void disconnect: () => void } export interface RootState { /** Set current state */ set: StoreApi['setState'] /** Get current state */ get: StoreApi['getState'] /** The instance of the renderer */ gl: THREE.WebGLRenderer /** Default camera */ camera: Camera /** Default scene */ scene: THREE.Scene /** Default raycaster */ raycaster: THREE.Raycaster /** Default clock */ clock: THREE.Clock /** Event layer interface, contains the event handler and the node they're connected to */ events: EventManager /** XR interface */ xr: XRManager /** Currently used controls */ controls: THREE.EventDispatcher | null /** Normalized event coordinates */ pointer: THREE.Vector2 /** @deprecated Normalized event coordinates, use "pointer" instead! */ mouse: THREE.Vector2 /* Whether to enable r139's THREE.ColorManagement */ legacy: boolean /** Shortcut to gl.outputColorSpace = THREE.LinearSRGBColorSpace */ linear: boolean /** Shortcut to gl.toneMapping = NoTonemapping */ flat: boolean /** Render loop flags */ frameloop: Frameloop performance: Performance /** Reactive pixel-size of the canvas */ size: Size /** Reactive size of the viewport in threejs units */ viewport: Viewport & { getCurrentViewport: ( camera?: Camera, target?: THREE.Vector3 | Parameters, size?: Size, ) => Omit } /** Flags the canvas for render, but doesn't render in itself */ invalidate: (frames?: number) => void /** Advance (render) one step */ advance: (timestamp: number, runGlobalEffects?: boolean) => void /** Shortcut to setting the event layer */ setEvents: (events: Partial>) => void /** Shortcut to manual sizing */ setSize: (width: number, height: number, top?: number, left?: number) => void /** Shortcut to manual setting the pixel ratio */ setDpr: (dpr: Dpr) => void /** Shortcut to setting frameloop flags */ setFrameloop: (frameloop: Frameloop) => void /** When the canvas was clicked but nothing was hit */ onPointerMissed?: (event: MouseEvent) => void /** If this state model is layered (via createPortal) then this contains the previous layer */ previousRoot?: RootStore /** Internals */ internal: InternalState } export type RootStore = UseBoundStoreWithEqualityFn> export const context = /* @__PURE__ */ React.createContext(null!) export const createStore = ( invalidate: (state?: RootState, frames?: number) => void, advance: (timestamp: number, runGlobalEffects?: boolean, state?: RootState, frame?: XRFrame) => void, ): RootStore => { const rootStore = createWithEqualityFn((set, get) => { const position = new THREE.Vector3() const defaultTarget = new THREE.Vector3() const tempTarget = new THREE.Vector3() function getCurrentViewport( camera: Camera = get().camera, target: THREE.Vector3 | Parameters = defaultTarget, size: Size = get().size, ): Omit { const { width, height, top, left } = size const aspect = width / height if ((target as THREE.Vector3).isVector3) tempTarget.copy(target as THREE.Vector3) else tempTarget.set(...(target as Parameters)) const distance = camera.getWorldPosition(position).distanceTo(tempTarget) if (isOrthographicCamera(camera)) { return { width: width / camera.zoom, height: height / camera.zoom, top, left, factor: 1, distance, aspect } } else { const fov = (camera.fov * Math.PI) / 180 // convert vertical fov to radians const h = 2 * Math.tan(fov / 2) * distance // visible height const w = h * (width / height) return { width: w, height: h, top, left, factor: width / w, distance, aspect } } } let performanceTimeout: ReturnType | undefined = undefined const setPerformanceCurrent = (current: number) => set((state) => ({ performance: { ...state.performance, current } })) const pointer = new THREE.Vector2() const rootState: RootState = { set, get, // Mock objects that have to be configured gl: null as unknown as THREE.WebGLRenderer, camera: null as unknown as Camera, raycaster: null as unknown as THREE.Raycaster, events: { priority: 1, enabled: true, connected: false }, scene: null as unknown as THREE.Scene, xr: null as unknown as XRManager, invalidate: (frames = 1) => invalidate(get(), frames), advance: (timestamp: number, runGlobalEffects?: boolean) => advance(timestamp, runGlobalEffects, get()), legacy: false, linear: false, flat: false, controls: null, clock: new THREE.Clock(), pointer, mouse: pointer, frameloop: 'always', onPointerMissed: undefined, performance: { current: 1, min: 0.5, max: 1, debounce: 200, regress: () => { const state = get() // Clear timeout if (performanceTimeout) clearTimeout(performanceTimeout) // Set lower bound performance if (state.performance.current !== state.performance.min) setPerformanceCurrent(state.performance.min) // Go back to upper bound performance after a while unless something regresses meanwhile performanceTimeout = setTimeout( () => setPerformanceCurrent(get().performance.max), state.performance.debounce, ) }, }, size: { width: 0, height: 0, top: 0, left: 0 }, viewport: { initialDpr: 0, dpr: 0, width: 0, height: 0, top: 0, left: 0, aspect: 0, distance: 0, factor: 0, getCurrentViewport, }, setEvents: (events: Partial>) => set((state) => ({ ...state, events: { ...state.events, ...events } })), setSize: (width: number, height: number, top: number = 0, left: number = 0) => { const camera = get().camera const size = { width, height, top, left } set((state) => ({ size, viewport: { ...state.viewport, ...getCurrentViewport(camera, defaultTarget, size) } })) }, setDpr: (dpr: Dpr) => set((state) => { const resolved = calculateDpr(dpr) return { viewport: { ...state.viewport, dpr: resolved, initialDpr: state.viewport.initialDpr || resolved } } }), setFrameloop: (frameloop: Frameloop = 'always') => { const clock = get().clock // if frameloop === "never" clock.elapsedTime is updated using advance(timestamp) clock.stop() clock.elapsedTime = 0 if (frameloop !== 'never') { clock.start() clock.elapsedTime = 0 } set(() => ({ frameloop })) }, previousRoot: undefined, internal: { // Events interaction: [], hovered: new Map>(), subscribers: [], initialClick: [0, 0], initialHits: [], capturedMap: new Map(), lastEvent: React.createRef(), // Updates active: false, frames: 0, priority: 0, subscribe: (ref: React.RefObject, priority: number, store: RootStore) => { const internal = get().internal // If this subscription was given a priority, it takes rendering into its own hands // For that reason we switch off automatic rendering and increase the manual flag // As long as this flag is positive there can be no internal rendering at all // because there could be multiple render subscriptions internal.priority = internal.priority + (priority > 0 ? 1 : 0) internal.subscribers.push({ ref, priority, store }) // Register subscriber and sort layers from lowest to highest, meaning, // highest priority renders last (on top of the other frames) internal.subscribers = internal.subscribers.sort((a, b) => a.priority - b.priority) return () => { const internal = get().internal if (internal?.subscribers) { // Decrease manual flag if this subscription had a priority internal.priority = internal.priority - (priority > 0 ? 1 : 0) // Remove subscriber from list internal.subscribers = internal.subscribers.filter((s) => s.ref !== ref) } } }, }, } return rootState }) const state = rootStore.getState() let oldSize = state.size let oldDpr = state.viewport.dpr let oldCamera = state.camera rootStore.subscribe(() => { const { camera, size, viewport, gl, set } = rootStore.getState() // Resize camera and renderer on changes to size and pixelratio if (size.width !== oldSize.width || size.height !== oldSize.height || viewport.dpr !== oldDpr) { oldSize = size oldDpr = viewport.dpr // Update camera & renderer updateCamera(camera, size) if (viewport.dpr > 0) gl.setPixelRatio(viewport.dpr) const updateStyle = typeof HTMLCanvasElement !== 'undefined' && gl.domElement instanceof HTMLCanvasElement gl.setSize(size.width, size.height, updateStyle) } // Update viewport once the camera changes if (camera !== oldCamera) { oldCamera = camera // Update viewport set((state) => ({ viewport: { ...state.viewport, ...state.viewport.getCurrentViewport(camera) } })) } }) // Invalidate on any change rootStore.subscribe((state) => invalidate(state)) // Return root state return rootStore } ================================================ FILE: packages/fiber/src/core/utils.tsx ================================================ import * as THREE from 'three' import * as React from 'react' import { useFiber, traverseFiber, useContextBridge } from 'its-fine' import { Instance } from './reconciler' import type { EventHandlers } from './events' import type { Dpr, Renderer, RootStore, Size } from './store' export type NonFunctionKeys

    = { [K in keyof P]-?: P[K] extends Function ? never : K }[keyof P] export type Overwrite = Omit> & O export type Properties = Pick> export type Mutable

    = { [K in keyof P]: P[K] | Readonly } export type IsOptional = undefined extends T ? true : false export type IsAllOptional = T extends [infer First, ...infer Rest] ? IsOptional extends true ? IsAllOptional : false : true /** * Returns the instance's initial (outmost) root. */ export function findInitialRoot(instance: Instance): RootStore { let root = instance.root while (root.getState().previousRoot) root = root.getState().previousRoot! return root } export type Act = (cb: () => Promise) => Promise /** * Safely flush async effects when testing, simulating a legacy root. * @deprecated Import from React instead. import { act } from 'react' */ // Reference with computed key to break Webpack static analysis // https://github.com/webpack/webpack/issues/14814 export const act: Act = React[('act' + '') as 'act'] export type Camera = (THREE.OrthographicCamera | THREE.PerspectiveCamera) & { manual?: boolean } export const isOrthographicCamera = (def: Camera): def is THREE.OrthographicCamera => def && (def as THREE.OrthographicCamera).isOrthographicCamera export const isRef = (obj: any): obj is React.RefObject => obj && obj.hasOwnProperty('current') export const isColorRepresentation = (value: unknown): value is THREE.ColorRepresentation => value != null && (typeof value === 'string' || typeof value === 'number' || (value as THREE.Color).isColor) /** * An SSR-friendly useLayoutEffect. * * React currently throws a warning when using useLayoutEffect on the server. * To get around it, we can conditionally useEffect on the server (no-op) and * useLayoutEffect elsewhere. * * @see https://github.com/facebook/react/issues/14927 */ export const useIsomorphicLayoutEffect = /* @__PURE__ */ (() => typeof window !== 'undefined' && (window.document?.createElement || window.navigator?.product === 'ReactNative'))() ? React.useLayoutEffect : React.useEffect export function useMutableCallback(fn: T): React.RefObject { const ref = React.useRef(fn) useIsomorphicLayoutEffect(() => void (ref.current = fn), [fn]) return ref } export type Bridge = React.FC<{ children?: React.ReactNode }> /** * Bridges renderer Context and StrictMode from a primary renderer. */ export function useBridge(): Bridge { const fiber = useFiber() const ContextBridge = useContextBridge() return React.useMemo( () => ({ children }) => { const strict = !!traverseFiber(fiber, true, (node) => node.type === React.StrictMode) const Root = strict ? React.StrictMode : React.Fragment return ( {children} ) }, [fiber, ContextBridge], ) } export type SetBlock = false | Promise | null export type UnblockProps = { set: React.Dispatch>; children: React.ReactNode } export function Block({ set }: Omit) { useIsomorphicLayoutEffect(() => { set(new Promise(() => null)) return () => set(false) }, [set]) return null } // NOTE: static members get down-level transpiled to mutations which break tree-shaking export const ErrorBoundary = /* @__PURE__ */ (() => class ErrorBoundary extends React.Component< { set: React.Dispatch; children: React.ReactNode }, { error: boolean } > { state = { error: false } static getDerivedStateFromError = () => ({ error: true }) componentDidCatch(err: Error) { this.props.set(err) } render() { return this.state.error ? null : this.props.children } })() export interface ObjectMap { nodes: { [name: string]: THREE.Object3D } materials: { [name: string]: THREE.Material } meshes: { [name: string]: THREE.Mesh } } export function calculateDpr(dpr: Dpr): number { // Err on the side of progress by assuming 2x dpr if we can't detect it // This will happen in workers where window is defined but dpr isn't. const target = typeof window !== 'undefined' ? window.devicePixelRatio ?? 2 : 1 return Array.isArray(dpr) ? Math.min(Math.max(dpr[0], target), dpr[1]) : dpr } /** * Returns instance root state */ export function getRootState(obj: T) { return (obj as Instance['object']).__r3f?.root.getState() } export interface EquConfig { /** Compare arrays by reference equality a === b (default), or by shallow equality */ arrays?: 'reference' | 'shallow' /** Compare objects by reference equality a === b (default), or by shallow equality */ objects?: 'reference' | 'shallow' /** If true the keys in both a and b must match 1:1 (default), if false a's keys must intersect b's */ strict?: boolean } // A collection of compare functions export const is = { obj: (a: any) => a === Object(a) && !is.arr(a) && typeof a !== 'function', fun: (a: any): a is Function => typeof a === 'function', str: (a: any): a is string => typeof a === 'string', num: (a: any): a is number => typeof a === 'number', boo: (a: any): a is boolean => typeof a === 'boolean', und: (a: any) => a === void 0, nul: (a: any) => a === null, arr: (a: any) => Array.isArray(a), equ(a: any, b: any, { arrays = 'shallow', objects = 'reference', strict = true }: EquConfig = {}) { // Wrong type or one of the two undefined, doesn't match if (typeof a !== typeof b || !!a !== !!b) return false // Atomic, just compare a against b if (is.str(a) || is.num(a) || is.boo(a)) return a === b const isObj = is.obj(a) if (isObj && objects === 'reference') return a === b const isArr = is.arr(a) if (isArr && arrays === 'reference') return a === b // Array or Object, shallow compare first to see if it's a match if ((isArr || isObj) && a === b) return true // Last resort, go through keys let i // Check if a has all the keys of b for (i in a) if (!(i in b)) return false // Check if values between keys match if (isObj && arrays === 'shallow' && objects === 'shallow') { for (i in strict ? b : a) if (!is.equ(a[i], b[i], { strict, objects: 'reference' })) return false } else { for (i in strict ? b : a) if (a[i] !== b[i]) return false } // If i is undefined if (is.und(i)) { // If both arrays are empty we consider them equal if (isArr && a.length === 0 && b.length === 0) return true // If both objects are empty we consider them equal if (isObj && Object.keys(a).length === 0 && Object.keys(b).length === 0) return true // Otherwise match them by value if (a !== b) return false } return true }, } // Collects nodes and materials from a THREE.Object3D export function buildGraph(object: THREE.Object3D): ObjectMap { const data: ObjectMap = { nodes: {}, materials: {}, meshes: {} } if (object) { object.traverse((obj: any) => { if (obj.name) data.nodes[obj.name] = obj if (obj.material && !data.materials[obj.material.name]) data.materials[obj.material.name] = obj.material if (obj.isMesh && !data.meshes[obj.name]) data.meshes[obj.name] = obj }) } return data } export interface Disposable { type?: string dispose?: () => void } // Disposes an object and all its properties export function dispose(obj: T): void { if (obj.type !== 'Scene') obj.dispose?.() for (const p in obj) { const prop = obj[p] as Disposable | undefined if (prop?.type !== 'Scene') prop?.dispose?.() } } export const REACT_INTERNAL_PROPS = ['children', 'key', 'ref'] // Gets only instance props from reconciler fibers export function getInstanceProps(pendingProps: Record): Instance['props'] { const props: Instance['props'] = {} for (const key in pendingProps) { if (!REACT_INTERNAL_PROPS.includes(key)) props[key] = pendingProps[key] } return props } // Each object in the scene carries a small LocalState descriptor export function prepare(target: T, root: RootStore, type: string, props: Instance['props']): Instance { const object = target as unknown as Instance['object'] // Create instance descriptor let instance = object?.__r3f if (!instance) { instance = { root, type, parent: null, children: [], props: getInstanceProps(props), object, eventCount: 0, handlers: {}, isHidden: false, } if (object) object.__r3f = instance } return instance } export function resolve(root: any, key: string): { root: any; key: string; target: any } { if (!key.includes('-')) return { root, key, target: root[key] } // First try the entire key as a single property (e.g., 'foo-bar') if (key in root) { return { root, key, target: root[key] } } // Try piercing (e.g., 'material-color' -> material.color) let target = root const parts = key.split('-') for (const part of parts) { if (typeof target !== 'object' || target === null) { if (target !== undefined) { // Property exists but has unexpected shape const remaining = parts.slice(parts.indexOf(part)).join('-') return { root: target, key: remaining, target: undefined } } // Property doesn't exist - fallback to original key return { root, key, target: undefined } } key = part root = target target = target[key] } return { root, key, target } } // Checks if a dash-cased string ends with an integer const INDEX_REGEX = /-\d+$/ export function attach(parent: Instance, child: Instance): void { if (is.str(child.props.attach)) { // If attaching into an array (foo-0), create one if (INDEX_REGEX.test(child.props.attach)) { const index = child.props.attach.replace(INDEX_REGEX, '') const { root, key } = resolve(parent.object, index) if (!Array.isArray(root[key])) root[key] = [] } const { root, key } = resolve(parent.object, child.props.attach) child.previousAttach = root[key] root[key] = child.object } else if (is.fun(child.props.attach)) { child.previousAttach = child.props.attach(parent.object, child.object) } } export function detach(parent: Instance, child: Instance): void { if (is.str(child.props.attach)) { const { root, key } = resolve(parent.object, child.props.attach) const previous = child.previousAttach // When the previous value was undefined, it means the value was never set to begin with if (previous === undefined) delete root[key] // Otherwise set the previous value else root[key] = previous } else { child.previousAttach?.(parent.object, child.object) } delete child.previousAttach } export const RESERVED_PROPS = [ ...REACT_INTERNAL_PROPS, // Instance props 'args', 'dispose', 'attach', 'object', 'onUpdate', // Behavior flags 'dispose', ] const MEMOIZED_PROTOTYPES = new Map() function getMemoizedPrototype(root: any) { let ctor = MEMOIZED_PROTOTYPES.get(root.constructor) try { if (!ctor) { ctor = new root.constructor() MEMOIZED_PROTOTYPES.set(root.constructor, ctor) } } catch (e) { // ... } return ctor } // This function prepares a set of changes to be applied to the instance export function diffProps(instance: Instance, newProps: Instance['props']): Instance['props'] { const changedProps: Instance['props'] = {} // Sort through props for (const prop in newProps) { // Skip reserved keys if (RESERVED_PROPS.includes(prop)) continue // Skip if props match if (is.equ(newProps[prop], instance.props[prop])) continue // Props changed, add them changedProps[prop] = newProps[prop] // Reset pierced props for (const other in newProps) { if (other.startsWith(`${prop}-`)) changedProps[other] = newProps[other] } } // Reset removed props for HMR for (const prop in instance.props) { if (RESERVED_PROPS.includes(prop) || newProps.hasOwnProperty(prop)) continue const { root, key } = resolve(instance.object, prop) // https://github.com/mrdoob/three.js/issues/21209 // HMR/fast-refresh relies on the ability to cancel out props, but threejs // has no means to do this. Hence we curate a small collection of value-classes // with their respective constructor/set arguments // For removed props, try to set default values, if possible if (root.constructor && root.constructor.length === 0) { // create a blank slate of the instance and copy the particular parameter. const ctor = getMemoizedPrototype(root) if (!is.und(ctor)) changedProps[key] = ctor[key] } else { // instance does not have constructor, just set it to 0 changedProps[key] = 0 } } return changedProps } // https://github.com/mrdoob/three.js/pull/27042 // https://github.com/mrdoob/three.js/pull/22748 const colorMaps = ['map', 'emissiveMap', 'sheenColorMap', 'specularColorMap', 'envMap'] const EVENT_REGEX = /^on(Pointer|Click|DoubleClick|ContextMenu|Wheel)/ type ClassConstructor = { new (): void } // This function applies a set of changes to the instance export function applyProps(object: Instance['object'], props: Instance['props']): Instance['object'] { const instance = object.__r3f const rootState = instance && findInitialRoot(instance).getState() const prevHandlers = instance?.eventCount for (const prop in props) { let value = props[prop] // Don't mutate reserved keys if (RESERVED_PROPS.includes(prop)) continue // Deal with pointer events, including removing them if undefined if (instance && EVENT_REGEX.test(prop)) { if (typeof value === 'function') instance.handlers[prop as keyof EventHandlers] = value as any else delete instance.handlers[prop as keyof EventHandlers] instance.eventCount = Object.keys(instance.handlers).length continue } // Ignore setting undefined props // https://github.com/pmndrs/react-three-fiber/issues/274 if (value === undefined) continue let { root, key, target } = resolve(object, prop) // Throw an error if we attempted to set a pierced prop to a non-object if (target === undefined && (typeof root !== 'object' || root === null)) { throw Error(`R3F: Cannot set "${prop}". Ensure it is an object before setting "${key}".`) } // Layers must be written to the mask property if (target instanceof THREE.Layers && value instanceof THREE.Layers) { target.mask = value.mask } // Set colors if valid color representation for automatic conversion (copy) else if (target instanceof THREE.Color && isColorRepresentation(value)) { target.set(value) } // Copy if properties match signatures and implement math interface (likely read-only) else if ( target !== null && typeof target === 'object' && typeof target.set === 'function' && typeof target.copy === 'function' && (value as ClassConstructor | null)?.constructor && (target as ClassConstructor).constructor === (value as ClassConstructor).constructor ) { target.copy(value) } // Set array types else if ( target !== null && typeof target === 'object' && typeof target.set === 'function' && Array.isArray(value) ) { if (typeof target.fromArray === 'function') target.fromArray(value) else target.set(...value) } // Set literal types else if ( target !== null && typeof target === 'object' && typeof target.set === 'function' && typeof value === 'number' ) { // Allow setting array scalars if (typeof target.setScalar === 'function') target.setScalar(value) // Otherwise just set single value else target.set(value) } // Else, just overwrite the value else { root[key] = value // Auto-convert sRGB texture parameters for built-in materials // https://github.com/pmndrs/react-three-fiber/issues/344 // https://github.com/mrdoob/three.js/pull/25857 if ( rootState && !rootState.linear && colorMaps.includes(key) && (root[key] as unknown as THREE.Texture | undefined)?.isTexture && // sRGB textures must be RGBA8 since r137 https://github.com/mrdoob/three.js/pull/23129 root[key].format === THREE.RGBAFormat && root[key].type === THREE.UnsignedByteType ) { // NOTE: this cannot be set from the renderer (e.g. sRGB source textures rendered to P3) root[key].colorSpace = THREE.SRGBColorSpace } } } // Register event handlers if ( instance?.parent && rootState?.internal && (instance.object as unknown as THREE.Object3D | undefined)?.isObject3D && prevHandlers !== instance.eventCount ) { const object = instance.object as unknown as THREE.Object3D // Pre-emptively remove the instance from the interaction manager const index = rootState.internal.interaction.indexOf(object) if (index > -1) rootState.internal.interaction.splice(index, 1) // Add the instance to the interaction manager only when it has handlers if (instance.eventCount && object.raycast !== null) { rootState.internal.interaction.push(object) } } // Auto-attach geometries and materials if (instance && instance.props.attach === undefined) { if ((instance.object as unknown as THREE.BufferGeometry).isBufferGeometry) instance.props.attach = 'geometry' else if ((instance.object as unknown as THREE.Material).isMaterial) instance.props.attach = 'material' } // Instance was updated, request a frame if (instance) invalidateInstance(instance) return object } export function invalidateInstance(instance: Instance): void { if (!instance.parent) return instance.props.onUpdate?.(instance.object) const state = instance.root?.getState?.() if (state && state.internal.frames === 0) state.invalidate() } export function updateCamera(camera: Camera, size: Size): void { // Do not mess with the camera if it belongs to the user // https://github.com/pmndrs/react-three-fiber/issues/92 if (camera.manual) return if (isOrthographicCamera(camera)) { camera.left = size.width / -2 camera.right = size.width / 2 camera.top = size.height / 2 camera.bottom = size.height / -2 } else { camera.aspect = size.width / size.height } camera.updateProjectionMatrix() } export const isObject3D = (object: any): object is THREE.Object3D => object?.isObject3D ================================================ FILE: packages/fiber/src/index.tsx ================================================ import * as ReactThreeFiber from './three-types' export { ReactThreeFiber } export * from './three-types' export * from './core' export * from './web/Canvas' export { createPointerEvents as events } from './web/events' ================================================ FILE: packages/fiber/src/native/Canvas.tsx ================================================ import * as React from 'react' import * as THREE from 'three' import { View, type ViewProps, type ViewStyle, type GestureResponderHandlers, type GestureResponderEvent, PanResponder, type LayoutChangeEvent, StyleSheet, PixelRatio, } from 'react-native' import { ExpoWebGLRenderingContext, GLView } from 'expo-gl' import { FiberProvider } from 'its-fine' import { SetBlock, Block, ErrorBoundary, useMutableCallback, useBridge, useIsomorphicLayoutEffect } from '../core/utils' import { extend, createRoot, unmountComponentAtNode, RenderProps, ReconcilerRoot } from '../core' import { createPointerEvents } from '../web/events' import { RootState, Size } from '../core/store' // TODO: React 19 needs support from react-native const _View = View as any export interface CanvasProps extends Omit, 'size' | 'dpr'>, Omit { children?: React.ReactNode style?: ViewStyle ref?: React.Ref } function CanvasImpl({ children, style, gl, events = createPointerEvents, shadows, linear, flat, legacy, orthographic, frameloop, performance, raycaster, camera, scene, onPointerMissed, onCreated, ref, pointerEvents, ...props }: CanvasProps) { // Create a known catalogue of Threejs-native elements // This will include the entire THREE namespace by default, users can extend // their own elements by using the createRoot API instead React.useMemo(() => extend(THREE as any), []) const Bridge = useBridge() const [{ width, height, top, left }, setSize] = React.useState({ width: 0, height: 0, top: 0, left: 0 }) const [canvas, setCanvas] = React.useState(null) const [bind, setBind] = React.useState() React.useImperativeHandle(ref, () => viewRef.current) const handlePointerMissed = useMutableCallback(onPointerMissed) const [block, setBlock] = React.useState(false) const [error, setError] = React.useState(undefined) // Suspend this component if block is a promise (2nd run) if (block) throw block // Throw exception outwards if anything within canvas throws if (error) throw error const viewRef = React.useRef(null!) const root = React.useRef>(null!) const [antialias, setAntialias] = React.useState(true) const onLayout = React.useCallback((e: LayoutChangeEvent) => { const { width, height, x, y } = e.nativeEvent.layout setSize({ width, height, top: y, left: x }) }, []) // Called on context create or swap // https://github.com/pmndrs/react-three-fiber/pull/2297 const onContextCreate = React.useCallback((context: ExpoWebGLRenderingContext) => { const listeners = new Map() const canvas = { style: {}, width: context.drawingBufferWidth, height: context.drawingBufferHeight, clientWidth: context.drawingBufferWidth, clientHeight: context.drawingBufferHeight, getContext: (_: any, { antialias = false }) => { setAntialias(antialias) return context }, addEventListener(type: string, listener: EventListener) { let callbacks = listeners.get(type) if (!callbacks) { callbacks = [] listeners.set(type, callbacks) } callbacks.push(listener) }, removeEventListener(type: string, listener: EventListener) { const callbacks = listeners.get(type) if (callbacks) { const index = callbacks.indexOf(listener) if (index !== -1) callbacks.splice(index, 1) } }, dispatchEvent(event: Event) { Object.assign(event, { target: this }) const callbacks = listeners.get(event.type) if (callbacks) { for (const callback of callbacks) { callback(event) } } }, setPointerCapture() { // TODO }, releasePointerCapture() { // TODO }, } as unknown as HTMLCanvasElement // TODO: this is wrong but necessary to trick controls // @ts-ignore canvas.ownerDocument = canvas canvas.getRootNode = () => canvas root.current = createRoot(canvas) setCanvas(canvas) function handleTouch(gestureEvent: GestureResponderEvent, type: string): true { gestureEvent.persist() canvas.dispatchEvent( Object.assign(gestureEvent.nativeEvent, { type, offsetX: gestureEvent.nativeEvent.locationX, offsetY: gestureEvent.nativeEvent.locationY, pointerType: 'touch', pointerId: gestureEvent.nativeEvent.identifier, }) as unknown as Event, ) return true } const responder = PanResponder.create({ onStartShouldSetPanResponder: () => true, onMoveShouldSetPanResponder: () => true, onMoveShouldSetPanResponderCapture: () => true, onPanResponderTerminationRequest: () => true, onStartShouldSetPanResponderCapture: (e) => handleTouch(e, 'pointercapture'), onPanResponderStart: (e) => handleTouch(e, 'pointerdown'), onPanResponderMove: (e) => handleTouch(e, 'pointermove'), onPanResponderEnd: (e, state) => { handleTouch(e, 'pointerup') if (Math.hypot(state.dx, state.dy) < 20) handleTouch(e, 'click') }, onPanResponderRelease: (e) => handleTouch(e, 'pointerleave'), onPanResponderTerminate: (e) => handleTouch(e, 'lostpointercapture'), onPanResponderReject: (e) => handleTouch(e, 'lostpointercapture'), }) setBind(responder.panHandlers) }, []) useIsomorphicLayoutEffect(() => { if (root.current && width > 0 && height > 0) { async function run() { await root.current.configure({ gl, events, shadows, linear, flat, legacy, orthographic, frameloop, performance, raycaster, camera, scene, // expo-gl can only render at native dpr/resolution // https://github.com/expo/expo-three/issues/39 dpr: PixelRatio.get(), size: { width, height, top, left }, // Pass mutable reference to onPointerMissed so it's free to update onPointerMissed: (...args) => handlePointerMissed.current?.(...args), // Overwrite onCreated to apply RN bindings onCreated: (state: RootState) => { // Bind render to RN bridge const context = state.gl.getContext() as ExpoWebGLRenderingContext const renderFrame = state.gl.render.bind(state.gl) state.gl.render = (scene: THREE.Scene, camera: THREE.Camera) => { renderFrame(scene, camera) context.endFrameEXP() } return onCreated?.(state) }, }) root.current.render( }>{children ?? null} , ) } run() } }) React.useEffect(() => { if (canvas) { return () => unmountComponentAtNode(canvas!) } }, [canvas]) return ( <_View {...props} ref={viewRef} onLayout={onLayout} style={{ flex: 1, ...style }}> {width > 0 && ( )} <_View style={StyleSheet.absoluteFill} pointerEvents={pointerEvents} {...bind} /> ) } /** * A native canvas which accepts threejs elements as children. * @see https://docs.pmnd.rs/react-three-fiber/api/canvas */ export function Canvas(props: CanvasProps) { return ( ) } ================================================ FILE: packages/fiber/src/native/events.ts ================================================ import { RootState, RootStore } from '../core/store' import { createEvents, DomEvent, EventManager, Events } from '../core/events' import { type GestureResponderEvent, PanResponder } from 'react-native' /** Default R3F event manager for react-native */ export function createTouchEvents(store: RootStore): EventManager { const { handlePointer } = createEvents(store) const handleTouch = (event: GestureResponderEvent, name: string): true => { event.persist() // Apply offset ;(event as any).nativeEvent.offsetX = event.nativeEvent.locationX ;(event as any).nativeEvent.offsetY = event.nativeEvent.locationY // Emulate DOM event const callback = handlePointer(name) callback(event.nativeEvent as any) return true } const responder = PanResponder.create({ onStartShouldSetPanResponder: () => true, onMoveShouldSetPanResponder: () => true, onMoveShouldSetPanResponderCapture: () => true, onPanResponderTerminationRequest: () => true, onStartShouldSetPanResponderCapture: (e) => handleTouch(e, 'onPointerCapture'), onPanResponderStart: (e) => handleTouch(e, 'onPointerDown'), onPanResponderMove: (e) => handleTouch(e, 'onPointerMove'), onPanResponderEnd: (e, state) => { handleTouch(e, 'onPointerUp') if (Math.hypot(state.dx, state.dy) < 20) handleTouch(e, 'onClick') }, onPanResponderRelease: (e) => handleTouch(e, 'onPointerLeave'), onPanResponderTerminate: (e) => handleTouch(e, 'onLostPointerCapture'), onPanResponderReject: (e) => handleTouch(e, 'onLostPointerCapture'), }) return { priority: 1, enabled: true, compute(event: DomEvent, state: RootState, previous?: RootState) { // https://github.com/pmndrs/react-three-fiber/pull/782 // Events trigger outside of canvas when moved, use offsetX/Y by default and allow overrides state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1) state.raycaster.setFromCamera(state.pointer, state.camera) }, connected: undefined, handlers: responder.panHandlers as unknown as Events, update: () => { const { events, internal } = store.getState() if (internal.lastEvent?.current && events.handlers) { handlePointer('onPointerMove')(internal.lastEvent.current) } }, connect: () => { const { set, events } = store.getState() events.disconnect?.() set((state) => ({ events: { ...state.events, connected: true } })) }, disconnect: () => { const { set } = store.getState() set((state) => ({ events: { ...state.events, connected: false } })) }, } } ================================================ FILE: packages/fiber/src/native/polyfills.ts ================================================ import * as THREE from 'three' import { Image, NativeModules, Platform } from 'react-native' import { Asset } from 'expo-asset' import { fromByteArray } from 'base64-js' import { Buffer } from 'buffer' // Conditionally import expo-file-system/legacy to support Expo 54 const getFileSystem = () => { try { return require('expo-file-system/legacy') } catch { return require('expo-file-system') } } const fs = getFileSystem() // http://stackoverflow.com/questions/105034 function uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8 return v.toString(16) }) } async function getAsset(input: string | number): Promise { if (typeof input === 'string') { // Don't process storage if (input.startsWith('file:')) return input // Unpack Blobs from react-native BlobManager // https://github.com/facebook/react-native/issues/22681#issuecomment-523258955 if (input.startsWith('blob:') || input.startsWith(NativeModules.BlobModule?.BLOB_URI_SCHEME)) { const blob = await new Promise((res, rej) => { const xhr = new XMLHttpRequest() xhr.open('GET', input as string) xhr.responseType = 'blob' xhr.onload = () => res(xhr.response) xhr.onerror = rej xhr.send() }) const data = await new Promise((res, rej) => { const reader = new FileReader() reader.onload = () => res(reader.result as string) reader.onerror = rej reader.readAsText(blob) }) input = `data:${blob.type};base64,${data}` } // Create safe URI for JSI serialization if (input.startsWith('data:')) { const [header, data] = input.split(';base64,') const [, type] = header.split('/') const uri = fs.cacheDirectory + uuidv4() + `.${type}` await fs.writeAsStringAsync(uri, data, { encoding: fs.EncodingType.Base64 }) return uri } } // Download bundler module or external URL const asset = await Asset.fromModule(input).downloadAsync() let uri = asset.localUri || asset.uri // Unpack assets in Android Release Mode if (!uri.includes(':')) { const file = `${fs.cacheDirectory}ExponentAsset-${asset.hash}.${asset.type}` await fs.copyAsync({ from: uri, to: file }) uri = file } return uri } export function polyfills() { // Patch Blob for ArrayBuffer and URL if unsupported // https://github.com/facebook/react-native/pull/39276 // https://github.com/pmndrs/react-three-fiber/issues/3058 if (Platform.OS !== 'web') { try { const blob = new Blob([new ArrayBuffer(4) as any]) const url = URL.createObjectURL(blob) URL.revokeObjectURL(url) } catch (_) { const BlobManagerModule = require('react-native/Libraries/Blob/BlobManager.js') const BlobManager = BlobManagerModule.default ?? BlobManagerModule const createObjectURL = URL.createObjectURL URL.createObjectURL = function (blob: Blob): string { if ((blob as any).data._base64) { return `data:${blob.type};base64,${(blob as any).data._base64}` } return createObjectURL(blob) } const createFromParts = BlobManager.createFromParts BlobManager.createFromParts = function (parts: Array, options: any) { parts = parts.map((part) => { if (part instanceof ArrayBuffer || ArrayBuffer.isView(part)) { part = fromByteArray(new Uint8Array(part as ArrayBuffer)) } return part }) const blob = createFromParts(parts, options) // Always enable slow but safe path for iOS (previously for Android unauth) // https://github.com/pmndrs/react-three-fiber/issues/3075 // if (!NativeModules.BlobModule?.BLOB_URI_SCHEME) { blob.data._base64 = '' for (const part of parts) { blob.data._base64 += (part as any).data?._base64 ?? part } // } return blob } } } // Don't pre-process urls, let expo-asset generate an absolute URL const extractUrlBase = THREE.LoaderUtils.extractUrlBase.bind(THREE.LoaderUtils) THREE.LoaderUtils.extractUrlBase = (url: string) => (typeof url === 'string' ? extractUrlBase(url) : './') // There's no Image in native, so create a data texture instead THREE.TextureLoader.prototype.load = function load(this: THREE.TextureLoader, url, onLoad, onProgress, onError) { if (this.path && typeof url === 'string') url = this.path + url const texture = new THREE.Texture() getAsset(url) .then(async (uri) => { // https://github.com/expo/expo-three/pull/266 const { width, height } = await new Promise<{ width: number; height: number }>((res, rej) => Image.getSize(uri, (width, height) => res({ width, height }), rej), ) texture.image = { // Special case for EXGLImageUtils::loadImage data: { localUri: uri }, width, height, } texture.flipY = true // Since expo-gl@12.4.0 texture.needsUpdate = true // Force non-DOM upload for EXGL texImage2D // @ts-expect-error texture.isDataTexture = true onLoad?.(texture) }) .catch(onError) return texture } // Fetches assets via FS THREE.FileLoader.prototype.load = function load(this: THREE.FileLoader, url, onLoad, onProgress, onError) { if (this.path && typeof url === 'string') url = this.path + url this.manager.itemStart(url) getAsset(url) .then(async (uri) => { const base64 = await fs.readAsStringAsync(uri, { encoding: fs.EncodingType.Base64 }) const data = Buffer.from(base64, 'base64') onLoad?.(data.buffer) }) .catch((error) => { onError?.(error) this.manager.itemError(url) }) .finally(() => { this.manager.itemEnd(url) }) } } ================================================ FILE: packages/fiber/src/native.tsx ================================================ import * as ReactThreeFiber from './three-types' export { ReactThreeFiber } export * from './three-types' export * from './core' export * from './native/Canvas' export { createTouchEvents as events } from './native/events' import { Platform } from 'react-native' import { polyfills } from './native/polyfills' if (Platform.OS !== 'web') polyfills() ================================================ FILE: packages/fiber/src/three-types.ts ================================================ import type * as THREE from 'three' import type {} from 'react' import type {} from 'react/jsx-runtime' import type {} from 'react/jsx-dev-runtime' import type { Args, EventHandlers, InstanceProps, ConstructorRepresentation } from './core' import type { Overwrite, Mutable } from './core/utils' type MutableOrReadonlyParameters any> = Parameters | Readonly> export interface MathRepresentation { set(...args: number[]): any } export interface VectorRepresentation extends MathRepresentation { setScalar(value: number): any } export type MathTypes = MathRepresentation | THREE.Euler | THREE.Color export type MathType = T extends THREE.Color ? Args | THREE.ColorRepresentation : T extends VectorRepresentation | THREE.Layers | THREE.Euler ? T | MutableOrReadonlyParameters | number : T | MutableOrReadonlyParameters export type MathProps

    = { [K in keyof P as P[K] extends MathTypes ? K : never]: P[K] extends MathTypes ? MathType : never } export type Vector2 = MathType export type Vector3 = MathType export type Vector4 = MathType export type Color = MathType export type Layers = MathType export type Quaternion = MathType export type Euler = MathType export type Matrix3 = MathType export type Matrix4 = MathType export interface RaycastableRepresentation { raycast(raycaster: THREE.Raycaster, intersects: THREE.Intersection[]): void } export type EventProps

    = P extends RaycastableRepresentation ? Partial : {} export interface ReactProps

    { children?: React.ReactNode ref?: React.Ref

    key?: React.Key } export type ElementProps> = Partial< Overwrite & ReactProps

    & EventProps

    > > export type ThreeElement = Mutable< Overwrite, Omit, T>, 'object'>> > export type ThreeToJSXElements> = { [K in keyof T & string as Uncapitalize]: T[K] extends ConstructorRepresentation ? ThreeElement : never } type ThreeExports = typeof THREE type ThreeElementsImpl = ThreeToJSXElements export interface ThreeElements extends Omit { primitive: Omit, 'args'> & { object: object } // Conflicts with DOM types can be accessed through a three* prefix threeAudio: ThreeElementsImpl['audio'] threeSource: ThreeElementsImpl['source'] threeLine: ThreeElementsImpl['line'] threePath: ThreeElementsImpl['path'] } declare module 'react' { namespace JSX { interface IntrinsicElements extends ThreeElements {} } } declare module 'react/jsx-runtime' { namespace JSX { interface IntrinsicElements extends ThreeElements {} } } declare module 'react/jsx-dev-runtime' { namespace JSX { interface IntrinsicElements extends ThreeElements {} } } ================================================ FILE: packages/fiber/src/web/Canvas.tsx ================================================ import * as React from 'react' import * as THREE from 'three' import useMeasure, { Options as ResizeOptions } from 'react-use-measure' import { FiberProvider } from 'its-fine' import { isRef, SetBlock, Block, ErrorBoundary, useMutableCallback, useIsomorphicLayoutEffect, useBridge, } from '../core/utils' import { ReconcilerRoot, extend, createRoot, unmountComponentAtNode, RenderProps } from '../core' import { createPointerEvents } from './events' import { DomEvent } from '../core/events' export interface CanvasProps extends Omit, 'size'>, React.HTMLAttributes { children?: React.ReactNode ref?: React.Ref /** Canvas fallback content, similar to img's alt prop */ fallback?: React.ReactNode /** * Options to pass to useMeasure. * @see https://github.com/pmndrs/react-use-measure#api */ resize?: ResizeOptions /** The target where events are being subscribed to, default: the div that wraps canvas */ eventSource?: HTMLElement | React.RefObject /** The event prefix that is cast into canvas pointer x/y events, default: "offset" */ eventPrefix?: 'offset' | 'client' | 'page' | 'layer' | 'screen' } function CanvasImpl({ ref, children, fallback, resize, style, gl, events = createPointerEvents, eventSource, eventPrefix, shadows, linear, flat, legacy, orthographic, frameloop, dpr, performance, raycaster, camera, scene, onPointerMissed, onCreated, ...props }: CanvasProps) { // Create a known catalogue of Threejs-native elements // This will include the entire THREE namespace by default, users can extend // their own elements by using the createRoot API instead React.useMemo(() => extend(THREE as any), []) const Bridge = useBridge() const [containerRef, containerRect] = useMeasure({ scroll: true, debounce: { scroll: 50, resize: 0 }, ...resize }) const canvasRef = React.useRef(null!) const divRef = React.useRef(null!) React.useImperativeHandle(ref, () => canvasRef.current) const handlePointerMissed = useMutableCallback(onPointerMissed) const [block, setBlock] = React.useState(false) const [error, setError] = React.useState(false) // Suspend this component if block is a promise (2nd run) if (block) throw block // Throw exception outwards if anything within canvas throws if (error) throw error const root = React.useRef>(null!) useIsomorphicLayoutEffect(() => { const canvas = canvasRef.current if (containerRect.width > 0 && containerRect.height > 0 && canvas) { if (!root.current) root.current = createRoot(canvas) async function run() { await root.current.configure({ gl, scene, events, shadows, linear, flat, legacy, orthographic, frameloop, dpr, performance, raycaster, camera, size: containerRect, // Pass mutable reference to onPointerMissed so it's free to update onPointerMissed: (...args) => handlePointerMissed.current?.(...args), onCreated: (state) => { // Connect to event source state.events.connect?.( eventSource ? (isRef(eventSource) ? eventSource.current : eventSource) : divRef.current, ) // Set up compute function if (eventPrefix) { state.setEvents({ compute: (event, state) => { const x = event[(eventPrefix + 'X') as keyof DomEvent] as number const y = event[(eventPrefix + 'Y') as keyof DomEvent] as number state.pointer.set((x / state.size.width) * 2 - 1, -(y / state.size.height) * 2 + 1) state.raycaster.setFromCamera(state.pointer, state.camera) }, }) } // Call onCreated callback onCreated?.(state) }, }) root.current.render( }>{children ?? null} , ) } run() } }) React.useEffect(() => { const canvas = canvasRef.current if (canvas) return () => unmountComponentAtNode(canvas) }, []) // When the event source is not this div, we need to set pointer-events to none // Or else the canvas will block events from reaching the event source const pointerEvents = eventSource ? 'none' : 'auto' return (

    {fallback}
    ) } /** * A DOM canvas which accepts threejs elements as children. * @see https://docs.pmnd.rs/react-three-fiber/api/canvas */ export function Canvas(props: CanvasProps) { return ( ) } ================================================ FILE: packages/fiber/src/web/events.ts ================================================ import { RootState, RootStore } from '../core/store' import { EventManager, Events, createEvents, DomEvent } from '../core/events' const DOM_EVENTS = { onClick: ['click', false], onContextMenu: ['contextmenu', false], onDoubleClick: ['dblclick', false], onWheel: ['wheel', true], onPointerDown: ['pointerdown', true], onPointerUp: ['pointerup', true], onPointerLeave: ['pointerleave', true], onPointerMove: ['pointermove', true], onPointerCancel: ['pointercancel', true], onLostPointerCapture: ['lostpointercapture', true], } as const /** Default R3F event manager for web */ export function createPointerEvents(store: RootStore): EventManager { const { handlePointer } = createEvents(store) return { priority: 1, enabled: true, compute(event: DomEvent, state: RootState, previous?: RootState) { // https://github.com/pmndrs/react-three-fiber/pull/782 // Events trigger outside of canvas when moved, use offsetX/Y by default and allow overrides state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1) state.raycaster.setFromCamera(state.pointer, state.camera) }, connected: undefined, handlers: Object.keys(DOM_EVENTS).reduce( (acc, key) => ({ ...acc, [key]: handlePointer(key) }), {}, ) as unknown as Events, update: () => { const { events, internal } = store.getState() if (internal.lastEvent?.current && events.handlers) events.handlers.onPointerMove(internal.lastEvent.current) }, connect: (target: HTMLElement) => { const { set, events } = store.getState() events.disconnect?.() set((state) => ({ events: { ...state.events, connected: target } })) if (events.handlers) { for (const name in events.handlers) { const event = events.handlers[name as keyof typeof events.handlers] const [eventName, passive] = DOM_EVENTS[name as keyof typeof DOM_EVENTS] target.addEventListener(eventName, event, { passive }) } } }, disconnect: () => { const { set, events } = store.getState() if (events.connected) { if (events.handlers) { for (const name in events.handlers) { const event = events.handlers[name as keyof typeof events.handlers] const [eventName] = DOM_EVENTS[name as keyof typeof DOM_EVENTS] events.connected.removeEventListener(eventName, event) } } set((state) => ({ events: { ...state.events, connected: undefined } })) } }, } } ================================================ FILE: packages/fiber/tests/__snapshots__/canvas.native.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`native Canvas should correctly mount 1`] = `"{\\"type\\":\\"view\\",\\"props\\":{\\"style\\":{\\"flex\\":1}},\\"children\\":[{\\"type\\":\\"glview\\",\\"props\\":{\\"msaaSamples\\":4,\\"style\\":{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0}},\\"children\\":[]},{\\"type\\":\\"view\\",\\"props\\":{\\"style\\":{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0}},\\"children\\":[]}]}"`; ================================================ FILE: packages/fiber/tests/__snapshots__/canvas.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`web Canvas should correctly mount 1`] = `
    `; ================================================ FILE: packages/fiber/tests/__snapshots__/index.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`exports matches public API 1`] = ` Array [ "Act", "Args", "AttachFnType", "AttachType", "Camera", "CameraProps", "Canvas", "CanvasProps", "Catalogue", "Color", "ComputeFunction", "ConstructorRepresentation", "Disposable", "DomEvent", "Dpr", "ElementProps", "Euler", "EventHandlers", "EventManager", "EventProps", "Events", "Extensions", "FilterFunction", "Frameloop", "GLProps", "GlobalEffectType", "GlobalRenderCallback", "InjectState", "Instance", "InstanceProps", "Intersection", "Layers", "LoaderResult", "MathProps", "MathRepresentation", "MathType", "MathTypes", "Matrix3", "Matrix4", "ObjectMap", "Performance", "Quaternion", "RaycastableRepresentation", "ReactProps", "ReactThreeFiber", "ReconcilerRoot", "RenderCallback", "RenderProps", "Renderer", "RootState", "RootStore", "Size", "Subscription", "ThreeElement", "ThreeElements", "ThreeEvent", "ThreeToJSXElements", "Vector2", "Vector3", "Vector4", "VectorRepresentation", "Viewport", "XRManager", "_roots", "act", "addAfterEffect", "addEffect", "addTail", "advance", "applyProps", "buildGraph", "context", "createEvents", "createPortal", "createRoot", "dispose", "events", "extend", "flushGlobalEffects", "flushSync", "getRootState", "invalidate", "reconciler", "unmountComponentAtNode", "useFrame", "useGraph", "useInstanceHandle", "useLoader", "useStore", "useThree", ] `; ================================================ FILE: packages/fiber/tests/__snapshots__/utils.test.ts.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`updateCamera updates camera matrices 1`] = ` Array [ 2.1445069205095586, 0, 0, 0, 0, 2.1445069205095586, 0, 0, 0, 0, -1.00010000500025, -1, 0, 0, -0.200010000500025, 0, ] `; exports[`updateCamera updates camera matrices 2`] = ` Array [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -0.001000050002500125, 0, -0, -0, -1.00010000500025, 1, ] `; ================================================ FILE: packages/fiber/tests/canvas.native.test.tsx ================================================ import * as React from 'react' import { View } from 'react-native' // @ts-ignore TS2305 remove with modern TS config import { render } from 'react-nil' import { Canvas, act } from '../src/native' describe('native Canvas', () => { it('should correctly mount', async () => { const container = await act(async () => render( , ), ) expect(JSON.stringify(container.head)).toMatchSnapshot() }) it('should forward ref', async () => { const ref = React.createRef() await act(async () => render( , ), ) expect(ref.current).toBeInstanceOf(View) }) it('should forward context', async () => { const ParentContext = React.createContext(null!) let receivedValue!: boolean function Test() { receivedValue = React.useContext(ParentContext) return null } await act(async () => { render( , ) }) expect(receivedValue).toBe(true) }) it('should correctly unmount', async () => { await act(async () => render( , ), ) expect(async () => await act(async () => render(null))).not.toThrow() }) }) ================================================ FILE: packages/fiber/tests/canvas.test.tsx ================================================ import React from 'react' import { render } from '@testing-library/react' import { Canvas, act } from '../src' describe('web Canvas', () => { it('should correctly mount', async () => { const renderer = await act(async () => render( , ), ) expect(renderer.container).toMatchSnapshot() }) it('should forward ref', async () => { const ref = React.createRef() await act(async () => render( , ), ) expect(ref.current).toBeInstanceOf(HTMLCanvasElement) }) it('should forward context', async () => { const ParentContext = React.createContext(null!) let receivedValue!: boolean function Test() { receivedValue = React.useContext(ParentContext) return null } await act(async () => { render( , ) }) expect(receivedValue).toBe(true) }) it('should correctly unmount', async () => { const renderer = await act(async () => render( , ), ) expect(() => renderer.unmount()).not.toThrow() }) it('plays nice with react SSR', async () => { const useLayoutEffect = jest.spyOn(React, 'useLayoutEffect') await act(async () => render( , ), ) expect(useLayoutEffect).not.toHaveBeenCalled() }) }) ================================================ FILE: packages/fiber/tests/events.test.tsx ================================================ import * as React from 'react' import { render, fireEvent, RenderResult } from '@testing-library/react' import { Canvas, act, extend } from '../src' import THREE from 'three' extend(THREE as any) const getContainer = () => document.querySelector('canvas')?.parentNode?.parentNode as HTMLDivElement describe('events', () => { it('can handle onPointerDown', async () => { const handlePointerDown = jest.fn() await act(async () => { render( , ) }) const evt = new PointerEvent('pointerdown') Object.defineProperty(evt, 'offsetX', { get: () => 577 }) Object.defineProperty(evt, 'offsetY', { get: () => 480 }) fireEvent(getContainer(), evt) expect(handlePointerDown).toHaveBeenCalled() }) it('can handle onPointerMissed', async () => { const handleClick = jest.fn() const handleMissed = jest.fn() await act(async () => { render( , ) }) const evt = new MouseEvent('click') Object.defineProperty(evt, 'offsetX', { get: () => 0 }) Object.defineProperty(evt, 'offsetY', { get: () => 0 }) fireEvent(getContainer(), evt) expect(handleClick).not.toHaveBeenCalled() expect(handleMissed).toHaveBeenCalledWith(evt) }) it('should not fire onPointerMissed when same element is clicked', async () => { const handleClick = jest.fn() const handleMissed = jest.fn() await act(async () => { render( , ) }) const down = new PointerEvent('pointerdown') Object.defineProperty(down, 'offsetX', { get: () => 577 }) Object.defineProperty(down, 'offsetY', { get: () => 480 }) fireEvent(getContainer(), down) const up = new PointerEvent('pointerup') Object.defineProperty(up, 'offsetX', { get: () => 577 }) Object.defineProperty(up, 'offsetY', { get: () => 480 }) const evt = new MouseEvent('click') Object.defineProperty(evt, 'offsetX', { get: () => 577 }) Object.defineProperty(evt, 'offsetY', { get: () => 480 }) fireEvent(getContainer(), evt) expect(handleClick).toHaveBeenCalled() expect(handleMissed).not.toHaveBeenCalled() }) it('should not fire onPointerMissed on parent when child element is clicked', async () => { const handleClick = jest.fn() const handleMissed = jest.fn() await act(async () => { render( , ) }) const down = new PointerEvent('pointerdown') Object.defineProperty(down, 'offsetX', { get: () => 577 }) Object.defineProperty(down, 'offsetY', { get: () => 480 }) fireEvent(getContainer(), down) const up = new PointerEvent('pointerup') Object.defineProperty(up, 'offsetX', { get: () => 577 }) Object.defineProperty(up, 'offsetY', { get: () => 480 }) const evt = new MouseEvent('click') Object.defineProperty(evt, 'offsetX', { get: () => 577 }) Object.defineProperty(evt, 'offsetY', { get: () => 480 }) fireEvent(getContainer(), evt) expect(handleClick).toHaveBeenCalled() expect(handleMissed).not.toHaveBeenCalled() }) it('can handle onPointerMissed on Canvas', async () => { const handleMissed = jest.fn() await act(async () => { render( , ) }) const evt = new MouseEvent('click') Object.defineProperty(evt, 'offsetX', { get: () => 0 }) Object.defineProperty(evt, 'offsetY', { get: () => 0 }) fireEvent(getContainer(), evt) expect(handleMissed).toHaveBeenCalledWith(evt) }) it('can handle onPointerMove', async () => { const handlePointerMove = jest.fn() const handlePointerOver = jest.fn() const handlePointerEnter = jest.fn() const handlePointerOut = jest.fn() await act(async () => { render( , ) }) const evt1 = new PointerEvent('pointermove') Object.defineProperty(evt1, 'offsetX', { get: () => 577 }) Object.defineProperty(evt1, 'offsetY', { get: () => 480 }) fireEvent(getContainer(), evt1) expect(handlePointerMove).toHaveBeenCalled() expect(handlePointerOver).toHaveBeenCalled() expect(handlePointerEnter).toHaveBeenCalled() const evt2 = new PointerEvent('pointermove') Object.defineProperty(evt2, 'offsetX', { get: () => 0 }) Object.defineProperty(evt2, 'offsetY', { get: () => 0 }) fireEvent(getContainer(), evt2) expect(handlePointerOut).toHaveBeenCalled() }) it('should handle stopPropagation', async () => { const handlePointerEnter = jest.fn().mockImplementation((e) => { expect(() => e.stopPropagation()).not.toThrow() }) const handlePointerLeave = jest.fn() await act(async () => { render( , ) }) const evt1 = new PointerEvent('pointermove') Object.defineProperty(evt1, 'offsetX', { get: () => 577 }) Object.defineProperty(evt1, 'offsetY', { get: () => 480 }) fireEvent(getContainer(), evt1) expect(handlePointerEnter).toHaveBeenCalled() const evt2 = new PointerEvent('pointermove') Object.defineProperty(evt2, 'offsetX', { get: () => 0 }) Object.defineProperty(evt2, 'offsetY', { get: () => 0 }) fireEvent(getContainer(), evt2) expect(handlePointerLeave).toHaveBeenCalled() }) it('should handle stopPropagation on click events', async () => { const handleClickFront = jest.fn((e) => e.stopPropagation()) const handleClickRear = jest.fn() await act(async () => { render( , ) }) const down = new PointerEvent('pointerdown') Object.defineProperty(down, 'offsetX', { get: () => 577 }) Object.defineProperty(down, 'offsetY', { get: () => 480 }) fireEvent(getContainer(), down) const up = new PointerEvent('pointerup') Object.defineProperty(up, 'offsetX', { get: () => 577 }) Object.defineProperty(up, 'offsetY', { get: () => 480 }) fireEvent(getContainer(), up) const event = new MouseEvent('click') Object.defineProperty(event, 'offsetX', { get: () => 577 }) Object.defineProperty(event, 'offsetY', { get: () => 480 }) fireEvent(getContainer(), event) expect(handleClickFront).toHaveBeenCalled() expect(handleClickRear).not.toHaveBeenCalled() }) describe('web pointer capture', () => { const handlePointerMove = jest.fn() const handlePointerDown = jest.fn((ev) => (ev.target as any).setPointerCapture(ev.pointerId)) const handlePointerUp = jest.fn((ev) => (ev.target as any).releasePointerCapture(ev.pointerId)) const handlePointerEnter = jest.fn() const handlePointerLeave = jest.fn() /* This component lets us unmount the event-handling object */ function PointerCaptureTest(props: { hasMesh: boolean; manualRelease?: boolean }) { return ( {props.hasMesh && ( )} ) } const pointerId = 1234 it('should release when the capture target is unmounted', async () => { let renderResult: RenderResult = undefined! await act(async () => { renderResult = render() return renderResult }) const canvas = getContainer() canvas.setPointerCapture = jest.fn() canvas.releasePointerCapture = jest.fn() const down = new PointerEvent('pointerdown', { pointerId }) Object.defineProperty(down, 'offsetX', { get: () => 577 }) Object.defineProperty(down, 'offsetY', { get: () => 480 }) /* testing-utils/react's fireEvent wraps the event like React does, so it doesn't match how our event handlers are called in production, so we call dispatchEvent directly. */ await act(async () => canvas.dispatchEvent(down)) /* This should have captured the DOM pointer */ expect(handlePointerDown).toHaveBeenCalledTimes(1) expect(canvas.setPointerCapture).toHaveBeenCalledWith(pointerId) expect(canvas.releasePointerCapture).not.toHaveBeenCalled() /* Now remove the mesh */ await act(async () => renderResult.rerender()) expect(canvas.releasePointerCapture).toHaveBeenCalledWith(pointerId) const move = new PointerEvent('pointerdown', { pointerId }) Object.defineProperty(move, 'offsetX', { get: () => 577 }) Object.defineProperty(move, 'offsetY', { get: () => 480 }) await act(async () => canvas.dispatchEvent(move)) /* There should now be no pointer capture */ expect(handlePointerMove).not.toHaveBeenCalled() }) it('should not leave when captured', async () => { let renderResult: RenderResult = undefined! await act(async () => { renderResult = render() return renderResult }) const canvas = getContainer() canvas.setPointerCapture = jest.fn() canvas.releasePointerCapture = jest.fn() const moveIn = new PointerEvent('pointermove', { pointerId }) Object.defineProperty(moveIn, 'offsetX', { get: () => 577 }) Object.defineProperty(moveIn, 'offsetY', { get: () => 480 }) const moveOut = new PointerEvent('pointermove', { pointerId }) Object.defineProperty(moveOut, 'offsetX', { get: () => -10000 }) Object.defineProperty(moveOut, 'offsetY', { get: () => -10000 }) /* testing-utils/react's fireEvent wraps the event like React does, so it doesn't match how our event handlers are called in production, so we call dispatchEvent directly. */ await act(async () => canvas.dispatchEvent(moveIn)) expect(handlePointerEnter).toHaveBeenCalledTimes(1) expect(handlePointerMove).toHaveBeenCalledTimes(1) const down = new PointerEvent('pointerdown', { pointerId }) Object.defineProperty(down, 'offsetX', { get: () => 577 }) Object.defineProperty(down, 'offsetY', { get: () => 480 }) await act(async () => canvas.dispatchEvent(down)) // If we move the pointer now, when it is captured, it should raise the onPointerMove event even though the pointer is not over the element, // and NOT raise the onPointerLeave event. await act(async () => canvas.dispatchEvent(moveOut)) expect(handlePointerMove).toHaveBeenCalledTimes(2) expect(handlePointerLeave).not.toHaveBeenCalled() await act(async () => canvas.dispatchEvent(moveIn)) expect(handlePointerMove).toHaveBeenCalledTimes(3) const up = new PointerEvent('pointerup', { pointerId }) Object.defineProperty(up, 'offsetX', { get: () => 577 }) Object.defineProperty(up, 'offsetY', { get: () => 480 }) const lostpointercapture = new PointerEvent('lostpointercapture', { pointerId }) await act(async () => canvas.dispatchEvent(up)) await act(async () => canvas.dispatchEvent(lostpointercapture)) // The pointer is still over the element, so onPointerLeave should not have been called. expect(handlePointerLeave).not.toHaveBeenCalled() // The element pointer should no longer be captured, so moving it away should call onPointerLeave. await act(async () => canvas.dispatchEvent(moveOut)) expect(handlePointerEnter).toHaveBeenCalledTimes(1) expect(handlePointerLeave).toHaveBeenCalledTimes(1) }) }) it('can handle primitives', async () => { const handlePointerDownOuter = jest.fn() const handlePointerDownInner = jest.fn() const object = new THREE.Group() object.add(new THREE.Mesh(new THREE.BoxGeometry(2, 2), new THREE.MeshBasicMaterial())) await act(async () => { render( , ) }) const evt = new PointerEvent('pointerdown') Object.defineProperty(evt, 'offsetX', { get: () => 577 }) Object.defineProperty(evt, 'offsetY', { get: () => 480 }) fireEvent(getContainer(), evt) expect(handlePointerDownOuter).toHaveBeenCalled() expect(handlePointerDownInner).toHaveBeenCalled() }) it('can handle a DOM offset canvas', async () => { const handlePointerDown = jest.fn() await act(async () => { render( { state.size.left = 100 state.size.top = 100 }}> , ) }) const evt = new PointerEvent('pointerdown') Object.defineProperty(evt, 'offsetX', { get: () => 577 }) Object.defineProperty(evt, 'offsetY', { get: () => 480 }) fireEvent(getContainer(), evt) expect(handlePointerDown).toHaveBeenCalled() }) it.todo('can handle different event prefixes') }) ================================================ FILE: packages/fiber/tests/hooks.test.tsx ================================================ import * as React from 'react' import * as THREE from 'three' import { createCanvas } from '@react-three/test-renderer/src/createTestCanvas' import { createRoot, advance, useLoader, act, useThree, useGraph, useFrame, ObjectMap, useInstanceHandle, Instance, extend, } from '../src' extend(THREE as any) const root = createRoot(document.createElement('canvas')) describe('hooks', () => { let canvas: HTMLCanvasElement = null! beforeEach(() => { canvas = createCanvas() }) it('can handle useThree hook', async () => { let result = {} as { camera: THREE.Camera scene: THREE.Scene raycaster: THREE.Raycaster size: { width: number; height: number } } const Component = () => { /** * this causes an act problem, it'd be * good to figure out the best way to * resolve this at some point */ const res = useThree((state) => ({ camera: state.camera, scene: state.scene, size: state.size, raycaster: state.raycaster, })) result = res return } await act(async () => root.render()) expect(result.camera instanceof THREE.Camera).toBeTruthy() expect(result.scene instanceof THREE.Scene).toBeTruthy() expect(result.raycaster instanceof THREE.Raycaster).toBeTruthy() expect(result.size).toEqual({ height: 0, width: 0, top: 0, left: 0 }) }) it('can handle useFrame hook', async () => { const frameCalls: number[] = [] const Component = () => { const ref = React.useRef(null!) useFrame((_, delta) => { frameCalls.push(delta) ref.current.position.x = 1 }) return ( ) } const store = await act(async () => (await root.configure({ frameloop: 'never' })).render()) const { scene } = store.getState() advance(Date.now()) expect(scene.children[0].position.x).toEqual(1) expect(frameCalls.length).toBeGreaterThan(0) }) it('can handle useLoader hook', async () => { const MockMesh = new THREE.Mesh() MockMesh.name = 'Scene' interface GLTF { scene: THREE.Object3D } class GLTFLoader extends THREE.Loader { load(url: string, onLoad: (gltf: GLTF) => void): void { onLoad({ scene: MockMesh }) } } let gltf!: GLTF & ObjectMap const Component = () => { gltf = useLoader(GLTFLoader, '/suzanne.glb') return } const store = await act(async () => root.render()) const { scene } = store.getState() expect(scene.children[0]).toBe(MockMesh) expect(gltf.scene).toBe(MockMesh) expect(gltf.nodes.Scene).toBe(MockMesh) }) it('can handle useLoader hook with an array of strings', async () => { const MockMesh = new THREE.Mesh() const MockGroup = new THREE.Group() const mat1 = new THREE.MeshBasicMaterial() mat1.name = 'Mat 1' const mesh1 = new THREE.Mesh(new THREE.BoxGeometry(2, 2), mat1) mesh1.name = 'Mesh 1' const mat2 = new THREE.MeshBasicMaterial() mat2.name = 'Mat 2' const mesh2 = new THREE.Mesh(new THREE.BoxGeometry(2, 2), mat2) mesh2.name = 'Mesh 2' MockGroup.add(mesh1, mesh2) class TestLoader extends THREE.Loader { load = jest .fn() .mockImplementationOnce((_url, onLoad) => { onLoad(MockMesh) }) .mockImplementationOnce((_url, onLoad) => { onLoad(MockGroup) }) } const extensions = jest.fn() const Component = () => { const [mockMesh, mockScene] = useLoader(TestLoader, ['/suzanne.glb', '/myModels.glb'], extensions) return ( <> ) } const store = await act(async () => root.render()) const { scene } = store.getState() expect(scene.children[0]).toBe(MockMesh) expect(scene.children[1]).toBe(MockGroup) expect(extensions).toHaveBeenCalledTimes(1) }) it('can handle useLoader with an existing loader instance', async () => { class Loader extends THREE.Loader { load(_url: string, onLoad: (result: null) => void): void { onLoad(null) } } const loader = new Loader() let proto!: Loader function Test(): null { return useLoader(loader, '', (loader) => (proto = loader)) } await act(async () => root.render()) expect(proto).toBe(loader) }) it('can handle useLoader with a loader extension', async () => { class Loader extends THREE.Loader { load(_url: string, onLoad: (result: null) => void): void { onLoad(null) } } let proto!: Loader function Test(): null { return useLoader(Loader, '', (loader) => (proto = loader)) } await act(async () => root.render()) expect(proto).toBeInstanceOf(Loader) }) it('can handle useGraph hook', async () => { const group = new THREE.Group() const mat1 = new THREE.MeshBasicMaterial() mat1.name = 'Mat 1' const mesh1 = new THREE.Mesh(new THREE.BoxGeometry(2, 2), mat1) mesh1.name = 'Mesh 1' const mat2 = new THREE.MeshBasicMaterial() mat2.name = 'Mat 2' const mesh2 = new THREE.Mesh(new THREE.BoxGeometry(2, 2), mat2) mesh2.name = 'Mesh 2' const subGroup = new THREE.Group() const mat3 = new THREE.MeshBasicMaterial() mat3.name = 'Mat 3' const mesh3 = new THREE.Mesh(new THREE.BoxGeometry(2, 2), mat3) mesh3.name = 'Mesh 3' const mat4 = new THREE.MeshBasicMaterial() mat4.name = 'Mat 4' const mesh4 = new THREE.Mesh(new THREE.BoxGeometry(2, 2), mat4) mesh4.name = 'Mesh 4' subGroup.add(mesh3, mesh4) group.add(mesh1, mesh2, subGroup) let result = {} as ObjectMap const Component = () => { const data = useGraph(group) result = data return } await act(async () => root.render()) expect(result).toEqual({ nodes: { [mesh1.name]: mesh1, [mesh2.name]: mesh2, [mesh3.name]: mesh3, [mesh4.name]: mesh4, }, materials: { [mat1.name]: mat1, [mat2.name]: mat2, [mat3.name]: mat3, [mat4.name]: mat4, }, meshes: { [mesh1.name]: mesh1, [mesh2.name]: mesh2, [mesh3.name]: mesh3, [mesh4.name]: mesh4, }, }) }) it('can handle useInstanceHandle hook', async () => { const ref = React.createRef() let instance!: React.RefObject const Component = () => { instance = useInstanceHandle(ref) return } await act(async () => root.render()) expect(instance.current).toBe((ref.current as unknown as Instance['object']).__r3f) }) it('can handle future (19.x) hooks without crashing', async () => { function Component() { // @ts-ignore React.useEffectEvent?.(() => {}) return null } expect(async () => await act(async () => root.render())).not.toThrow() }) }) ================================================ FILE: packages/fiber/tests/index.test.tsx ================================================ import * as React from 'react' import * as THREE from 'three' import ts from 'typescript' import * as path from 'path' import { createCanvas } from '@react-three/test-renderer/src/createTestCanvas' import { ReconcilerRoot, createRoot as createRootImpl, act, useFrame, useThree, createPortal, RootState, RootStore, extend, } from '../src/index' extend(THREE as any) let root: ReconcilerRoot = null! const roots: ReconcilerRoot[] = [] function createRoot() { const canvas = createCanvas() const root = createRootImpl(canvas) roots.push(root) return root } beforeEach(() => (root = createRoot())) afterEach(async () => { for (const root of roots) { await act(async () => root.unmount()) } roots.length = 0 }) describe('createRoot', () => { it('should return a Zustand store', async () => { const store = await act(async () => root.render(null)) expect(() => store.getState()).not.toThrow() }) it('will make an Orthographic Camera & set the position', async () => { const store = await act(async () => (await root.configure({ orthographic: true, camera: { position: [0, 0, 5] } })).render(), ) const { camera } = store.getState() expect(camera).toBeInstanceOf(THREE.OrthographicCamera) expect(camera.position.z).toEqual(5) }) // TODO: deprecate it('should handle an performance changing functions', async () => { let store: RootStore = null! await act(async () => { store = (await root.configure({ dpr: [1, 2], performance: { min: 0.2 } })).render() }) expect(store.getState().viewport.initialDpr).toEqual(window.devicePixelRatio) expect(store.getState().performance.min).toEqual(0.2) expect(store.getState().performance.current).toEqual(1) await act(async () => { store.getState().setDpr(0.1) }) expect(store.getState().viewport.dpr).toEqual(0.1) jest.useFakeTimers() await act(async () => { store.getState().performance.regress() jest.advanceTimersByTime(100) }) expect(store.getState().performance.current).toEqual(0.2) await act(async () => { jest.advanceTimersByTime(200) }) expect(store.getState().performance.current).toEqual(1) jest.useRealTimers() }) it('should handle the DPR prop reactively', async () => { // Initial clamp const store = await act(async () => (await root.configure({ dpr: [1, 2] })).render()) expect(store.getState().viewport.dpr).toEqual(window.devicePixelRatio) // Reactive update await act(async () => store.getState().setDpr(0.1)) expect(store.getState().viewport.dpr).toEqual(0.1) // Reactive clamp await act(async () => store.getState().setDpr([1, 2])) expect(store.getState().viewport.dpr).toEqual(window.devicePixelRatio) }) it('should set PCFSoftShadowMap as the default shadow map', async () => { const store = await act(async () => (await root.configure({ shadows: true })).render()) const { gl } = store.getState() expect(gl.shadowMap.type).toBe(THREE.PCFSoftShadowMap) }) it('should set tonemapping to ACESFilmicToneMapping and outputColorSpace to SRGBColorSpace if linear is false', async () => { const store = await act(async () => (await root.configure({ linear: false })).render()) const { gl } = store.getState() expect(gl.toneMapping).toBe(THREE.ACESFilmicToneMapping) expect(gl.outputColorSpace).toBe(THREE.SRGBColorSpace) }) it('should toggle render mode in xr', async () => { let state: RootState = null! await act(async () => { state = root.render().getState() state.gl.xr.isPresenting = true state.gl.xr.dispatchEvent({ type: 'sessionstart' }) }) expect(state.gl.xr.enabled).toEqual(true) await act(async () => { state.gl.xr.isPresenting = false state.gl.xr.dispatchEvent({ type: 'sessionend' }) }) expect(state.gl.xr.enabled).toEqual(false) }) it('should respect frameloop="never" in xr', async () => { let respected = true const Test = () => useFrame(() => (respected = false)) await act(async () => { const state = (await root.configure({ frameloop: 'never' })).render().getState() state.gl.xr.isPresenting = true state.gl.xr.dispatchEvent({ type: 'sessionstart' }) }) expect(respected).toEqual(true) }) it('should set renderer props via gl prop', async () => { const store = await act(async () => (await root.configure({ gl: { logarithmicDepthBuffer: true } })).render(), ) const { gl } = store.getState() expect(gl.capabilities.logarithmicDepthBuffer).toBe(true) }) it('should update scene via scene prop', async () => { let scene: THREE.Scene = null! await act(async () => { scene = (await root.configure({ scene: { name: 'test' } })).render().getState().scene }) expect(scene.name).toBe('test') }) it('should set a custom scene via scene prop', async () => { let scene: THREE.Scene = null! const prop = new THREE.Scene() await act(async () => { scene = (await root.configure({ scene: prop })).render().getState().scene }) expect(prop).toBe(scene) }) it('should set a renderer via gl callback', async () => { class Renderer extends THREE.WebGLRenderer {} let gl: Renderer = null! await act(async () => { gl = (await root.configure({ gl: (props) => new Renderer(props) })).render().getState().gl }) expect(gl instanceof Renderer).toBe(true) }) it('should respect color management preferences via gl', async () => { let gl: THREE.WebGLRenderer & { outputColorSpace?: string } = null! let texture: THREE.Texture & { colorSpace?: string } = null! let key = 0 function Test() { gl = useThree((state) => state.gl) texture = new THREE.Texture() return } await act(async () => (await createRoot().configure({ linear: true })).render()) expect(gl.outputColorSpace).toBe(THREE.LinearSRGBColorSpace) expect(texture.colorSpace).toBe(THREE.NoColorSpace) await act(async () => (await createRoot().configure({ linear: false })).render()) expect(gl.outputColorSpace).toBe(THREE.SRGBColorSpace) expect(texture.colorSpace).toBe(THREE.SRGBColorSpace) }) it('should respect legacy prop', async () => { THREE.ColorManagement.enabled = true await act(async () => { ;(await root.configure({ legacy: true })).render() }) expect(THREE.ColorManagement.enabled).toBe(false) await act(async () => { ;(await root.configure({ legacy: false })).render() }) expect(THREE.ColorManagement.enabled).toBe(true) }) }) describe('createPortal', () => { it('should create a state enclave', async () => { const scene = new THREE.Scene() let state: RootState = null! let portalState: RootState = null! const Normal = () => { const three = useThree() state = three return } const Portal = () => { const three = useThree() portalState = three return } await act(async () => { root.render( <> {createPortal(, scene, { scene })} , ) }) // Renders into portal target expect(scene.children.length).not.toBe(0) // Creates an isolated state enclave expect(state.scene).not.toBe(scene) expect(portalState.scene).toBe(scene) }) it('should handle unmounted containers', async () => { let groupHandle!: THREE.Group | null function Test(props: any) { const [group, setGroup] = React.useState(null) groupHandle = group return ( {group && createPortal(, group)} ) } await act(async () => root.render()) expect(groupHandle).toBeDefined() const prevUUID = groupHandle!.uuid await act(async () => root.render()) expect(groupHandle).toBeDefined() expect(prevUUID).not.toBe(groupHandle!.uuid) }) }) function getExports(source: string): string[] { const program = ts.createProgram([source], { jsx: ts.JsxEmit.React }) const checker = program.getTypeChecker() const sourceFile = program.getSourceFile(source)! const sourceFileSymbol = checker.getSymbolAtLocation(sourceFile)! const moduleExports = checker.getExportsOfModule(sourceFileSymbol) return moduleExports.map(({ escapedName }) => escapedName).sort() as unknown as string[] } describe('exports', () => { it('matches public API', () => { const webExports = getExports(path.resolve(__dirname, '../src/index.tsx')) expect(webExports).toMatchSnapshot() }) it('are consistent between targets', () => { const webExports = getExports(path.resolve(__dirname, '../src/index.tsx')) const nativeExports = getExports(path.resolve(__dirname, '../src/native.tsx')) expect(webExports).toStrictEqual(nativeExports) }) }) ================================================ FILE: packages/fiber/tests/polyfills.test.ts ================================================ import * as THREE from 'three' import { polyfills } from '../src/native/polyfills' polyfills() const pixel = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=' describe('polyfills', () => { it('loads images via data textures', async () => { const texture = await new THREE.TextureLoader().loadAsync(pixel) expect((texture as any).isDataTexture).toBe(true) expect(texture.image.width).toBe(1) expect(texture.image.height).toBe(1) }) it('creates a safe image URI for JSI', async () => { const texture = await new THREE.TextureLoader().loadAsync(pixel) expect(texture.image.data.localUri.startsWith('file:///')).toBe(true) }) it('unpacks drawables in Android APK', async () => { const texture = await new THREE.TextureLoader().loadAsync('drawable.png') expect(texture.image.data.localUri.includes(':')).toBe(true) }) it('loads files via the file system', async () => { const asset = 1 const file = await new THREE.FileLoader().loadAsync(asset as any) expect(typeof (file as ArrayBuffer).byteLength).toBe('number') // TODO: ArrayBuffer instanceof }) it('loads files via http', async () => { const file = await new THREE.FileLoader().loadAsync('https://example.com/test.png') expect(typeof (file as ArrayBuffer).byteLength).toBe('number') // TODO: ArrayBuffer instanceof }) }) ================================================ FILE: packages/fiber/tests/reconciler.test.ts ================================================ import * as THREE from 'three' import { createCanvas } from '@react-three/test-renderer/src/createTestCanvas' async function act(fn: () => Promise) { // Silence act warning since we have a custom act implementation const error = console.error console.error = function (message) { if (/was not wrapped in act/.test(message)) return return error.call(this, arguments) } const value = fn() return new Promise((res) => { requestAnimationFrame(() => requestAnimationFrame(() => requestAnimationFrame(() => res(value)))) }).finally(() => { console.error = error }) } describe('reconciler', () => { const NODE_ENV = process.env.NODE_ENV for (const env of ['development', 'production']) { it(`should work with ${env} builds of React`, async () => { jest.resetModules() // @ts-ignore if (typeof window !== 'undefined') delete window.__THREE__ process.env.NODE_ENV = env const React = await import('react') const R3F = await import('../src/index') // Ensure that the correct build was loaded expect(typeof React.act === (env === 'production' ? 'undefined' : 'function')) R3F.extend(THREE as any) const canvas = createCanvas() const root = R3F.createRoot(canvas) const lifecycle: string[] = [] const object = {} const ref = React.createRef<{}>() function Test() { lifecycle.push('render') React.useImperativeHandle(React.useRef(undefined), () => void lifecycle.push('ref')) React.useInsertionEffect(() => void lifecycle.push('useInsertionEffect'), []) React.useLayoutEffect(() => void lifecycle.push('useLayoutEffect'), []) React.useEffect(() => void lifecycle.push('useEffect'), []) return React.createElement('primitive', { ref, object }) } await act(async () => root.render(React.createElement(Test))) expect(lifecycle).toStrictEqual(['render', 'useInsertionEffect', 'ref', 'useLayoutEffect', 'useEffect']) expect(ref.current).toBe(object) await act(async () => root.unmount()) expect(ref.current).toBe(null) }) } // @ts-ignore if (typeof window !== 'undefined') delete window.__THREE__ process.env.NODE_ENV = NODE_ENV jest.resetModules() }) ================================================ FILE: packages/fiber/tests/renderer.test.tsx ================================================ import * as React from 'react' import * as THREE from 'three' import { ReconcilerRoot, createRoot, act, extend, ThreeElement, ThreeElements, flushSync, useThree } from '../src/index' import { suspend } from 'suspend-react' extend(THREE as any) class Mock extends THREE.Group { static instances: string[] constructor(name: string = '') { super() this.name = name Mock.instances.push(name) } } declare module '@react-three/fiber' { interface ThreeElements { mock: ThreeElement threeRandom: ThreeElement } } extend({ Mock }) type ComponentMesh = THREE.Mesh const expectToThrow = async (callback: () => any, message: string) => { let error: Error | undefined try { await callback() } catch (e) { error = e as Error } expect(error?.message).toBe(message) } describe('renderer', () => { let root: ReconcilerRoot = null! beforeEach(() => { root = createRoot(document.createElement('canvas')) Mock.instances = [] }) afterEach(async () => act(async () => root.unmount())) it('should render empty JSX', async () => { const store = await act(async () => root.render(null)) const { scene } = store.getState() expect(scene.children.length).toBe(0) }) it('should render native elements', async () => { const store = await act(async () => root.render()) const { scene } = store.getState() expect(scene.children.length).toBe(1) expect(scene.children[0]).toBeInstanceOf(THREE.Group) expect(scene.children[0].name).toBe('native') }) it('should render extended elements', async () => { const store = await act(async () => root.render()) const { scene } = store.getState() expect(scene.children.length).toBe(1) expect(scene.children[0]).toBeInstanceOf(Mock) expect(scene.children[0].name).toBe('mock') const Component = extend(THREE.Mesh) await act(async () => root.render()) expect(scene.children.length).toBe(1) expect(scene.children[0]).toBeInstanceOf(THREE.Mesh) }) it('should render primitives', async () => { const object = new THREE.Object3D() const store = await act(async () => root.render()) const { scene } = store.getState() expect(scene.children.length).toBe(1) expect(scene.children[0]).toBe(object) expect(object.name).toBe('primitive') }) it('should remove children from primitive when unmounted', async () => { const object = new THREE.Group() function Parent({ children, show }: { children: React.ReactNode; show: boolean }) { return show ? {children} : null } function Component({ show }: { show: boolean }) { return ( ) } const store = await act(async () => root.render()) const { scene } = store.getState() expect(scene.children.length).toBe(1) expect(scene.children[0]).toBe(object) expect(object.children.length).toBe(2) await act(async () => root.render()) expect(scene.children.length).toBe(0) expect(object.children.length).toBe(0) }) it('should remove then add children from primitive when key changes', async () => { const object = new THREE.Group() function Parent({ children, primitiveKey }: { children: React.ReactNode; primitiveKey: string }) { return ( {children} ) } function Component({ primitiveKey }: { primitiveKey: string }) { return ( ) } const store = await act(async () => root.render()) const { scene } = store.getState() expect(scene.children.length).toBe(1) expect(scene.children[0]).toBe(object) expect(object.children.length).toBe(2) await act(async () => root.render()) expect(scene.children.length).toBe(1) expect(scene.children[0]).toBe(object) expect(object.children.length).toBe(2) }) it('should go through lifecycle', async () => { const lifecycle: string[] = [] function Test() { React.useInsertionEffect(() => void lifecycle.push('useInsertionEffect'), []) React.useImperativeHandle(React.useRef(null), () => { lifecycle.push('refCallback') return null }) React.useLayoutEffect(() => void lifecycle.push('useLayoutEffect'), []) React.useEffect(() => void lifecycle.push('useEffect'), []) lifecycle.push('render') return void lifecycle.push('ref')} /> } await act(async () => root.render()) expect(lifecycle).toStrictEqual([ 'render', 'useInsertionEffect', 'ref', 'refCallback', 'useLayoutEffect', 'useEffect', ]) }) it('should forward ref three object', async () => { // Note: Passing directly should be less strict, and assigning current should be more strict let immutableRef!: React.RefObject let mutableRef!: React.RefObject let mutableRefSpecific!: React.RefObject const RefTest = () => { immutableRef = React.createRef() mutableRef = React.useRef(null) mutableRefSpecific = React.useRef(null) return ( <> (mutableRefSpecific.current = r)} /> ) } await act(async () => root.render()) expect(immutableRef.current).toBeInstanceOf(THREE.Mesh) expect(mutableRef.current).toBeInstanceOf(THREE.Mesh) expect(mutableRefSpecific.current).toBeInstanceOf(THREE.Mesh) }) it('should handle children', async () => { const Test = () => ( ) const store = await act(async () => root.render()) const { scene } = store.getState() expect(scene.children.length).toBe(1) expect(scene.children[0]).toBeInstanceOf(THREE.Group) expect(scene.children[0].children.length).toBe(1) expect(scene.children[0].children[0]).toBeInstanceOf(THREE.Mesh) }) it('should handle attach', async () => { const lifecycle: string[] = [] const Test = () => { return ( void lifecycle.push('mount')} attach={() => (lifecycle.push('attach'), () => lifecycle.push('detach'))} /> ) } const store = await act(async () => root.render()) const { scene } = store.getState() expect(scene.children.length).toBe(1) expect(scene.children[0]).toBeInstanceOf(THREE.Mesh) // Handles geometry & material attach expect((scene.children[0] as ComponentMesh).geometry).toBeInstanceOf(THREE.BoxGeometry) expect((scene.children[0] as ComponentMesh).material).toBeInstanceOf(THREE.MeshStandardMaterial) // Handles nested attach expect(scene.children[0].userData.group).toBeInstanceOf(THREE.Group) // attach bypasses scene-graph expect(scene.children[0].children.length).toBe(0) // attaches before presenting expect(lifecycle).toStrictEqual(['attach', 'mount']) }) it('should update props reactively', async () => { const store = await act(async () => root.render()) const { scene } = store.getState() const group = scene.children[0] as THREE.Group // Initial expect(group.name).toBe(new THREE.Group().name) // Set await act(async () => root.render()) expect(group.name).toBe('one') // Update await act(async () => root.render()) expect(group.name).toBe('two') // Unset await act(async () => root.render()) expect(group.name).toBe(new THREE.Group().name) }) it('should handle event props reactively', async () => { const store = await act(async () => root.render()) const { scene, internal } = store.getState() const mesh = scene.children[0] as ComponentMesh mesh.name = 'current' // Initial expect(internal.interaction.length).toBe(0) // Set await act(async () => root.render( void 0} />)) expect(internal.interaction.length).toBe(1) expect(internal.interaction).toStrictEqual([mesh]) // Update await act(async () => root.render( void 0} />)) expect(internal.interaction.length).toBe(1) expect(internal.interaction).toStrictEqual([mesh]) // Unset await act(async () => root.render()) expect(internal.interaction.length).toBe(0) }) it('should handle the args prop reactively', async () => { const ref = React.createRef() const child = React.createRef() const attachedChild = React.createRef() const Test = (props: ThreeElements['mesh']) => ( ) // Initial await act(async () => root.render()) expect(ref.current!.geometry).toBeInstanceOf(THREE.BufferGeometry) expect(ref.current!.geometry).not.toBeInstanceOf(THREE.BoxGeometry) expect(ref.current!.material).toBeInstanceOf(THREE.Material) expect(ref.current!.material).not.toBeInstanceOf(THREE.MeshStandardMaterial) expect(ref.current!.children[0]).toBe(child.current) expect(ref.current!.userData.attach).toBe(attachedChild.current) // Throw on non-array value await expectToThrow( async () => await act(async () => root.render()), 'R3F: The args prop must be an array!', ) // Set const geometry1 = new THREE.BoxGeometry() const material1 = new THREE.MeshStandardMaterial() await act(async () => root.render()) expect(ref.current!.geometry).toBe(geometry1) expect(ref.current!.material).toBe(material1) expect(ref.current!.children[0]).toBe(child.current) expect(ref.current!.userData.attach).toBe(attachedChild.current) // Update const geometry2 = new THREE.BoxGeometry() const material2 = new THREE.MeshStandardMaterial() await act(async () => root.render()) expect(ref.current!.geometry).toBe(geometry2) expect(ref.current!.material).toBe(material2) expect(ref.current!.children[0]).toBe(child.current) expect(ref.current!.userData.attach).toBe(attachedChild.current) // Unset await act(async () => root.render()) expect(ref.current!.geometry).toBeInstanceOf(THREE.BufferGeometry) expect(ref.current!.geometry).not.toBeInstanceOf(THREE.BoxGeometry) expect(ref.current!.material).toBeInstanceOf(THREE.Material) expect(ref.current!.material).not.toBeInstanceOf(THREE.MeshStandardMaterial) expect(ref.current!.children[0]).toBe(child.current) expect(ref.current!.userData.attach).toBe(attachedChild.current) }) it('should handle the object prop reactively', async () => { const ref = React.createRef() const child = React.createRef() const attachedChild = React.createRef() const Test = (props: ThreeElements['primitive']) => ( ) const object1 = new THREE.Object3D() const child1 = new THREE.Object3D() object1.add(child1) const object2 = new THREE.Object3D() const child2 = new THREE.Object3D() object2.add(child2) // Initial await act(async () => root.render()) expect(ref.current).toBe(object1) expect(ref.current!.children).toStrictEqual([child1, child.current]) expect(ref.current!.userData.attach).toBe(attachedChild.current) // Throw on undefined await expectToThrow( async () => await act(async () => root.render()), "R3F: Primitives without 'object' are invalid!", ) // Update await act(async () => root.render()) expect(ref.current).toBe(object2) expect(ref.current!.children).toStrictEqual([child2, child.current]) expect(ref.current!.userData.attach).toBe(attachedChild.current) // Revert await act(async () => root.render()) expect(ref.current).toBe(object1) expect(ref.current!.children).toStrictEqual([child1, child.current]) expect(ref.current!.userData.attach).toBe(attachedChild.current) }) it('should handle unmount', async () => { const dispose = jest.fn() const childDispose = jest.fn() const attachDispose = jest.fn() const flagDispose = jest.fn() const attach = jest.fn() const detach = jest.fn() const object = Object.assign(new THREE.Object3D(), { dispose: jest.fn() }) const objectExternal = Object.assign(new THREE.Object3D(), { dispose: jest.fn() }) object.add(objectExternal) const disposeDeclarativePrimitive = jest.fn() const Test = (props: ThreeElements['mesh']) => ( { if (!self) return self.dispose = dispose }} onClick={() => void 0}> { if (!self) return self.dispose = childDispose }} /> { if (!self) return self.dispose = attachDispose }} attach={() => (attach(), detach)} /> { if (!self) return self.dispose = flagDispose }} /> { if (!self) return self.dispose = disposeDeclarativePrimitive }} /> ) const store = await act(async () => root.render()) await act(async () => root.render(null)) const { scene, internal } = store.getState() // Cleans up scene-graph expect(scene.children.length).toBe(0) // Removes events expect(internal.interaction.length).toBe(0) // Calls dispose on top-level instance expect(dispose).toHaveBeenCalled() // Also disposes of children expect(childDispose).toHaveBeenCalled() // Disposes of attached children expect(attachDispose).toHaveBeenCalled() // Properly detaches attached children expect(attach).toHaveBeenCalledTimes(1) expect(detach).toHaveBeenCalledTimes(1) // Respects dispose={null} expect(flagDispose).not.toHaveBeenCalled() // Does not dispose of primitives expect(object.dispose).not.toHaveBeenCalled() // Only disposes of declarative primitive children expect(objectExternal.dispose).not.toHaveBeenCalled() expect(disposeDeclarativePrimitive).toHaveBeenCalled() }) it('can swap 4 array primitives', async () => { const a = new THREE.Group() a.name = 'a' const b = new THREE.Group() b.name = 'b' const c = new THREE.Group() c.name = 'c' const d = new THREE.Group() d.name = 'd' const Test = ({ array }: { array: THREE.Group[] }) => ( <> {array.map((group, i) => ( ))} ) const array = [a, b, c, d] const store = await act(async () => root.render()) const { scene } = store.getState() expect(scene.children.map((o) => o.name)).toStrictEqual(array.map((o) => o.name)) const reversedArray = [d, c, b, a] await act(async () => root.render()) expect(scene.children.map((o) => o.name)).toStrictEqual(reversedArray.map((o) => o.name)) const mixedArray = [b, a, d, c] await act(async () => root.render()) expect(scene.children.map((o) => o.name)).toStrictEqual(mixedArray.map((o) => o.name)) }) // https://github.com/pmndrs/react-three-fiber/issues/3125 // https://github.com/pmndrs/react-three-fiber/issues/3143 it('can swap 4 array primitives via attach', async () => { const a = new THREE.Group() a.name = 'a' const b = new THREE.Group() b.name = 'b' const c = new THREE.Group() c.name = 'c' const d = new THREE.Group() d.name = 'd' const array = [a, b, c, d] const Test = ({ array }: { array: THREE.Group[] }) => ( <> {array.map((group, i) => ( ))} ) const store = await act(async () => root.render()) const { scene } = store.getState() expect(scene.children.length).toBe(0) expect(scene.userData.objects.map((o: THREE.Object3D) => o.name)).toStrictEqual(array.map((o) => o.name)) const reversedArray = [d, c, b, a] await act(async () => root.render()) expect(scene.children.length).toBe(0) expect(scene.userData.objects.map((o: THREE.Object3D) => o.name)).toStrictEqual(reversedArray.map((o) => o.name)) const mixedArray = [b, a, d, c] await act(async () => root.render()) expect(scene.children.length).toBe(0) expect(scene.userData.objects.map((o: THREE.Object3D) => o.name)).toStrictEqual(mixedArray.map((o) => o.name)) }) it('should gracefully handle text', async () => { // Mount await act(async () => root.render(<>one)) // Update await act(async () => root.render(<>two)) // Unmount await act(async () => root.render(<>)) // Suspense const Test = () => suspend(async () => <>four, []) await act(async () => root.render()) }) it('should gracefully interrupt when building up the tree', async () => { const calls: string[] = [] let lastAttached!: string | undefined let lastMounted!: string | undefined function SuspenseComponent({ reconstruct = false }: { reconstruct?: boolean }) { suspend(async (reconstruct) => reconstruct, [reconstruct]) return ( void (lastMounted = self?.uuid)} attach={(_, self) => { calls.push('attach') lastAttached = self.uuid return () => calls.push('detach') }} /> ) } function Test(props: { reconstruct?: boolean }) { React.useLayoutEffect(() => void calls.push('useLayoutEffect'), []) return ( ) } await act(async () => root.render()) // Should complete tree before layout-effects fire expect(calls).toStrictEqual(['attach', 'useLayoutEffect']) expect(lastAttached).toBe(lastMounted) expect(Mock.instances).toStrictEqual(['suspense', 'parent', 'child']) await act(async () => root.render()) expect(calls).toStrictEqual(['attach', 'useLayoutEffect', 'detach', 'attach']) expect(lastAttached).toBe(lastMounted) expect(Mock.instances).toStrictEqual(['suspense', 'parent', 'child', 'parent', 'child']) }) it('should toggle visibility during Suspense non-destructively', async () => { const a = Promise.resolve(new THREE.Object3D()) const b = Promise.resolve(new THREE.Object3D()) function AsyncPrimitive({ object }: { object: Promise }) { return } for (let i = 0; i < 3; i++) { await act(async () => ( await root.configure() ).render( , ), ) } expect((await a).visible).toBe(true) expect((await b).visible).toBe(true) }) it('should hide suspended objects when displaying fallback', async () => { const a = new THREE.Object3D() const b = new THREE.Object3D() const fallback = new THREE.Object3D() let resolveA: () => void const aPromise = new Promise((res) => { resolveA = () => res(a) }) let resolveB: () => void const bPromise = new Promise((res) => { resolveB = () => res(b) }) function Fallback() { return } function AsyncPrimitive({ object }: { object: Promise }) { return } // Mount unresolved A promise. // Fallback should be mounted and nothing else. const store = await act(async () => ( await root.configure() ).render( }> , ), ) const scene = store.getState().scene as THREE.Scene expect(a.visible).toBe(true) expect(b.visible).toBe(true) expect(scene.children.includes(fallback)).toBe(true) expect(scene.children.includes(a)).toBe(false) // Resolve A promise. // A should be mounted and visible and fallback should be unmounted. await act(async () => resolveA()) await act(async () => ( await root.configure() ).render( }> , ), ) expect(a.visible).toBe(true) expect(b.visible).toBe(true) expect(scene.children.includes(fallback)).toBe(false) expect(scene.children.includes(a)).toBe(true) // Mount unresolved B promise. // A should remain mounted but be invisible, Fallback is mounted, B is unmounted. await act(async () => ( await root.configure() ).render( }> , ), ) expect(a.visible).toBe(false) expect(b.visible).toBe(true) expect(scene.children.includes(fallback)).toBe(true) expect(scene.children.includes(a)).toBe(true) expect(scene.children.includes(b)).toBe(false) // Resolve B promise. // B should be mounted and visible, fallback should be unmounted, A also unmounted and unhidden. await act(async () => resolveB()) await act(async () => ( await root.configure() ).render( }> , ), ) expect(a.visible).toBe(true) expect(b.visible).toBe(true) expect(scene.children.includes(fallback)).toBe(false) expect(scene.children.includes(a)).toBe(false) expect(scene.children.includes(b)).toBe(true) // Remount resolved A promise. // A should be mounted and visible, B should be unmounted and visible (not hidden), fallback should be unmounted. await act(async () => ( await root.configure() ).render( }> , ), ) expect(a.visible).toBe(true) expect(b.visible).toBe(true) expect(scene.children.includes(fallback)).toBe(false) expect(scene.children.includes(a)).toBe(true) expect(scene.children.includes(b)).toBe(false) }) it('preserves camera frustum props for perspective', async () => { const store = await act(async () => (await root.configure({ camera: { aspect: 0 } })).render(null)) const camera = store.getState().camera as THREE.PerspectiveCamera expect(camera.aspect).toBe(0) }) it('preserves camera frustum props for orthographic', async () => { const store = await act(async () => (await root.configure({ orthographic: true, camera: { left: 0, right: 0, top: 0, bottom: 0 } })).render(null), ) const camera = store.getState().camera as THREE.OrthographicCamera expect(camera.left).toBe(0) expect(camera.right).toBe(0) expect(camera.top).toBe(0) expect(camera.bottom).toBe(0) }) it('resolves conflicting and prefixed elements', async () => { extend({ ThreeRandom: THREE.Group }) const store = await act(async () => root.render()) expect(store.getState().scene.children[0]).toBeInstanceOf(THREE.Line) await act(async () => root.render(null)) expect(store.getState().scene.children.length).toBe(0) await act(async () => root.render()) expect(store.getState().scene.children[0]).toBeInstanceOf(THREE.Line) await act(async () => root.render(null)) expect(store.getState().scene.children.length).toBe(0) await act(async () => root.render()) expect(store.getState().scene.children[0]).toBeInstanceOf(THREE.Group) }) it('should properly handle array of components with changing keys and order', async () => { // Component that renders a mesh with a specific ID const MeshComponent = ({ id }: { id: number }) => { return } // Component that maps over an array of values to render MeshComponents const Test = ({ values }: { values: number[] }) => ( <> {values.map((value) => ( ))} ) // Initial render with 4 values const initialValues = [1, 2, 3, 4] const store = await act(async () => root.render()) const { scene } = store.getState() // Check initial state expect(scene.children.length).toBe(4) const initialNames = scene.children.map((child) => child.name).sort() expect(initialNames).toEqual(['mesh-1', 'mesh-2', 'mesh-3', 'mesh-4']) // Update with one less value and different order const updatedValues = [3, 1, 4] await act(async () => root.render()) // Check that the scene has exactly the meshes we expect expect(scene.children.length).toBe(3) const updatedNames = scene.children.map((child) => child.name).sort() expect(updatedNames).toEqual(['mesh-1', 'mesh-3', 'mesh-4']) // Verify mesh-2 was removed expect(scene.children.find((child) => child.name === 'mesh-2')).toBeUndefined() // Verify no duplicates by checking unique names const uniqueNames = new Set(scene.children.map((child) => child.name)) expect(uniqueNames.size).toBe(scene.children.length) // Update with different order again const reorderedValues = [4, 1] await act(async () => root.render()) // Check final state expect(scene.children.length).toBe(2) const finalNames = scene.children.map((child) => child.name).sort() expect(finalNames).toEqual(['mesh-1', 'mesh-4']) // Verify mesh-3 was removed expect(scene.children.find((child) => child.name === 'mesh-3')).toBeUndefined() // Verify no duplicates in final state const finalUniqueNames = new Set(scene.children.map((child) => child.name)) expect(finalUniqueNames.size).toBe(scene.children.length) }) it('should update scene synchronously with flushSync', async () => { let updateSynchronously: (value: number) => void function TestComponent() { const [positionX, setPositionX] = React.useState(0) const scene = useThree((state) => state.scene) updateSynchronously = React.useCallback( (value: number) => { flushSync(() => { setPositionX(value) }) expect(scene.children.length).toBe(1) expect(scene.children[0].position.x).toBe(value) }, [scene, setPositionX], ) return } await act(async () => root.render()) await act(async () => updateSynchronously(1)) }) }) ================================================ FILE: packages/fiber/tests/utils.test.ts ================================================ import * as THREE from 'three' import { type RootStore, type Instance, act, createRoot } from '../src' import { is, dispose, REACT_INTERNAL_PROPS, getInstanceProps, prepare, resolve, attach, detach, RESERVED_PROPS, diffProps, applyProps, updateCamera, findInitialRoot, } from '../src/core/utils' function createMockStore(): RootStore { let store!: RootStore try { act(async () => (store = createRoot(document.createElement('canvas')).render(null))).then(() => null) } catch (e) { console.error(e) } return store } const store = createMockStore() describe('is', () => { const myFunc = () => null const myObj = { myProp: 'test-prop' } const myStr = 'test-string' const myNum = 1 const myUnd = undefined const myArr = [1, 2, 3] it('should tell me if something is a function', () => { expect(is.fun(myFunc)).toBe(true) expect(is.fun(myObj)).toBe(false) expect(is.fun(myStr)).toBe(false) expect(is.fun(myNum)).toBe(false) expect(is.fun(myUnd)).toBe(false) expect(is.fun(myArr)).toBe(false) }) it('should tell me if something is an object', () => { expect(is.obj(myFunc)).toBe(false) expect(is.obj(myObj)).toBe(true) expect(is.obj(myStr)).toBe(false) expect(is.obj(myNum)).toBe(false) expect(is.obj(myUnd)).toBe(false) expect(is.obj(myArr)).toBe(false) }) it('should tell me if something is a string', () => { expect(is.str(myFunc)).toBe(false) expect(is.str(myObj)).toBe(false) expect(is.str(myStr)).toBe(true) expect(is.str(myNum)).toBe(false) expect(is.str(myUnd)).toBe(false) expect(is.str(myArr)).toBe(false) }) it('should tell me if something is a number', () => { expect(is.num(myFunc)).toBe(false) expect(is.num(myObj)).toBe(false) expect(is.num(myStr)).toBe(false) expect(is.num(myNum)).toBe(true) expect(is.num(myUnd)).toBe(false) expect(is.num(myArr)).toBe(false) }) it('should tell me if something is undefined', () => { expect(is.und(myFunc)).toBe(false) expect(is.und(myObj)).toBe(false) expect(is.und(myStr)).toBe(false) expect(is.und(myNum)).toBe(false) expect(is.und(myUnd)).toBe(true) expect(is.und(myArr)).toBe(false) }) it('should tell me if something is an array', () => { expect(is.arr(myFunc)).toBe(false) expect(is.arr(myObj)).toBe(false) expect(is.arr(myStr)).toBe(false) expect(is.arr(myNum)).toBe(false) expect(is.arr(myUnd)).toBe(false) expect(is.arr(myArr)).toBe(true) }) it('should tell me if something is equal', () => { expect(is.equ([], '')).toBe(false) expect(is.equ('hello', 'hello')).toBe(true) expect(is.equ(1, 1)).toBe(true) const obj = { type: 'Mesh' } expect(is.equ(obj, obj)).toBe(true) expect(is.equ({}, {})).toBe(false) expect(is.equ({}, {}, { objects: 'reference' })).toBe(false) expect(is.equ({}, {}, { objects: 'shallow' })).toBe(true) expect(is.equ({ a: 1 }, { a: 1 })).toBe(false) expect(is.equ({ a: 1 }, { a: 1 }, { objects: 'reference' })).toBe(false) expect(is.equ({ a: 1 }, { a: 1 }, { objects: 'shallow' })).toBe(true) expect(is.equ({ a: 1, b: 1 }, { a: 1 }, { objects: 'shallow' })).toBe(false) expect(is.equ({ a: 1 }, { a: 1, b: 1 }, { objects: 'shallow' })).toBe(false) expect(is.equ({ a: 1 }, { a: 1, b: 1 }, { objects: 'shallow', strict: false })).toBe(true) expect(is.equ({ a: [1, 2, 3] }, { a: [1, 2, 3] }, { arrays: 'reference', objects: 'reference' })).toBe(false) expect(is.equ({ a: [1, 2, 3] }, { a: [1, 2, 3] }, { objects: 'reference' })).toBe(false) expect(is.equ({ a: [1, 2, 3] }, { a: [1, 2, 3] }, { objects: 'shallow' })).toBe(true) expect(is.equ({ a: [1, 2, 3] }, { a: [1, 2, 3, 4] }, { objects: 'shallow' })).toBe(false) expect(is.equ({ a: [1, 2, 3] }, { a: [1, 2, 3, 4] }, { objects: 'shallow', strict: false })).toBe(true) expect(is.equ({ a: [1, 2, 3] }, { a: [1, 2, 3], b: 1 }, { objects: 'shallow' })).toBe(false) expect(is.equ({ a: [1, 2, 3] }, { a: [1, 2, 3], b: 1 }, { objects: 'shallow', strict: false })).toBe(true) const arr = [1, 2, 3] expect(is.equ(arr, arr)).toBe(true) expect(is.equ([], [])).toBe(true) expect(is.equ([], [], { arrays: 'reference' })).toBe(false) expect(is.equ([], [], { arrays: 'shallow' })).toBe(true) expect(is.equ([1, 2, 3], [1, 2, 3])).toBe(true) expect(is.equ([1, 2, 3], [1, 2, 3], { arrays: 'shallow' })).toBe(true) expect(is.equ([1, 2, 3], [1, 2, 3], { arrays: 'reference' })).toBe(false) expect(is.equ([1, 2], [1, 2, 3])).toBe(false) expect(is.equ([1, 2, 3, 4], [1, 2, 3])).toBe(false) expect(is.equ([1, 2], [1, 2, 3], { strict: false })).toBe(true) }) }) describe('dispose', () => { it('should dispose of objects and their properties', () => { const mesh = Object.assign(new THREE.Mesh(), { dispose: jest.fn() }) mesh.material.dispose = jest.fn() mesh.geometry.dispose = jest.fn() dispose(mesh) expect(mesh.dispose).toHaveBeenCalled() expect(mesh.material.dispose).toHaveBeenCalled() expect(mesh.geometry.dispose).toHaveBeenCalled() }) it('should not dispose of a THREE.Scene', () => { const scene = Object.assign(new THREE.Scene(), { dispose: jest.fn() }) dispose(scene) expect(scene.dispose).not.toHaveBeenCalled() const disposable = { dispose: jest.fn(), scene } dispose(disposable) expect(disposable.dispose).toHaveBeenCalled() expect(disposable.scene.dispose).not.toHaveBeenCalled() }) }) describe('getInstanceProps', () => { it('should filter internal props without accessing them', () => { const get = jest.fn() const set = jest.fn() const props = { foo: true } const filtered = getInstanceProps( REACT_INTERNAL_PROPS.reduce((acc, key) => ({ ...acc, [key]: { get, set } }), props), ) expect(filtered).toStrictEqual(props) expect(get).not.toHaveBeenCalled() expect(set).not.toHaveBeenCalled() }) }) describe('prepare', () => { it('should create an instance descriptor', () => { const object = new THREE.Object3D() const instance = prepare(object, store, 'object3D', { name: 'object' }) expect(instance.root).toBe(store) expect(instance.type).toBe('object3D') expect(instance.props.name).toBe('object') expect(instance.object).toBe(object) expect((object as Instance['object']).__r3f).toBe(instance) }) it('should not overwrite descriptors', () => { const containerDesc = {} const container = { __r3f: containerDesc } const instance = prepare(container, store, 'container', {}) expect(container.__r3f).toBe(containerDesc) expect(instance).toBe(containerDesc) }) }) describe('resolve', () => { it('should resolve pierced props', () => { const object = { foo: { bar: 1 } } const { root, key, target } = resolve(object, 'foo-bar') expect(root).toBe(object['foo']) expect(key).toBe('bar') expect(target).toBe(root[key]) }) it('should prioritize direct property over piercing', () => { const object = { 'foo-bar': 'direct', foo: { bar: 'pierced' }, } const { root, key, target } = resolve(object, 'foo-bar') expect(root).toBe(object) expect(key).toBe('foo-bar') expect(target).toBe('direct') }) it('should handle undefined direct property values', () => { const object = { 'foo-bar': undefined } const { root, key, target } = resolve(object, 'foo-bar') expect(root).toBe(object) expect(key).toBe('foo-bar') expect(target).toBe(undefined) }) it('should handle null direct property values', () => { const object = { 'foo-bar': null } const { root, key, target } = resolve(object, 'foo-bar') expect(root).toBe(object) expect(key).toBe('foo-bar') expect(target).toBe(null) }) it('should return non-object as root when piercing fails due to non-object intermediate', () => { const object = { foo: 'not-an-object' } const { root, key, target } = resolve(object, 'foo-bar') expect(root).toBe('not-an-object') expect(key).toBe('bar') expect(target).toBe(undefined) }) it('should return null as root when piercing fails due to null intermediate', () => { const object = { foo: null } const { root, key, target } = resolve(object, 'foo-bar') expect(root).toBe(null) expect(key).toBe('bar') expect(target).toBe(undefined) }) it('should handle non-dashed keys normally', () => { const object = { foo: 'value' } const { root, key, target } = resolve(object, 'foo') expect(root).toBe(object) expect(key).toBe('foo') expect(target).toBe('value') }) }) describe('attach / detach', () => { it('should attach & detach using string values', () => { const parent = prepare({ prop: null }, store, '', {}) const child = prepare({}, store, '', { attach: 'prop' }) attach(parent, child) expect(parent.object.prop).toBe(child.object) expect(child.previousAttach).toBe(null) detach(parent, child) expect(parent.object.prop).toBe(null) expect(child.previousAttach).toBe(undefined) }) it('should attach & detach using attachFns', () => { const mount = jest.fn() const unmount = jest.fn() const parent = prepare({}, store, '', {}) const child = prepare({}, store, '', { attach: () => (mount(), unmount) }) attach(parent, child) expect(mount).toHaveBeenCalledTimes(1) expect(unmount).toHaveBeenCalledTimes(0) expect(child.previousAttach).toBe(unmount) detach(parent, child) expect(mount).toHaveBeenCalledTimes(1) expect(unmount).toHaveBeenCalledTimes(1) expect(child.previousAttach).toBe(undefined) }) it('should create array when using array-index syntax', () => { const parent = prepare({ prop: null }, store, '', {}) const child = prepare({}, store, '', { attach: 'prop-0' }) attach(parent, child) expect(parent.object.prop).toStrictEqual([child.object]) expect(child.previousAttach).toBe(undefined) detach(parent, child) expect((parent.object.prop as unknown as Array).length).toBe(1) expect((parent.object.prop as unknown as Array)[0]).toBe(undefined) expect(child.previousAttach).toBe(undefined) }) }) describe('diffProps', () => { it('should filter changed props', () => { const instance = prepare({}, store, '', { foo: true }) const newProps = { foo: true, bar: false } const filtered = diffProps(instance, newProps) expect(filtered).toStrictEqual({ bar: false }) }) it('invalidates pierced props when root is changed', () => { const texture1 = { needsUpdate: false, name: '' } as THREE.Texture const texture2 = { needsUpdate: false, name: '' } as THREE.Texture const oldProps = { map: texture1, 'map-needsUpdate': true, 'map-name': 'test' } const newProps = { map: texture2, 'map-needsUpdate': true, 'map-name': 'test' } const instance = prepare({}, store, '', oldProps) const filtered = diffProps(instance, newProps) expect(filtered).toStrictEqual(newProps) }) it('should pick removed props for HMR', () => { const instance = prepare(new THREE.Object3D(), store, '', { position: [0, 0, 1] }) const newProps = {} const filtered = diffProps(instance, newProps) expect(filtered).toStrictEqual({ position: new THREE.Object3D().position }) }) it('should reset removed props for HMR', () => { const instance = prepare(new THREE.Object3D(), store, '', { scale: 10 }) const filtered = diffProps(instance, {}) expect((filtered.scale as THREE.Vector3).toArray()).toStrictEqual([1, 1, 1]) }) it('should filter reserved props without accessing them', () => { const get = jest.fn() const set = jest.fn() const props = { foo: true } const filtered = diffProps( prepare({}, store, '', {}), RESERVED_PROPS.reduce((acc, key) => ({ ...acc, [key]: { get, set } }), props), ) expect(filtered).toStrictEqual(props) expect(get).not.toHaveBeenCalled() expect(set).not.toHaveBeenCalled() }) }) describe('applyProps', () => { it('should apply props to foreign objects', () => { const target = new THREE.Object3D() expect(() => applyProps(target, {})).not.toThrow() }) it('should not throw when applying unknown props', () => { const target = new THREE.Object3D() applyProps(target, {}) expect(() => applyProps(target, { ['foo-bar']: 1 })).not.toThrow() expect((target as any)['foo-bar']).toBe(undefined) }) it('should throw when applying unknown props due to non-object intermediate', () => { const target = new THREE.Object3D() applyProps(target, { foo: 1 }) expect(() => applyProps(target, { ['foo-bar']: 1 })).toThrow() }) it('should filter reserved props without accessing them', () => { const get = jest.fn() const set = jest.fn() const props = { foo: true } const target = {} applyProps( target, RESERVED_PROPS.reduce((acc, key) => ({ ...acc, [key]: { get, set } }), props), ) expect(target).toStrictEqual(props) expect(get).not.toHaveBeenCalled() expect(set).not.toHaveBeenCalled() }) it('should overwrite non-atomic properties', () => { const foo = { value: true } const target = { foo } applyProps(target, { foo: { value: false } }) expect(target.foo).not.toBe(foo) expect(target.foo.value).toBe(false) }) it('should prefer to copy potentially read-only math classes', () => { const one = new THREE.Vector3(1, 1, 1) const two = new THREE.Vector3(2, 2, 2) const target = { test: one } applyProps(target, { test: two }) expect(target.test).toBe(one) expect(target.test.toArray()).toStrictEqual([2, 2, 2]) }) it('should not copy if props are supersets of another', () => { const copy = jest.fn() const set = jest.fn() class Test { copy = copy set = set } class SuperTest extends Test { copy = copy set = set } const one = new Test() const two = new SuperTest() const target = { test: one } applyProps(target, { test: two }) expect(one.copy).not.toHaveBeenCalled() expect(two.copy).not.toHaveBeenCalled() expect(one.set).not.toHaveBeenCalled() expect(two.set).not.toHaveBeenCalled() expect(target.test).toBe(two) }) it('should prefer to set when props are an array', () => { const target = new THREE.Object3D() applyProps(target, { position: [1, 2, 3] }) expect(target.position.toArray()).toStrictEqual([1, 2, 3]) }) it('should set with scalar shorthand where applicable', () => { // Vector3#setScalar const target = new THREE.Object3D() applyProps(target, { scale: 5 }) expect(target.scale.toArray()).toStrictEqual([5, 5, 5]) // Color#set const material = new THREE.MeshBasicMaterial() applyProps(material, { color: 0x000000 }) expect(material.color.getHex()).toBe(0x000000) applyProps(material, { color: 'white' }) expect(material.color.getHex()).toBe(0xffffff) applyProps(material, { color: new THREE.Color('red') }) expect(material.color.getHex()).toBe(0xff0000) // No-op on undefined const mesh = new THREE.Mesh() applyProps(mesh, { position: undefined }) expect(mesh.position.toArray()).toStrictEqual([0, 0, 0]) }) it('should pierce into nested properties', () => { const target = new THREE.Mesh() applyProps(target, { 'material-color': 0x000000 }) expect(target.material.color.getHex()).toBe(0x000000) }) it('should not apply a prop if it is undefined', () => { const target = { value: 'initial' } applyProps(target, { value: undefined }) expect(target.value).toBe('initial') }) it('should not apply a prop to an instance if it is a reserved event', () => { const target = prepare(new THREE.Object3D(), store, '', {}) applyProps(target.object, { onClick: () => null }) expect('onClick' in target).toBe(false) }) // https://github.com/pmndrs/koota/issues/47 it('should not fallthrough to set/copy for primitive types', () => { const set = jest.fn() const copy = jest.fn() // @ts-ignore Number.prototype.set = set // @ts-ignore Number.prototype.copy = copy const target = { scale: 1, rotation: new THREE.Vector3(1, 2, 3) } applyProps(target, { scale: 10, 'rotation-z': 4 }) // @ts-ignore delete Number.prototype.set // @ts-ignore delete Number.prototype.copy expect(set).not.toHaveBeenCalled() expect(copy).not.toHaveBeenCalled() expect(target.scale).toBe(10) expect(target.rotation.z).toBe(4) }) }) describe('updateCamera', () => { it('updates camera matrices', () => { const size = { width: 1280, height: 800, left: 0, top: 0 } const perspective = new THREE.PerspectiveCamera() perspective.updateProjectionMatrix = jest.fn() updateCamera(perspective, size) expect(perspective.updateProjectionMatrix).toHaveBeenCalled() expect(perspective.projectionMatrix.toArray()).toMatchSnapshot() const orthographic = new THREE.OrthographicCamera() orthographic.updateProjectionMatrix = jest.fn() updateCamera(orthographic, size) expect(orthographic.updateProjectionMatrix).toHaveBeenCalled() expect(orthographic.projectionMatrix.toArray()).toMatchSnapshot() }) it('respects camera.manual', () => { const size = { width: 0, height: 0, left: 0, top: 0 } const perspective = Object.assign(new THREE.PerspectiveCamera(), { manual: true }) perspective.updateProjectionMatrix = jest.fn() updateCamera(perspective, size) expect(perspective.updateProjectionMatrix).not.toHaveBeenCalled() const orthographic = Object.assign(new THREE.OrthographicCamera(), { manual: true }) orthographic.updateProjectionMatrix = jest.fn() updateCamera(orthographic, size) expect(orthographic.updateProjectionMatrix).not.toHaveBeenCalled() }) }) describe('findInitialRoot', () => { it('finds the nearest root for portals', () => { const portalStore = createMockStore() portalStore.getState().previousRoot = store const instance = prepare(new THREE.Object3D(), portalStore, '', {}) const root = findInitialRoot(instance) expect(root).toBe(store) }) it('falls back to the local root', () => { const instance = prepare(new THREE.Object3D(), store, '', {}) const root = findInitialRoot(instance) expect(root).toBe(store) }) }) ================================================ FILE: packages/shared/setupTests.ts ================================================ import * as THREE from 'three' import { WebGL2RenderingContext } from '@react-three/test-renderer/src/WebGL2RenderingContext' import { extend } from '@react-three/fiber' declare global { var IS_REACT_ACT_ENVIRONMENT: boolean } // Let React know that we'll be testing effectful components global.IS_REACT_ACT_ENVIRONMENT = true // PointerEvent is not in JSDOM // https://github.com/jsdom/jsdom/pull/2666#issuecomment-691216178 // https://w3c.github.io/pointerevents/#pointerevent-interface if (!global.PointerEvent) { global.PointerEvent = class extends MouseEvent implements PointerEvent { readonly pointerId: number = 0 readonly width: number = 1 readonly height: number = 1 readonly pressure: number = 0 readonly tangentialPressure: number = 0 readonly tiltX: number = 0 readonly tiltY: number = 0 readonly twist: number = 0 readonly pointerType: string = '' readonly isPrimary: boolean = false readonly altitudeAngle: number = 0 readonly azimuthAngle: number = 0 constructor(type: string, params: PointerEventInit = {}) { super(type, params) Object.assign(this, params) } getCoalescedEvents = () => [] getPredictedEvents = () => [] } } globalThis.WebGL2RenderingContext = WebGL2RenderingContext as any globalThis.WebGLRenderingContext = class WebGLRenderingContext extends WebGL2RenderingContext {} as any HTMLCanvasElement.prototype.getContext = function (this: HTMLCanvasElement) { return new WebGL2RenderingContext(this) as any } // Extend catalogue for render API in tests extend(THREE as any) ================================================ FILE: packages/test-renderer/.npmignore ================================================ /src/ markdown/ ================================================ FILE: packages/test-renderer/CHANGELOG.md ================================================ # @react-three/test-renderer ## 9.1.0 ### Minor Changes - 31781e5a1fdc464cb67617cc3d7bc5d8690cd4cd: feat(RTTR): handle primitives in test-renderer and fix queries in TestInstances ## 9.0.1 ### Patch Changes - 754861f16ac7ee93844d52057d2b8515b145fdb2: Republish as latest ## 9.0.0 ### Major Changes - 226d2ec: feat: React 19 support ## 8.2.4 ### Patch Changes - dec2cb28: fix(test-renderer): include types in output ## 8.2.3 ### Patch Changes - 2007c19b: fix: republish with types ## 8.2.2 ### Patch Changes - ff1a16f1: fix: narrow React peer dep range ## 8.2.1 ### Patch Changes - 020bb194: fix(RTTR): set initial size for NaN in viewport ## 8.2.0 ### Minor Changes - a5ffb08e: feat(RTTR): waitFor util ## 8.1.5 ### Patch Changes - 673927f7: fix(RTTR): implement HTMLCanvasElement.getContext ## 8.1.4 ### Patch Changes - 53ba22d3: fix(RTTR): fallback to canvas shim ## 8.1.3 ### Patch Changes - 2b0be267: fix: support WebGL2 ## 8.1.2 ### Patch Changes - d9e6316d: fix: transpile class properties ## 8.1.1 ### Patch Changes - 9a776b71: fix(RTTR): backport traverse, update fixes ## 8.1.0 ### Minor Changes - 24c4dba4: shortcut for shadow type ## 8.0.17 ### Patch Changes - 786ccb46: fix: restore RTTR version ## 8.0.16 ### Patch Changes - 7b6df9df: fix: infinite loop updating cam viewport - Updated dependencies [7b6df9df] - @react-three/fiber@8.0.26 ## 8.0.15 ### Patch Changes - b7cd0f42: update viewport on camera changes - Updated dependencies [b7cd0f42] - @react-three/fiber@8.0.25 ## 8.0.14 ### Patch Changes - 29d03c64: revert multi attach - Updated dependencies [29d03c64] - @react-three/fiber@8.0.23 ## 8.0.13 ### Patch Changes - 4c87bce: fix: attach, devtools, and perf fixes - Updated dependencies [4c87bce] - @react-three/fiber@8.0.20 ## 8.0.12 ### Patch Changes - c4715d5f: allow invalidate to preempt more than 1 frame - Updated dependencies [c4715d5f] - @react-three/fiber@8.0.15 ## 8.0.11 ### Patch Changes - 5559a119: Add support for recoverable errors - Updated dependencies [5559a119] - @react-three/fiber@8.0.14 ## 8.0.10 ### Patch Changes - 9d77d8e2: fix: detach attribute removal - Updated dependencies [9d77d8e2] - @react-three/fiber@8.0.13 ## 8.0.9 ### Patch Changes - 3d10413f: fix portal layers - Updated dependencies [3d10413f] - @react-three/fiber@8.0.12 ## 8.0.8 ### Patch Changes - 5167b1e4: memoized.args can be undefined - Updated dependencies [5167b1e4] - @react-three/fiber@8.0.11 ## 8.0.7 ### Patch Changes - eb321afd: fix: remount bug, allow portals to inject custom size - Updated dependencies [eb321afd] - @react-three/fiber@8.0.10 ## 8.0.6 ### Patch Changes - 624df949: fix: canvas unmount race condition" - Updated dependencies [624df949] - @react-three/fiber@8.0.9 ## 8.0.5 ### Patch Changes - d4bafb9: fix re-parenting, useframe not working properly in portals, attach crash - Updated dependencies [d4bafb9] - @react-three/fiber@8.0.6 ## 8.0.4 ### Patch Changes - 227c328: fix pointer for root and portals - Updated dependencies [227c328] - @react-three/fiber@8.0.5 ## 8.0.3 ### Patch Changes - 3252aed: setevents needs to spread and be mirrored in portals - Updated dependencies [3252aed] - @react-three/fiber@8.0.3 ## 8.0.2 ### Patch Changes - 8035d1f: fix: legacy mode - Updated dependencies [8035d1f] - @react-three/fiber@8.0.2 ## 8.0.1 ### Patch Changes - 26db195: add legacy flag to turn of three.colormanagement - Updated dependencies [26db195] - @react-three/fiber@8.0.1 ## 8.0.0 ### Major Changes - 385ba9c: v8 major, react-18 compat - 04c07b8: v8 major, react-18 compat ### Patch Changes - 347ea79: new beta for library testing - Updated dependencies [385ba9c] - Updated dependencies [04c07b8] - Updated dependencies [347ea79] - @react-three/fiber@8.0.0 ## 8.0.0-beta.0 ### Major Changes - 385ba9c: v8 major, react-18 compat ### Patch Changes - Updated dependencies [385ba9c] - @react-three/fiber@8.0.0-beta.0 ## 7.0.25-beta.0 ### Patch Changes - cf6316c: new beta for library testing - Updated dependencies [cf6316c] - @react-three/fiber@8.0.0-beta.0 ## 7.0.24 ### Patch Changes - 8698734: Release latest patches - Updated dependencies [8698734] - @react-three/fiber@7.0.25 ## 7.0.23 ### Patch Changes - 7f46ddf: cleanup captured pointers when released (#1914) - Updated dependencies [7f46ddf] - @react-three/fiber@7.0.24 ## 7.0.22 ### Patch Changes - 30d38b1: remove logs - Updated dependencies [30d38b1] - @react-three/fiber@7.0.23 ## 7.0.21 ### Patch Changes - 259e1fa: add camera:manual - Updated dependencies [259e1fa] - @react-three/fiber@7.0.22 ## 7.0.20 ### Patch Changes - 65e4147: up usemeasure, add last event to internals" - Updated dependencies [65e4147] - @react-three/fiber@7.0.21 ## 7.0.19 ### Patch Changes - 54cb0fd: update react-use-measure, allow it to use the offsetSize - Updated dependencies [54cb0fd] - @react-three/fiber@7.0.20 ## 7.0.18 ### Patch Changes - 7aa2eab: fix: remove zustand subscribe selector - Updated dependencies [7aa2eab] - @react-three/fiber@7.0.19 ## 7.0.17 ### Patch Changes - 6780f58: fix unmount pointer capture - Updated dependencies [6780f58] - @react-three/fiber@7.0.18 ## 7.0.16 ### Patch Changes - 894c550: fix: event count - Updated dependencies [894c550] - @react-three/fiber@7.0.17 ## 7.0.15 ### Patch Changes - c7a4220: patch: applyprops returns the same instance - Updated dependencies [c7a4220] - @react-three/fiber@7.0.16 ## 7.0.14 ### Patch Changes - c5645e8: fix primitive leftovers on switch - Updated dependencies [c5645e8] - @react-three/fiber@7.0.15 ## 7.0.13 ### Patch Changes - 05af996: fix: revert the is function - Updated dependencies [05af996] - @react-three/fiber@7.0.14 ## 7.0.12 ### Patch Changes - 0df6073: fix: missed events - Updated dependencies [0df6073] - @react-three/fiber@7.0.12 ## 7.0.11 ### Patch Changes - 62b0a3a: fix: event order of missed pointers - Updated dependencies [62b0a3a] - @react-three/fiber@7.0.11 ## 7.0.10 ### Patch Changes - e019dd4: fixes - Updated dependencies [e019dd4] - @react-three/fiber@7.0.10 ## 7.0.8 ### Patch Changes - cd266e4: Fix diffProps dashed keys - Updated dependencies [cd266e4] - @react-three/fiber@7.0.9 ## 7.0.7 ### Patch Changes - 0375896: Simplify useframe, support instanced event cancelation, silence disposal - Updated dependencies [0375896] - @react-three/fiber@7.0.7 ## 7.0.6 ### Patch Changes - fb052ad: Fix babel-env browserslist transpiling into old code" - Updated dependencies [fb052ad] - @react-three/fiber@7.0.6 ## 7.0.5 ### Patch Changes - c97794a: Add useLoader.clear(Loader, input) - Updated dependencies [c97794a] - @react-three/fiber@7.0.5 ## 7.0.4 ### Patch Changes - 974ecfb: Allow elements to define attachFns for specific mount/unmount - Updated dependencies [974ecfb] - @react-three/fiber@7.0.4 ## 7.0.1 ### Patch Changes - a97aca3: Add controls state field - Updated dependencies [a97aca3] - 4c703d6: fix rttr didn't work with r130 - Updated dependencies [4c703d6] - @react-three/fiber@7.0.2 ## 7.0.0 ### Major Changes - 96ae1ad: fix javascript interpreting renderpriority as positive ### Patch Changes - Updated dependencies [96ae1ad] - @react-three/fiber@7.0.0 ## 6.2.3 ### Patch Changes - 26bc7eb: typescript changes - Updated dependencies [26bc7eb] - @react-three/fiber@6.2.3 ## 6.2.2 ### Patch Changes - 4f44a2c: use more helpful name with event handling in rttr - Updated dependencies [4f44a2c] - @react-three/fiber@6.2.2 ## 6.1.5 ### Patch Changes - fix(rttr): if children is undefined return an array to map with - Updated dependencies [undefined] - @react-three/fiber@6.1.5 ## 6.1.3 ### Patch Changes - 6faa090: Add shape to types, exclude event functions from event data - Updated dependencies [6faa090] - @react-three/fiber@6.1.4 ## 6.1.2 ### Patch Changes - 71e72c0: Fix constructor args with attached children (#1348) - Updated dependencies [71e72c0] - @react-three/fiber@6.1.3 ## 6.1.1 ================================================ FILE: packages/test-renderer/README.md ================================================ # React Three Test Renderer ⚛️🔼🧪 [![Version](https://img.shields.io/npm/v/@react-three/test-renderer?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/@react-three/test-renderer) [![Downloads](https://img.shields.io/npm/dt/@react-three/test-renderer.svg?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/@react-three/test-renderer) [![Twitter](https://img.shields.io/twitter/follow/pmndrs?label=%40pmndrs&style=flat&colorA=000000&colorB=000000&logo=twitter&logoColor=000000)](https://twitter.com/pmndrs) [![Twitter](https://img.shields.io/twitter/follow/_josh_ellis_?label=%40_josh_ellis_&style=flat&colorA=000000&colorB=000000&logo=twitter&logoColor=000000)](https://twitter.com/_josh_ellis_) [![Discord](https://img.shields.io/discord/740090768164651008?style=flat&colorA=000000&colorB=000000&label=discord&logo=discord&logoColor=000000)](https://discord.gg/ZZjjNvJ) `@react-three/test-renderer` is a React testing renderer for threejs in node. ```bash yarn add @react-three/fiber three yarn add -D @react-three/test-renderer ``` --- ## The problem You've written a complex and amazing webgl experience using [`@react-three/fiber`](https://github.com/pmndrs/react-three-fiber) and you want to test it to make sure it works even after you add even more features. You go to use `react-dom` but hang on, `THREE` elements aren't in the DOM! You decide to use `@react-three/test-renderer` you can see the container & the canvas but you can't see the tree for the scene!? That's because `@react-three/fiber` renders to a different react root with it's own reconciler. ## The solution You use `@react-three/test-renderer` ⚛️-🔼-🧪, an experimental React renderer using `@react-three/fiber` under the hood to expose the scene graph wrapped in a test instance providing helpful utilities to test with. Essentially, this package makes it easy to grab a snapshot of the Scene Graph rendered by `three` without the need for webgl & browser. --- ## Usage RTTR is testing library agnostic, so we hope that it works with libraries such as [`jest`](https://jestjs.io/), [`jasmine`](https://jasmine.github.io/) etc. ```tsx import ReactThreeTestRenderer from '@react-three/test-renderer' const renderer = await ReactThreeTestRenderer.create( , ) // assertions using the TestInstance & Scene Graph console.log(renderer.toGraph()) ``` --- ## API - [React Three Test Renderer API](/packages/test-renderer/markdown/rttr.md) - [React Three Test Instance API](/packages/test-renderer/markdown/rttr-instance.md) ================================================ FILE: packages/test-renderer/markdown/rttr-instance.md ================================================ # React Three Test Instance API ## Table of Contents - [`ReactThreeTestInstance`](#instance) - Properties - [`instance`](#instance-prop-instance) - [`type`](#instance-prop-type) - [`props`](#instance-prop-props) - [`parent`](#instance-prop-parent) - [`children`](#instance-prop-children) - [`allChildren`](#instance-prop-allChildren) - Methods - [`find`](#instance-meth-find) - [`findAll`](#instance-meth-findall) - [`findByType`](#instance-meth-findbytype) - [`findAllByType`](#instance-meth-findallbytype) - [`findByProps`](#instance-meth-findbyprops) - [`findAllByProps`](#instance-meth-findallbyprops) --- ## `ReactThreeTestInstance` ⚛️ This is an internal class that wraps the elements returned from [`ReactThreeTestRenderer.create`](/packages/test-renderer/markdown/rttr.md#create). It has several properties & methods to enhance the testing experience. Similar to the core API, it closely mirrors the API of [`react-test-renderer`](https://reactjs.org/docs/test-renderer.html). ### `instance` ```ts testInstance.instance ``` Returns the instance object of the specific testInstance. This will be the `THREE` initialized class. ### `type` ```ts testInstance.type ``` Returns the `THREE` type of the test instance, e.g `Scene` or `Mesh`. ### `props` ```ts testInstance.props ``` Returns an object of the props that are currently being passed to the element. This will include hidden ones such as `attach="geometry"` which are automatically applied in the reconciler. ### `parent` ```ts testInstance.parent ``` Returns the parent testInstance of this testInstance. If no parent is available, it will return `null`. ### `children` ```ts testInstance.children ``` Returns the children test instances of this test instance according to the property `children`, this will not include Geometries, Materials etc. ### `allChildren` ```ts testInstance.allChildren ``` Returns all the children testInstances of this test instance, this will be as thorough as [`testRenderer.toTree()`](/packages/test-renderer/markdown/rttr.md#create-totree) capturing all react components in the tree. ### `find()` ```ts testInstance.find(test) ``` Find a single test instance for which `test(testInstance)` returns `true`. If `test(testInstance)` does not return `true` for exactly one test instance it will throw an error. ### `findAll()` ```ts testInstance.findAll(test) ``` Finds all test instances for which `test(testInstance)` returns `true`. If no test instances are found, it will return an empty array. ### `findByType()` ```ts testInstance.findByType(type) ``` Find a single test instance with the provided type. If there is not exactly one test instance with the provided type it will throw an error. ### `findAllByType()` ```ts testInstance.findAllByType(type) ``` Finds all test instances with the provided type. If no test instances are found, it will return an empty array. ### `findByProps()` ```ts testInstance.findByProps(props) // Also accepts RegExp matchers testInstance.findByProps({ [prop]: /^match/i }) ``` Find a single test instance with the provided props. If there is not exactly one test instance with the provided props it will throw an error. ### `findAllByProps()` ```ts testInstance.findAllByProps(props) // Also accepts RegExp matchers testInstance.findAllByProps({ [prop]: /^matches/i }) ``` Finds all test instances with the provided props. If no test instances are found, it will return an empty array. ================================================ FILE: packages/test-renderer/markdown/rttr.md ================================================ # React Three Test Renderer API ## Table of Contents - [`create()`](#create) - [`scene`](#create-scene) - [`getInstance()`](#create-getinstance) - [`toTree()`](#create-totree) - [`toGraph()`](#create-tograph) - [`fireEvent()`](#create-fireevent) - [`advanceFrames()`](#create-advanceframes) - [`update()`](#create-update) - [`unmount()`](#create-unmount) - [`act()`](#act) --- ## `create()` 🧪 ```tsx const renderer = ReactThreeTestRenderer.create(element, options) ``` Create a ReactThreeTestRenderer instance with a passed `three` element e.g. ``. By default, it won't create an actual `THREE.WebGLRenderer` and there will be no loop. But it will still render the complete scene graph. Returns the properties below. #### CreateOptions ```ts // RenderProps is from react-three-fiber interface CreateOptions extends RenderProps { width?: number // width of canvas height?: number // height of canvas } ``` ### `scene` ```tsx renderer.scene ``` Returns the root “react three test instance” object that is useful for making assertions. You can use it to find other “test instances” deeper below. ### `getInstance()` ```tsx renderer.getInstance() ``` Return the instance corresponding to the root three element, if available. This will not work if the root element is a function component because they don’t have instances. ### `toTree()` ```tsx renderer.toTree() ``` Returns an object representing the rendered tree similar to [`react-test-renderer`](https://reactjs.org/docs/test-renderer.html#overview). This will include all elements written as react components. ### `toGraph()` ```tsx renderer.toGraph() ``` Returns an object representing the [`scene graph`](https://threejs.org/manual/#en/scenegraph). This will not include all elements such as ones that use `attach`. ### `fireEvent()` ```tsx renderer.fireEvent(testInstance, eventName, mockEventData) ``` Native method to fire events on the specific part of the rendered tree through passing an element within the tree and an event name. The third argument is appended to the [`MockSyntheticEvent`](#create-fireevent-mocksyntheticevent) passed to the event handler. Event names follow camelCase convention (e.g. `pointerUp`), or you can pass event handler name instead (e.g. `onPointerUp`). #### `MockSyntheticEvent` ```ts type MockSyntheticEvent = { camera: Camera // the default camera of the rendered scene stopPropagation: () => void target: ReactThreeTestInstance currentTarget: ReactThreeTestInstance sourceEvent: MockEventData ...mockEventData } ``` ### `advanceFrames()` ```tsx renderer.advanceFrames(frames, delta) ``` Native method to advance the frames (therefore running subscribers to the GL Render loop such as `useFrame`). Requires an amount of frames to advance by & a parameter of delta to pass to the subscribers creating a more controlled testing environment. ### `update()` ```tsx renderer.update(element) ``` Rerender the tree with the new root element. This simulates a react update at the update, thus updating the children below. If the new element has the same type and key as the previous element, the tree will be updated. ### `unmount()` ```tsx renderer.unmount() ``` Unmount the tree, triggering the appropriate lifecycle events. --- ## `act()` ⚛️ ```tsx ReactThreeTestRenderer.act(callback) ``` Similar to the [`act()` in `react-test-renderer`](https://reactjs.org/docs/test-renderer.html#testrendereract). `ReactThreeTestRenderer.act` prepares a component for assertions. Unlike `react-test-renderer` you do not have to wrap calls to `ReactThreeTestRenderer.create` and `renderer.update`. #### Act example (using jest) ```tsx import ReactThreeTestRenderer from '@react-three/test-renderer' const Mesh = () => { const meshRef = React.useRef() useFrame((_, delta) => { meshRef.current.rotation.x += delta }) return ( ) } const renderer = await ReactThreeTestRenderer.create() expect(renderer.scene.children[0].instance.rotation.x).toEqual(0) await ReactThreeTestRenderer.act(async () => { await renderer.advanceFrames(2, 1) }) expect(renderer.scene.children[0].instance.rotation.x).toEqual(2) ``` ================================================ FILE: packages/test-renderer/package.json ================================================ { "name": "@react-three/test-renderer", "version": "9.1.0", "description": "Test Renderer for react-three-fiber", "author": "Josh Ellis", "license": "MIT", "private": false, "main": "dist/react-three-test-renderer.cjs.js", "module": "dist/react-three-test-renderer.esm.js", "types": "dist/react-three-test-renderer.cjs.d.ts", "bugs": { "url": "https://github.com/pmndrs/react-three-fiber/issues" }, "homepage": "https://github.com/pmndrs/react-three-fiber/packages/react-three-test-renderer", "repository": { "type": "git", "url": "git+https://github.com/pmndrs/react-three-fiber.git" }, "preconstruct": { "entrypoints": [ "index.tsx" ] }, "peerDependencies": { "react": "^19.0.0", "@react-three/fiber": ">=9.0.0", "three": ">=0.156" } } ================================================ FILE: packages/test-renderer/src/WebGL2RenderingContext.ts ================================================ const functions = [ 'activeTexture', 'attachShader', 'beginQuery', 'beginTransformFeedback', 'bindAttribLocation', 'bindBufferBase', 'bindBufferRange', 'bindRenderbuffer', 'bindSampler', 'bindTransformFeedback', 'bindVertexArray', 'blendColor', 'blendEquation', 'blendEquationSeparate', 'blendFunc', 'blendFuncSeparate', 'blitFramebuffer', 'bufferData', 'bufferSubData', 'checkFramebufferStatus', 'clientWaitSync', 'compileShader', 'compressedTexImage2D', 'compressedTexImage3D', 'compressedTexSubImage2D', 'compressedTexSubImage3D', 'copyBufferSubData', 'copyTexImage2D', 'copyTexSubImage2D', 'copyTexSubImage3D', 'createBuffer', 'createFramebuffer', 'createProgram', 'createQuery', 'createRenderbuffer', 'createSampler', 'createShader', 'createTexture', 'createTransformFeedback', 'createVertexArray', 'cullFace', 'deleteBuffer', 'deleteFramebuffer', 'deleteProgram', 'deleteQuery', 'deleteRenderbuffer', 'deleteSampler', 'deleteShader', 'deleteSync', 'deleteTexture', 'deleteTransformFeedback', 'deleteVertexArray', 'depthFunc', 'depthMask', 'depthRange', 'detachShader', 'disable', 'drawArraysInstanced', 'drawElementsInstanced', 'drawRangeElements', 'enable', 'endQuery', 'endTransformFeedback', 'fenceSync', 'finish', 'flush', 'framebufferRenderbuffer', 'framebufferTexture2D', 'framebufferTextureLayer', 'frontFace', 'generateMipmap', 'getActiveAttrib', 'getActiveUniform', 'getActiveUniformBlockName', 'getActiveUniformBlockParameter', 'getActiveUniforms', 'getAttachedShaders', 'getAttribLocation', 'getBufferParameter', 'getBufferSubData', 'getContextAttributes', 'getError', 'getExtension', 'getFragDataLocation', 'getFramebufferAttachmentParameter', 'getIndexedParameter', 'getInternalformatParameter', 'getParameter', 'getProgramInfoLog', 'getProgramParameter', 'getQuery', 'getQueryParameter', 'getRenderbufferParameter', 'getSamplerParameter', 'getShaderInfoLog', 'getShaderParameter', 'getShaderPrecisionFormat', 'getShaderSource', 'getSupportedExtensions', 'getSyncParameter', 'getTexParameter', 'getTransformFeedbackVarying', 'getUniform', 'getUniformBlockIndex', 'getUniformIndices', 'getUniformLocation', 'getVertexAttrib', 'getVertexAttribOffset', 'hint', 'invalidateFramebuffer', 'invalidateSubFramebuffer', 'isBuffer', 'isContextLost', 'isEnabled', 'isFramebuffer', 'isProgram', 'isQuery', 'isRenderbuffer', 'isSampler', 'isShader', 'isSync', 'isTexture', 'isTransformFeedback', 'isVertexArray', 'lineWidth', 'linkProgram', 'pauseTransformFeedback', 'pixelStorei', 'polygonOffset', 'readBuffer', 'readPixels', 'renderbufferStorage', 'renderbufferStorageMultisample', 'resumeTransformFeedback', 'sampleCoverage', 'samplerParameterf', 'samplerParameteri', 'shaderSource', 'stencilFunc', 'stencilFuncSeparate', 'stencilMask', 'stencilMaskSeparate', 'stencilOp', 'stencilOpSeparate', 'texImage2D', 'texImage3D', 'texParameterf', 'texParameteri', 'texStorage2D', 'texStorage3D', 'texSubImage2D', 'texSubImage3D', 'transformFeedbackVaryings', 'uniform1ui', 'uniform2ui', 'uniform3ui', 'uniform4ui', 'uniformBlockBinding', 'useProgram', 'validateProgram', 'vertexAttribDivisor', 'vertexAttribI4i', 'vertexAttribI4ui', 'vertexAttribIPointer', 'waitSync', 'bindBuffer', 'bindFramebuffer', 'bindTexture', 'clear', 'clearBufferfi', 'clearBufferfv', 'clearBufferiv', 'clearBufferuiv', 'clearColor', 'clearDepth', 'clearStencil', 'colorMask', 'disableVertexAttribArray', 'drawArrays', 'drawBuffers', 'drawElements', 'enableVertexAttribArray', 'scissor', 'uniform1f', 'uniform1fv', 'uniform1i', 'uniform1iv', 'uniform1uiv', 'uniform2f', 'uniform2fv', 'uniform2i', 'uniform2iv', 'uniform2uiv', 'uniform3f', 'uniform3fv', 'uniform3i', 'uniform3iv', 'uniform3uiv', 'uniform4f', 'uniform4fv', 'uniform4i', 'uniform4iv', 'uniform4uiv', 'uniformMatrix2fv', 'uniformMatrix2x3fv', 'uniformMatrix2x4fv', 'uniformMatrix3fv', 'uniformMatrix3x2fv', 'uniformMatrix3x4fv', 'uniformMatrix4fv', 'uniformMatrix4x2fv', 'uniformMatrix4x3fv', 'vertexAttrib1f', 'vertexAttrib1fv', 'vertexAttrib2f', 'vertexAttrib2fv', 'vertexAttrib3f', 'vertexAttrib3fv', 'vertexAttrib4f', 'vertexAttrib4fv', 'vertexAttribI4iv', 'vertexAttribI4uiv', 'vertexAttribPointer', 'viewport', 'makeXRCompatible', ] const enums: Record = { DEPTH_BUFFER_BIT: 256, STENCIL_BUFFER_BIT: 1024, COLOR_BUFFER_BIT: 16384, POINTS: 0, LINES: 1, LINE_LOOP: 2, LINE_STRIP: 3, TRIANGLES: 4, TRIANGLE_STRIP: 5, TRIANGLE_FAN: 6, ZERO: 0, ONE: 1, SRC_COLOR: 768, ONE_MINUS_SRC_COLOR: 769, SRC_ALPHA: 770, ONE_MINUS_SRC_ALPHA: 771, DST_ALPHA: 772, ONE_MINUS_DST_ALPHA: 773, DST_COLOR: 774, ONE_MINUS_DST_COLOR: 775, SRC_ALPHA_SATURATE: 776, FUNC_ADD: 32774, BLEND_EQUATION: 32777, BLEND_EQUATION_RGB: 32777, BLEND_EQUATION_ALPHA: 34877, FUNC_SUBTRACT: 32778, FUNC_REVERSE_SUBTRACT: 32779, BLEND_DST_RGB: 32968, BLEND_SRC_RGB: 32969, BLEND_DST_ALPHA: 32970, BLEND_SRC_ALPHA: 32971, CONSTANT_COLOR: 32769, ONE_MINUS_CONSTANT_COLOR: 32770, CONSTANT_ALPHA: 32771, ONE_MINUS_CONSTANT_ALPHA: 32772, BLEND_COLOR: 32773, ARRAY_BUFFER: 34962, ELEMENT_ARRAY_BUFFER: 34963, ARRAY_BUFFER_BINDING: 34964, ELEMENT_ARRAY_BUFFER_BINDING: 34965, STREAM_DRAW: 35040, STATIC_DRAW: 35044, DYNAMIC_DRAW: 35048, BUFFER_SIZE: 34660, BUFFER_USAGE: 34661, CURRENT_VERTEX_ATTRIB: 34342, FRONT: 1028, BACK: 1029, FRONT_AND_BACK: 1032, TEXTURE_2D: 3553, CULL_FACE: 2884, BLEND: 3042, DITHER: 3024, STENCIL_TEST: 2960, DEPTH_TEST: 2929, SCISSOR_TEST: 3089, POLYGON_OFFSET_FILL: 32823, SAMPLE_ALPHA_TO_COVERAGE: 32926, SAMPLE_COVERAGE: 32928, NO_ERROR: 0, INVALID_ENUM: 1280, INVALID_VALUE: 1281, INVALID_OPERATION: 1282, OUT_OF_MEMORY: 1285, CW: 2304, CCW: 2305, LINE_WIDTH: 2849, ALIASED_POINT_SIZE_RANGE: 33901, ALIASED_LINE_WIDTH_RANGE: 33902, CULL_FACE_MODE: 2885, FRONT_FACE: 2886, DEPTH_RANGE: 2928, DEPTH_WRITEMASK: 2930, DEPTH_CLEAR_VALUE: 2931, DEPTH_FUNC: 2932, STENCIL_CLEAR_VALUE: 2961, STENCIL_FUNC: 2962, STENCIL_FAIL: 2964, STENCIL_PASS_DEPTH_FAIL: 2965, STENCIL_PASS_DEPTH_PASS: 2966, STENCIL_REF: 2967, STENCIL_VALUE_MASK: 2963, STENCIL_WRITEMASK: 2968, STENCIL_BACK_FUNC: 34816, STENCIL_BACK_FAIL: 34817, STENCIL_BACK_PASS_DEPTH_FAIL: 34818, STENCIL_BACK_PASS_DEPTH_PASS: 34819, STENCIL_BACK_REF: 36003, STENCIL_BACK_VALUE_MASK: 36004, STENCIL_BACK_WRITEMASK: 36005, VIEWPORT: 2978, SCISSOR_BOX: 3088, COLOR_CLEAR_VALUE: 3106, COLOR_WRITEMASK: 3107, UNPACK_ALIGNMENT: 3317, PACK_ALIGNMENT: 3333, MAX_TEXTURE_SIZE: 3379, MAX_VIEWPORT_DIMS: 3386, SUBPIXEL_BITS: 3408, RED_BITS: 3410, GREEN_BITS: 3411, BLUE_BITS: 3412, ALPHA_BITS: 3413, DEPTH_BITS: 3414, STENCIL_BITS: 3415, POLYGON_OFFSET_UNITS: 10752, POLYGON_OFFSET_FACTOR: 32824, TEXTURE_BINDING_2D: 32873, SAMPLE_BUFFERS: 32936, SAMPLES: 32937, SAMPLE_COVERAGE_VALUE: 32938, SAMPLE_COVERAGE_INVERT: 32939, COMPRESSED_TEXTURE_FORMATS: 34467, DONT_CARE: 4352, FASTEST: 4353, NICEST: 4354, GENERATE_MIPMAP_HINT: 33170, BYTE: 5120, UNSIGNED_BYTE: 5121, SHORT: 5122, UNSIGNED_SHORT: 5123, INT: 5124, UNSIGNED_INT: 5125, FLOAT: 5126, DEPTH_COMPONENT: 6402, ALPHA: 6406, RGB: 6407, RGBA: 6408, LUMINANCE: 6409, LUMINANCE_ALPHA: 6410, UNSIGNED_SHORT_4_4_4_4: 32819, UNSIGNED_SHORT_5_5_5_1: 32820, UNSIGNED_SHORT_5_6_5: 33635, FRAGMENT_SHADER: 35632, VERTEX_SHADER: 35633, MAX_VERTEX_ATTRIBS: 34921, MAX_VERTEX_UNIFORM_VECTORS: 36347, MAX_VARYING_VECTORS: 36348, MAX_COMBINED_TEXTURE_IMAGE_UNITS: 35661, MAX_VERTEX_TEXTURE_IMAGE_UNITS: 35660, MAX_TEXTURE_IMAGE_UNITS: 34930, MAX_FRAGMENT_UNIFORM_VECTORS: 36349, SHADER_TYPE: 35663, DELETE_STATUS: 35712, LINK_STATUS: 35714, VALIDATE_STATUS: 35715, ATTACHED_SHADERS: 35717, ACTIVE_UNIFORMS: 35718, ACTIVE_ATTRIBUTES: 35721, SHADING_LANGUAGE_VERSION: 35724, CURRENT_PROGRAM: 35725, NEVER: 512, LESS: 513, EQUAL: 514, LEQUAL: 515, GREATER: 516, NOTEQUAL: 517, GEQUAL: 518, ALWAYS: 519, KEEP: 7680, REPLACE: 7681, INCR: 7682, DECR: 7683, INVERT: 5386, INCR_WRAP: 34055, DECR_WRAP: 34056, VENDOR: 7936, RENDERER: 7937, VERSION: 7938, NEAREST: 9728, LINEAR: 9729, NEAREST_MIPMAP_NEAREST: 9984, LINEAR_MIPMAP_NEAREST: 9985, NEAREST_MIPMAP_LINEAR: 9986, LINEAR_MIPMAP_LINEAR: 9987, TEXTURE_MAG_FILTER: 10240, TEXTURE_MIN_FILTER: 10241, TEXTURE_WRAP_S: 10242, TEXTURE_WRAP_T: 10243, TEXTURE: 5890, TEXTURE_CUBE_MAP: 34067, TEXTURE_BINDING_CUBE_MAP: 34068, TEXTURE_CUBE_MAP_POSITIVE_X: 34069, TEXTURE_CUBE_MAP_NEGATIVE_X: 34070, TEXTURE_CUBE_MAP_POSITIVE_Y: 34071, TEXTURE_CUBE_MAP_NEGATIVE_Y: 34072, TEXTURE_CUBE_MAP_POSITIVE_Z: 34073, TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, MAX_CUBE_MAP_TEXTURE_SIZE: 34076, TEXTURE0: 33984, TEXTURE1: 33985, TEXTURE2: 33986, TEXTURE3: 33987, TEXTURE4: 33988, TEXTURE5: 33989, TEXTURE6: 33990, TEXTURE7: 33991, TEXTURE8: 33992, TEXTURE9: 33993, TEXTURE10: 33994, TEXTURE11: 33995, TEXTURE12: 33996, TEXTURE13: 33997, TEXTURE14: 33998, TEXTURE15: 33999, TEXTURE16: 34000, TEXTURE17: 34001, TEXTURE18: 34002, TEXTURE19: 34003, TEXTURE20: 34004, TEXTURE21: 34005, TEXTURE22: 34006, TEXTURE23: 34007, TEXTURE24: 34008, TEXTURE25: 34009, TEXTURE26: 34010, TEXTURE27: 34011, TEXTURE28: 34012, TEXTURE29: 34013, TEXTURE30: 34014, TEXTURE31: 34015, ACTIVE_TEXTURE: 34016, REPEAT: 10497, CLAMP_TO_EDGE: 33071, MIRRORED_REPEAT: 33648, FLOAT_VEC2: 35664, FLOAT_VEC3: 35665, FLOAT_VEC4: 35666, INT_VEC2: 35667, INT_VEC3: 35668, INT_VEC4: 35669, BOOL: 35670, BOOL_VEC2: 35671, BOOL_VEC3: 35672, BOOL_VEC4: 35673, FLOAT_MAT2: 35674, FLOAT_MAT3: 35675, FLOAT_MAT4: 35676, SAMPLER_2D: 35678, SAMPLER_CUBE: 35680, VERTEX_ATTRIB_ARRAY_ENABLED: 34338, VERTEX_ATTRIB_ARRAY_SIZE: 34339, VERTEX_ATTRIB_ARRAY_STRIDE: 34340, VERTEX_ATTRIB_ARRAY_TYPE: 34341, VERTEX_ATTRIB_ARRAY_NORMALIZED: 34922, VERTEX_ATTRIB_ARRAY_POINTER: 34373, VERTEX_ATTRIB_ARRAY_BUFFER_BINDING: 34975, IMPLEMENTATION_COLOR_READ_TYPE: 35738, IMPLEMENTATION_COLOR_READ_FORMAT: 35739, COMPILE_STATUS: 35713, LOW_FLOAT: 36336, MEDIUM_FLOAT: 36337, HIGH_FLOAT: 36338, LOW_INT: 36339, MEDIUM_INT: 36340, HIGH_INT: 36341, FRAMEBUFFER: 36160, RENDERBUFFER: 36161, RGBA4: 32854, RGB5_A1: 32855, RGB565: 36194, DEPTH_COMPONENT16: 33189, STENCIL_INDEX: 6401, STENCIL_INDEX8: 36168, DEPTH_STENCIL: 34041, RENDERBUFFER_WIDTH: 36162, RENDERBUFFER_HEIGHT: 36163, RENDERBUFFER_INTERNAL_FORMAT: 36164, RENDERBUFFER_RED_SIZE: 36176, RENDERBUFFER_GREEN_SIZE: 36177, RENDERBUFFER_BLUE_SIZE: 36178, RENDERBUFFER_ALPHA_SIZE: 36179, RENDERBUFFER_DEPTH_SIZE: 36180, RENDERBUFFER_STENCIL_SIZE: 36181, FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE: 36048, FRAMEBUFFER_ATTACHMENT_OBJECT_NAME: 36049, FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL: 36050, FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE: 36051, COLOR_ATTACHMENT0: 36064, DEPTH_ATTACHMENT: 36096, STENCIL_ATTACHMENT: 36128, DEPTH_STENCIL_ATTACHMENT: 33306, NONE: 0, FRAMEBUFFER_COMPLETE: 36053, FRAMEBUFFER_INCOMPLETE_ATTACHMENT: 36054, FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: 36055, FRAMEBUFFER_INCOMPLETE_DIMENSIONS: 36057, FRAMEBUFFER_UNSUPPORTED: 36061, FRAMEBUFFER_BINDING: 36006, RENDERBUFFER_BINDING: 36007, MAX_RENDERBUFFER_SIZE: 34024, INVALID_FRAMEBUFFER_OPERATION: 1286, UNPACK_FLIP_Y_WEBGL: 37440, UNPACK_PREMULTIPLY_ALPHA_WEBGL: 37441, CONTEXT_LOST_WEBGL: 37442, UNPACK_COLORSPACE_CONVERSION_WEBGL: 37443, BROWSER_DEFAULT_WEBGL: 37444, READ_BUFFER: 3074, UNPACK_ROW_LENGTH: 3314, UNPACK_SKIP_ROWS: 3315, UNPACK_SKIP_PIXELS: 3316, PACK_ROW_LENGTH: 3330, PACK_SKIP_ROWS: 3331, PACK_SKIP_PIXELS: 3332, COLOR: 6144, DEPTH: 6145, STENCIL: 6146, RED: 6403, UNPACK_SKIP_IMAGES: 32877, UNPACK_IMAGE_HEIGHT: 32878, TEXTURE_WRAP_R: 32882, MAX_ELEMENTS_VERTICES: 33000, MAX_ELEMENTS_INDICES: 33001, TEXTURE_MIN_LOD: 33082, TEXTURE_MAX_LOD: 33083, TEXTURE_BASE_LEVEL: 33084, TEXTURE_MAX_LEVEL: 33085, MIN: 32775, MAX: 32776, MAX_TEXTURE_LOD_BIAS: 34045, TEXTURE_COMPARE_MODE: 34892, TEXTURE_COMPARE_FUNC: 34893, CURRENT_QUERY: 34917, QUERY_RESULT: 34918, QUERY_RESULT_AVAILABLE: 34919, STREAM_READ: 35041, STREAM_COPY: 35042, STATIC_READ: 35045, STATIC_COPY: 35046, DYNAMIC_READ: 35049, DYNAMIC_COPY: 35050, MAX_DRAW_BUFFERS: 34852, MAX_FRAGMENT_UNIFORM_COMPONENTS: 35657, MAX_VERTEX_UNIFORM_COMPONENTS: 35658, FRAGMENT_SHADER_DERIVATIVE_HINT: 35723, PIXEL_PACK_BUFFER: 35051, PIXEL_UNPACK_BUFFER: 35052, PIXEL_PACK_BUFFER_BINDING: 35053, PIXEL_UNPACK_BUFFER_BINDING: 35055, SRGB: 35904, COMPARE_REF_TO_TEXTURE: 34894, VERTEX_ATTRIB_ARRAY_INTEGER: 35069, MAX_ARRAY_TEXTURE_LAYERS: 35071, MIN_PROGRAM_TEXEL_OFFSET: 35076, MAX_PROGRAM_TEXEL_OFFSET: 35077, MAX_VARYING_COMPONENTS: 35659, TRANSFORM_FEEDBACK_BUFFER_MODE: 35967, MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS: 35968, TRANSFORM_FEEDBACK_VARYINGS: 35971, TRANSFORM_FEEDBACK_BUFFER_START: 35972, TRANSFORM_FEEDBACK_BUFFER_SIZE: 35973, TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN: 35976, RASTERIZER_DISCARD: 35977, MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS: 35978, MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS: 35979, INTERLEAVED_ATTRIBS: 35980, SEPARATE_ATTRIBS: 35981, TRANSFORM_FEEDBACK_BUFFER: 35982, TRANSFORM_FEEDBACK_BUFFER_BINDING: 35983, RED_INTEGER: 36244, RGB_INTEGER: 36248, RGBA_INTEGER: 36249, SAMPLER_CUBE_SHADOW: 36293, INT_SAMPLER_CUBE: 36300, UNSIGNED_INT_SAMPLER_CUBE: 36308, FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING: 33296, FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE: 33297, FRAMEBUFFER_ATTACHMENT_RED_SIZE: 33298, FRAMEBUFFER_ATTACHMENT_GREEN_SIZE: 33299, FRAMEBUFFER_ATTACHMENT_BLUE_SIZE: 33300, FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE: 33301, FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE: 33302, FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE: 33303, FRAMEBUFFER_DEFAULT: 33304, UNSIGNED_NORMALIZED: 35863, DRAW_FRAMEBUFFER_BINDING: 36006, READ_FRAMEBUFFER: 36008, DRAW_FRAMEBUFFER: 36009, READ_FRAMEBUFFER_BINDING: 36010, RENDERBUFFER_SAMPLES: 36011, FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER: 36052, MAX_COLOR_ATTACHMENTS: 36063, FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: 36182, MAX_SAMPLES: 36183, HALF_FLOAT: 5131, RG: 33319, RG_INTEGER: 33320, VERTEX_ARRAY_BINDING: 34229, SIGNED_NORMALIZED: 36764, COPY_READ_BUFFER: 36662, COPY_WRITE_BUFFER: 36663, COPY_READ_BUFFER_BINDING: 36662, COPY_WRITE_BUFFER_BINDING: 36663, UNIFORM_BUFFER: 35345, UNIFORM_BUFFER_BINDING: 35368, UNIFORM_BUFFER_START: 35369, UNIFORM_BUFFER_SIZE: 35370, MAX_VERTEX_UNIFORM_BLOCKS: 35371, MAX_FRAGMENT_UNIFORM_BLOCKS: 35373, MAX_COMBINED_UNIFORM_BLOCKS: 35374, MAX_UNIFORM_BUFFER_BINDINGS: 35375, MAX_UNIFORM_BLOCK_SIZE: 35376, MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS: 35377, MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS: 35379, UNIFORM_BUFFER_OFFSET_ALIGNMENT: 35380, ACTIVE_UNIFORM_BLOCKS: 35382, UNIFORM_TYPE: 35383, UNIFORM_SIZE: 35384, UNIFORM_BLOCK_INDEX: 35386, UNIFORM_OFFSET: 35387, UNIFORM_ARRAY_STRIDE: 35388, UNIFORM_MATRIX_STRIDE: 35389, UNIFORM_IS_ROW_MAJOR: 35390, UNIFORM_BLOCK_BINDING: 35391, UNIFORM_BLOCK_DATA_SIZE: 35392, UNIFORM_BLOCK_ACTIVE_UNIFORMS: 35394, UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES: 35395, UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER: 35396, UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER: 35398, INVALID_INDEX: 4294967295, MAX_VERTEX_OUTPUT_COMPONENTS: 37154, MAX_FRAGMENT_INPUT_COMPONENTS: 37157, MAX_SERVER_WAIT_TIMEOUT: 37137, OBJECT_TYPE: 37138, SYNC_CONDITION: 37139, SYNC_STATUS: 37140, SYNC_FLAGS: 37141, SYNC_FENCE: 37142, SYNC_GPU_COMMANDS_COMPLETE: 37143, UNSIGNALED: 37144, SIGNALED: 37145, ALREADY_SIGNALED: 37146, TIMEOUT_EXPIRED: 37147, CONDITION_SATISFIED: 37148, WAIT_FAILED: 37149, SYNC_FLUSH_COMMANDS_BIT: 1, VERTEX_ATTRIB_ARRAY_DIVISOR: 35070, ANY_SAMPLES_PASSED: 35887, ANY_SAMPLES_PASSED_CONSERVATIVE: 36202, SAMPLER_BINDING: 35097, TRANSFORM_FEEDBACK: 36386, TRANSFORM_FEEDBACK_PAUSED: 36387, TRANSFORM_FEEDBACK_ACTIVE: 36388, TRANSFORM_FEEDBACK_BINDING: 36389, TEXTURE_IMMUTABLE_FORMAT: 37167, MAX_ELEMENT_INDEX: 36203, TEXTURE_IMMUTABLE_LEVELS: 33503, TIMEOUT_IGNORED: -1, MAX_CLIENT_WAIT_TIMEOUT_WEBGL: 37447, } const extensions: { [key: string]: any } = { // ratified OES_texture_float: {}, OES_texture_half_float: {}, WEBGL_lose_context: { loseContext: () => {}, }, OES_standard_derivatives: {}, OES_vertex_array_object: { createVertexArrayOES: () => {}, bindVertexArrayOES: () => {}, deleteVertexArrayOES: () => {}, }, WEBGL_debug_renderer_info: null, WEBGL_debug_shaders: null, WEBGL_compressed_texture_s3tc: null, WEBGL_depth_texture: {}, OES_element_index_uint: {}, EXT_texture_filter_anisotropic: null, EXT_frag_depth: {}, WEBGL_draw_buffers: {}, ANGLE_instanced_arrays: null, OES_texture_float_linear: null, OES_texture_half_float_linear: null, EXT_blend_minmax: { MIN_EXT: 0, MAX_EXT: 0 }, EXT_shader_texture_lod: null, // community WEBGL_compressed_texture_atc: null, WEBGL_compressed_texture_pvrtc: null, EXT_color_buffer_half_float: null, WEBGL_color_buffer_float: null, EXT_sRGB: null, WEBGL_compressed_texture_etc1: null, } export class WebGL2RenderingContext { [key: string]: any constructor(canvas: HTMLCanvasElement) { this.canvas = canvas this.drawingBufferWidth = canvas.width this.drawingBufferHeight = canvas.height for (const method of functions) { this[method] ??= () => {} } Object.assign(this, enums) } getShaderPrecisionFormat = () => { return { rangeMin: 127, rangeMax: 127, precision: 23, } } private GL_VERSION = 7938 private SCISSOR_BOX = 3088 private VIEWPORT = 2978 getParameter(paramId: number) { switch (paramId) { case this.GL_VERSION: return ['WebGL2'] case this.SCISSOR_BOX: case this.VIEWPORT: return [0, 0, 1, 1] } } getExtension(ext: string) { return extensions[ext] } getProgramInfoLog = () => '' getShaderInfoLog = () => '' } ================================================ FILE: packages/test-renderer/src/__tests__/RTTR.core.test.tsx ================================================ import { useFrame } from '@react-three/fiber' import * as React from 'react' import * as THREE from 'three' import ReactThreeTestRenderer from '../index' type ExampleComp = THREE.Mesh describe('ReactThreeTestRenderer Core', () => { it('renders JSX', async () => { const Mesh = () => { return ( ) } const renderer = await ReactThreeTestRenderer.create() expect(renderer.scene.children[0].type).toEqual('Mesh') await renderer.update() expect(renderer.scene.children[0].type).toEqual('Mesh') }) it('renders a simple component with hooks', async () => { const Mesh = () => { const meshRef = React.useRef>(null) useFrame(() => void (meshRef.current!.position.x += 0.01)) return ( ) } const renderer = await ReactThreeTestRenderer.create() expect(renderer.scene.children[0].type).toEqual('Mesh') await renderer.update() expect(renderer.scene.children[0].type).toEqual('Mesh') }) it('renders a simple component with useTransition', async () => { const Mesh = () => { const [name, setName] = React.useState() React.useLayoutEffect(() => { React.startTransition(() => void setName('mesh')) }) return ( ) } const renderer = await ReactThreeTestRenderer.create( , ) expect(renderer.scene.children[0].props.name).toEqual('mesh') }) it('renders an empty scene', async () => { const Empty = () => { return null } const renderer = await ReactThreeTestRenderer.create() expect(renderer.scene.type).toEqual('Scene') expect(renderer.scene.children).toEqual([]) expect(renderer.toGraph()).toEqual([]) }) it('can render a composite component & correctly build simple graph', async () => { class Parent extends React.Component { render() { return ( ) } } const Child = () => { return ( ) } const renderer = await ReactThreeTestRenderer.create() expect(renderer.toGraph()).toMatchSnapshot() }) it('renders some basics with an update', async () => { let renders = 0 class Component extends React.PureComponent { state = { pos: 3, } componentDidMount() { this.setState({ pos: 7, }) } render() { renders++ return ( ) } } const Child = () => { renders++ return } const Null = () => { renders++ return null } const renderer = await ReactThreeTestRenderer.create() expect(renderer.scene.children[0].instance.position.x).toEqual(7) expect(renders).toBe(6) }) it('updates types & names', async () => { const renderer = await ReactThreeTestRenderer.create( , ) let childInstance = renderer.scene.children[0].instance as ExampleComp expect(childInstance.material.type).toEqual('MeshBasicMaterial') expect(childInstance.material.name).toEqual('basicMat') await renderer.update( , ) childInstance = renderer.scene.children[0].instance as ExampleComp expect(childInstance.material.type).toEqual('MeshStandardMaterial') expect(childInstance.material.name).toEqual('standardMat') }) it('exposes the instance', async () => { class Instance extends React.PureComponent { state = { standardMat: false } handleStandard() { this.setState({ standardMat: true }) } render() { return ( {this.state.standardMat ? : } ) } } const renderer = await ReactThreeTestRenderer.create() expect(renderer.toTree()).toMatchSnapshot() const instance = renderer.getInstance() as Instance await ReactThreeTestRenderer.act(async () => { instance.handleStandard() }) expect(renderer.toTree()).toMatchSnapshot() }) it('updates children', async () => { const renderer = await ReactThreeTestRenderer.create( , ) expect(renderer.toTree()).toMatchSnapshot() await renderer.update( , ) expect(renderer.toTree()).toMatchSnapshot() }) it('does the full lifecycle', async () => { const log: string[] = [] class Log extends React.Component<{ name: string }> { render() { log.push('render ' + this.props.name) return ( ) } componentDidMount() { log.push('mount ' + this.props.name) } componentWillUnmount() { log.push('unmount ' + this.props.name) } } const renderer = await ReactThreeTestRenderer.create() await renderer.update() await renderer.unmount() expect(log).toEqual(['render Foo', 'mount Foo', 'render Bar', 'unmount Foo', 'mount Bar', 'unmount Bar']) }) it('gives a ref to native components', async () => { const log: THREE.Mesh[] = [] await ReactThreeTestRenderer.create( log.push(r as THREE.Mesh)} />) expect(log.length).toEqual(1) expect(log[0].type).toEqual('Mesh') }) it('toTree() handles nested Fragments', async () => { const Component = () => ( <> <> ) const renderer = await ReactThreeTestRenderer.create() expect(renderer.toTree()).toMatchSnapshot() }) it('correctly builds a tree', async () => { const Component = () => { return ( ) } const vertices = new Float32Array([ -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, ]) const Mesh = () => { return ( ) } const Child = ({ col }: { col: [number, number, number] }) => { return } const Null = () => { return null } const renderer = await ReactThreeTestRenderer.create() expect(renderer.toTree()).toMatchSnapshot() }) it('toTree() handles complicated tree of fragments', async () => { const renderer = await ReactThreeTestRenderer.create( <> <> <> , ) expect(renderer.toTree()).toMatchSnapshot() }) it('correctly searches through multiple levels in regular objects', async () => { // Create a deep tree: group -> mesh -> mesh -> mesh const renderer = await ReactThreeTestRenderer.create( , ) // Test from the root const allMeshes = renderer.scene.findAllByType('Mesh') expect(allMeshes.length).toBe(3) // Should find all three meshes // Test from an intermediate node const topMesh = renderer.scene.find((node) => node.props.name === 'level1-mesh') const nestedMeshes = topMesh.findAllByType('Mesh') expect(nestedMeshes.length).toBe(2) // Should find the two nested meshes // Find a deeply nested mesh from an intermediate node by property const level3 = topMesh.find((node) => node.props.name === 'level3-mesh') expect(level3).toBeDefined() expect(level3.type).toBe('Mesh') }) it('Can search from retrieved primitive Instance', async () => { const group = new THREE.Group() group.name = 'PrimitiveGroup' const childMesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color: 'red' })) childMesh.name = 'PrimitiveChildMesh' group.add(childMesh) const nestedMesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color: 'red' })) nestedMesh.name = 'PrimitiveNestedChildMesh' childMesh.add(nestedMesh) const renderer = await ReactThreeTestRenderer.create() const foundGroup = renderer.scene.findByType('Group') const foundMesh = foundGroup.children[0] const foundNestedMesh = foundMesh.findByType('Mesh') expect(foundNestedMesh).toBeDefined() }) it('root instance and refs return the same value', async () => { let refInst = null const renderer = await ReactThreeTestRenderer.create( (refInst = ref)} />) const root = renderer.getInstance() // this will be Mesh expect(root).toEqual(refInst) }) it('handles primitive objects and their children correctly in toGraph', async () => { // Create a component with both regular objects and primitives with children const PrimitiveTestComponent = () => { // Create a THREE.js group with mesh children const group = new THREE.Group() group.name = 'PrimitiveGroup' const childMesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color: 'red' })) childMesh.name = 'PrimitiveChildMesh' group.add(childMesh) // Add a nested group to test deeper hierarchies const nestedGroup = new THREE.Group() nestedGroup.name = 'NestedGroup' const nestedMesh = new THREE.Mesh(new THREE.SphereGeometry(0.5), new THREE.MeshBasicMaterial({ color: 'blue' })) nestedMesh.name = 'NestedMesh' nestedGroup.add(nestedMesh) group.add(nestedGroup) return ( <> ) } const renderer = await ReactThreeTestRenderer.create() expect(renderer.toGraph()).toMatchSnapshot() }) }) ================================================ FILE: packages/test-renderer/src/__tests__/RTTR.events.test.tsx ================================================ import * as React from 'react' import ReactThreeTestRenderer from '../index' import type { ReactThreeTest } from '../index' describe('ReactThreeTestRenderer Events', () => { it('should fire an event', async () => { const handlePointerDown = jest.fn().mockImplementationOnce((event: ReactThreeTest.MockSyntheticEvent) => { expect(() => event.stopPropagation()).not.toThrow() expect(event.offsetX).toEqual(640) expect(event.offsetY).toEqual(400) }) const Component = () => { return ( ) } const { scene, fireEvent } = await ReactThreeTestRenderer.create() const eventData = { offsetX: 640, offsetY: 400, } await fireEvent(scene.children[0], 'onPointerDown', eventData) expect(handlePointerDown).toHaveBeenCalledTimes(1) await fireEvent(scene.children[0], 'pointerDown') expect(handlePointerDown).toHaveBeenCalledTimes(2) }) it('should not throw if the handle name is incorrect', async () => { const handlePointerDown = jest.fn() const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementationOnce(jest.fn()) const Component = () => { return ( ) } const { scene, fireEvent } = await ReactThreeTestRenderer.create() expect(async () => await fireEvent(scene.children[0], 'onPointerUp')).not.toThrow() expect(handlePointerDown).not.toHaveBeenCalled() expect(consoleWarnSpy).toHaveBeenCalledTimes(1) expect(consoleWarnSpy).toHaveBeenCalledWith( 'Handler for onPointerUp was not found. You must pass event names in camelCase or name of the handler https://github.com/pmndrs/react-three-fiber/blob/master/packages/test-renderer/markdown/rttr.md#create-fireevent', ) }) }) ================================================ FILE: packages/test-renderer/src/__tests__/RTTR.hooks.test.tsx ================================================ import * as React from 'react' import * as THREE from 'three' import { useFrame, useLoader, useThree } from '@react-three/fiber' import ReactThreeTestRenderer from '../index' describe('ReactThreeTestRenderer Hooks', () => { it('can handle useThree hook', async () => { let result = {} as { camera: THREE.Camera scene: THREE.Scene raycaster: THREE.Raycaster size: { width: number; height: number } } const Component = () => { const res = useThree((state) => ({ camera: state.camera, scene: state.scene, size: state.size, raycaster: state.raycaster, })) result = res return } await ReactThreeTestRenderer.create(, { width: 1280, height: 800 }) expect(result.camera instanceof THREE.Camera).toBeTruthy() expect(result.scene instanceof THREE.Scene).toBeTruthy() expect(result.raycaster instanceof THREE.Raycaster).toBeTruthy() expect(result.size).toEqual({ height: 800, width: 1280, top: 0, left: 0 }) }) it('can handle useLoader hook', async () => { const MockMesh = new THREE.Mesh() class Loader extends THREE.Loader { load(url: string, onLoad: (mesh: THREE.Mesh) => void): void { onLoad(MockMesh) } } const Component = () => { const model = useLoader(Loader, '/suzanne.glb') return } const renderer = await ReactThreeTestRenderer.create() expect(renderer.scene.children[0].instance).toBe(MockMesh) }) it('can handle useFrame hook using test renderers advanceFrames function', async () => { const Component = () => { const meshRef = React.useRef(null!) useFrame((_, delta) => { meshRef.current.rotation.x += delta }) return ( ) } const renderer = await ReactThreeTestRenderer.create() expect(renderer.scene.children[0].instance.rotation.x).toEqual(0) await ReactThreeTestRenderer.act(async () => { await renderer.advanceFrames(2, 1) }) expect(renderer.scene.children[0].instance.rotation.x).toEqual(2) }) }) ================================================ FILE: packages/test-renderer/src/__tests__/RTTR.methods.test.tsx ================================================ import * as React from 'react' import ReactThreeTestRenderer from '../index' describe('ReactThreeTestRenderer instance methods', () => { const ExampleComponent = () => { return ( ) } it('should pass the parent', async () => { const { scene } = await ReactThreeTestRenderer.create() expect(scene.parent).toBeNull() expect(scene.children[0].parent).toBeDefined() expect(scene.children[0].parent!.type).toEqual('Scene') }) it('searches via .find() / .findAll()', async () => { const { scene } = await ReactThreeTestRenderer.create() const foundByName = scene.find((node) => node.instance.name === 'mesh_01') expect(foundByName.type).toEqual('Mesh') const foundAllByColor = scene.findAll((node) => node.props.color === 0x0000ff) expect(foundAllByColor).toHaveLength(2) expect(foundAllByColor[0].type).toEqual('MeshStandardMaterial') expect(foundAllByColor[1].type).toEqual('MeshBasicMaterial') const foundAllByType = scene.findAll((node) => node.type === 'InstancedMesh') expect(foundAllByType).toHaveLength(0) expect(foundAllByType).toEqual([]) expect(() => scene.find((node) => node.props.color === 0x0000ff)).toThrow() }) it('searches via .findByType() / findAllByType()', async () => { const { scene } = await ReactThreeTestRenderer.create() const foundByStandardMaterial = scene.findByType('MeshStandardMaterial') expect(foundByStandardMaterial).toBeDefined() const foundAllByMesh = scene.findAllByType('Mesh') expect(foundAllByMesh).toHaveLength(2) expect(foundAllByMesh[0].instance.name).toEqual('mesh_01') expect(foundAllByMesh[1].instance.name).toEqual('mesh_02') const foundAllByBoxBufferGeometry = scene.findAllByType('BoxBufferGeometry') expect(foundAllByBoxBufferGeometry).toHaveLength(0) expect(foundAllByBoxBufferGeometry).toEqual([]) expect(() => scene.findByType('BufferGeometry')).toThrow() }) it('searches via .findByProps() / .findAllByProps()', async () => { const { scene } = await ReactThreeTestRenderer.create() const foundByName = scene.findByProps({ name: 'mesh_01', }) expect(foundByName.type).toEqual('Mesh') const foundAllByColor = scene.findAllByProps({ color: 0x0000ff, }) expect(foundAllByColor).toHaveLength(2) expect(foundAllByColor[0].type).toEqual('MeshStandardMaterial') expect(foundAllByColor[1].type).toEqual('MeshBasicMaterial') const foundAllByColorAndName = scene.findAllByProps({ color: 0x0000ff, name: 'mesh_01', }) expect(foundAllByColorAndName).toHaveLength(0) expect(foundAllByColorAndName).toEqual([]) expect(() => scene.findByProps({ color: 0x0000ff })).toThrow() }) it('searches RegExp via .findByProps() / .findAllByProps()', async () => { const { scene } = await ReactThreeTestRenderer.create() const single = scene.findByProps({ name: /^mesh_01$/, }) expect(single.type).toEqual('Mesh') const multiple = scene.findAllByProps({ name: /^mesh_\d+$/, }) expect(multiple.length).toEqual(2) }) }) ================================================ FILE: packages/test-renderer/src/__tests__/__snapshots__/RTTR.core.test.tsx.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ReactThreeTestRenderer Core can render a composite component & correctly build simple graph 1`] = ` Array [ Object { "children": Array [ Object { "children": Array [], "name": "", "type": undefined, }, Object { "children": Array [ Object { "children": Array [], "name": "", "type": "BoxGeometry", }, Object { "children": Array [], "name": "", "type": "MeshBasicMaterial", }, ], "name": "", "type": "Mesh", }, ], "name": "", "type": "Group", }, ] `; exports[`ReactThreeTestRenderer Core correctly builds a tree 1`] = ` Array [ Object { "children": Array [ Object { "children": Array [], "props": Object { "args": Array [ 0, 0, 255, ], "attach": "background", }, "type": "color", }, Object { "children": Array [ Object { "children": Array [ Object { "children": Array [], "props": Object { "args": Array [ Float32Array [ -1, -1, 1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, ], 3, ], "attach": "attributes-position", }, "type": "bufferAttribute", }, ], "props": Object { "attach": "geometry", }, "type": "bufferGeometry", }, Object { "children": Array [], "props": Object { "attach": "material", "color": "hotpink", }, "type": "meshBasicMaterial", }, ], "props": Object {}, "type": "mesh", }, ], "props": Object { "position": Array [ 1, 2, 3, ], }, "type": "group", }, ] `; exports[`ReactThreeTestRenderer Core exposes the instance 1`] = ` Array [ Object { "children": Array [ Object { "children": Array [], "props": Object { "args": Array [ 2, 2, ], "attach": "geometry", }, "type": "boxGeometry", }, Object { "children": Array [], "props": Object { "attach": "material", }, "type": "meshBasicMaterial", }, ], "props": Object {}, "type": "mesh", }, ] `; exports[`ReactThreeTestRenderer Core exposes the instance 2`] = ` Array [ Object { "children": Array [ Object { "children": Array [], "props": Object { "args": Array [ 2, 2, ], "attach": "geometry", }, "type": "boxGeometry", }, Object { "children": Array [], "props": Object { "attach": "material", }, "type": "meshStandardMaterial", }, ], "props": Object {}, "type": "mesh", }, ] `; exports[`ReactThreeTestRenderer Core handles primitive objects and their children correctly in toGraph 1`] = ` Array [ Object { "children": Array [ Object { "children": Array [], "name": "", "type": "BoxGeometry", }, Object { "children": Array [], "name": "", "type": "MeshBasicMaterial", }, ], "name": "RegularMesh", "type": "Mesh", }, Object { "children": Array [ Object { "children": Array [], "name": "PrimitiveChildMesh", "type": "Mesh", }, Object { "children": Array [ Object { "children": Array [], "name": "NestedMesh", "type": "Mesh", }, ], "name": "NestedGroup", "type": "Group", }, ], "name": "PrimitiveGroup", "type": "Group", }, ] `; exports[`ReactThreeTestRenderer Core toTree() handles complicated tree of fragments 1`] = ` Array [ Object { "children": Array [ Object { "children": Array [], "props": Object { "args": Array [ 0, 0, 0, ], "attach": "background", }, "type": "color", }, ], "props": Object {}, "type": "group", }, Object { "children": Array [ Object { "children": Array [], "props": Object { "args": Array [ 0, 0, 255, ], "attach": "background", }, "type": "color", }, ], "props": Object {}, "type": "group", }, Object { "children": Array [ Object { "children": Array [], "props": Object { "args": Array [ 255, 0, 0, ], "attach": "background", }, "type": "color", }, ], "props": Object {}, "type": "group", }, ] `; exports[`ReactThreeTestRenderer Core toTree() handles nested Fragments 1`] = ` Array [ Object { "children": Array [], "props": Object {}, "type": "group", }, ] `; exports[`ReactThreeTestRenderer Core updates children 1`] = ` Array [ Object { "children": Array [ Object { "children": Array [ Object { "children": Array [], "props": Object { "args": Array [ 2, 2, ], "attach": "geometry", }, "type": "boxGeometry", }, Object { "children": Array [], "props": Object { "attach": "material", }, "type": "meshBasicMaterial", }, ], "props": Object { "position-z": 12, }, "type": "mesh", }, Object { "children": Array [ Object { "children": Array [], "props": Object { "args": Array [ 4, 4, ], "attach": "geometry", }, "type": "boxGeometry", }, Object { "children": Array [], "props": Object { "attach": "material", }, "type": "meshBasicMaterial", }, ], "props": Object { "position-y": 12, }, "type": "mesh", }, Object { "children": Array [ Object { "children": Array [], "props": Object { "args": Array [ 6, 6, ], "attach": "geometry", }, "type": "boxGeometry", }, Object { "children": Array [], "props": Object { "attach": "material", }, "type": "meshBasicMaterial", }, ], "props": Object { "position-x": 12, }, "type": "mesh", }, ], "props": Object {}, "type": "group", }, ] `; exports[`ReactThreeTestRenderer Core updates children 2`] = ` Array [ Object { "children": Array [ Object { "children": Array [ Object { "children": Array [], "props": Object { "args": Array [ 6, 6, ], "attach": "geometry", }, "type": "boxGeometry", }, Object { "children": Array [], "props": Object { "attach": "material", }, "type": "meshBasicMaterial", }, ], "props": Object { "rotation-x": 1, }, "type": "mesh", }, Object { "children": Array [ Object { "children": Array [], "props": Object { "args": Array [ 4, 4, ], "attach": "geometry", }, "type": "boxGeometry", }, Object { "children": Array [], "props": Object { "attach": "material", }, "type": "meshBasicMaterial", }, ], "props": Object { "position-y": 12, }, "type": "mesh", }, Object { "children": Array [ Object { "children": Array [], "props": Object { "args": Array [ 2, 2, ], "attach": "geometry", }, "type": "boxGeometry", }, Object { "children": Array [], "props": Object { "attach": "material", }, "type": "meshBasicMaterial", }, ], "props": Object { "position-x": 12, }, "type": "mesh", }, ], "props": Object {}, "type": "group", }, ] `; ================================================ FILE: packages/test-renderer/src/createTestCanvas.ts ================================================ import { WebGL2RenderingContext } from './WebGL2RenderingContext' import type { CreateCanvasParameters } from './types/internal' export const createCanvas = ({ beforeReturn, width = 1280, height = 800 }: CreateCanvasParameters = {}) => { let canvas: HTMLCanvasElement if (typeof document !== 'undefined' && typeof document.createElement === 'function') { canvas = document.createElement('canvas') } else { canvas = { style: {}, addEventListener: (() => {}) as any, removeEventListener: (() => {}) as any, clientWidth: width, clientHeight: height, getContext: (() => new WebGL2RenderingContext(canvas)) as any, } as HTMLCanvasElement } canvas.width = width canvas.height = height if (globalThis.HTMLCanvasElement) { const getContext = HTMLCanvasElement.prototype.getContext HTMLCanvasElement.prototype.getContext = function (id: string) { if (id.startsWith('webgl')) return new WebGL2RenderingContext(this) return getContext.apply(this, arguments as any) } as any } beforeReturn?.(canvas) class WebGLRenderingContext extends WebGL2RenderingContext {} // @ts-expect-error globalThis.WebGLRenderingContext ??= WebGLRenderingContext // @ts-expect-error globalThis.WebGL2RenderingContext ??= WebGL2RenderingContext return canvas } ================================================ FILE: packages/test-renderer/src/createTestInstance.ts ================================================ import type * as THREE from 'three' import type { Instance } from '@react-three/fiber' import type { Obj, TestInstanceChildOpts } from './types/internal' import { expectOne, matchProps, findAll } from './helpers/testInstance' // Helper to create a minimal wrapper for THREE.Object3D children of primitives const createVirtualInstance = (object: THREE.Object3D, parent: Instance): Instance => { // Create the virtual instance for this object // we can't import the prepare method from packages/fiber/src/core/utils.tsx so we do what we can const instance: Instance = { root: parent.root, type: object.type.toLowerCase(), // Convert to lowercase to match R3F convention parent, children: [], props: { object }, object, eventCount: 0, handlers: {}, isHidden: false, } // Recursively process children if they exist if (object.children && object.children.length > 0) { const objectChildren = object.children as THREE.Object3D[] instance.children = Array.from(objectChildren).map((child) => createVirtualInstance(child, instance)) } return instance } export class ReactThreeTestInstance { _fiber: Instance constructor(fiber: Instance) { this._fiber = fiber } public get fiber(): Instance { return this._fiber } public get instance(): TObject { return this._fiber.object } public get type(): string { return this._fiber.object.type } public get props(): Obj { return this._fiber.props } public get parent(): ReactThreeTestInstance | null { const parent = this._fiber.parent if (parent !== null) { return wrapFiber(parent) } return parent } public get children(): ReactThreeTestInstance[] { return this.getChildren(this._fiber) } public get allChildren(): ReactThreeTestInstance[] { return this.getChildren(this._fiber, { exhaustive: true }) } private getChildren = ( fiber: Instance, opts: TestInstanceChildOpts = { exhaustive: false }, ): ReactThreeTestInstance[] => { // Get standard R3F children const r3fChildren = fiber.children .filter((child) => !child.props.attach || opts.exhaustive) .map((fib) => wrapFiber(fib as Instance)) // For primitives, also add THREE.js object children if (fiber.type === 'primitive' && fiber.object.children?.length) { const threeChildren = fiber.object.children.map((child: THREE.Object3D) => { // Create a virtual instance that wraps the THREE.js child const virtualInstance = createVirtualInstance(child, fiber) return wrapFiber(virtualInstance) }) r3fChildren.push(...threeChildren) return r3fChildren } return r3fChildren } public findAll = (decider: (node: ReactThreeTestInstance) => boolean): ReactThreeTestInstance[] => findAll(this as unknown as ReactThreeTestInstance, decider, { includeRoot: false }) public find = (decider: (node: ReactThreeTestInstance) => boolean): ReactThreeTestInstance => expectOne(this.findAll(decider), `matching custom checker: ${decider.toString()}`) public findByType = (type: string): ReactThreeTestInstance => expectOne( this.findAll((node) => Boolean(node.type && node.type === type)), `with node type: "${type || 'Unknown'}"`, ) public findAllByType = (type: string): ReactThreeTestInstance[] => this.findAll((node) => Boolean(node.type && node.type === type)) public findByProps = (props: Obj): ReactThreeTestInstance => expectOne(this.findAllByProps(props), `with props: ${JSON.stringify(props)}`) public findAllByProps = (props: Obj): ReactThreeTestInstance[] => this.findAll((node: ReactThreeTestInstance) => Boolean(node.props && matchProps(node.props, props))) } const fiberToWrapper = new WeakMap() export const wrapFiber = (fiber: Instance): ReactThreeTestInstance => { let wrapper = fiberToWrapper.get(fiber) if (wrapper === undefined) { wrapper = new ReactThreeTestInstance(fiber) fiberToWrapper.set(fiber, wrapper) } return wrapper } ================================================ FILE: packages/test-renderer/src/fireEvent.ts ================================================ import type { RootStore } from '@react-three/fiber' import { toEventHandlerName } from './helpers/strings' import { ReactThreeTestInstance } from './createTestInstance' import type { Act, MockSyntheticEvent } from './types/public' import type { MockEventData } from './types/internal' export const createEventFirer = (act: Act, store: RootStore) => { const findEventHandler = ( element: ReactThreeTestInstance, eventName: string, ): ((event: MockSyntheticEvent) => any) | null => { const eventHandlerName = toEventHandlerName(eventName) const props = element.props if (typeof props[eventHandlerName] === 'function') { return props[eventHandlerName] } if (typeof props[eventName] === 'function') { return props[eventName] } console.warn( `Handler for ${eventName} was not found. You must pass event names in camelCase or name of the handler https://github.com/pmndrs/react-three-fiber/blob/master/packages/test-renderer/markdown/rttr.md#create-fireevent`, ) return null } const createSyntheticEvent = (element: ReactThreeTestInstance, data: MockEventData): MockSyntheticEvent => { const raycastEvent = { camera: store.getState().camera, stopPropagation: () => {}, target: element, currentTarget: element, sourceEvent: data, ...data, } return raycastEvent } const invokeEvent = async (element: ReactThreeTestInstance, eventName: string, data: MockEventData): Promise => { const handler = findEventHandler(element, eventName) if (!handler) { return } let returnValue: any await act(async () => { returnValue = handler(createSyntheticEvent(element, data)) }) return returnValue } const fireEvent = async ( element: ReactThreeTestInstance, eventName: string, data: MockEventData = {}, ): Promise => await invokeEvent(element, eventName, data) return fireEvent } ================================================ FILE: packages/test-renderer/src/helpers/events.ts ================================================ import type { MockEventData } from '../types/internal' export const calculateDistance = (event: MockEventData) => { if (event.offsetX && event.offsetY && event.initialClick.x && event.initialClick.y) { const dx = event.offsetX - event.initialClick.x const dy = event.offsetY - event.initialClick.y return Math.round(Math.sqrt(dx * dx + dy * dy)) } return 0 } ================================================ FILE: packages/test-renderer/src/helpers/graph.ts ================================================ import type { Instance } from '@react-three/fiber' import type { SceneGraphItem } from '../types/public' import type * as THREE from 'three' const graphObjectFactory = ( type: SceneGraphItem['type'], name: SceneGraphItem['name'], children: SceneGraphItem['children'], ): SceneGraphItem => ({ type, name, children, }) // Helper function to process raw THREE.js children objects function processThreeChildren(children: THREE.Object3D[]): SceneGraphItem[] { return children.map((object) => graphObjectFactory( object.type, object.name || '', object.children && object.children.length > 0 ? processThreeChildren(object.children) : [], ), ) } export const toGraph = (object: Instance): SceneGraphItem[] => { return object.children.map((child) => { // Process standard R3F children const children = toGraph(child) // For primitives, also include THREE.js object children if (child.type === 'primitive' && child.object.children?.length) { const threeChildren = processThreeChildren(child.object.children) children.push(...threeChildren) } return graphObjectFactory(child.object.type, child.object.name ?? '', children) }) } ================================================ FILE: packages/test-renderer/src/helpers/strings.ts ================================================ export const lowerCaseFirstLetter = (str: string) => `${str.charAt(0).toLowerCase()}${str.slice(1)}` export const toEventHandlerName = (eventName: string) => `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}` ================================================ FILE: packages/test-renderer/src/helpers/testInstance.ts ================================================ import type { ReactThreeTestInstance } from '../createTestInstance' import type { Obj } from '../types/internal' export const expectOne = (items: TItem[], msg: string) => { if (items.length === 1) { return items[0] } const prefix = items.length === 0 ? 'RTTR: No instances found' : `RTTR: Expected 1 but found ${items.length} instances` throw new Error(`${prefix} ${msg}`) } export const matchProps = (props: Obj, filter: Obj) => { for (const key in filter) { // Check for matches if filter contains regex matchers const isRegex = filter[key] instanceof RegExp const shouldMatch = isRegex && typeof props[key] === 'string' const match = shouldMatch && filter[key].test(props[key]) // Bail if props aren't identical and filters found no match if (props[key] !== filter[key] && !match) { return false } } return true } interface FindAllOptions { /** * Whether to include the root node in search results. * When false, only searches within children. * @default true */ includeRoot?: boolean } export const findAll = ( root: ReactThreeTestInstance, decider: (node: ReactThreeTestInstance) => boolean, options: FindAllOptions = { includeRoot: true }, ) => { const results = [] // Only include the root node if the option is enabled if (options.includeRoot !== false && decider(root)) { results.push(root) } // Always search through children root.allChildren.forEach((child) => { // When recursively searching children, we always want to include their roots results.push(...findAll(child, decider, { includeRoot: true })) }) return results } ================================================ FILE: packages/test-renderer/src/helpers/tree.ts ================================================ import * as THREE from 'three' import type { Instance } from '@react-three/fiber' import type { TreeNode, Tree } from '../types/public' import { lowerCaseFirstLetter } from './strings' const treeObjectFactory = ( type: TreeNode['type'], props: TreeNode['props'], children: TreeNode['children'], ): TreeNode => ({ type, props, children, }) const toTreeBranch = (children: Instance[]): TreeNode[] => children.map((child) => { return treeObjectFactory( lowerCaseFirstLetter(child.object.type || child.object.constructor.name), child.props, toTreeBranch(child.children), ) }) export const toTree = (root: Instance): Tree => toTreeBranch(root.children) ================================================ FILE: packages/test-renderer/src/helpers/waitFor.ts ================================================ import { act } from '@react-three/fiber' export interface WaitOptions { interval?: number timeout?: number } export async function waitFor( callback: () => boolean | void, { interval = 50, timeout = 5000 }: WaitOptions = {}, ): Promise { await act(async () => { const start = performance.now() while (true) { const result = callback() if (result || result == null) break if (interval) await new Promise((resolve) => setTimeout(resolve, interval)) if (timeout && performance.now() - start >= timeout) throw new Error(`Timed out after ${timeout}ms.`) } }) } ================================================ FILE: packages/test-renderer/src/index.tsx ================================================ import * as React from 'react' import * as THREE from 'three' import { extend, _roots as mockRoots, createRoot, reconciler, act, Instance } from '@react-three/fiber' import { toTree } from './helpers/tree' import { toGraph } from './helpers/graph' import { createCanvas } from './createTestCanvas' import { createEventFirer } from './fireEvent' import type { CreateOptions, Renderer } from './types/public' import { wrapFiber } from './createTestInstance' import { waitFor, WaitOptions } from './helpers/waitFor' // Extend catalogue for render API in tests. extend(THREE as any) const create = async (element: React.ReactNode, options?: Partial): Promise => { const canvas = createCanvas(options) const _root = createRoot(canvas) await _root.configure({ frameloop: 'never', // TODO: remove and use default behavior size: { width: options?.width ?? 1280, height: options?.height ?? 800, top: 0, left: 0, }, ...options, events: undefined, }) const _store = mockRoots.get(canvas)!.store await act(async () => _root.render(element)) const _scene = (_store.getState().scene as Instance['object']).__r3f! return { scene: wrapFiber(_scene), async unmount() { await act(async () => { _root.unmount() }) }, getInstance() { // Bail if canvas is unmounted if (!mockRoots.has(canvas)) return null // Traverse fiber nodes for R3F root const root = { current: mockRoots.get(canvas)!.fiber.current } while (!root.current.child?.stateNode) root.current = root.current.child // Return R3F instance from root return reconciler.getPublicRootInstance(root) }, async update(newElement: React.ReactNode) { if (!mockRoots.has(canvas)) return console.warn('RTTR: attempted to update an unmounted root!') await act(async () => { _root.render(newElement) }) }, toTree() { return toTree(_scene) }, toGraph() { return toGraph(_scene) }, fireEvent: createEventFirer(act, _store), async advanceFrames(frames: number, delta: number | number[] = 1) { const state = _store.getState() const storeSubscribers = state.internal.subscribers const promises: Promise[] = [] storeSubscribers.forEach((subscriber) => { for (let i = 0; i < frames; i++) { if (Array.isArray(delta)) { promises.push( new Promise(() => subscriber.ref.current(state, (delta as number[])[i] || (delta as number[])[-1])), ) } else { promises.push(new Promise(() => subscriber.ref.current(state, delta as number))) } } }) Promise.all(promises) }, } } export { create, act, waitFor } export type { WaitOptions } export * as ReactThreeTest from './types' export default { create, act, waitFor } ================================================ FILE: packages/test-renderer/src/types/index.ts ================================================ export * from './public' ================================================ FILE: packages/test-renderer/src/types/internal.ts ================================================ export type CreateCanvasParameters = { beforeReturn?: (canvas: HTMLCanvasElement) => void width?: number height?: number } export interface Obj { [key: string]: any } /** * this is an empty object of any, * the data is passed to a new event * and subsequently passed to the * event handler you're calling */ export type MockEventData = { [key: string]: any } export interface TestInstanceChildOpts { exhaustive: boolean } ================================================ FILE: packages/test-renderer/src/types/public.ts ================================================ import type { Camera, RenderProps } from '@react-three/fiber' import { ReactThreeTestInstance } from '../createTestInstance' import type { MockEventData, CreateCanvasParameters } from './internal' export { ReactThreeTestInstance } export type MockSyntheticEvent = { camera: Camera stopPropagation: () => void target: ReactThreeTestInstance currentTarget: ReactThreeTestInstance sourceEvent: MockEventData [key: string]: any } export type CreateOptions = CreateCanvasParameters & RenderProps export type Renderer = { scene: ReactThreeTestInstance unmount: () => Promise getInstance: () => null | unknown update: (el: React.ReactNode) => Promise toTree: () => Tree | undefined toGraph: () => SceneGraph | undefined fireEvent: (element: ReactThreeTestInstance, handler: string, data?: MockEventData) => Promise advanceFrames: (frames: number, delta: number | number[]) => Promise } export interface SceneGraphItem { type: string name: string children: SceneGraphItem[] | null } export type SceneGraph = SceneGraphItem[] export interface TreeNode { type: string props: { [key: string]: any } children: TreeNode[] } export type Tree = TreeNode[] export type { Act } from '@react-three/fiber' ================================================ FILE: readme.md ================================================

    @react-three/fiber

    [![Version](https://img.shields.io/npm/v/@react-three/fiber?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/@react-three/fiber) [![Downloads](https://img.shields.io/npm/dt/@react-three/fiber.svg?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/@react-three/fiber) [![Twitter](https://img.shields.io/twitter/follow/pmndrs?label=%40pmndrs&style=flat&colorA=000000&colorB=000000&logo=twitter&logoColor=000000)](https://twitter.com/pmndrs) [![Discord](https://img.shields.io/discord/740090768164651008?style=flat&colorA=000000&colorB=000000&label=discord&logo=discord&logoColor=000000)](https://discord.gg/ZZjjNvJ) [![Open Collective](https://img.shields.io/opencollective/all/react-three-fiber?style=flat&colorA=000000&colorB=000000)](https://opencollective.com/react-three-fiber) [![ETH](https://img.shields.io/badge/ETH-f5f5f5?style=flat&colorA=000000&colorB=000000)](https://blockchain.com/eth/address/0x6E3f79Ea1d0dcedeb33D3fC6c34d2B1f156F2682) [![BTC](https://img.shields.io/badge/BTC-f5f5f5?style=flat&colorA=000000&colorB=000000)](https://blockchain.com/btc/address/36fuguTPxGCNnYZSRdgdh6Ea94brCAjMbH) react-three-fiber is a React renderer for threejs. Build your scene declaratively with re-usable, self-contained components that react to state, are readily interactive and can participate in React's ecosystem. ```bash npm install three @types/three @react-three/fiber ``` > [!WARNING] > Three-fiber is a React renderer, it must pair with a major version of React, just like react-dom, react-native, etc. @react-three/fiber@8 pairs with react@18, @react-three/fiber@9 pairs with react@19. --- #### Does it have limitations? None. Everything that works in Threejs will work here without exception. #### Is it slower than plain Threejs? No. There is no overhead. Components render outside of React. It outperforms Threejs in scale due to React's scheduling abilities. #### Can it keep up with frequent feature updates to Threejs? Yes. It merely expresses Threejs in JSX, `` dynamically turns into `new THREE.Mesh()`. If a new Threejs version adds, removes or changes features, it will be available to you instantly without depending on updates to this library. ### What does it look like?
    Let's make a re-usable component that has its own state, reacts to user-input and participates in the render-loop. (live demo).
    ```jsx import { createRoot } from 'react-dom/client' import React, { useRef, useState } from 'react' import { Canvas, useFrame } from '@react-three/fiber' function Box(props) { // This reference gives us direct access to the THREE.Mesh object const ref = useRef() // Hold state for hovered and clicked events const [hovered, hover] = useState(false) const [clicked, click] = useState(false) // Subscribe this component to the render-loop, rotate the mesh every frame useFrame((state, delta) => (ref.current.rotation.x += delta)) // Return the view, these are regular Threejs elements expressed in JSX return ( click(!clicked)} onPointerOver={(event) => hover(true)} onPointerOut={(event) => hover(false)}> ) } createRoot(document.getElementById('root')).render( , ) ```
    Show TypeScript example ```bash npm install @types/three ``` ```tsx import * as THREE from 'three' import { createRoot } from 'react-dom/client' import React, { useRef, useState } from 'react' import { Canvas, useFrame, ThreeElements } from '@react-three/fiber' function Box(props: ThreeElements['mesh']) { const ref = useRef(null!) const [hovered, hover] = useState(false) const [clicked, click] = useState(false) useFrame((state, delta) => (ref.current.rotation.x += delta)) return ( click(!clicked)} onPointerOver={(event) => hover(true)} onPointerOut={(event) => hover(false)}> ) } createRoot(document.getElementById('root') as HTMLElement).render( , ) ``` Live demo: https://codesandbox.io/s/icy-tree-brnsm?file=/src/App.tsx
    Show React Native example This example relies on react 18 and uses `expo-cli`, but you can create a bare project with their template or with the `react-native` CLI. ```bash # Install expo-cli, this will create our app npm install expo-cli -g # Create app and cd into it expo init my-app cd my-app # Install dependencies npm install three @react-three/fiber@beta react@rc # Start expo start ``` Some configuration may be required to tell the Metro bundler about your assets if you use `useLoader` or Drei abstractions like `useGLTF` and `useTexture`: ```js // metro.config.js module.exports = { resolver: { sourceExts: ['js', 'jsx', 'json', 'ts', 'tsx', 'cjs'], assetExts: ['glb', 'png', 'jpg'], }, } ``` ```tsx import React, { useRef, useState } from 'react' import { Canvas, useFrame } from '@react-three/fiber/native' function Box(props) { const mesh = useRef(null) const [hovered, setHover] = useState(false) const [active, setActive] = useState(false) useFrame((state, delta) => (mesh.current.rotation.x += delta)) return ( setActive(!active)} onPointerOver={(event) => setHover(true)} onPointerOut={(event) => setHover(false)}> ) } export default function App() { return ( ) } ```
    --- # Documentation, tutorials, examples Visit [docs.pmnd.rs](https://docs.pmnd.rs/react-three-fiber) # First steps You need to be versed in both React and Threejs before rushing into this. If you are unsure about React consult the official [React docs](https://react.dev/learn), especially [the section about hooks](https://react.dev/reference/react). As for Threejs, make sure you at least glance over the following links: 1. Make sure you have a [basic grasp of Threejs](https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene). Keep that site open. 2. When you know what a scene is, a camera, mesh, geometry, material, fork the [demo above](https://github.com/pmndrs/react-three-fiber#what-does-it-look-like). 3. [Look up](https://threejs.org/docs/index.html#api/en/objects/Mesh) the JSX elements that you see (mesh, ambientLight, etc), _all_ threejs exports are native to three-fiber. 4. Try changing some values, scroll through our [API](https://docs.pmnd.rs/react-three-fiber) to see what the various settings and hooks do. Some helpful material: - [Threejs-docs](https://threejs.org/docs) and [examples](https://threejs.org/examples) - [Discover Threejs](https://discoverthreejs.com), especially the [Tips and Tricks](https://discoverthreejs.com/tips-and-tricks) chapter for best practices - [Bruno Simons Threejs Journey](https://threejs-journey.com), arguably the best learning resource, now includes a full [R3F chapter](https://threejs-journey.com/lessons/what-are-react-and-react-three-fiber) # Ecosystem There is a vibrant and extensive eco system around three-fiber, full of libraries, helpers and abstractions. - [`@react-three/drei`](https://github.com/pmndrs/drei) – useful helpers, this is an eco system in itself - [`@react-three/gltfjsx`](https://github.com/pmndrs/gltfjsx) – turns GLTFs into JSX components - [`@react-three/postprocessing`](https://github.com/pmndrs/react-postprocessing) – post-processing effects - [`@react-three/uikit`](https://github.com/pmndrs/uikit) – WebGL rendered UI components for three-fiber - [`@react-three/test-renderer`](https://github.com/pmndrs/react-three-fiber/tree/master/packages/test-renderer) – for unit tests in node - [`@react-three/offscreen`](https://github.com/pmndrs/react-three-offscreen) – offscreen/worker canvas for react-three-fiber - [`@react-three/flex`](https://github.com/pmndrs/react-three-flex) – flexbox for react-three-fiber - [`@react-three/xr`](https://github.com/pmndrs/react-xr) – VR/AR controllers and events - [`@react-three/csg`](https://github.com/pmndrs/react-three-csg) – constructive solid geometry - [`@react-three/rapier`](https://github.com/pmndrs/react-three-rapier) – 3D physics using Rapier - [`@react-three/cannon`](https://github.com/pmndrs/use-cannon) – 3D physics using Cannon - [`@react-three/p2`](https://github.com/pmndrs/use-p2) – 2D physics using P2 - [`@react-three/a11y`](https://github.com/pmndrs/react-three-a11y) – real a11y for your scene - [`@react-three/gpu-pathtracer`](https://github.com/pmndrs/react-three-gpu-pathtracer) – realistic path tracing - [`create-r3f-app next`](https://github.com/pmndrs/react-three-next) – nextjs starter - [`lamina`](https://github.com/pmndrs/lamina) – layer based shader materials - [`zustand`](https://github.com/pmndrs/zustand) – flux based state management - [`jotai`](https://github.com/pmndrs/jotai) – atoms based state management - [`valtio`](https://github.com/pmndrs/valtio) – proxy based state management - [`react-spring`](https://github.com/pmndrs/react-spring) – a spring-physics-based animation library - [`framer-motion-3d`](https://www.framer.com/docs/three-introduction/) – framer motion, a popular animation library - [`use-gesture`](https://github.com/pmndrs/react-use-gesture) – mouse/touch gestures - [`leva`](https://github.com/pmndrs/leva) – create GUI controls in seconds - [`maath`](https://github.com/pmndrs/maath) – a kitchen sink for math helpers - [`miniplex`](https://github.com/hmans/miniplex) – ECS (entity management system) - [`composer-suite`](https://github.com/hmans/composer-suite) – composing shaders, particles, effects and game mechanics - [`triplex`](https://triplex.dev/) – visual editor for react-three-fiber - [`koestlich`](https://github.com/coconut-xr/koestlich) – UI component library for react-three-fiber [Usage Trend of the @react-three Family](https://npm-compare.com/@react-three/a11y,@react-three/cannon,@react-three/csg,@react-three/drei,@react-three/flex,@react-three/gltfjsx,@react-three/gpu-pathtracer,@react-three/offscreen,@react-three/p2,@react-three/postprocessing,@react-three/rapier,@react-three/test-renderer,@react-three/uikit,@react-three/xr) # Who is using Three-fiber A small selection of companies and projects relying on three-fiber. - [`vercel`](https://www.vercel.com) (design agency) - [`basement`](https://basement.studio) (design agency) - [`studio freight`](https://studiofreight.com) (design agency) - [`14 islands`](https://www.14islands.com) (design agency) - [`ueno`](https://dribbble.com/ueno) (design agency) — [video](https://twitter.com/0xca0a/status/1204373807408013312) - [`flux.ai`](https://www.flux.ai) (PCB builder) - [`colorful.app`](https://www.colorful.app) (modeller) - [`bezi`](https://www.bezi.com) (modeller) - [`readyplayer.me`](https://readyplayer.me) (avatar configurator) - [`zillow`](https://www.zillow.com) (real estate) - [`lumalabs.ai/genie`](https://lumalabs.ai/genie) (AI models) - [`skybox.blockadelabs`](https://skybox.blockadelabs.com) (AI envmaps) - [`3dconfig`](https://3dconfig.com) (floor planer) - [`buerli.io`](https://buerli.io) (CAD) - [`getencube`](https://www.getencube.com) (CAD) - [`glowbuzzer`](https://www.glowbuzzer.com) (CAD) — [video](https://twitter.com/glowbuzzer/status/1678396014644940800) - [`triplex`](https://triplex.dev) (editor) — [video](https://twitter.com/_douges/status/1708859381369221539) - [`theatrejs`](https://www.theatrejs.com) (editor) — [video](https://twitter.com/0xca0a/status/1566838823170068480) # How to contribute If you like this project, please consider helping out. All contributions are welcome as well as donations to [Opencollective](https://opencollective.com/react-three-fiber), or in crypto `BTC: 36fuguTPxGCNnYZSRdgdh6Ea94brCAjMbH`, `ETH: 0x6E3f79Ea1d0dcedeb33D3fC6c34d2B1f156F2682`. #### Backers Thank you to all our backers! 🙏 #### Contributors This project exists thanks to all the people who contribute. ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "es6", "lib": ["es2019", "dom"], "module": "commonjs", "moduleResolution": "node", "esModuleInterop": true, "jsx": "react-jsx", "pretty": true, "strict": true, "skipLibCheck": true, "declaration": true, "removeComments": false, "emitDeclarationOnly": true, "resolveJsonModule": true, "noImplicitThis": false, "baseUrl": "./", "types": ["jest", "node", "offscreencanvas"] }, "include": ["packages/**/*"], "exclude": ["node_modules", "dist"] } ================================================ FILE: vite.config.ts ================================================ import { defineConfig, transformWithEsbuild } from 'vite' import * as path from 'node:path' import * as fs from 'node:fs' export default defineConfig({ build: { outDir: 'packages/fiber/react-reconciler', target: 'esnext', lib: { formats: ['es'], entry: { index: 'packages/fiber/node_modules/react-reconciler/index.js', constants: 'packages/fiber/node_modules/react-reconciler/cjs/react-reconciler-constants.production.js', }, fileName: '[name]', }, rollupOptions: { treeshake: false, external: (id: string) => !id.startsWith('.') && !path.isAbsolute(id), output: { entryFileNames: '[name].js', chunkFileNames: '[name].js', }, }, }, plugins: [ { name: 'vite-ts', enforce: 'pre', transform(code, id) { // Vite does not preserve static exports which breaks ESM transpilation // Instead, we manually transpile constants from CJS -> ESM if (id.includes('react-reconciler-constants')) { return ( code // Export top-level constants .replace(/exports\.(\w+)\s*=\s*(.*?);/g, 'export const $1 = $2;') // Remove "use strict" .replace(/"use strict";\s*/g, '') ) } }, generateBundle(_options, bundle) { for (const key in bundle) { if (!('isEntry' in bundle[key]) || !bundle[key].isEntry) continue const name = bundle[key].name const source = fs.readFileSync(`packages/fiber/node_modules/@types/react-reconciler/${name}.d.ts`, 'utf-8') this.emitFile({ type: 'asset', fileName: `${name}.d.ts`, source }) } }, }, { name: 'vite-minify', renderChunk: { order: 'post', handler(code, { fileName }) { return transformWithEsbuild(code, fileName, { minify: true }) }, }, }, ], })