├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .eslintrc.js ├── .github └── workflows │ └── release.yml.txt ├── .gitignore ├── .prettierrc ├── .releaserc ├── CHANGELOG.md ├── CONTRIBUTION.md ├── README.md ├── index.html ├── package.json ├── playground ├── App.tsx ├── Box.tsx ├── main.tsx └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── core │ ├── events.ts │ ├── index.tsx │ ├── is.ts │ ├── loop.ts │ ├── renderer.ts │ ├── store.ts │ └── utils.ts ├── hooks.ts ├── index.tsx ├── renderer.tsx ├── solid.ts ├── three-types.ts └── web │ ├── Canvas.tsx │ └── events.ts ├── tsconfig.json └── vite.config.ts /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # For more information, please refer to https://aka.ms/vscode-docker-python 2 | FROM mcr.microsoft.com/devcontainers/typescript-node:0-20 3 | 4 | WORKDIR /app 5 | COPY . /app 6 | 7 | # Install pip requirements 8 | RUN apt-get update 9 | 10 | # Creates a non-root user with an explicit UID and adds permission to access the /app folder 11 | # For more info, please refer to https://aka.ms/vscode-docker-python-configure-containers 12 | RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app 13 | USER appuser -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | //"image": "" 7 | 8 | "build": { 9 | // Sets the run context to one level up instead of the .devcontainer folder. 10 | "context": "..", 11 | // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename. 12 | "dockerfile": "Dockerfile" 13 | }, 14 | 15 | 16 | "features": { 17 | "ghcr.io/akhildevelops/devcontainer-features/apt:0": {} 18 | }, 19 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 20 | // "forwardPorts": [], 21 | // Use 'postCreateCommand' to run commands after the container is created. 22 | "postCreateCommand": "pnpm install", 23 | // Configure tool-specific properties. 24 | "customizations": { 25 | "vscode": { 26 | "settings": {}, 27 | "extensions": [ 28 | //"streetsidesoftware.code-spell-checker", 29 | "ms-azuretools.vscode-docker", 30 | "wayou.vscode-todo-highlight", 31 | "gruntfuggly.todo-tree", 32 | "eamodio.gitlens", 33 | "github.vscode-pull-request-github", 34 | "dbaeumer.vscode-eslint", 35 | "pkief.material-icon-theme", 36 | "esbenp.prettier-vscode", 37 | "ms-vscode.vscode-typescript-tslint-plugin" 38 | ] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@vinxi/scripts/eslint-preset"); 2 | -------------------------------------------------------------------------------- /.github/workflows/release.yml.txt: -------------------------------------------------------------------------------- 1 | name: Lib Builder 2 | on: 3 | workflow_dispatch: 4 | push: 5 | tags: 6 | - "v*" 7 | branches: 8 | - main 9 | - master 10 | 11 | env: 12 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | permissions: 18 | contents: write 19 | 20 | jobs: 21 | build: 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | node-version: [18.x] 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v3 30 | with: 31 | token: ${{ env.GITHUB_TOKEN }} 32 | - name: Node.js setup ${{ matrix.node-version }} 33 | uses: actions/setup-node@v3 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | # node-version-file: '.nvmrc' 37 | - name: Install dependencies 38 | run: | 39 | sudo apt-get update 40 | npm install -g pnpm 41 | npm install -g typescript 42 | - name: Build 43 | id: app_build 44 | run: | 45 | pnpm install 46 | pnpm run build:lib 47 | - uses: actions/upload-artifact@v3 48 | with: 49 | name: production-files 50 | path: "${{ join(fromJSON(steps.app_build.outputs.artifacts), '\n') }}" 51 | deploy: 52 | runs-on: ubuntu-latest 53 | name: Deploy 54 | needs: [build] 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v3 58 | with: 59 | fetch-depth: 0 60 | - name: Create Deploy Directory 61 | run: mkdir -p dist 62 | - name: Download artifact 63 | uses: actions/download-artifact@v2 64 | with: 65 | name: production-files 66 | path: ./dist 67 | - name: Setup node 68 | uses: actions/setup-node@v3 69 | with: 70 | node-version: 18 71 | - run: npm install -g conventional-changelog-conventionalcommits 72 | - run: npm install -g semantic-release@v19.0.5 73 | - run: npm install -g @semantic-release/exec 74 | - run: npm install -g @semantic-release/git 75 | - run: npm install -g @semantic-release/release-notes-generator 76 | - run: npm install -g @semantic-release/changelog 77 | - run: npm install -g @semantic-release/github 78 | - name: Release 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | #sudo apt-get install -y jq 82 | #chmod +x ./scripts/prepareCMD.sh 83 | run: | 84 | semantic-release 85 | 86 | cleanup: 87 | name: Cleanup actions 88 | needs: 89 | - deploy 90 | runs-on: ubuntu-latest 91 | timeout-minutes: 10 92 | steps: 93 | - name: "♻️ remove build artifacts" 94 | uses: geekyeggo/delete-artifact@v1 95 | with: 96 | name: production-files 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .scratch 2 | dist 3 | types 4 | node_modules 5 | packed/ 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "semi": true, 6 | "singleQuote": false, 7 | "useTabs": false, 8 | "arrowParens": "avoid", 9 | "bracketSpacing": true, 10 | "endOfLine": "lf", 11 | "plugins": ["prettier-plugin-tailwindcss"] 12 | } -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main", 4 | "master", 5 | "release", 6 | { 7 | "name": "staging", 8 | "prerelease": true 9 | } 10 | ], 11 | "plugins": [ 12 | [ 13 | "@semantic-release/commit-analyzer", 14 | { 15 | "preset": "conventionalcommits", 16 | "parserOpts": { 17 | "noteKeywords": [ 18 | "BREAKING CHANGE", 19 | "BREAKING CHANGES", 20 | "BREAKING" 21 | ] 22 | }, 23 | "releaseRules": [ 24 | { 25 | "breaking": true, 26 | "release": "major" 27 | }, 28 | { 29 | "type": "feat", 30 | "release": "minor" 31 | }, 32 | { 33 | "type": "fix", 34 | "release": "patch" 35 | }, 36 | { 37 | "type": "perf", 38 | "release": "patch" 39 | }, 40 | { 41 | "type": "revert", 42 | "release": "patch" 43 | }, 44 | { 45 | "type": "docs", 46 | "scope": "docs-*", 47 | "release": "minor" 48 | }, 49 | { 50 | "type": "docs", 51 | "release": false 52 | }, 53 | { 54 | "type": "style", 55 | "release": "patch" 56 | }, 57 | { 58 | "type": "refactor", 59 | "release": "patch" 60 | }, 61 | { 62 | "type": "test", 63 | "release": "patch" 64 | }, 65 | { 66 | "type": "build", 67 | "release": "patch" 68 | }, 69 | { 70 | "type": "ci", 71 | "scope": "ci-*", 72 | "release": "patch" 73 | }, 74 | { 75 | "type": "chore", 76 | "release": false 77 | }, 78 | { 79 | "type": "no-release", 80 | "release": false 81 | } 82 | ] 83 | } 84 | ], 85 | [ 86 | "@semantic-release/release-notes-generator", 87 | { 88 | "preset": "conventionalcommits", 89 | "parserOpts": { 90 | "noteKeywords": [ 91 | "BREAKING CHANGE", 92 | "BREAKING CHANGES", 93 | "BREAKING" 94 | ] 95 | }, 96 | "writerOpts": { 97 | "commitsSort": [ 98 | "subject", 99 | "scope" 100 | ] 101 | }, 102 | "presetConfig": { 103 | "types": [ 104 | { 105 | "type": "feat", 106 | "section": "🍕 Features" 107 | }, 108 | { 109 | "type": "feature", 110 | "section": "🍕 Features" 111 | }, 112 | { 113 | "type": "fix", 114 | "section": "🐛 Bug Fixes" 115 | }, 116 | { 117 | "type": "perf", 118 | "section": "🔥 Performance Improvements" 119 | }, 120 | { 121 | "type": "revert", 122 | "section": "⏩ Reverts" 123 | }, 124 | { 125 | "type": "docs", 126 | "section": "📝 Documentation" 127 | }, 128 | { 129 | "type": "style", 130 | "section": "🎨 Styles" 131 | }, 132 | { 133 | "type": "refactor", 134 | "section": "🧑‍💻 Code Refactoring" 135 | }, 136 | { 137 | "type": "test", 138 | "section": "✅ Tests" 139 | }, 140 | { 141 | "type": "build", 142 | "section": "🤖 Build System" 143 | }, 144 | { 145 | "type": "ci", 146 | "section": "🔁 Continuous Integration" 147 | } 148 | ] 149 | } 150 | } 151 | ], 152 | [ 153 | "@semantic-release/changelog", 154 | { 155 | "changelogTitle": "# 📦 Changelog \n[![conventional commits](https://img.shields.io/badge/conventional%20commits-1.0.0-yellow.svg)](https://conventionalcommits.org)\n[![semantic versioning](https://img.shields.io/badge/semantic%20versioning-2.0.0-green.svg)](https://semver.org)\n> All notable changes to this project will be documented in this file" 156 | } 157 | ], 158 | [ 159 | "@semantic-release/exec", 160 | { 161 | "prepareCmd": "./scripts/prepareCMD.sh ${nextRelease.version}", 162 | "publishCmd": "echo Publishing ${nextRelease.version}" 163 | } 164 | ], 165 | [ 166 | "@semantic-release/git", 167 | { 168 | "assets": [ 169 | "package.json", 170 | "LICENSE*", 171 | "CHANGELOG.md" 172 | ], 173 | "message": "chore(${nextRelease.type}): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 174 | } 175 | ] 176 | ] 177 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # solid-three 2 | 3 | ## 0.0.3 4 | 5 | ### Patch Changes 6 | 7 | - c3ee88e: fix for typescript docs 8 | 9 | ## 0.0.2 10 | 11 | ### Patch Changes 12 | 13 | - 91678f0: add args support in three renderer 14 | - 5b261fc: fix tsconfig and bug fixes 15 | 16 | ## 0.0.1 17 | 18 | ### Patch Changes 19 | 20 | - 299db0f: fixed leva build and types 21 | -------------------------------------------------------------------------------- /CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | # Contributing to Solid Three 2 | 3 | :+1::tada: Thank you for checking out the project and wanting to contribute! :tada::+1: 4 | 5 | ## Contribution Process 6 | 7 | Solid Three strives to provide idiomatic Solid principles but also allow room for innovation and experimentation using modern ThreeJS and WebGL tools. In a growing community many opinions and patterns merge together to produce a de facto standard. Managing opinions and expectations can be difficult. As a result this project will inherit the Solid standard set out by `Solid Primitives` in November 2021, Solid Primitives implemented a ratification/approval tracking process roughly modelled on [TC39 Proposal Stage Process](https://tc39.es/process-document/). The following summarizes these stages briefly: 8 | 9 | | Stage | Description | 10 | | ----- | --------------------------- | 11 | | X | Deprecated or rejected | 12 | | 0 | Initial submission | 13 | | 1 | Demonstrations and examples | 14 | | 2 | General use (experimental) | 15 | | 3 | Pre-shipping (final effort) | 16 | | 4 | Accepted/shipped | 17 | 18 | Any feature Stage 0-1 should be used with caution and with the understanding that the design or implementation may change. Beyond Stage 2 we make an effort to mitigate changes. If a feature reaches Stage 2 it's likely to remain an official package with additional approvement until fully accepted and shipped. 19 | 20 | ## Design Maxims 21 | 22 | Other frameworks have large and extremely well established ecosystems. Notably React which has a vast array of component and hooks, such as `React Three Fiber` and `React Drie`. The amount of choice within the ecosystem is great but often these tools are built as one-offs resulting in often un-tested logic or are designed with narrow needs. Over time the less concise these building blocks are the more they tend to repeat themselves. Our goal with Solid Three is to bring the community together to contribute, evolve and utilize a powerful centralized ThreeJS library for SolidJS. 23 | 24 | All our work is meant to be consistent and sustain a level of quality. We guarantee that each is created with the utmost care. We strive to follow these design maxims: 25 | 26 | 1. Documented and follow a consistent style guide 27 | 2. Be well tested 28 | 3. Small, concise and practical as possible 29 | 4. A single Component for a single purpose 30 | 5. No dependencies or as few as possible 31 | 6. Wrap base level Browser APIs 32 | 7. Should be progressively improved for future features 33 | 8. Be focused on composition vs. isolation of logic 34 | 9. Community voice and needs guide road map and planning 35 | 10. Strong TypeScript support 36 | 11. Support for both CJS and ESM 37 | 12. Solid performance! 38 | 39 | ### Managing ThreeJS Complexity 40 | 41 | Solid Three is mostly about supplying 80-90% of the common-use cases of vanilla `ThreeJS` for the end-user. We prefer to be less prescriptive. The remaining 10-20% of complex use cases are likely not to be covered with this library. This is on purpose to limit the potential of bloat and extended complexity. This project strives to provide foundations and not cumulative solutions. We expect the broader ecosystem will fill the remaining need as further composition to this projects effort. This allows for just the right amount of prescription and opinion. 42 | 43 | ## NPM Release and Repository Structure 44 | 45 | Solid Three is a large and growing project and the way we manage and release updates has been setup to suit the projects scope. 46 | 47 | To that end we are currently using [`semantic-release`](https://github.com/semantic-release/semantic-release) to manage our packages and releases. 48 | 49 | There are a number of benefits to this including small download sizes, reducing bloat and not shipping experimental/unnecessary changes that users don't need or want locally. This also allows us to ship updates to individual packages as needed. 50 | 51 | ## Tooling 52 | 53 | ### Package Management 54 | 55 | This repository is a monorepo managed by [**pnpm workspaces**](https://pnpm.io/workspaces) which means that you need to install [**pnpm**](https://pnpm.io/installation) to work on it. If you don't have it installed, you can install it with `npm install -g pnpm`. 56 | 57 | If this is your first time pulling the repository onto a local branch, then run `pnpm install` to install all the dependencies and build all the local packages — this is important because all of the workspace packages are linked together. Furthermore, you should run `pnpm install` whenever you pull from the main branch. If you experience any further issues, try removing the `node_modules` folder (`rm -Force -Recurse .\node_modules\` or `rm -rf node_modules/`) and reinstalling the dependencies. 58 | 59 | ### Formatting and Linting 60 | 61 | We use [**eslint**](https://eslint.org/) and [**prettier**](https://prettier.io/) to lint and format the code. You can run `pnpm lint` to check for linting errors and `pnpm format` to format the code. 62 | 63 | Having them installed and enabled in your editor is not required but should help you in the development process. 64 | 65 | ### Operating System 66 | 67 | This repository should work on any operating system, but if any issues arise, you might try using [**Gitpod**](https://gitpod.io) to quickly spin up a fresh remote development environment. 68 | 69 | ### CLI Helpers 70 | 71 | > **Note**: Coming Soon 72 | 73 | ## Planned Features 74 | 75 | > **Note**: Coming Soon 76 | 77 | ## Acknowledgements 78 | 79 | Deeply inspired by the following projects: 80 | 81 | - [React Three Fiber](https://github.com/pmndrs/react-three-fiber) 82 | - [Solid Primitives](https://github.com/solidjs-community/solid-primitives) 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | solid-three 3 |

4 | 5 | # solid-three 6 | 7 | [![pnpm](https://img.shields.io/badge/maintained%20with-pnpm-cc00ff.svg?style=for-the-badge&logo=pnpm)](https://pnpm.io/) 8 | 9 | Solid-three is a port of [React-three-fiber](https://github.com/pmndrs/react-three-fiber) to [SolidJS](https://www.solidjs.com/), 10 | originally created by [Nikhil Saraf](https://github.com/nksaraf). 11 | It allows you to declaratively construct a [Three.js](https://threejs.org/) 12 | scene, with reactive primitives, just as you would construct a DOM tree in SolidJS. 13 | 14 | > **Note** This library has just been published to NPM from this repo. 15 | > It is still in early development, and is not yet ready for production use. 16 | > Please feel free to try it out and report any issues you find! 17 | 18 | ## Quick start 19 | 20 | Install it: 21 | 22 | ```bash 23 | npm i solid-three 24 | # or 25 | yarn add solid-three 26 | # or 27 | pnpm add solid-three 28 | ``` 29 | 30 | Use it: 31 | 32 | ```tsx 33 | import { Canvas } from 'solid-three' 34 | ``` 35 | 36 | ### Dev Container 37 | 38 | If you are using VSCode on windows (or just prefer to develope in a container), you can use the included dev container to get started quickly. 39 | 40 | 1. Clone this repo to a directory _inside of your wsl instance_ such as `~/Github` 41 | 2. Navigate to the `solid-three` directory and run `code .` 42 | 3. Open the workspace from the provided file. 43 | 4. Make sure the [DevContainers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension is installed Click the bottom left corner of the window and select `Reopen in Container` - if the extension is installed, vscode should prompt you to open the project in a dev container when you open the workspace file. 44 | 45 | #### Dev Container Notes 46 | 47 | - We clone into the `wsl` instance because the dev container is running a linux container, and the windows filesystem will cause extreme performance loss due to IO overhead. 48 | - If you are using a different shell, you may need to modify the `devcontainer.json` file to use your shell of choice. 49 | - A port will automatically be forwarded when you run the project in dev mode, so you can access the dev server from your browser on windows at `localhost:` - the port will be displayed in the terminal when you run the project. This can be configured by you as well. 50 | 51 | ## Documentation 52 | 53 | > **Note**: Coming Soon! 54 | 55 | ## Sample Applications 56 | 57 | > **Note**: More Coming Soon! 58 | 59 | ### solid-three-template 60 | 61 | This is a template for a SolidJS application that uses solid-three. 62 | 63 | This project is a bare-bones `Vite` project that has been configured to use `SolidJS`, `solid-three`, and `ESLint` with `Prettier` for formatting. 64 | 65 | [solid-three-template](https://github.com/ZanzyTHEbar/solid-three-template) 66 | 67 | ## Contributing 68 | 69 | > **Note**: Coming Soon! 70 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solid-three", 3 | "version": "0.2.0", 4 | "description": "SolidJS bindings for ThreeJS", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/solidjs-community/solid-three.git" 8 | }, 9 | "module": "./dist/index.mjs", 10 | "main": "./dist/index.js", 11 | "types": "./types/index.d.ts", 12 | "sideEffects": false, 13 | "license": "MIT", 14 | "files": [ 15 | "dist/**", 16 | "types/**", 17 | "README.md" 18 | ], 19 | "scripts": { 20 | "test": "vitest", 21 | "build:lib": "BUILD_MODE=lib vite build", 22 | "build": "vite build", 23 | "types": "tsc --emitDeclarationOnly --declarationDir types", 24 | "dev": "vite", 25 | "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" 26 | }, 27 | "exports": { 28 | ".": { 29 | "import": "./dist/index.mjs", 30 | "require": "./dist/index.cjs.js" 31 | } 32 | }, 33 | "devDependencies": { 34 | "@types/node": "^18.11.18", 35 | "@types/three": "0.149.0", 36 | "@vinxi/tsconfig": "0.0.3", 37 | "esbuild": "^0.16.16", 38 | "esbuild-register": "^3.4.2", 39 | "remark-gfm": "^3.0.1", 40 | "rollup": "^3.9.1", 41 | "rollup-plugin-dts": "^5.1.1", 42 | "solid-app-router": "^0.1.14", 43 | "solid-js": "^1.7.0", 44 | "three": "0.149.0", 45 | "tsm": "^2.3.0", 46 | "tsup": "^6.5.0", 47 | "typescript": "^4.9.4", 48 | "vite": "4.1.1", 49 | "vite-plugin-inspect": "0.7.14", 50 | "vite-plugin-solid": "2.5.0" 51 | }, 52 | "peerDependencies": { 53 | "solid-js": "*", 54 | "three": "*" 55 | }, 56 | "jest": { 57 | "preset": "scripts/jest/node" 58 | }, 59 | "dependencies": { 60 | "@types/three": "0.149.0", 61 | "zustand": "^3.7.2" 62 | }, 63 | "packageManager": "pnpm@7.26.0" 64 | } -------------------------------------------------------------------------------- /playground/App.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from "../src"; 2 | import { Box } from "./Box"; 3 | 4 | export function App() { 5 | return ( 6 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /playground/Box.tsx: -------------------------------------------------------------------------------- 1 | import { Mesh } from "three"; 2 | import { createSignal, useFrame } from "../src"; 3 | 4 | export function Box() { 5 | let mesh: Mesh | undefined; 6 | const [hovered, setHovered] = createSignal(false); 7 | 8 | useFrame(() => (mesh!.rotation.y += 0.01)); 9 | 10 | return ( 11 | setHovered(true)} 14 | onPointerLeave={e => setHovered(false)} 15 | > 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /playground/main.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "solid-js/web"; 2 | import { App } from "./App"; 3 | 4 | render(() => , document.getElementById("root")!); 5 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vinxi/tsconfig/vite-solid.json", 3 | "include": ["./"], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'playground' 3 | - 'src' -------------------------------------------------------------------------------- /src/core/events.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | // @ts-ignore 3 | // import { 4 | // ContinuousEventPriority, 5 | // DiscreteEventPriority, 6 | // DefaultEventPriority, 7 | // } from "react-reconciler/constants"; 8 | import type { StoreApi as UseStore } from "zustand/vanilla"; 9 | import type { Instance } from "./renderer"; 10 | import type { RootState } from "./store"; 11 | 12 | export interface Intersection extends THREE.Intersection { 13 | eventObject: THREE.Object3D; 14 | } 15 | 16 | export interface IntersectionEvent extends Intersection { 17 | intersections: Intersection[]; 18 | stopped: boolean; 19 | unprojectedPoint: THREE.Vector3; 20 | ray: THREE.Ray; 21 | camera: Camera; 22 | stopPropagation: () => void; 23 | /** 24 | * @deprecated in favour of nativeEvent. Please use that instead. 25 | */ 26 | sourceEvent: TSourceEvent; 27 | nativeEvent: TSourceEvent; 28 | delta: number; 29 | spaceX: number; 30 | spaceY: number; 31 | } 32 | 33 | export type Camera = THREE.OrthographicCamera | THREE.PerspectiveCamera; 34 | export type ThreeEvent = IntersectionEvent; 35 | export type DomEvent = PointerEvent | MouseEvent | WheelEvent; 36 | 37 | export type Events = { 38 | onClick: EventListener; 39 | onContextMenu: EventListener; 40 | onDoubleClick: EventListener; 41 | onWheel: EventListener; 42 | onPointerDown: EventListener; 43 | onPointerUp: EventListener; 44 | onPointerLeave: EventListener; 45 | onPointerMove: EventListener; 46 | onPointerCancel: EventListener; 47 | onLostPointerCapture: EventListener; 48 | }; 49 | 50 | export type EventHandlers = { 51 | onClick?: (event: ThreeEvent) => void; 52 | onContextMenu?: (event: ThreeEvent) => void; 53 | onDoubleClick?: (event: ThreeEvent) => void; 54 | onPointerUp?: (event: ThreeEvent) => void; 55 | onPointerDown?: (event: ThreeEvent) => void; 56 | onPointerOver?: (event: ThreeEvent) => void; 57 | onPointerOut?: (event: ThreeEvent) => void; 58 | onPointerEnter?: (event: ThreeEvent) => void; 59 | onPointerLeave?: (event: ThreeEvent) => void; 60 | onPointerMove?: (event: ThreeEvent) => void; 61 | onPointerMissed?: (event: MouseEvent) => void; 62 | onPointerCancel?: (event: ThreeEvent) => void; 63 | onWheel?: (event: ThreeEvent) => void; 64 | }; 65 | 66 | export interface EventManager { 67 | connected: TTarget | boolean; 68 | handlers?: Events; 69 | connect?: (target: TTarget) => void; 70 | disconnect?: () => void; 71 | } 72 | 73 | export interface PointerCaptureTarget { 74 | intersection: Intersection; 75 | target: Element; 76 | } 77 | 78 | function makeId(event: Intersection) { 79 | return ( 80 | (event.eventObject || event.object).uuid + 81 | "/" + 82 | event.index + 83 | event.instanceId 84 | ); 85 | } 86 | 87 | // https://github.com/facebook/react/tree/main/packages/react-reconciler#getcurrenteventpriority 88 | // Gives React a clue as to how import the current interaction is 89 | // export function getEventPriority() { 90 | // let name = window?.event?.type; 91 | // switch (name) { 92 | // case "click": 93 | // case "contextmenu": 94 | // case "dblclick": 95 | // case "pointercancel": 96 | // case "pointerdown": 97 | // case "pointerup": 98 | // return DiscreteEventPriority; 99 | // case "pointermove": 100 | // case "pointerout": 101 | // case "pointerover": 102 | // case "pointerenter": 103 | // case "pointerleave": 104 | // case "wheel": 105 | // return ContinuousEventPriority; 106 | // default: 107 | // return DefaultEventPriority; 108 | // } 109 | // } 110 | 111 | /** 112 | * Release pointer captures. 113 | * This is called by releasePointerCapture in the API, and when an object is removed. 114 | */ 115 | function releaseInternalPointerCapture( 116 | capturedMap: Map>, 117 | obj: THREE.Object3D, 118 | captures: Map, 119 | pointerId: number 120 | ): void { 121 | const captureData: PointerCaptureTarget | undefined = captures.get(obj); 122 | if (captureData) { 123 | captures.delete(obj); 124 | // If this was the last capturing object for this pointer 125 | if (captures.size === 0) { 126 | capturedMap.delete(pointerId); 127 | captureData.target.releasePointerCapture(pointerId); 128 | } 129 | } 130 | } 131 | 132 | export function removeInteractivity( 133 | store: UseStore, 134 | object: THREE.Object3D 135 | ) { 136 | const { internal } = store.getState(); 137 | // Removes every trace of an object from the data store 138 | internal.interaction = internal.interaction.filter((o) => o !== object); 139 | internal.initialHits = internal.initialHits.filter((o) => o !== object); 140 | internal.hovered.forEach((value, key) => { 141 | if (value.eventObject === object || value.object === object) { 142 | internal.hovered.delete(key); 143 | } 144 | }); 145 | internal.capturedMap.forEach((captures, pointerId) => { 146 | releaseInternalPointerCapture( 147 | internal.capturedMap, 148 | object, 149 | captures, 150 | pointerId 151 | ); 152 | }); 153 | } 154 | 155 | export function createEvents(store: UseStore) { 156 | const temp = new THREE.Vector3(); 157 | 158 | /** Sets up defaultRaycaster */ 159 | function prepareRay(event: DomEvent) { 160 | const state = store.getState(); 161 | const { raycaster, mouse, camera, size } = state; 162 | // https://github.com/pmndrs/react-three-fiber/pull/782 163 | // Events trigger outside of canvas when moved 164 | const { offsetX, offsetY } = 165 | raycaster.computeOffsets?.(event, state) ?? event; 166 | const { width, height } = size; 167 | mouse.set((offsetX / width) * 2 - 1, -(offsetY / height) * 2 + 1); 168 | raycaster.setFromCamera(mouse, camera); 169 | } 170 | 171 | /** Calculates delta */ 172 | function calculateDistance(event: DomEvent) { 173 | const { internal } = store.getState(); 174 | const dx = event.offsetX - internal.initialClick[0]; 175 | const dy = event.offsetY - internal.initialClick[1]; 176 | return Math.round(Math.sqrt(dx * dx + dy * dy)); 177 | } 178 | 179 | /** Returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc */ 180 | function filterPointerEvents(objects: THREE.Object3D[]) { 181 | return objects.filter((obj) => 182 | ["Move", "Over", "Enter", "Out", "Leave"].some( 183 | (name) => 184 | (obj as unknown as Instance).__r3f?.handlers[ 185 | ("onPointer" + name) as keyof EventHandlers 186 | ] 187 | ) 188 | ); 189 | } 190 | 191 | function intersect(filter?: (objects: THREE.Object3D[]) => THREE.Object3D[]) { 192 | const state = store.getState(); 193 | const { raycaster, internal } = state; 194 | // Skip event handling when noEvents is set 195 | if (!raycaster.enabled) return []; 196 | 197 | const seen = new Set(); 198 | const intersections: Intersection[] = []; 199 | 200 | // Allow callers to eliminate event objects 201 | const eventsObjects = filter 202 | ? filter(internal.interaction) 203 | : internal.interaction; 204 | 205 | // Intersect known handler objects and filter against duplicates 206 | let intersects = raycaster 207 | .intersectObjects(eventsObjects, true) 208 | .filter((item) => { 209 | const id = makeId(item as Intersection); 210 | if (seen.has(id)) return false; 211 | seen.add(id); 212 | return true; 213 | }); 214 | 215 | // https://github.com/mrdoob/three.js/issues/16031 216 | // Allow custom userland intersect sort order 217 | if (raycaster.filter) intersects = raycaster.filter(intersects, state); 218 | 219 | for (const intersect of intersects) { 220 | let eventObject: THREE.Object3D | null = intersect.object; 221 | // Bubble event up 222 | while (eventObject) { 223 | if ((eventObject as unknown as Instance).__r3f?.eventCount) 224 | intersections.push({ ...intersect, eventObject }); 225 | eventObject = eventObject.parent; 226 | } 227 | } 228 | return intersections; 229 | } 230 | 231 | /** Creates filtered intersects and returns an array of positive hits */ 232 | function patchIntersects(intersections: Intersection[], event: DomEvent) { 233 | const { internal } = store.getState(); 234 | // If the interaction is captured, make all capturing targets part of the 235 | // intersect. 236 | if ("pointerId" in event && internal.capturedMap.has(event.pointerId)) { 237 | for (let captureData of internal.capturedMap 238 | .get(event.pointerId)! 239 | .values()) { 240 | intersections.push(captureData.intersection); 241 | } 242 | } 243 | return intersections; 244 | } 245 | 246 | /** Handles intersections by forwarding them to handlers */ 247 | function handleIntersects( 248 | intersections: Intersection[], 249 | event: DomEvent, 250 | delta: number, 251 | callback: (event: ThreeEvent) => void 252 | ) { 253 | const { raycaster, mouse, camera, internal } = store.getState(); 254 | // If anything has been found, forward it to the event listeners 255 | if (intersections.length) { 256 | const unprojectedPoint = temp.set(mouse.x, mouse.y, 0).unproject(camera); 257 | 258 | const localState = { stopped: false }; 259 | 260 | for (const hit of intersections) { 261 | const hasPointerCapture = (id: number) => 262 | internal.capturedMap.get(id)?.has(hit.eventObject) ?? false; 263 | 264 | const setPointerCapture = (id: number) => { 265 | const captureData = { 266 | intersection: hit, 267 | target: event.target as Element, 268 | }; 269 | if (internal.capturedMap.has(id)) { 270 | // if the pointerId was previously captured, we add the hit to the 271 | // event capturedMap. 272 | internal.capturedMap.get(id)!.set(hit.eventObject, captureData); 273 | } else { 274 | // if the pointerId was not previously captured, we create a map 275 | // containing the hitObject, and the hit. hitObject is used for 276 | // faster access. 277 | internal.capturedMap.set( 278 | id, 279 | new Map([[hit.eventObject, captureData]]) 280 | ); 281 | } 282 | // Call the original event now 283 | (event.target as Element).setPointerCapture(id); 284 | }; 285 | 286 | const releasePointerCapture = (id: number) => { 287 | const captures = internal.capturedMap.get(id); 288 | if (captures) { 289 | releaseInternalPointerCapture( 290 | internal.capturedMap, 291 | hit.eventObject, 292 | captures, 293 | id 294 | ); 295 | } 296 | }; 297 | 298 | // Add native event props 299 | let extractEventProps: any = {}; 300 | // 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. 301 | for (let prop in event) { 302 | let property = event[prop as keyof DomEvent]; 303 | // Only copy over atomics, leave functions alone as these should be 304 | // called as event.nativeEvent.fn() 305 | if (typeof property !== "function") 306 | extractEventProps[prop] = property; 307 | } 308 | 309 | let raycastEvent: any = { 310 | ...hit, 311 | ...extractEventProps, 312 | spaceX: mouse.x, 313 | spaceY: mouse.y, 314 | intersections, 315 | stopped: localState.stopped, 316 | delta, 317 | unprojectedPoint, 318 | ray: raycaster.ray, 319 | camera: camera, 320 | // Hijack stopPropagation, which just sets a flag 321 | stopPropagation: () => { 322 | // https://github.com/pmndrs/react-three-fiber/issues/596 323 | // Events are not allowed to stop propagation if the pointer has been captured 324 | const capturesForPointer = 325 | "pointerId" in event && internal.capturedMap.get(event.pointerId); 326 | 327 | // We only authorize stopPropagation... 328 | if ( 329 | // ...if this pointer hasn't been captured 330 | !capturesForPointer || 331 | // ... or if the hit object is capturing the pointer 332 | capturesForPointer.has(hit.eventObject) 333 | ) { 334 | raycastEvent.stopped = localState.stopped = true; 335 | // Propagation is stopped, remove all other hover records 336 | // An event handler is only allowed to flush other handlers if it is hovered itself 337 | if ( 338 | internal.hovered.size && 339 | Array.from(internal.hovered.values()).find( 340 | (i) => i.eventObject === hit.eventObject 341 | ) 342 | ) { 343 | // Objects cannot flush out higher up objects that have already caught the event 344 | const higher = intersections.slice( 345 | 0, 346 | intersections.indexOf(hit) 347 | ); 348 | cancelPointer([...higher, hit]); 349 | } 350 | } 351 | }, 352 | // there should be a distinction between target and currentTarget 353 | target: { 354 | hasPointerCapture, 355 | setPointerCapture, 356 | releasePointerCapture, 357 | }, 358 | currentTarget: { 359 | hasPointerCapture, 360 | setPointerCapture, 361 | releasePointerCapture, 362 | }, 363 | sourceEvent: event, // deprecated 364 | nativeEvent: event, 365 | }; 366 | 367 | // Call subscribers 368 | callback(raycastEvent); 369 | // Event bubbling may be interrupted by stopPropagation 370 | if (localState.stopped === true) break; 371 | } 372 | } 373 | return intersections; 374 | } 375 | 376 | function cancelPointer(hits: Intersection[]) { 377 | const { internal } = store.getState(); 378 | Array.from(internal.hovered.values()).forEach((hoveredObj) => { 379 | // When no objects were hit or the the hovered object wasn't found underneath the cursor 380 | // we call onPointerOut and delete the object from the hovered-elements map 381 | if ( 382 | !hits.length || 383 | !hits.find( 384 | (hit) => 385 | hit.object === hoveredObj.object && 386 | hit.index === hoveredObj.index && 387 | hit.instanceId === hoveredObj.instanceId 388 | ) 389 | ) { 390 | const eventObject = hoveredObj.eventObject; 391 | const instance = (eventObject as unknown as Instance).__r3f; 392 | const handlers = instance?.handlers; 393 | internal.hovered.delete(makeId(hoveredObj)); 394 | if (instance?.eventCount) { 395 | // Clear out intersects, they are outdated by now 396 | const data = { ...hoveredObj, intersections: hits || [] }; 397 | handlers.onPointerOut?.(data as ThreeEvent); 398 | handlers.onPointerLeave?.(data as ThreeEvent); 399 | } 400 | } 401 | }); 402 | } 403 | 404 | const handlePointer = (name: string) => { 405 | // Deal with cancelation 406 | switch (name) { 407 | case "onPointerLeave": 408 | case "onPointerCancel": 409 | return () => cancelPointer([]); 410 | case "onLostPointerCapture": 411 | return (event: DomEvent) => { 412 | const { internal } = store.getState(); 413 | if ( 414 | "pointerId" in event && 415 | !internal.capturedMap.has(event.pointerId) 416 | ) { 417 | // If the object event interface had onLostPointerCapture, we'd call it here on every 418 | // object that's getting removed. 419 | internal.capturedMap.delete(event.pointerId); 420 | cancelPointer([]); 421 | } 422 | }; 423 | } 424 | 425 | // Any other pointer goes here ... 426 | return (event: DomEvent) => { 427 | const { onPointerMissed, internal } = store.getState(); 428 | 429 | prepareRay(event); 430 | internal.lastEvent.current = event; 431 | 432 | // Get fresh intersects 433 | const isPointerMove = name === "onPointerMove"; 434 | const isClickEvent = 435 | name === "onClick" || 436 | name === "onContextMenu" || 437 | name === "onDoubleClick"; 438 | const filter = isPointerMove ? filterPointerEvents : undefined; 439 | const hits = patchIntersects(intersect(filter), event); 440 | const delta = isClickEvent ? calculateDistance(event) : 0; 441 | 442 | // Save initial coordinates on pointer-down 443 | if (name === "onPointerDown") { 444 | internal.initialClick = [event.offsetX, event.offsetY]; 445 | internal.initialHits = hits.map((hit) => hit.eventObject); 446 | } 447 | 448 | // If a click yields no results, pass it back to the user as a miss 449 | // Missed events have to come first in order to establish user-land side-effect clean up 450 | if (isClickEvent && !hits.length) { 451 | if (delta <= 2) { 452 | pointerMissed(event, internal.interaction); 453 | if (onPointerMissed) onPointerMissed(event); 454 | } 455 | } 456 | // Take care of unhover 457 | if (isPointerMove) cancelPointer(hits); 458 | 459 | handleIntersects(hits, event, delta, (data: ThreeEvent) => { 460 | const eventObject = data.eventObject; 461 | const instance = (eventObject as unknown as Instance).__r3f; 462 | const handlers = instance?.handlers; 463 | // Check presence of handlers 464 | if (!instance?.eventCount) return; 465 | 466 | if (isPointerMove) { 467 | // Move event ... 468 | if ( 469 | handlers.onPointerOver || 470 | handlers.onPointerEnter || 471 | handlers.onPointerOut || 472 | handlers.onPointerLeave 473 | ) { 474 | // When enter or out is present take care of hover-state 475 | const id = makeId(data); 476 | const hoveredItem = internal.hovered.get(id); 477 | if (!hoveredItem) { 478 | // If the object wasn't previously hovered, book it and call its handler 479 | internal.hovered.set(id, data); 480 | handlers.onPointerOver?.(data as ThreeEvent); 481 | handlers.onPointerEnter?.(data as ThreeEvent); 482 | } else if (hoveredItem.stopped) { 483 | // If the object was previously hovered and stopped, we shouldn't allow other items to proceed 484 | data.stopPropagation(); 485 | } 486 | } 487 | // Call mouse move 488 | handlers.onPointerMove?.(data as ThreeEvent); 489 | } else { 490 | // All other events ... 491 | const handler = handlers[name as keyof EventHandlers] as ( 492 | event: ThreeEvent 493 | ) => void; 494 | if (handler) { 495 | // Forward all events back to their respective handlers with the exception of click events, 496 | // which must use the initial target 497 | if (!isClickEvent || internal.initialHits.includes(eventObject)) { 498 | // Missed events have to come first 499 | pointerMissed( 500 | event, 501 | internal.interaction.filter( 502 | (object) => !internal.initialHits.includes(object) 503 | ) 504 | ); 505 | // Now call the handler 506 | handler(data as ThreeEvent); 507 | } 508 | } else { 509 | // Trigger onPointerMissed on all elements that have pointer over/out handlers, but not click and weren't hit 510 | if (isClickEvent && internal.initialHits.includes(eventObject)) { 511 | pointerMissed( 512 | event, 513 | internal.interaction.filter( 514 | (object) => !internal.initialHits.includes(object) 515 | ) 516 | ); 517 | } 518 | } 519 | } 520 | }); 521 | }; 522 | }; 523 | 524 | function pointerMissed(event: MouseEvent, objects: THREE.Object3D[]) { 525 | objects.forEach((object: THREE.Object3D) => 526 | (object as unknown as Instance).__r3f?.handlers.onPointerMissed?.(event) 527 | ); 528 | } 529 | 530 | return { handlePointer }; 531 | } 532 | -------------------------------------------------------------------------------- /src/core/index.tsx: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { StoreApi as UseStore } from "zustand/vanilla"; 3 | import { dispose, calculateDpr, applyProps } from "./utils"; 4 | import { 5 | Renderer, 6 | createThreeStore, 7 | StoreProps, 8 | isRenderer, 9 | ThreeContext, 10 | RootState, 11 | Size 12 | } from "./store"; 13 | import { extend, Root } from "./renderer"; 14 | import { createLoop, addEffect, addAfterEffect, addTail } from "./loop"; 15 | import { EventManager } from "./events"; 16 | import { createEffect, PropsWithChildren } from "solid-js"; 17 | 18 | export type { IntersectionEvent, EventHandlers, Intersection, Camera } from "./events"; 19 | export { attach, applyProp } from "./utils"; 20 | export { catalogue } from "./renderer"; 21 | export type { InternalState, Raycaster } from "./store"; 22 | export type { DiffSet } from "./utils"; 23 | 24 | export const roots = new Map(); 25 | const { invalidate, advance } = createLoop(roots); 26 | 27 | type Properties = Pick any ? never : K }[keyof T]>; 28 | 29 | type GLProps = 30 | | Renderer 31 | | ((canvas: HTMLCanvasElement) => Renderer) 32 | | Partial | THREE.WebGLRendererParameters> 33 | | undefined; 34 | 35 | export type RenderProps = Omit & { 36 | gl?: GLProps; 37 | events?: (store: UseStore) => EventManager; 38 | size?: Size; 39 | onCreated?: (state: RootState) => void; 40 | }; 41 | 42 | const createRendererInstance = ( 43 | gl: GLProps, 44 | canvas: TElement 45 | ): THREE.WebGLRenderer => { 46 | const customRenderer = ( 47 | typeof gl === "function" ? gl(canvas as unknown as HTMLCanvasElement) : gl 48 | ) as THREE.WebGLRenderer; 49 | if (isRenderer(customRenderer)) return customRenderer; 50 | 51 | const renderer = new THREE.WebGLRenderer({ 52 | powerPreference: "high-performance", 53 | canvas: canvas as unknown as HTMLCanvasElement, 54 | antialias: true, 55 | alpha: true, 56 | ...gl 57 | }); 58 | 59 | // Set color management 60 | renderer.outputEncoding = THREE.sRGBEncoding; 61 | renderer.toneMapping = THREE.ACESFilmicToneMapping; 62 | 63 | // Set gl props 64 | if (gl) applyProps(renderer as any, gl as any); 65 | 66 | return renderer; 67 | }; 68 | 69 | function createThreeRoot( 70 | canvas: TCanvas, 71 | config?: RenderProps 72 | ) { 73 | let { gl, size, events, onCreated, ...props } = config || {}; 74 | // Allow size to take on container bounds initially 75 | if (!size) { 76 | size = canvas.parentElement?.getBoundingClientRect() ?? { 77 | width: 0, 78 | height: 0 79 | }; 80 | } 81 | 82 | // if (fiber && state) { 83 | // // When a root was found, see if any fundamental props must be changed or exchanged 84 | 85 | // // Check pixelratio 86 | // if ( 87 | // props.dpr !== undefined && 88 | // state.viewport.dpr !== calculateDpr(props.dpr) 89 | // ) 90 | // state.setDpr(props.dpr); 91 | // // Check size 92 | // if ( 93 | // state.size.width !== size.width || 94 | // state.size.height !== size.height 95 | // ) 96 | // state.setSize(size.width, size.height); 97 | // // Check frameloop 98 | // if (state.frameloop !== props.frameloop) 99 | // state.setFrameloop(props.frameloop); 100 | 101 | // // For some props we want to reset the entire root 102 | 103 | // // Changes to the color-space 104 | // const linearChanged = props.linear !== state.internal.lastProps.linear; 105 | // if (linearChanged) { 106 | // unmountComponentAtNode(canvas); 107 | // fiber = undefined; 108 | // } 109 | // } 110 | 111 | // Create gl 112 | const glRenderer = createRendererInstance(gl, canvas); 113 | 114 | // Create store 115 | const store = createThreeStore(applyProps, invalidate, advance, { 116 | gl: glRenderer, 117 | size, 118 | ...props 119 | }); 120 | 121 | const state = store.getState(); 122 | 123 | // Map it 124 | roots.set(canvas, { store }); 125 | // Store events internally 126 | if (events) state.set({ events: events(store) }); 127 | 128 | createEffect(() => { 129 | const state = store.getState(); 130 | // Flag the canvas active, rendering will now begin 131 | state.set(state => ({ internal: { ...state.internal, active: true } })); 132 | // Connect events 133 | state.events.connect?.(canvas); 134 | // Notifiy that init is completed, the scene graph exists, but nothing has yet rendered 135 | onCreated?.(state); 136 | 137 | state.invalidate(); 138 | // eslint-disable-next-line react-hooks/exhaustive-deps 139 | }); 140 | 141 | return store; 142 | } 143 | 144 | function unmountComponentAtNode( 145 | canvas: TElement, 146 | callback?: (canvas: TElement) => void 147 | ) { 148 | const root = roots.get(canvas); 149 | // const fiber = root?.fiber; 150 | // if (fiber) { 151 | // const state = root?.store.getState(); 152 | // if (state) state.internal.active = false; 153 | 154 | // setTimeout(() => { 155 | // try { 156 | // state.events.disconnect?.(); 157 | // state.gl?.renderLists?.dispose?.(); 158 | // state.gl?.forceContextLoss?.(); 159 | // if (state.gl?.xr) state.internal.xr.disconnect(); 160 | // dispose(state); 161 | // roots.delete(canvas); 162 | // if (callback) callback(canvas); 163 | // } catch (e) { 164 | // /* ... */ 165 | // } 166 | // }, 500); 167 | // } 168 | } 169 | 170 | // function createPortal( 171 | // children: React.ReactNode, 172 | // container: THREE.Object3D 173 | // ): React.ReactNode { 174 | // return reconciler.createPortal(children, container, null, null); 175 | // } 176 | 177 | export { 178 | ThreeContext as context, 179 | // render, 180 | createThreeRoot, 181 | unmountComponentAtNode, 182 | // createPortal, 183 | applyProps, 184 | dispose, 185 | invalidate, 186 | advance, 187 | extend, 188 | addEffect, 189 | addAfterEffect, 190 | addTail, 191 | // act, 192 | roots as _roots 193 | }; 194 | -------------------------------------------------------------------------------- /src/core/is.ts: -------------------------------------------------------------------------------- 1 | export const is = { 2 | obj: (a: any) => a === Object(a) && !is.arr(a) && typeof a !== 'function', 3 | fun: (a: any): a is Function => typeof a === 'function', 4 | str: (a: any): a is string => typeof a === 'string', 5 | num: (a: any): a is number => typeof a === 'number', 6 | und: (a: any) => a === void 0, 7 | arr: (a: any) => Array.isArray(a), 8 | equ(a: any, b: any) { 9 | // Wrong type or one of the two undefined, doesn't match 10 | if (typeof a !== typeof b || !!a !== !!b) return false 11 | // Atomic, just compare a against b 12 | if (is.str(a) || is.num(a) || is.obj(a)) return a === b 13 | // Array, shallow compare first to see if it's a match 14 | if (is.arr(a) && a == b) return true 15 | // Last resort, go through keys 16 | let i 17 | for (i in a) if (!(i in b)) return false 18 | for (i in b) if (a[i] !== b[i]) return false 19 | return is.und(i) ? a === b : true 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /src/core/loop.ts: -------------------------------------------------------------------------------- 1 | import { Root } from "./renderer"; 2 | import { RootState } from "./store"; 3 | 4 | type GlobalRenderCallback = (timeStamp: number) => void; 5 | 6 | function createSubs( 7 | callback: GlobalRenderCallback, 8 | subs: GlobalRenderCallback[] 9 | ): () => void { 10 | const index = subs.length; 11 | subs.push(callback); 12 | return () => void subs.splice(index, 1); 13 | } 14 | 15 | let i; 16 | let globalEffects: GlobalRenderCallback[] = []; 17 | let globalAfterEffects: GlobalRenderCallback[] = []; 18 | let globalTailEffects: GlobalRenderCallback[] = []; 19 | export const addEffect = (callback: GlobalRenderCallback) => 20 | createSubs(callback, globalEffects); 21 | export const addAfterEffect = (callback: GlobalRenderCallback) => 22 | createSubs(callback, globalAfterEffects); 23 | export const addTail = (callback: GlobalRenderCallback) => 24 | createSubs(callback, globalTailEffects); 25 | 26 | function run(effects: GlobalRenderCallback[], timestamp: number) { 27 | for (i = 0; i < effects.length; i++) effects[i](timestamp); 28 | } 29 | 30 | function render(timestamp: number, state: RootState) { 31 | // Run local effects 32 | let delta = state.clock.getDelta(); 33 | // In frameloop='never' mode, clock times are updated using the provided timestamp 34 | if (state.frameloop === "never" && typeof timestamp === "number") { 35 | delta = timestamp - state.clock.elapsedTime; 36 | state.clock.oldTime = state.clock.elapsedTime; 37 | state.clock.elapsedTime = timestamp; 38 | } 39 | // Call subscribers (useFrame) 40 | for (i = 0; i < state.internal.subscribers.length; i++) 41 | state.internal.subscribers[i].ref(state, delta); 42 | // Render content 43 | if (!state.internal.priority && state.gl.render) 44 | state.gl.render(state.scene, state.camera); 45 | // Decrease frame count 46 | state.internal.frames = Math.max(0, state.internal.frames - 1); 47 | return state.frameloop === "always" ? 1 : state.internal.frames; 48 | } 49 | 50 | export function createLoop(roots: Map) { 51 | let running = false; 52 | let repeat: number; 53 | function loop(timestamp: number) { 54 | running = true; 55 | repeat = 0; 56 | 57 | // Run effects 58 | run(globalEffects, timestamp); 59 | // Render all roots 60 | roots.forEach((root) => { 61 | const state = root.store.getState(); 62 | // If the frameloop is invalidated, do not run another frame 63 | if ( 64 | state.internal.active && 65 | (state.frameloop === "always" || state.internal.frames > 0) && 66 | !state.gl.xr?.isPresenting 67 | ) { 68 | repeat += render(timestamp, state); 69 | } 70 | }); 71 | // Run after-effects 72 | run(globalAfterEffects, timestamp); 73 | 74 | // Keep on looping if anything invalidates the frameloop 75 | if (repeat > 0) return requestAnimationFrame(loop); 76 | // Tail call effects, they are called when rendering stops 77 | else run(globalTailEffects, timestamp); 78 | 79 | // Flag end of operation 80 | running = false; 81 | } 82 | 83 | function invalidate(state?: RootState): void { 84 | if (!state) 85 | return roots.forEach((root) => invalidate(root.store.getState())); 86 | if ( 87 | state.gl.xr?.isPresenting || 88 | !state.internal.active || 89 | state.frameloop === "never" 90 | ) 91 | return; 92 | // Increase frames, do not go higher than 60 93 | state.internal.frames = Math.min(60, state.internal.frames + 1); 94 | // If the render-loop isn't active, start it 95 | if (!running) { 96 | running = true; 97 | requestAnimationFrame(loop); 98 | } 99 | } 100 | 101 | function advance( 102 | timestamp: number, 103 | runGlobalEffects: boolean = true, 104 | state?: RootState 105 | ): void { 106 | if (runGlobalEffects) run(globalEffects, timestamp); 107 | if (!state) 108 | roots.forEach((root) => render(timestamp, root.store.getState())); 109 | else render(timestamp, state); 110 | if (runGlobalEffects) run(globalAfterEffects, timestamp); 111 | } 112 | 113 | return { loop, invalidate, advance }; 114 | } 115 | -------------------------------------------------------------------------------- /src/core/renderer.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { StoreApi as UseStore } from "zustand/vanilla"; 3 | import { 4 | is, 5 | prepare, 6 | diffProps, 7 | DiffSet, 8 | applyProps, 9 | updateInstance, 10 | invalidateInstance, 11 | attach, 12 | detach, 13 | applyProp 14 | } from "./utils"; 15 | import { RootState } from "./store"; 16 | import { EventHandlers, removeInteractivity } from "./events"; 17 | import { log } from "../solid"; 18 | import { dispose } from "."; 19 | 20 | export type Root = { store: UseStore }; 21 | 22 | export type LocalState = { 23 | root: UseStore; 24 | // objects and parent are used when children are added with `attach` instead of being added to the Object3D scene graph 25 | objects: Instance[]; 26 | parent: Instance | null; 27 | primitive?: boolean; 28 | eventCount: number; 29 | handlers: Partial; 30 | attach?: AttachType; 31 | previousAttach?: any; 32 | memoizedProps: { 33 | [key: string]: any; 34 | }; 35 | }; 36 | 37 | export type AttachFnType = (parent: Instance, self: Instance) => void; 38 | export type AttachType = string | [attach: string | AttachFnType, detach: string | AttachFnType]; 39 | 40 | // This type clamps down on a couple of assumptions that we can make regarding native types, which 41 | // could anything from scene objects, THREE.Objects, JSM, user-defined classes and non-scene objects. 42 | // What they all need to have in common is defined here ... 43 | export type BaseInstance = Omit< 44 | THREE.Object3D, 45 | "children" | "attach" | "add" | "remove" | "raycast" 46 | > & { 47 | __r3f: LocalState; 48 | children: Instance[]; 49 | remove: (...object: Instance[]) => Instance; 50 | add: (...object: Instance[]) => Instance; 51 | raycast?: (raycaster: THREE.Raycaster, intersects: THREE.Intersection[]) => void; 52 | }; 53 | export type Instance = BaseInstance & { [key: string]: any }; 54 | 55 | export type InstanceProps = { 56 | [key: string]: unknown; 57 | } & { 58 | args?: any[]; 59 | object?: object; 60 | visible?: boolean; 61 | dispose?: null; 62 | attach?: AttachType; 63 | }; 64 | 65 | interface Catalogue { 66 | [name: string]: { 67 | new (...args: any): Instance; 68 | }; 69 | } 70 | 71 | // Type guard to tell a store from a portal 72 | const isStore = (def: any): def is UseStore => 73 | def && !!(def as UseStore).getState; 74 | // const getContainer = ( 75 | // container: UseStore | Instance, 76 | // child: Instance 77 | // ) => ({ 78 | // // If the container is not a root-store then it must be a THREE.Object3D into which part of the 79 | // // scene is portalled into. Now there can be two variants of this, either that object is part of 80 | // // the regular jsx tree, in which case it already has __r3f with a valid root attached, or it lies 81 | // // outside react, in which case we must take the root of the child that is about to be attached to it. 82 | // root: isStore(container) 83 | // ? container 84 | // : container.__r3f?.root ?? child.__r3f.root, 85 | // // The container is the eventual target into which objects are mounted, it has to be a THREE.Object3D 86 | // container: isStore(container) 87 | // ? (container.getState().scene as unknown as Instance) 88 | // : container, 89 | // }); 90 | 91 | export let catalogue: Catalogue = {}; 92 | let extend = (objects: object): void => void (catalogue = { ...catalogue, ...objects }); 93 | 94 | function createThreeRenderer(roots: Map, getEventPriority?: () => any) { 95 | function createInstance( 96 | type: string, 97 | { args = [], attach, ...props }: InstanceProps, 98 | root: UseStore | Instance 99 | ) { 100 | let name = `${type[0].toUpperCase()}${type.slice(1)}`; 101 | let instance: Instance; 102 | 103 | // https://github.com/facebook/react/issues/17147 104 | // Portals do not give us a root, they are themselves treated as a root by the reconciler 105 | // In order to figure out the actual root we have to climb through fiber internals :( 106 | // if (!isStore(root) && internalInstanceHandle) { 107 | // const fn = (node: Reconciler.Fiber): UseStore => { 108 | // if (!node.return) return node.stateNode && node.stateNode.containerInfo; 109 | // else return fn(node.return); 110 | // }; 111 | // root = fn(internalInstanceHandle); 112 | // } 113 | // Assert that by now we have a valid root 114 | if (!root || !isStore(root)) throw `No valid root for ${name}!`; 115 | 116 | // Auto-attach geometries and materials 117 | if (attach === undefined) { 118 | if (name.endsWith("Geometry")) attach = "geometry"; 119 | else if (name.endsWith("Material")) attach = "material"; 120 | } 121 | 122 | if (type === "primitive") { 123 | if (props.object === undefined) throw `Primitives without 'object' are invalid!`; 124 | const object = props.object as Instance; 125 | instance = prepare(object, { 126 | root, 127 | attach, 128 | primitive: true 129 | }); 130 | } else { 131 | const target = catalogue[name]; 132 | if (!target) { 133 | throw `${name} is not part of the THREE namespace! Did you forget to extend? See: https://github.com/pmndrs/react-three-fiber/blob/master/markdown/api.md#using-3rd-party-objects-declaratively`; 134 | } 135 | 136 | // Throw if an object or literal was passed for args 137 | if (!Array.isArray(args)) throw "The args prop must be an array!"; 138 | 139 | // Instanciate new object, link it to the root 140 | // Append memoized props with args so it's not forgotten 141 | instance = prepare(new target(...args), { 142 | root, 143 | attach, 144 | // TODO: Figure out what this is for 145 | memoizedProps: { args: args.length === 0 ? null : args } 146 | }); 147 | } 148 | 149 | // It should NOT call onUpdate on object instanciation, because it hasn't been added to the 150 | // view yet. If the callback relies on references for instance, they won't be ready yet, this is 151 | // why it passes "true" here 152 | applyProps(instance, props); 153 | return instance; 154 | } 155 | 156 | function appendChild(parentInstance: Instance, child: Instance) { 157 | let added = false; 158 | if (child) { 159 | // The attach attribute implies that the object attaches itself on the parent 160 | if (child.__r3f.attach) { 161 | attach(parentInstance, child, child.__r3f.attach); 162 | } else if (child.isObject3D && parentInstance.isObject3D) { 163 | // add in the usual parent-child way 164 | parentInstance.add(child); 165 | added = true; 166 | } 167 | // This is for anything that used attach, and for non-Object3Ds that don't get attached to props; 168 | // that is, anything that's a child in React but not a child in the scenegraph. 169 | if (!added) parentInstance.__r3f.objects.push(child); 170 | if (!child.__r3f) prepare(child, {}); 171 | child.__r3f.parent = parentInstance; 172 | updateInstance(child); 173 | invalidateInstance(child); 174 | } 175 | } 176 | 177 | function insertBefore(parentInstance: Instance, child: Instance, beforeChild: Instance) { 178 | let added = false; 179 | if (child) { 180 | if (child.__r3f.attach) { 181 | attach(parentInstance, child, child.__r3f.attach); 182 | } else if (child.isObject3D && parentInstance.isObject3D) { 183 | child.parent = parentInstance as unknown as THREE.Object3D; 184 | child.dispatchEvent({ type: "added" }); 185 | const restSiblings = parentInstance.children.filter(sibling => sibling !== child); 186 | const index = restSiblings.indexOf(beforeChild); 187 | parentInstance.children = [ 188 | ...restSiblings.slice(0, index), 189 | child, 190 | ...restSiblings.slice(index) 191 | ]; 192 | added = true; 193 | } 194 | 195 | if (!added) parentInstance.__r3f.objects.push(child); 196 | if (!child.__r3f) prepare(child, {}); 197 | child.__r3f.parent = parentInstance; 198 | updateInstance(child); 199 | invalidateInstance(child); 200 | } 201 | } 202 | 203 | function removeRecursive(array: Instance[], parent: Instance, dispose: boolean = false) { 204 | if (array) [...array].forEach(child => removeChild(parent, child, dispose)); 205 | } 206 | 207 | function removeChild(parentInstance: Instance, child: Instance, canDispose?: boolean) { 208 | if (child) { 209 | // Clear the parent reference 210 | if (child.__r3f) child.__r3f.parent = null; 211 | // Remove child from the parents objects 212 | if (parentInstance.__r3f?.objects) 213 | parentInstance.__r3f.objects = parentInstance.__r3f.objects.filter(x => x !== child); 214 | // Remove attachment 215 | if (child.__r3f?.attach) { 216 | detach(parentInstance, child, child.__r3f.attach); 217 | } else if (child.isObject3D && parentInstance.isObject3D) { 218 | log("three", "removeObject", parentInstance, child); 219 | parentInstance.remove(child); 220 | // Remove interactivity 221 | if (child.__r3f?.root) { 222 | removeInteractivity(child.__r3f.root, child as unknown as THREE.Object3D); 223 | } 224 | } 225 | 226 | // Allow objects to bail out of recursive dispose alltogether by passing dispose={null} 227 | // Never dispose of primitives because their state may be kept outside of React! 228 | // In order for an object to be able to dispose it has to have 229 | // - a dispose method, 230 | // - it cannot be a 231 | // - it cannot be a THREE.Scene, because three has broken it's own api 232 | // 233 | // Since disposal is recursive, we can check the optional dispose arg, which will be undefined 234 | // when the reconciler calls it, but then carry our own check recursively 235 | const isPrimitive = child.__r3f?.primitive; 236 | const shouldDispose = 237 | canDispose === undefined ? child.dispose !== null && !isPrimitive : canDispose; 238 | 239 | // Remove nested child objects. Primitives should not have objects and children that are 240 | // attached to them declaratively ... 241 | if (!isPrimitive) { 242 | removeRecursive(child.__r3f?.objects, child, shouldDispose); 243 | removeRecursive(child.children, child, shouldDispose); 244 | } 245 | 246 | // Remove references 247 | if (child.__r3f) { 248 | delete ((child as Partial).__r3f as Partial).root; 249 | delete ((child as Partial).__r3f as Partial).objects; 250 | delete ((child as Partial).__r3f as Partial).handlers; 251 | delete ((child as Partial).__r3f as Partial).memoizedProps; 252 | if (!isPrimitive) delete (child as Partial).__r3f; 253 | } 254 | 255 | // Dispose item whenever the reconciler feels like it 256 | if (shouldDispose && child.type !== "Scene") { 257 | // scheduleCallback(idlePriority, () => { 258 | try { 259 | log("three", "dispose", child); 260 | child.dispose?.(); 261 | dispose(child); 262 | } catch (e) { 263 | /* ... */ 264 | } 265 | // }); 266 | } 267 | 268 | invalidateInstance(parentInstance); 269 | } 270 | } 271 | 272 | function switchInstance(instance: Instance, type: string, newProps: InstanceProps) { 273 | const parent = instance.__r3f?.parent; 274 | if (!parent) return; 275 | 276 | const newInstance = createInstance(type, newProps, instance.__r3f.root); 277 | 278 | // https://github.com/pmndrs/react-three-fiber/issues/1348 279 | // When args change the instance has to be re-constructed, which then 280 | // forces r3f to re-parent the children and non-scene objects 281 | // This can not include primitives, which should not have declarative children 282 | if (type !== "primitive" && instance.children) { 283 | instance.children.forEach(child => appendChild(newInstance, child)); 284 | instance.children = []; 285 | } 286 | 287 | instance.__r3f.objects.forEach(child => appendChild(newInstance, child)); 288 | instance.__r3f.objects = []; 289 | 290 | removeChild(parent, instance); 291 | appendChild(parent, newInstance); 292 | 293 | // This evil hack switches the react-internal fiber node 294 | // https://github.com/facebook/react/issues/14983 295 | // https://github.com/facebook/react/pull/15021 296 | // [fiber, fiber.alternate].forEach((fiber) => { 297 | // if (fiber !== null) { 298 | // fiber.stateNode = newInstance; 299 | // if (fiber.ref) { 300 | // if (typeof fiber.ref === "function") 301 | // (fiber as unknown as any).ref(newInstance); 302 | // else (fiber.ref as Reconciler.RefObject).current = newInstance; 303 | // } 304 | // } 305 | // }); 306 | } 307 | 308 | return { 309 | applyProps, 310 | applyProp, 311 | appendChild, 312 | createInstance, 313 | switchInstance, 314 | insertBefore, 315 | removeChild, 316 | removeRecursive, 317 | attach 318 | }; 319 | } 320 | export type ThreeRenderer = ReturnType; 321 | 322 | export { prepare, createThreeRenderer, extend }; 323 | -------------------------------------------------------------------------------- /src/core/store.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import * as ReactThreeFiber from "../three-types"; 3 | import create, { 4 | GetState, 5 | SetState, 6 | StoreApi as UseStore, 7 | } from "zustand/vanilla"; 8 | import { prepare, Instance, InstanceProps } from "./renderer"; 9 | import { 10 | DomEvent, 11 | EventManager, 12 | PointerCaptureTarget, 13 | ThreeEvent, 14 | } from "./events"; 15 | import { calculateDpr } from "./utils"; 16 | import { createContext } from "solid-js"; 17 | import { subscribeWithSelector } from "zustand/middleware"; 18 | 19 | export interface Intersection extends THREE.Intersection { 20 | eventObject: THREE.Object3D; 21 | } 22 | 23 | export type Subscription = { 24 | ref: RenderCallback; 25 | priority: number; 26 | }; 27 | 28 | export type Dpr = number | [min: number, max: number]; 29 | export type Size = { width: number; height: number }; 30 | export type Viewport = Size & { 31 | initialDpr: number; 32 | dpr: number; 33 | factor: number; 34 | distance: number; 35 | aspect: number; 36 | }; 37 | 38 | export type Camera = THREE.OrthographicCamera | THREE.PerspectiveCamera; 39 | export type Raycaster = THREE.Raycaster & { 40 | enabled: boolean; 41 | filter?: FilterFunction; 42 | computeOffsets?: ComputeOffsetsFunction; 43 | }; 44 | 45 | export type RenderCallback = (state: RootState, delta: number) => void; 46 | 47 | export type Performance = { 48 | current: number; 49 | min: number; 50 | max: number; 51 | debounce: number; 52 | regress: () => void; 53 | }; 54 | 55 | export type Renderer = { 56 | render: (scene: THREE.Scene, camera: THREE.Camera) => any; 57 | }; 58 | 59 | export const isRenderer = (def: Renderer) => !!def?.render; 60 | export const isOrthographicCamera = ( 61 | def: THREE.Camera 62 | ): def is THREE.OrthographicCamera => 63 | def && (def as THREE.OrthographicCamera).isOrthographicCamera; 64 | 65 | export type InternalState = { 66 | active: boolean; 67 | priority: number; 68 | frames: number; 69 | lastProps: StoreProps; 70 | lastEvent: { current: DomEvent | null }; 71 | 72 | interaction: THREE.Object3D[]; 73 | hovered: Map>; 74 | subscribers: Subscription[]; 75 | capturedMap: Map>; 76 | initialClick: [x: number, y: number]; 77 | initialHits: THREE.Object3D[]; 78 | 79 | xr: { connect: () => void; disconnect: () => void }; 80 | subscribe: (callback: RenderCallback, priority?: number) => () => void; 81 | }; 82 | 83 | export type RootState = { 84 | gl: THREE.WebGLRenderer; 85 | scene: THREE.Scene; 86 | camera: Camera & { manual?: boolean }; 87 | controls: THREE.EventDispatcher | null; 88 | raycaster: Raycaster; 89 | mouse: THREE.Vector2; 90 | clock: THREE.Clock; 91 | 92 | linear: boolean; 93 | flat: boolean; 94 | frameloop: "always" | "demand" | "never"; 95 | performance: Performance; 96 | 97 | size: Size; 98 | viewport: Viewport & { 99 | getCurrentViewport: ( 100 | camera?: Camera, 101 | target?: THREE.Vector3, 102 | size?: Size 103 | ) => Omit; 104 | }; 105 | 106 | set: SetState; 107 | get: GetState; 108 | invalidate: () => void; 109 | advance: (timestamp: number, runGlobalEffects?: boolean) => void; 110 | setSize: (width: number, height: number) => void; 111 | setDpr: (dpr: Dpr) => void; 112 | setFrameloop: (frameloop?: "always" | "demand" | "never") => void; 113 | onPointerMissed?: (event: MouseEvent) => void; 114 | 115 | events: EventManager; 116 | internal: InternalState; 117 | }; 118 | 119 | export type FilterFunction = ( 120 | items: THREE.Intersection[], 121 | state: RootState 122 | ) => THREE.Intersection[]; 123 | export type ComputeOffsetsFunction = ( 124 | event: any, 125 | state: RootState 126 | ) => { offsetX: number; offsetY: number }; 127 | 128 | export type StoreProps = { 129 | gl: THREE.WebGLRenderer; 130 | size: Size; 131 | shadows?: boolean | Partial; 132 | linear?: boolean; 133 | flat?: boolean; 134 | orthographic?: boolean; 135 | frameloop?: "always" | "demand" | "never"; 136 | performance?: Partial>; 137 | dpr?: Dpr; 138 | clock?: THREE.Clock; 139 | raycaster?: Partial; 140 | camera?: ( 141 | | Camera 142 | | Partial< 143 | ReactThreeFiber.Object3DNode & 144 | ReactThreeFiber.Object3DNode< 145 | THREE.PerspectiveCamera, 146 | typeof THREE.PerspectiveCamera 147 | > & 148 | ReactThreeFiber.Object3DNode< 149 | THREE.OrthographicCamera, 150 | typeof THREE.OrthographicCamera 151 | > 152 | > 153 | ) & { manual?: boolean }; 154 | onPointerMissed?: (event: MouseEvent) => void; 155 | }; 156 | 157 | export type ApplyProps = (instance: Instance, newProps: InstanceProps) => void; 158 | 159 | const ThreeContext = createContext>(null!); 160 | 161 | const createThreeStore = ( 162 | applyProps: ApplyProps, 163 | invalidate: (state?: RootState) => void, 164 | advance: ( 165 | timestamp: number, 166 | runGlobalEffects?: boolean, 167 | state?: RootState 168 | ) => void, 169 | props: StoreProps 170 | ): UseStore => { 171 | const { 172 | gl, 173 | size, 174 | shadows = false, 175 | linear = false, 176 | flat = false, 177 | orthographic = false, 178 | frameloop = "always", 179 | dpr = [1, 2], 180 | performance, 181 | clock = new THREE.Clock(), 182 | raycaster: raycastOptions, 183 | camera: cameraOptions, 184 | onPointerMissed, 185 | } = props; 186 | 187 | // Set shadowmap 188 | if (shadows) { 189 | gl.shadowMap.enabled = true; 190 | if (typeof shadows === "object") Object.assign(gl.shadowMap, shadows); 191 | else gl.shadowMap.type = THREE.PCFSoftShadowMap; 192 | } 193 | 194 | // Set color preferences 195 | if (linear) gl.outputEncoding = THREE.LinearEncoding; 196 | if (flat) gl.toneMapping = THREE.NoToneMapping; 197 | 198 | // clock.elapsedTime is updated using advance(timestamp) 199 | if (frameloop === "never") { 200 | clock.stop(); 201 | clock.elapsedTime = 0; 202 | } 203 | 204 | const rootState = create( 205 | subscribeWithSelector((set, get) => { 206 | // Create custom raycaster 207 | const raycaster = new THREE.Raycaster() as Raycaster; 208 | const { params, ...options } = raycastOptions || {}; 209 | applyProps(raycaster as any, { 210 | enabled: true, 211 | ...options, 212 | params: { ...raycaster.params, ...params }, 213 | }); 214 | 215 | // Create default camera 216 | const isCamera = cameraOptions instanceof THREE.Camera; 217 | const camera = isCamera 218 | ? (cameraOptions as Camera) 219 | : orthographic 220 | ? new THREE.OrthographicCamera(0, 0, 0, 0, 0.1, 1000) 221 | : new THREE.PerspectiveCamera(75, 0, 0.1, 1000); 222 | if (!isCamera) { 223 | camera.position.z = 5; 224 | if (cameraOptions) applyProps(camera as any, cameraOptions as any); 225 | // Always look at center by default 226 | if (!cameraOptions?.rotation) camera.lookAt(0, 0, 0); 227 | } 228 | 229 | const initialDpr = calculateDpr(dpr); 230 | 231 | const position = new THREE.Vector3(); 232 | const defaultTarget = new THREE.Vector3(); 233 | const tempTarget = new THREE.Vector3(); 234 | function getCurrentViewport( 235 | camera: Camera = get().camera, 236 | target: 237 | | THREE.Vector3 238 | | Parameters = defaultTarget, 239 | size: Size = get().size 240 | ) { 241 | const { width, height } = size; 242 | const aspect = width / height; 243 | if (target instanceof THREE.Vector3) tempTarget.copy(target); 244 | else tempTarget.set(...target); 245 | const distance = camera 246 | .getWorldPosition(position) 247 | .distanceTo(tempTarget); 248 | if (isOrthographicCamera(camera)) { 249 | return { 250 | width: width / camera.zoom, 251 | height: height / camera.zoom, 252 | factor: 1, 253 | distance, 254 | aspect, 255 | }; 256 | } else { 257 | const fov = (camera.fov * Math.PI) / 180; // convert vertical fov to radians 258 | const h = 2 * Math.tan(fov / 2) * distance; // visible height 259 | const w = h * (width / height); 260 | return { width: w, height: h, factor: width / w, distance, aspect }; 261 | } 262 | } 263 | 264 | let performanceTimeout: ReturnType | undefined = 265 | undefined; 266 | const setPerformanceCurrent = (current: number) => 267 | set((state) => ({ performance: { ...state.performance, current } })); 268 | 269 | // Handle frame behavior in WebXR 270 | const handleXRFrame = (timestamp: number) => { 271 | const state = get(); 272 | if (state.frameloop === "never") return; 273 | 274 | advance(timestamp, true); 275 | }; 276 | 277 | // Toggle render switching on session 278 | const handleSessionChange = () => { 279 | gl.xr.enabled = gl.xr.isPresenting; 280 | gl.setAnimationLoop(gl.xr.isPresenting ? handleXRFrame : null); 281 | 282 | // If exiting session, request frame 283 | if (!gl.xr.isPresenting) invalidate(get()); 284 | }; 285 | 286 | // WebXR session manager 287 | const xr = { 288 | connect() { 289 | gl.xr.addEventListener("sessionstart", handleSessionChange); 290 | gl.xr.addEventListener("sessionend", handleSessionChange); 291 | }, 292 | disconnect() { 293 | gl.xr.removeEventListener("sessionstart", handleSessionChange); 294 | gl.xr.removeEventListener("sessionend", handleSessionChange); 295 | }, 296 | }; 297 | 298 | // Subscribe to WebXR session events 299 | if (gl.xr) xr.connect(); 300 | 301 | return { 302 | gl, 303 | 304 | set, 305 | get, 306 | invalidate: () => invalidate(get()), 307 | advance: (timestamp: number, runGlobalEffects?: boolean) => 308 | advance(timestamp, runGlobalEffects, get()), 309 | 310 | linear, 311 | flat, 312 | scene: prepare(new THREE.Scene()), 313 | camera, 314 | controls: null, 315 | raycaster, 316 | clock, 317 | mouse: new THREE.Vector2(), 318 | 319 | frameloop, 320 | onPointerMissed, 321 | 322 | performance: { 323 | current: 1, 324 | min: 0.5, 325 | max: 1, 326 | debounce: 200, 327 | ...performance, 328 | regress: () => { 329 | const state = get(); 330 | // Clear timeout 331 | if (performanceTimeout) clearTimeout(performanceTimeout); 332 | // Set lower bound performance 333 | if (state.performance.current !== state.performance.min) 334 | setPerformanceCurrent(state.performance.min); 335 | // Go back to upper bound performance after a while unless something regresses meanwhile 336 | performanceTimeout = setTimeout( 337 | () => setPerformanceCurrent(get().performance.max), 338 | state.performance.debounce 339 | ); 340 | }, 341 | }, 342 | 343 | size: { width: 800, height: 600 }, 344 | viewport: { 345 | initialDpr, 346 | dpr: initialDpr, 347 | width: 0, 348 | height: 0, 349 | aspect: 0, 350 | distance: 0, 351 | factor: 0, 352 | getCurrentViewport, 353 | }, 354 | 355 | setSize: (width: number, height: number) => { 356 | const size = { width, height }; 357 | set((state) => ({ 358 | size, 359 | viewport: { 360 | ...state.viewport, 361 | ...getCurrentViewport(camera, defaultTarget, size), 362 | }, 363 | })); 364 | }, 365 | setDpr: (dpr: Dpr) => 366 | set((state) => ({ 367 | viewport: { ...state.viewport, dpr: calculateDpr(dpr) }, 368 | })), 369 | 370 | setFrameloop: (frameloop: "always" | "demand" | "never" = "always") => 371 | set(() => ({ frameloop })), 372 | 373 | events: { connected: false }, 374 | internal: { 375 | active: false, 376 | priority: 0, 377 | frames: 0, 378 | lastProps: props, 379 | lastEvent: { current: null }, 380 | 381 | interaction: [], 382 | hovered: new Map>(), 383 | subscribers: [], 384 | initialClick: [0, 0], 385 | initialHits: [], 386 | capturedMap: new Map(), 387 | 388 | xr, 389 | subscribe: (ref: RenderCallback, priority = 0) => { 390 | set(({ internal }) => ({ 391 | internal: { 392 | ...internal, 393 | // If this subscription was given a priority, it takes rendering into its own hands 394 | // For that reason we switch off automatic rendering and increase the manual flag 395 | // As long as this flag is positive there can be no internal rendering at all 396 | // because there could be multiple render subscriptions 397 | priority: internal.priority + (priority > 0 ? 1 : 0), 398 | // Register subscriber and sort layers from lowest to highest, meaning, 399 | // highest priority renders last (on top of the other frames) 400 | subscribers: [...internal.subscribers, { ref, priority }].sort( 401 | (a, b) => a.priority - b.priority 402 | ), 403 | }, 404 | })); 405 | return () => { 406 | set(({ internal }) => ({ 407 | internal: { 408 | ...internal, 409 | // Decrease manual flag if this subscription had a priority 410 | priority: internal.priority - (priority > 0 ? 1 : 0), 411 | // Remove subscriber from list 412 | subscribers: internal.subscribers.filter( 413 | (s) => s.ref !== ref 414 | ), 415 | }, 416 | })); 417 | }; 418 | }, 419 | }, 420 | }; 421 | }) 422 | ); 423 | 424 | const state = rootState.getState(); 425 | 426 | // Resize camera and renderer on changes to size and pixelratio 427 | let oldSize = state.size; 428 | let oldDpr = state.viewport.dpr; 429 | rootState.subscribe(() => { 430 | const { camera, size, viewport, internal } = rootState.getState(); 431 | if (size !== oldSize || viewport.dpr !== oldDpr) { 432 | // https://github.com/pmndrs/react-three-fiber/issues/92 433 | // Do not mess with the camera if it belongs to the user 434 | if ( 435 | !camera.manual && 436 | !(internal.lastProps.camera instanceof THREE.Camera) 437 | ) { 438 | if (isOrthographicCamera(camera)) { 439 | camera.left = size.width / -2; 440 | camera.right = size.width / 2; 441 | camera.top = size.height / 2; 442 | camera.bottom = size.height / -2; 443 | } else { 444 | camera.aspect = size.width / size.height; 445 | } 446 | camera.updateProjectionMatrix(); 447 | // https://github.com/pmndrs/react-three-fiber/issues/178 448 | // Update matrix world since the renderer is a frame late 449 | camera.updateMatrixWorld(); 450 | } 451 | // Update renderer 452 | gl.setPixelRatio(viewport.dpr); 453 | gl.setSize(size.width, size.height); 454 | 455 | oldSize = size; 456 | oldDpr = viewport.dpr; 457 | } 458 | }); 459 | 460 | // Update size 461 | if (size) state.setSize(size.width, size.height); 462 | 463 | // Invalidate on any change 464 | rootState.subscribe((state) => invalidate(state)); 465 | 466 | // Return root state 467 | return rootState; 468 | }; 469 | 470 | export { createThreeStore, ThreeContext }; 471 | -------------------------------------------------------------------------------- /src/core/utils.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { StoreApi as UseStore } from "zustand/vanilla"; 3 | import { log } from "../solid"; 4 | import { EventHandlers } from "./events"; 5 | import { AttachType, Instance, InstanceProps, LocalState } from "./renderer"; 6 | import { Dpr, RootState } from "./store"; 7 | 8 | export const DEFAULT = "__default"; 9 | 10 | export type DiffSet = { 11 | changes: [key: string, value: unknown, isEvent: boolean, keys: string[]][]; 12 | }; 13 | 14 | export const isDiffSet = (def: any): def is DiffSet => def && !!(def as DiffSet).changes; 15 | export type ClassConstructor = { new (): void }; 16 | 17 | export type ObjectMap = { 18 | nodes: { [name: string]: THREE.Object3D }; 19 | materials: { [name: string]: THREE.Material }; 20 | }; 21 | 22 | export function calculateDpr(dpr: Dpr) { 23 | return Array.isArray(dpr) ? Math.min(Math.max(dpr[0], window.devicePixelRatio), dpr[1]) : dpr; 24 | } 25 | 26 | /** 27 | * Picks or omits keys from an object 28 | * `omit` will filter out keys, and otherwise cherry-pick them. 29 | */ 30 | export function filterKeys< 31 | TObj extends { [key: string]: any }, 32 | TOmit extends boolean, 33 | TKey extends keyof TObj 34 | >( 35 | obj: TObj, 36 | omit: TOmit, 37 | ...keys: TKey[] 38 | ): TOmit extends true ? Omit : Pick { 39 | const keysToSelect = new Set(keys); 40 | 41 | return Object.entries(obj).reduce((acc, [key, value]) => { 42 | const shouldInclude = !omit; 43 | 44 | if (keysToSelect.has(key as TKey) === shouldInclude) { 45 | acc[key] = value; 46 | } 47 | 48 | return acc; 49 | }, {} as any); 50 | } 51 | 52 | /** 53 | * Clones an object and cherry-picks keys. 54 | */ 55 | export const pick = (obj: Partial, keys: Array) => 56 | filterKeys, false, keyof TObj>(obj, false, ...keys); 57 | 58 | /** 59 | * Clones an object and prunes or omits keys. 60 | */ 61 | export const omit = (obj: Partial, keys: Array) => 62 | filterKeys, true, keyof TObj>(obj, true, ...keys); 63 | 64 | // A collection of compare functions 65 | export const is = { 66 | obj: (a: any) => a === Object(a) && !is.arr(a) && typeof a !== "function", 67 | fun: (a: any): a is Function => typeof a === "function", 68 | str: (a: any): a is string => typeof a === "string", 69 | num: (a: any): a is number => typeof a === "number", 70 | und: (a: any) => a === void 0, 71 | arr: (a: any) => Array.isArray(a), 72 | equ(a: any, b: any) { 73 | // Wrong type or one of the two undefined, doesn't match 74 | if (typeof a !== typeof b || !!a !== !!b) return false; 75 | // Atomic, just compare a against b 76 | if (is.str(a) || is.num(a) || is.obj(a)) return a === b; 77 | // Array, shallow compare first to see if it's a match 78 | if (is.arr(a) && a == b) return true; 79 | // Last resort, go through keys 80 | let i; 81 | for (i in a) if (!(i in b)) return false; 82 | for (i in b) if (a[i] !== b[i]) return false; 83 | return is.und(i) ? a === b : true; 84 | } 85 | }; 86 | 87 | // Collects nodes and materials from a THREE.Object3D 88 | export function buildGraph(object: THREE.Object3D) { 89 | const data: ObjectMap = { nodes: {}, materials: {} }; 90 | if (object) { 91 | object.traverse((obj: any) => { 92 | if (obj.name) data.nodes[obj.name] = obj; 93 | if (obj.material && !data.materials[obj.material.name]) 94 | data.materials[obj.material.name] = obj.material; 95 | }); 96 | } 97 | return data; 98 | } 99 | 100 | // Disposes an object and all its properties 101 | export function dispose void; type?: string; [key: string]: any }>( 102 | obj: TObj 103 | ) { 104 | if (obj.dispose && obj.type !== "Scene") obj.dispose(); 105 | for (const p in obj) { 106 | (p as any).dispose?.(); 107 | delete obj[p]; 108 | } 109 | } 110 | 111 | // Each object in the scene carries a small LocalState descriptor 112 | export function prepare(object: T, state?: Partial) { 113 | const instance = object as unknown as Instance; 114 | if (state?.primitive || !instance.__r3f) { 115 | instance.__r3f = { 116 | root: null as unknown as UseStore, 117 | memoizedProps: {}, 118 | eventCount: 0, 119 | handlers: {}, 120 | objects: [], 121 | parent: null, 122 | ...state 123 | }; 124 | } 125 | return object; 126 | } 127 | 128 | function resolve(instance: Instance, key: string) { 129 | let target = instance; 130 | if (key.includes("-")) { 131 | const entries = key.split("-"); 132 | const last = entries.pop() as string; 133 | target = entries.reduce((acc, key) => acc[key], instance); 134 | return { target, key: last }; 135 | } else return { target, key }; 136 | } 137 | 138 | export function attach(parent: Instance, child: Instance, type: AttachType) { 139 | log("three", "attach", parent, child, type); 140 | if (is.str(type)) { 141 | const { target, key } = resolve(parent, type); 142 | parent.__r3f.previousAttach = target[key]; 143 | 144 | target[key] = child; 145 | } else if (is.arr(type)) { 146 | const [attach] = type; 147 | if (is.str(attach)) parent[attach](child); 148 | else if (is.fun(attach)) attach(parent, child); 149 | } 150 | } 151 | 152 | export function detach(parent: Instance, child: Instance, type: AttachType) { 153 | log("three", "detach", parent, child, type); 154 | if (is.str(type)) { 155 | const { target, key } = resolve(parent, type); 156 | if (child === parent.__r3f.previousAttach) { 157 | return; 158 | } 159 | 160 | target[key] = parent.__r3f.previousAttach; 161 | } else if (is.arr(type)) { 162 | const [, detach] = type; 163 | if (is.str(detach)) parent[detach](child); 164 | else if (is.fun(detach)) detach(parent, child); 165 | } 166 | } 167 | 168 | // Shallow check arrays, but check objects atomically 169 | function checkShallow(a: any, b: any) { 170 | if (is.arr(a) && is.equ(a, b)) return true; 171 | if (a === b) return true; 172 | return false; 173 | } 174 | 175 | // This function prepares a set of changes to be applied to the instance 176 | export function diffProps( 177 | instance: Instance, 178 | { children: cN, key: kN, ref: rN, ...props }: InstanceProps, 179 | { children: cP, key: kP, ref: rP, ...previous }: InstanceProps = {}, 180 | remove = false 181 | ): DiffSet { 182 | const localState = (instance?.__r3f ?? {}) as LocalState; 183 | const entries = Object.entries(props); 184 | const changes: [key: string, value: unknown, isEvent: boolean, keys: string[]][] = []; 185 | 186 | // Catch removed props, prepend them so they can be reset or removed 187 | if (remove) { 188 | const previousKeys = Object.keys(previous); 189 | for (let i = 0; i < previousKeys.length; i++) { 190 | if (!props.hasOwnProperty(previousKeys[i])) 191 | entries.unshift([previousKeys[i], DEFAULT + "remove"]); 192 | } 193 | } 194 | 195 | entries.forEach(([key, value]) => { 196 | // Bail out on primitive object 197 | if (instance.__r3f?.primitive && key === "object") return; 198 | // When props match bail out 199 | if (checkShallow(value, previous[key])) return; 200 | // Collect handlers and bail out 201 | if (/^on(Pointer|Click|DoubleClick|ContextMenu|Wheel)/.test(key)) 202 | return changes.push([key, value, true, []]); 203 | // Split dashed props 204 | let entries: string[] = []; 205 | if (key.includes("-")) entries = key.split("-"); 206 | changes.push([key, value, false, entries]); 207 | }); 208 | 209 | // const memoized: { [key: string]: any } = { ...props }; 210 | // if (localState.memoizedProps && localState.memoizedProps.args) 211 | // memoized.args = localState.memoizedProps.args; 212 | // if (localState.memoizedProps && localState.memoizedProps.attach) 213 | // memoized.attach = localState.memoizedProps.attach; 214 | 215 | return { changes }; 216 | } 217 | 218 | // This function applies a set of changes to the instance 219 | export function applyProps(instance: Instance, data: InstanceProps | DiffSet) { 220 | // Filter equals, events and reserved props 221 | const localState = (instance?.__r3f ?? {}) as LocalState; 222 | const root = localState.root; 223 | const rootState: RootState = root?.getState() ?? ({} as unknown as RootState); 224 | const { changes } = isDiffSet(data) ? data : diffProps(instance, data); 225 | const prevHandlers = localState.eventCount; 226 | 227 | // Prepare memoized props 228 | // if (instance.__r3f) instance.__r3f.memoizedProps = memoized; 229 | 230 | changes.forEach(change => { 231 | applyProp(instance, change, localState, rootState); 232 | }); 233 | 234 | if ( 235 | localState.parent && 236 | rootState.internal && 237 | instance.raycast && 238 | prevHandlers !== localState.eventCount 239 | ) { 240 | // Pre-emptively remove the instance from the interaction manager 241 | const index = rootState.internal.interaction.indexOf(instance as unknown as THREE.Object3D); 242 | if (index > -1) rootState.internal.interaction.splice(index, 1); 243 | // Add the instance to the interaction manager only when it has handlers 244 | if (localState.eventCount) 245 | rootState.internal.interaction.push(instance as unknown as THREE.Object3D); 246 | } 247 | 248 | // Call the update lifecycle when it is being updated, but only when it is part of the scene 249 | if (changes.length && instance.parent) updateInstance(instance); 250 | } 251 | 252 | export function applyProp( 253 | instance: Instance, 254 | [key, value, isEvent, keys]: [key: string, value: unknown, isEvent: boolean, keys: string[]], 255 | localState: LocalState = instance?.__r3f ?? ({} as unknown as LocalState), 256 | rootState: RootState = localState.root?.getState() 257 | ) { 258 | let currentInstance = instance; 259 | let targetProp = currentInstance[key]; 260 | 261 | // Revolve dashed props 262 | if (keys.length) { 263 | targetProp = keys.reduce((acc, key) => acc[key], instance); 264 | // If the target is atomic, it forces us to switch the root 265 | if (!(targetProp && targetProp.set)) { 266 | const [name, ...reverseEntries] = keys.reverse(); 267 | currentInstance = reverseEntries.reverse().reduce((acc, key) => acc[key], instance); 268 | key = name; 269 | } 270 | } 271 | 272 | // https://github.com/mrdoob/three.js/issues/21209 273 | // HMR/fast-refresh relies on the ability to cancel out props, but threejs 274 | // has no means to do this. Hence we curate a small collection of value-classes 275 | // with their respective constructor/set arguments 276 | // For removed props, try to set default values, if possible 277 | if (value === DEFAULT + "remove") { 278 | if (targetProp && targetProp.constructor) { 279 | // use the prop constructor to find the default it should be 280 | value = new targetProp.constructor(); 281 | } else if (currentInstance.constructor) { 282 | // create a blank slate of the instance and copy the particular parameter. 283 | // @ts-ignore 284 | const defaultClassCall = new currentInstance.constructor( 285 | currentInstance.__r3f.memoizedProps.args 286 | ); 287 | value = defaultClassCall[targetProp]; 288 | // destory the instance 289 | if (defaultClassCall.dispose) defaultClassCall.dispose(); 290 | // instance does not have constructor, just set it to 0 291 | } else { 292 | value = 0; 293 | } 294 | } 295 | 296 | // Deal with pointer events ... 297 | if (isEvent) { 298 | if (value) localState.handlers[key as keyof EventHandlers] = value as any; 299 | else delete localState.handlers[key as keyof EventHandlers]; 300 | localState.eventCount = Object.keys(localState.handlers).length; 301 | } 302 | 303 | // Special treatment for objects with support for set/copy, and layers 304 | else if ( 305 | targetProp && 306 | targetProp.set && 307 | (targetProp.copy || targetProp instanceof THREE.Layers) 308 | ) { 309 | // If value is an array 310 | if (Array.isArray(value)) { 311 | if (targetProp.fromArray) targetProp.fromArray(value); 312 | else targetProp.set(...value); 313 | } 314 | 315 | // Test again target.copy(class) next ... 316 | else if ( 317 | targetProp.copy && 318 | value && 319 | (value as ClassConstructor).constructor && 320 | targetProp.constructor.name === (value as ClassConstructor).constructor.name 321 | ) { 322 | targetProp.copy(value); 323 | } 324 | 325 | // If nothing else fits, just set the single value, ignore undefined 326 | // https://github.com/pmndrs/react-three-fiber/issues/274 327 | else if (value !== undefined) { 328 | const isColor = targetProp instanceof THREE.Color; 329 | // Allow setting array scalars 330 | if (!isColor && targetProp.setScalar) targetProp.setScalar(value); 331 | // Layers have no copy function, we must therefore copy the mask property 332 | else if (targetProp instanceof THREE.Layers && value instanceof THREE.Layers) 333 | targetProp.mask = value.mask; 334 | // Otherwise just set ... 335 | else targetProp.set(value); 336 | // Auto-convert sRGB colors, for now ... 337 | // https://github.com/pmndrs/react-three-fiber/issues/344 338 | if (!rootState.linear && isColor) targetProp.convertSRGBToLinear(); 339 | } 340 | // Else, just overwrite the value 341 | } else { 342 | currentInstance[key] = value; 343 | // Auto-convert sRGB textures, for now ... 344 | // https://github.com/pmndrs/react-three-fiber/issues/344 345 | if (!rootState.linear && currentInstance[key] instanceof THREE.Texture) { 346 | currentInstance[key].encoding = THREE.sRGBEncoding; 347 | } 348 | } 349 | 350 | if ( 351 | // localState.parent && 352 | rootState.internal && 353 | instance.raycast 354 | // prevHandlers !== localState.eventCount 355 | ) { 356 | // Pre-emptively remove the instance from the interaction manager 357 | const index = rootState.internal.interaction.indexOf(instance as unknown as THREE.Object3D); 358 | if (index > -1) rootState.internal.interaction.splice(index, 1); 359 | // Add the instance to the interaction manager only when it has handlers 360 | if (localState.eventCount) 361 | rootState.internal.interaction.push(instance as unknown as THREE.Object3D); 362 | } 363 | 364 | invalidateInstance(instance); 365 | return { __return: instance, key, value }; 366 | } 367 | 368 | export function invalidateInstance(instance: Instance) { 369 | const state = instance.__r3f?.root?.getState?.(); 370 | if (state && state.internal.frames === 0) state.invalidate(); 371 | } 372 | 373 | export function updateInstance(instance: Instance) { 374 | instance.onUpdate?.(instance); 375 | } 376 | 377 | export function toFirstUpper(string: string) { 378 | return `${string.charAt(0).toUpperCase()}${string.slice(1)}`; 379 | } 380 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { StateSelector, EqualityChecker } from "zustand/vanilla"; 3 | import { ThreeContext, RootState, RenderCallback } from "./core/store"; 4 | import { buildGraph, ObjectMap, is } from "./core/utils"; 5 | import { 6 | createComputed, 7 | createEffect, 8 | createMemo, 9 | createSignal, 10 | onCleanup, 11 | untrack, 12 | useContext, 13 | } from "solid-js"; 14 | 15 | export interface Loader extends THREE.Loader { 16 | load( 17 | url: string, 18 | onLoad?: (result: T) => void, 19 | onProgress?: (event: ProgressEvent) => void, 20 | onError?: (event: ErrorEvent) => void 21 | ): unknown; 22 | } 23 | 24 | export type Extensions = (loader: THREE.Loader) => void; 25 | export type LoaderResult = T extends any[] ? Loader : Loader; 26 | export type ConditionalType = Child extends Parent 27 | ? Truthy 28 | : Falsy; 29 | export type BranchingReturn = ConditionalType< 30 | T, 31 | Parent, 32 | Coerced, 33 | T 34 | >; 35 | 36 | export function useStore() { 37 | const store = useContext(ThreeContext); 38 | if (!store) throw `R3F hooks can only be used within the Canvas component!`; 39 | return store; 40 | } 41 | 42 | export function useThree( 43 | selector: StateSelector = (state) => state as unknown as U, 44 | equalityFn?: EqualityChecker 45 | ) { 46 | let store = useStore(); 47 | const [signal, setSignal] = createSignal(selector(store.getState())); 48 | 49 | createComputed(() => { 50 | let cleanup = useStore().subscribe( 51 | // @ts-expect-error 52 | selector, 53 | (v) => { 54 | // @ts-expect-error 55 | setSignal(() => v); 56 | }, 57 | equalityFn 58 | ); 59 | 60 | onCleanup(cleanup); 61 | }); 62 | 63 | return signal; 64 | } 65 | 66 | /** 67 | * Creates a signal that is updated when the given effect is run. 68 | * 69 | * @example 70 | * ```ts 71 | * const [count, setCount] = useSignal(0); 72 | * useFrame(() => { 73 | * setCount(count + 1); 74 | * }); 75 | * ``` 76 | * 77 | * @param callback - a function to run on every frame render 78 | * @param renderPriority - priority of the callback decides its order in the frameloop, higher is earlier 79 | */ 80 | export function useFrame( 81 | callback: RenderCallback, 82 | renderPriority: number = 0 83 | ): void { 84 | const subscribe = useStore().getState().internal.subscribe; 85 | let cleanup = subscribe( 86 | (t, delta) => untrack(() => callback(t, delta)), 87 | renderPriority 88 | ); 89 | 90 | onCleanup(cleanup); 91 | } 92 | 93 | export function useGraph(object: THREE.Object3D) { 94 | return createMemo(() => buildGraph(object)); 95 | } 96 | 97 | export function loadingFn( 98 | extensions?: Extensions, 99 | onProgress?: (event: ProgressEvent) => void 100 | ) { 101 | return function (Proto: new () => LoaderResult, ...input: string[]) { 102 | // Construct new loader and run extensions 103 | const loader = new Proto(); 104 | if (extensions) extensions(loader); 105 | // Go through the urls and load them 106 | return Promise.all( 107 | input.map( 108 | (input) => 109 | new Promise((res, reject) => 110 | loader.load( 111 | input, 112 | (data: any) => { 113 | if (data.scene) Object.assign(data, buildGraph(data.scene)); 114 | res(data); 115 | }, 116 | onProgress, 117 | (error) => reject(`Could not load ${input}: ${error.message}`) 118 | ) 119 | ) 120 | ) 121 | ); 122 | }; 123 | } 124 | 125 | // export function useLoader( 126 | // Proto: new () => LoaderResult, 127 | // input: U, 128 | // extensions?: Extensions, 129 | // onProgress?: (event: ProgressEvent) => void 130 | // ): U extends any[] 131 | // ? BranchingReturn[] 132 | // : BranchingReturn { 133 | // // Use suspense to load async assets 134 | // const keys = (Array.isArray(input) ? input : [input]) as string[]; 135 | // const results = suspend( 136 | // loadingFn(extensions, onProgress), 137 | // [Proto, ...keys], 138 | // { equal: is.equ } 139 | // ); 140 | // // Return the object/s 141 | // return (Array.isArray(input) ? results : results[0]) as U extends any[] 142 | // ? BranchingReturn[] 143 | // : BranchingReturn; 144 | // } 145 | 146 | // useLoader.preload = function ( 147 | // Proto: new () => LoaderResult, 148 | // input: U, 149 | // extensions?: Extensions 150 | // ) { 151 | // const keys = (Array.isArray(input) ? input : [input]) as string[]; 152 | // return preload(loadingFn(extensions), [Proto, ...keys]); 153 | // }; 154 | 155 | // useLoader.clear = function ( 156 | // Proto: new () => LoaderResult, 157 | // input: U 158 | // ) { 159 | // const keys = (Array.isArray(input) ? input : [input]) as string[]; 160 | // return clear([Proto, ...keys]); 161 | // }; 162 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./renderer"; 2 | export * from "./core/renderer"; 3 | export * from "./three-types"; 4 | import * as ThreeJSX from "./three-types"; 5 | export { ThreeJSX }; 6 | export type { 7 | Intersection, 8 | Subscription, 9 | Dpr, 10 | Size, 11 | Viewport, 12 | Camera, 13 | RenderCallback, 14 | Performance, 15 | RootState 16 | } from "./core/store"; 17 | export type { ThreeEvent, Events, EventManager, IntersectionEvent } from "./core/events"; 18 | export type { ObjectMap } from "./core/utils"; 19 | export * from "./hooks"; 20 | export * from "./web/Canvas"; 21 | export { createPointerEvents as events } from "./web/events"; 22 | export * from "./core"; 23 | -------------------------------------------------------------------------------- /src/renderer.tsx: -------------------------------------------------------------------------------- 1 | import { Accessor, Component, createMemo, JSX, splitProps, untrack } from "solid-js"; 2 | import { createThreeRenderer } from "./core/renderer"; 3 | import { roots } from "./core"; 4 | import { createSolidRenderer } from "./solid"; 5 | 6 | export const threeReconciler = createThreeRenderer(roots); 7 | export const threeRenderer = createSolidRenderer(threeReconciler); 8 | 9 | export const { 10 | render, 11 | effect, 12 | memo, 13 | createComponent, 14 | createElement, 15 | createTextNode, 16 | insertNode, 17 | insert, 18 | spread, 19 | setProp, 20 | mergeProps, 21 | use 22 | } = threeRenderer; 23 | 24 | export * from "solid-js"; 25 | 26 | type DynamicProps = T & { 27 | children?: any; 28 | component?: Component | string | keyof JSX.IntrinsicElements; 29 | }; 30 | 31 | /** 32 | * renders an arbitrary custom or native component and passes the other props 33 | * ```typescript 34 | * 35 | * ``` 36 | * @description https://www.solidjs.com/docs/latest/api#%3Cdynamic%3E 37 | */ 38 | export function Dynamic(props: DynamicProps): Accessor { 39 | const [p, others] = splitProps(props, ["component"]); 40 | return createMemo(() => { 41 | const component = p.component as Function | string; 42 | switch (typeof component) { 43 | case "function": 44 | return untrack(() => component(others)); 45 | 46 | case "string": 47 | // const isSvg = SVGElements.has(component); 48 | // const el = sharedConfig.context 49 | // ? getNextElement() 50 | let el = createElement(component); 51 | 52 | spread(el, others, true); 53 | return el; 54 | 55 | default: 56 | break; 57 | } 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /src/solid.ts: -------------------------------------------------------------------------------- 1 | import { prepare, toFirstUpper } from "./core/utils"; 2 | import { ThreeContext } from "./core/store"; 3 | import { useContext } from "solid-js"; 4 | import { Scene } from "three"; 5 | import { ThreeRenderer, catalogue, Instance } from "./core/renderer"; 6 | import { createRenderer } from "solid-js/universal"; 7 | 8 | export let DEBUG = window.location.search.indexOf("debug") > -1; 9 | 10 | export function log(renderer: string, action: any, ...options: any[]) { 11 | DEBUG && 12 | console.debug( 13 | `%c${renderer} %s %c`, 14 | "font-weight: bold", 15 | action, 16 | "font-weight: normal", 17 | ...options 18 | ); 19 | } 20 | 21 | function checkCatalogue(element: string) { 22 | return catalogue[toFirstUpper(element)] !== undefined || element === "primitive"; 23 | } 24 | 25 | export function createSolidRenderer({ 26 | createInstance, 27 | applyProp, 28 | appendChild, 29 | insertBefore, 30 | removeChild, 31 | attach 32 | }: ThreeRenderer) { 33 | return createRenderer({ 34 | // @ts-ignore 35 | createElement(element: string, args) { 36 | log("three", "createElement", element); 37 | if (element === "scene") { 38 | return prepare(new Scene() as unknown as Instance); 39 | } 40 | let root = useContext(ThreeContext); 41 | return createInstance( 42 | element, 43 | { 44 | args 45 | }, 46 | root 47 | ); 48 | }, 49 | createTextNode(value: string) { 50 | return prepare({ 51 | text: value, 52 | type: "text" 53 | }) as any; 54 | }, 55 | replaceText(textNode: Instance, value: string) { 56 | throw new Error("Cant replace text node in three"); 57 | }, 58 | setProperty(node: Instance, key: string, value: any) { 59 | log("three", "setProperty", node, key, node[key], value); 60 | if (/^on(Pointer|Click|DoubleClick|ContextMenu|Wheel)/.test(key)) 61 | return applyProp(node, [key, value, true, []]); 62 | // Split dashed props 63 | let entries: string[] = []; 64 | if (key.includes("-")) entries = key.split("-"); 65 | applyProp(node, [key, value, false, entries]); 66 | 67 | if (key === "attach" && node.__r3f.parent) { 68 | attach(node.__r3f.parent, node, value); 69 | } 70 | }, 71 | insertNode(parent, node, anchor) { 72 | log("three", "insertNode", parent, node, anchor); 73 | if (node instanceof Text) { 74 | return; 75 | } 76 | 77 | if (anchor) { 78 | insertBefore(parent, node, anchor); 79 | } else { 80 | appendChild(parent, node); 81 | } 82 | }, 83 | isTextNode(node) { 84 | return node.type === "text"; 85 | }, 86 | removeNode(parent, node) { 87 | log("three", "removeNode", parent, node); 88 | removeChild(parent, node, true); 89 | }, 90 | 91 | getParentNode(node) { 92 | log("three", "getParentNode", node); 93 | return node.__r3f.parent as unknown as Instance; 94 | }, 95 | getFirstChild(node) { 96 | log("three", "getFirstChild", node); 97 | return node.__r3f.objects?.length ? node.__r3f.objects[0] : node.children[0]; 98 | }, 99 | getNextSibling(node) { 100 | log("three", "getNextSibling", node); 101 | return node.nextSibling; 102 | } 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /src/three-types.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { EventHandlers } from "./core/events"; 3 | import { AttachType } from "./core/renderer"; 4 | import { JSX } from "solid-js"; 5 | import { BufferGeometry, Material } from "three"; 6 | export type NonFunctionKeys = { 7 | [K in keyof T]: T[K] extends Function ? never : K; 8 | }[keyof T]; 9 | export type Overwrite = Omit> & O; 10 | 11 | /** 12 | * If **T** contains a constructor, @see ConstructorParameters must be used, otherwise **T**. 13 | */ 14 | type Args = T extends new (...args: any) => any 15 | ? ConstructorParameters 16 | : T; 17 | 18 | export type Euler = THREE.Euler | Parameters; 19 | export type Matrix4 = THREE.Matrix4 | Parameters; 20 | export type Vector2 = 21 | | THREE.Vector2 22 | | Parameters 23 | | Parameters[0]; 24 | export type Vector3 = 25 | | THREE.Vector3 26 | | Parameters 27 | | Parameters[0]; 28 | export type Vector4 = 29 | | THREE.Vector4 30 | | Parameters 31 | | Parameters[0]; 32 | export type Color = 33 | | ConstructorParameters 34 | | THREE.Color 35 | | number 36 | | string; // Parameters will not work here because of multiple function signatures in three.js types 37 | export type ColorArray = typeof THREE.Color | Parameters; 38 | export type Layers = THREE.Layers | Parameters[0]; 39 | export type Quaternion = THREE.Quaternion | Parameters; 40 | 41 | export type AttachCallback = 42 | | string 43 | | ((child: any, parentInstance: any) => void); 44 | 45 | export interface NodeProps { 46 | attach?: AttachType; 47 | /** Constructor arguments */ 48 | args?: Args

; 49 | children?: any; 50 | ref?: T | ((instance: T) => void); 51 | // key?: React.Key; 52 | onUpdate?: (self: T) => void; 53 | } 54 | 55 | export type Node = Overwrite, NodeProps>; 56 | 57 | export type Object3DNode = Overwrite< 58 | Node, 59 | { 60 | position?: Vector3; 61 | up?: Vector3; 62 | scale?: Vector3; 63 | rotation?: Euler; 64 | matrix?: Matrix4; 65 | quaternion?: Quaternion; 66 | layers?: Layers; 67 | dispose?: (() => void) | null; 68 | 69 | geometry?: JSX.Element | BufferGeometry | null; 70 | material?: JSX.Element | Material | null; 71 | } 72 | > & 73 | EventHandlers; 74 | 75 | export type BufferGeometryNode = Overwrite< 76 | Node, 77 | {} 78 | >; 79 | export type MaterialNode = Overwrite< 80 | Node, 81 | { color?: Color } 82 | >; 83 | export type LightNode = Overwrite< 84 | Object3DNode, 85 | { color?: Color } 86 | >; 87 | 88 | // export type AudioProps = Object3DNode 89 | export type AudioListenerProps = Object3DNode< 90 | THREE.AudioListener, 91 | typeof THREE.AudioListener 92 | >; 93 | export type PositionalAudioProps = Object3DNode< 94 | THREE.PositionalAudio, 95 | typeof THREE.PositionalAudio 96 | >; 97 | 98 | export type MeshProps = Object3DNode; 99 | export type InstancedMeshProps = Object3DNode< 100 | THREE.InstancedMesh, 101 | typeof THREE.InstancedMesh 102 | >; 103 | export type SceneProps = Object3DNode; 104 | export type SpriteProps = Object3DNode; 105 | export type LODProps = Object3DNode; 106 | export type SkinnedMeshProps = Object3DNode< 107 | THREE.SkinnedMesh, 108 | typeof THREE.SkinnedMesh 109 | >; 110 | 111 | export type SkeletonProps = Object3DNode; 112 | export type BoneProps = Object3DNode; 113 | export type LineSegmentsProps = Object3DNode< 114 | THREE.LineSegments, 115 | typeof THREE.LineSegments 116 | >; 117 | export type LineLoopProps = Object3DNode; 118 | // export type LineProps = Object3DNode 119 | export type PointsProps = Object3DNode; 120 | export type GroupProps = Object3DNode; 121 | 122 | export type CameraProps = Object3DNode; 123 | export type PerspectiveCameraProps = Object3DNode< 124 | THREE.PerspectiveCamera, 125 | typeof THREE.PerspectiveCamera 126 | >; 127 | export type OrthographicCameraProps = Object3DNode< 128 | THREE.OrthographicCamera, 129 | typeof THREE.OrthographicCamera 130 | >; 131 | export type CubeCameraProps = Object3DNode< 132 | THREE.CubeCamera, 133 | typeof THREE.CubeCamera 134 | >; 135 | export type ArrayCameraProps = Object3DNode< 136 | THREE.ArrayCamera, 137 | typeof THREE.ArrayCamera 138 | >; 139 | 140 | export type InstancedBufferGeometryProps = BufferGeometryNode< 141 | THREE.InstancedBufferGeometry, 142 | typeof THREE.InstancedBufferGeometry 143 | >; 144 | export type BufferGeometryProps = BufferGeometryNode< 145 | THREE.BufferGeometry, 146 | typeof THREE.BufferGeometry 147 | >; 148 | export type BoxBufferGeometryProps = BufferGeometryNode< 149 | THREE.BoxBufferGeometry, 150 | typeof THREE.BoxBufferGeometry 151 | >; 152 | export type CircleBufferGeometryProps = BufferGeometryNode< 153 | THREE.CircleBufferGeometry, 154 | typeof THREE.CircleBufferGeometry 155 | >; 156 | export type ConeBufferGeometryProps = BufferGeometryNode< 157 | THREE.ConeBufferGeometry, 158 | typeof THREE.ConeBufferGeometry 159 | >; 160 | export type CylinderBufferGeometryProps = BufferGeometryNode< 161 | THREE.CylinderBufferGeometry, 162 | typeof THREE.CylinderBufferGeometry 163 | >; 164 | export type DodecahedronBufferGeometryProps = BufferGeometryNode< 165 | THREE.DodecahedronBufferGeometry, 166 | typeof THREE.DodecahedronBufferGeometry 167 | >; 168 | export type ExtrudeBufferGeometryProps = BufferGeometryNode< 169 | THREE.ExtrudeBufferGeometry, 170 | typeof THREE.ExtrudeBufferGeometry 171 | >; 172 | export type IcosahedronBufferGeometryProps = BufferGeometryNode< 173 | THREE.IcosahedronBufferGeometry, 174 | typeof THREE.IcosahedronBufferGeometry 175 | >; 176 | export type LatheBufferGeometryProps = BufferGeometryNode< 177 | THREE.LatheBufferGeometry, 178 | typeof THREE.LatheBufferGeometry 179 | >; 180 | export type OctahedronBufferGeometryProps = BufferGeometryNode< 181 | THREE.OctahedronBufferGeometry, 182 | typeof THREE.OctahedronBufferGeometry 183 | >; 184 | export type PlaneBufferGeometryProps = BufferGeometryNode< 185 | THREE.PlaneBufferGeometry, 186 | typeof THREE.PlaneBufferGeometry 187 | >; 188 | export type PolyhedronBufferGeometryProps = BufferGeometryNode< 189 | THREE.PolyhedronBufferGeometry, 190 | typeof THREE.PolyhedronBufferGeometry 191 | >; 192 | export type RingBufferGeometryProps = BufferGeometryNode< 193 | THREE.RingBufferGeometry, 194 | typeof THREE.RingBufferGeometry 195 | >; 196 | export type ShapeBufferGeometryProps = BufferGeometryNode< 197 | THREE.ShapeBufferGeometry, 198 | typeof THREE.ShapeBufferGeometry 199 | >; 200 | export type SphereBufferGeometryProps = BufferGeometryNode< 201 | THREE.SphereBufferGeometry, 202 | typeof THREE.SphereBufferGeometry 203 | >; 204 | export type TetrahedronBufferGeometryProps = BufferGeometryNode< 205 | THREE.TetrahedronBufferGeometry, 206 | typeof THREE.TetrahedronBufferGeometry 207 | >; 208 | export type TorusBufferGeometryProps = BufferGeometryNode< 209 | THREE.TorusBufferGeometry, 210 | typeof THREE.TorusBufferGeometry 211 | >; 212 | export type TorusKnotBufferGeometryProps = BufferGeometryNode< 213 | THREE.TorusKnotBufferGeometry, 214 | typeof THREE.TorusKnotBufferGeometry 215 | >; 216 | export type TubeBufferGeometryProps = BufferGeometryNode< 217 | THREE.TubeBufferGeometry, 218 | typeof THREE.TubeBufferGeometry 219 | >; 220 | export type WireframeGeometryProps = BufferGeometryNode< 221 | THREE.WireframeGeometry, 222 | typeof THREE.WireframeGeometry 223 | >; 224 | export type TetrahedronGeometryProps = BufferGeometryNode< 225 | THREE.TetrahedronGeometry, 226 | typeof THREE.TetrahedronGeometry 227 | >; 228 | export type OctahedronGeometryProps = BufferGeometryNode< 229 | THREE.OctahedronGeometry, 230 | typeof THREE.OctahedronGeometry 231 | >; 232 | export type IcosahedronGeometryProps = BufferGeometryNode< 233 | THREE.IcosahedronGeometry, 234 | typeof THREE.IcosahedronGeometry 235 | >; 236 | export type DodecahedronGeometryProps = BufferGeometryNode< 237 | THREE.DodecahedronGeometry, 238 | typeof THREE.DodecahedronGeometry 239 | >; 240 | export type PolyhedronGeometryProps = BufferGeometryNode< 241 | THREE.PolyhedronGeometry, 242 | typeof THREE.PolyhedronGeometry 243 | >; 244 | export type TubeGeometryProps = BufferGeometryNode< 245 | THREE.TubeGeometry, 246 | typeof THREE.TubeGeometry 247 | >; 248 | export type TorusKnotGeometryProps = BufferGeometryNode< 249 | THREE.TorusKnotGeometry, 250 | typeof THREE.TorusKnotGeometry 251 | >; 252 | export type TorusGeometryProps = BufferGeometryNode< 253 | THREE.TorusGeometry, 254 | typeof THREE.TorusGeometry 255 | >; 256 | export type SphereGeometryProps = BufferGeometryNode< 257 | THREE.SphereGeometry, 258 | typeof THREE.SphereGeometry 259 | >; 260 | export type RingGeometryProps = BufferGeometryNode< 261 | THREE.RingGeometry, 262 | typeof THREE.RingGeometry 263 | >; 264 | export type PlaneGeometryProps = BufferGeometryNode< 265 | THREE.PlaneGeometry, 266 | typeof THREE.PlaneGeometry 267 | >; 268 | export type LatheGeometryProps = BufferGeometryNode< 269 | THREE.LatheGeometry, 270 | typeof THREE.LatheGeometry 271 | >; 272 | export type ShapeGeometryProps = BufferGeometryNode< 273 | THREE.ShapeGeometry, 274 | typeof THREE.ShapeGeometry 275 | >; 276 | export type ExtrudeGeometryProps = BufferGeometryNode< 277 | THREE.ExtrudeGeometry, 278 | typeof THREE.ExtrudeGeometry 279 | >; 280 | export type EdgesGeometryProps = BufferGeometryNode< 281 | THREE.EdgesGeometry, 282 | typeof THREE.EdgesGeometry 283 | >; 284 | export type ConeGeometryProps = BufferGeometryNode< 285 | THREE.ConeGeometry, 286 | typeof THREE.ConeGeometry 287 | >; 288 | export type CylinderGeometryProps = BufferGeometryNode< 289 | THREE.CylinderGeometry, 290 | typeof THREE.CylinderGeometry 291 | >; 292 | export type CircleGeometryProps = BufferGeometryNode< 293 | THREE.CircleGeometry, 294 | typeof THREE.CircleGeometry 295 | >; 296 | export type BoxGeometryProps = BufferGeometryNode< 297 | THREE.BoxGeometry, 298 | typeof THREE.BoxGeometry 299 | >; 300 | 301 | export type MaterialProps = MaterialNode< 302 | THREE.Material, 303 | [THREE.MaterialParameters] 304 | >; 305 | export type ShadowMaterialProps = MaterialNode< 306 | THREE.ShadowMaterial, 307 | [THREE.ShaderMaterialParameters] 308 | >; 309 | export type SpriteMaterialProps = MaterialNode< 310 | THREE.SpriteMaterial, 311 | [THREE.SpriteMaterialParameters] 312 | >; 313 | export type RawShaderMaterialProps = MaterialNode< 314 | THREE.RawShaderMaterial, 315 | [THREE.ShaderMaterialParameters] 316 | >; 317 | export type ShaderMaterialProps = MaterialNode< 318 | THREE.ShaderMaterial, 319 | [THREE.ShaderMaterialParameters] 320 | >; 321 | export type PointsMaterialProps = MaterialNode< 322 | THREE.PointsMaterial, 323 | [THREE.PointsMaterialParameters] 324 | >; 325 | export type MeshPhysicalMaterialProps = MaterialNode< 326 | THREE.MeshPhysicalMaterial, 327 | [THREE.MeshPhysicalMaterialParameters] 328 | >; 329 | export type MeshStandardMaterialProps = MaterialNode< 330 | THREE.MeshStandardMaterial, 331 | [THREE.MeshStandardMaterialParameters] 332 | >; 333 | export type MeshPhongMaterialProps = MaterialNode< 334 | THREE.MeshPhongMaterial, 335 | [THREE.MeshPhongMaterialParameters] 336 | >; 337 | export type MeshToonMaterialProps = MaterialNode< 338 | THREE.MeshToonMaterial, 339 | [THREE.MeshToonMaterialParameters] 340 | >; 341 | export type MeshNormalMaterialProps = MaterialNode< 342 | THREE.MeshNormalMaterial, 343 | [THREE.MeshNormalMaterialParameters] 344 | >; 345 | export type MeshLambertMaterialProps = MaterialNode< 346 | THREE.MeshLambertMaterial, 347 | [THREE.MeshLambertMaterialParameters] 348 | >; 349 | export type MeshDepthMaterialProps = MaterialNode< 350 | THREE.MeshDepthMaterial, 351 | [THREE.MeshDepthMaterialParameters] 352 | >; 353 | export type MeshDistanceMaterialProps = MaterialNode< 354 | THREE.MeshDistanceMaterial, 355 | [THREE.MeshDistanceMaterialParameters] 356 | >; 357 | export type MeshBasicMaterialProps = MaterialNode< 358 | THREE.MeshBasicMaterial, 359 | [THREE.MeshBasicMaterialParameters] 360 | >; 361 | export type MeshMatcapMaterialProps = MaterialNode< 362 | THREE.MeshMatcapMaterial, 363 | [THREE.MeshMatcapMaterialParameters] 364 | >; 365 | export type LineDashedMaterialProps = MaterialNode< 366 | THREE.LineDashedMaterial, 367 | [THREE.LineDashedMaterialParameters] 368 | >; 369 | export type LineBasicMaterialProps = MaterialNode< 370 | THREE.LineBasicMaterial, 371 | [THREE.LineBasicMaterialParameters] 372 | >; 373 | 374 | export type PrimitiveProps = { object: any } & { [properties: string]: any }; 375 | 376 | export type LightProps = LightNode; 377 | export type SpotLightShadowProps = Node< 378 | THREE.SpotLightShadow, 379 | typeof THREE.SpotLightShadow 380 | >; 381 | export type SpotLightProps = LightNode; 382 | export type PointLightProps = LightNode< 383 | THREE.PointLight, 384 | typeof THREE.PointLight 385 | >; 386 | export type RectAreaLightProps = LightNode< 387 | THREE.RectAreaLight, 388 | typeof THREE.RectAreaLight 389 | >; 390 | export type HemisphereLightProps = LightNode< 391 | THREE.HemisphereLight, 392 | typeof THREE.HemisphereLight 393 | >; 394 | export type DirectionalLightShadowProps = Node< 395 | THREE.DirectionalLightShadow, 396 | typeof THREE.DirectionalLightShadow 397 | >; 398 | export type DirectionalLightProps = LightNode< 399 | THREE.DirectionalLight, 400 | typeof THREE.DirectionalLight 401 | >; 402 | export type AmbientLightProps = LightNode< 403 | THREE.AmbientLight, 404 | typeof THREE.AmbientLight 405 | >; 406 | export type LightShadowProps = Node< 407 | THREE.LightShadow, 408 | typeof THREE.LightShadow 409 | >; 410 | export type AmbientLightProbeProps = LightNode< 411 | THREE.AmbientLightProbe, 412 | typeof THREE.AmbientLightProbe 413 | >; 414 | export type HemisphereLightProbeProps = LightNode< 415 | THREE.HemisphereLightProbe, 416 | typeof THREE.HemisphereLightProbe 417 | >; 418 | export type LightProbeProps = LightNode< 419 | THREE.LightProbe, 420 | typeof THREE.LightProbe 421 | >; 422 | 423 | export type SpotLightHelperProps = Object3DNode< 424 | THREE.SpotLightHelper, 425 | typeof THREE.SpotLightHelper 426 | >; 427 | export type SkeletonHelperProps = Object3DNode< 428 | THREE.SkeletonHelper, 429 | typeof THREE.SkeletonHelper 430 | >; 431 | export type PointLightHelperProps = Object3DNode< 432 | THREE.PointLightHelper, 433 | typeof THREE.PointLightHelper 434 | >; 435 | export type HemisphereLightHelperProps = Object3DNode< 436 | THREE.HemisphereLightHelper, 437 | typeof THREE.HemisphereLightHelper 438 | >; 439 | export type GridHelperProps = Object3DNode< 440 | THREE.GridHelper, 441 | typeof THREE.GridHelper 442 | >; 443 | export type PolarGridHelperProps = Object3DNode< 444 | THREE.PolarGridHelper, 445 | typeof THREE.PolarGridHelper 446 | >; 447 | export type DirectionalLightHelperProps = Object3DNode< 448 | THREE.DirectionalLightHelper, 449 | typeof THREE.DirectionalLightHelper 450 | >; 451 | export type CameraHelperProps = Object3DNode< 452 | THREE.CameraHelper, 453 | typeof THREE.CameraHelper 454 | >; 455 | export type BoxHelperProps = Object3DNode< 456 | THREE.BoxHelper, 457 | typeof THREE.BoxHelper 458 | >; 459 | export type Box3HelperProps = Object3DNode< 460 | THREE.Box3Helper, 461 | typeof THREE.Box3Helper 462 | >; 463 | export type PlaneHelperProps = Object3DNode< 464 | THREE.PlaneHelper, 465 | typeof THREE.PlaneHelper 466 | >; 467 | export type ArrowHelperProps = Object3DNode< 468 | THREE.ArrowHelper, 469 | typeof THREE.ArrowHelper 470 | >; 471 | export type AxesHelperProps = Object3DNode< 472 | THREE.AxesHelper, 473 | typeof THREE.AxesHelper 474 | >; 475 | 476 | export type TextureProps = Node; 477 | export type VideoTextureProps = Node< 478 | THREE.VideoTexture, 479 | typeof THREE.VideoTexture 480 | >; 481 | export type DataTextureProps = Node< 482 | THREE.DataTexture, 483 | typeof THREE.DataTexture 484 | >; 485 | export type DataTexture3DProps = Node< 486 | THREE.DataTexture3D, 487 | typeof THREE.DataTexture3D 488 | >; 489 | export type CompressedTextureProps = Node< 490 | THREE.CompressedTexture, 491 | typeof THREE.CompressedTexture 492 | >; 493 | export type CubeTextureProps = Node< 494 | THREE.CubeTexture, 495 | typeof THREE.CubeTexture 496 | >; 497 | export type CanvasTextureProps = Node< 498 | THREE.CanvasTexture, 499 | typeof THREE.CanvasTexture 500 | >; 501 | export type DepthTextureProps = Node< 502 | THREE.DepthTexture, 503 | typeof THREE.DepthTexture 504 | >; 505 | 506 | export type RaycasterProps = Node; 507 | export type Vector2Props = Node; 508 | export type Vector3Props = Node; 509 | export type Vector4Props = Node; 510 | export type EulerProps = Node; 511 | export type Matrix3Props = Node; 512 | export type Matrix4Props = Node; 513 | export type QuaternionProps = Node; 514 | export type BufferAttributeProps = Node< 515 | THREE.BufferAttribute, 516 | typeof THREE.BufferAttribute 517 | >; 518 | export type Float32BufferAttributeProps = Node< 519 | THREE.Float32BufferAttribute, 520 | typeof THREE.Float32BufferAttribute 521 | >; 522 | export type InstancedBufferAttributeProps = Node< 523 | THREE.InstancedBufferAttribute, 524 | typeof THREE.InstancedBufferAttribute 525 | >; 526 | export type ColorProps = Node; 527 | export type FogProps = Node; 528 | export type FogExp2Props = Node; 529 | export type ShapeProps = Node; 530 | 531 | declare module "solid-js" { 532 | // eslint-disable-next-line @typescript-eslint/no-namespace 533 | namespace JSX { 534 | interface IntrinsicElements { 535 | // `audio` works but conflicts with @types/react. Try using Audio from react-three-fiber/components instead 536 | // audio: AudioProps 537 | audioListener: AudioListenerProps; 538 | positionalAudio: PositionalAudioProps; 539 | 540 | mesh: MeshProps; 541 | instancedMesh: InstancedMeshProps; 542 | scene: SceneProps; 543 | sprite: SpriteProps; 544 | lOD: LODProps; 545 | skinnedMesh: SkinnedMeshProps; 546 | skeleton: SkeletonProps; 547 | bone: BoneProps; 548 | lineSegments: LineSegmentsProps; 549 | lineLoop: LineLoopProps; 550 | // see `audio` 551 | // line: LineProps 552 | points: PointsProps; 553 | group: GroupProps; 554 | 555 | // cameras 556 | camera: CameraProps; 557 | perspectiveCamera: PerspectiveCameraProps; 558 | orthographicCamera: OrthographicCameraProps; 559 | cubeCamera: CubeCameraProps; 560 | arrayCamera: ArrayCameraProps; 561 | 562 | // geometry 563 | instancedBufferGeometry: InstancedBufferGeometryProps; 564 | bufferGeometry: BufferGeometryProps; 565 | boxBufferGeometry: BoxBufferGeometryProps; 566 | circleBufferGeometry: CircleBufferGeometryProps; 567 | coneBufferGeometry: ConeBufferGeometryProps; 568 | cylinderBufferGeometry: CylinderBufferGeometryProps; 569 | dodecahedronBufferGeometry: DodecahedronBufferGeometryProps; 570 | extrudeBufferGeometry: ExtrudeBufferGeometryProps; 571 | icosahedronBufferGeometry: IcosahedronBufferGeometryProps; 572 | latheBufferGeometry: LatheBufferGeometryProps; 573 | octahedronBufferGeometry: OctahedronBufferGeometryProps; 574 | planeBufferGeometry: PlaneBufferGeometryProps; 575 | polyhedronBufferGeometry: PolyhedronBufferGeometryProps; 576 | ringBufferGeometry: RingBufferGeometryProps; 577 | shapeBufferGeometry: ShapeBufferGeometryProps; 578 | sphereBufferGeometry: SphereBufferGeometryProps; 579 | tetrahedronBufferGeometry: TetrahedronBufferGeometryProps; 580 | torusBufferGeometry: TorusBufferGeometryProps; 581 | torusKnotBufferGeometry: TorusKnotBufferGeometryProps; 582 | tubeBufferGeometry: TubeBufferGeometryProps; 583 | wireframeGeometry: WireframeGeometryProps; 584 | tetrahedronGeometry: TetrahedronGeometryProps; 585 | octahedronGeometry: OctahedronGeometryProps; 586 | icosahedronGeometry: IcosahedronGeometryProps; 587 | dodecahedronGeometry: DodecahedronGeometryProps; 588 | polyhedronGeometry: PolyhedronGeometryProps; 589 | tubeGeometry: TubeGeometryProps; 590 | torusKnotGeometry: TorusKnotGeometryProps; 591 | torusGeometry: TorusGeometryProps; 592 | sphereGeometry: SphereGeometryProps; 593 | ringGeometry: RingGeometryProps; 594 | planeGeometry: PlaneGeometryProps; 595 | latheGeometry: LatheGeometryProps; 596 | shapeGeometry: ShapeGeometryProps; 597 | extrudeGeometry: ExtrudeGeometryProps; 598 | edgesGeometry: EdgesGeometryProps; 599 | coneGeometry: ConeGeometryProps; 600 | cylinderGeometry: CylinderGeometryProps; 601 | circleGeometry: CircleGeometryProps; 602 | boxGeometry: BoxGeometryProps; 603 | 604 | // materials 605 | material: MaterialProps; 606 | shadowMaterial: ShadowMaterialProps; 607 | spriteMaterial: SpriteMaterialProps; 608 | rawShaderMaterial: RawShaderMaterialProps; 609 | shaderMaterial: ShaderMaterialProps; 610 | pointsMaterial: PointsMaterialProps; 611 | meshPhysicalMaterial: MeshPhysicalMaterialProps; 612 | meshStandardMaterial: MeshStandardMaterialProps; 613 | meshPhongMaterial: MeshPhongMaterialProps; 614 | meshToonMaterial: MeshToonMaterialProps; 615 | meshNormalMaterial: MeshNormalMaterialProps; 616 | meshLambertMaterial: MeshLambertMaterialProps; 617 | meshDepthMaterial: MeshDepthMaterialProps; 618 | meshDistanceMaterial: MeshDistanceMaterialProps; 619 | meshBasicMaterial: MeshBasicMaterialProps; 620 | meshMatcapMaterial: MeshMatcapMaterialProps; 621 | lineDashedMaterial: LineDashedMaterialProps; 622 | lineBasicMaterial: LineBasicMaterialProps; 623 | 624 | // primitive 625 | primitive: PrimitiveProps; 626 | 627 | // lights and other 628 | light: LightProps; 629 | spotLightShadow: SpotLightShadowProps; 630 | spotLight: SpotLightProps; 631 | pointLight: PointLightProps; 632 | rectAreaLight: RectAreaLightProps; 633 | hemisphereLight: HemisphereLightProps; 634 | directionalLightShadow: DirectionalLightShadowProps; 635 | directionalLight: DirectionalLightProps; 636 | ambientLight: AmbientLightProps; 637 | lightShadow: LightShadowProps; 638 | ambientLightProbe: AmbientLightProbeProps; 639 | hemisphereLightProbe: HemisphereLightProbeProps; 640 | lightProbe: LightProbeProps; 641 | 642 | // helpers 643 | spotLightHelper: SpotLightHelperProps; 644 | skeletonHelper: SkeletonHelperProps; 645 | pointLightHelper: PointLightHelperProps; 646 | hemisphereLightHelper: HemisphereLightHelperProps; 647 | gridHelper: GridHelperProps; 648 | polarGridHelper: PolarGridHelperProps; 649 | directionalLightHelper: DirectionalLightHelperProps; 650 | cameraHelper: CameraHelperProps; 651 | boxHelper: BoxHelperProps; 652 | box3Helper: Box3HelperProps; 653 | planeHelper: PlaneHelperProps; 654 | arrowHelper: ArrowHelperProps; 655 | axesHelper: AxesHelperProps; 656 | 657 | // textures 658 | texture: TextureProps; 659 | videoTexture: VideoTextureProps; 660 | dataTexture: DataTextureProps; 661 | dataTexture3D: DataTexture3DProps; 662 | compressedTexture: CompressedTextureProps; 663 | cubeTexture: CubeTextureProps; 664 | canvasTexture: CanvasTextureProps; 665 | depthTexture: DepthTextureProps; 666 | 667 | // misc 668 | raycaster: RaycasterProps; 669 | vector2: Vector2Props; 670 | vector3: Vector3Props; 671 | vector4: Vector4Props; 672 | euler: EulerProps; 673 | matrix3: Matrix3Props; 674 | matrix4: Matrix4Props; 675 | quaternion: QuaternionProps; 676 | bufferAttribute: BufferAttributeProps; 677 | float32BufferAttribute: Float32BufferAttributeProps; 678 | instancedBufferAttribute: InstancedBufferAttributeProps; 679 | color: ColorProps; 680 | fog: FogProps; 681 | fogExp2: FogExp2Props; 682 | shape: ShapeProps; 683 | } 684 | } 685 | } 686 | -------------------------------------------------------------------------------- /src/web/Canvas.tsx: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { extend, createThreeRoot, RenderProps } from "../core"; 3 | import { createPointerEvents } from "./events"; 4 | import { RootState, ThreeContext } from "../core/store"; 5 | import { Accessor, createEffect, onCleanup, JSX, mergeProps } from "solid-js"; 6 | import { insert } from "../renderer"; 7 | import { Instance } from "../core/renderer"; 8 | import { StoreApi } from "zustand/vanilla"; 9 | import { EventManager } from "../core/events"; 10 | import { log } from "../solid"; 11 | import { threeReconciler } from ".."; 12 | 13 | extend(THREE); 14 | 15 | export interface Props extends Omit, "size" | "events"> { 16 | // , 17 | // HTMLAttributes 18 | children: JSX.Element; 19 | fallback?: JSX.Element; 20 | // resize?: ResizeOptions 21 | events?: (store: StoreApi) => EventManager; 22 | id?: string; 23 | class?: string; 24 | height?: string; 25 | width?: string; 26 | tabIndex?: number; 27 | // style?: CSSProperties; 28 | } 29 | 30 | // type SetBlock = false | Promise | null; 31 | 32 | // const CANVAS_PROPS: Array = [ 33 | // "gl", 34 | // "events", 35 | // "shadows", 36 | // "linear", 37 | // "flat", 38 | // "orthographic", 39 | // "frameloop", 40 | // "dpr", 41 | // "performance", 42 | // "clock", 43 | // "raycaster", 44 | // "camera", 45 | // "onPointerMissed", 46 | // "onCreated", 47 | // ]; 48 | 49 | export function Canvas(props: Props) { 50 | props = mergeProps( 51 | { 52 | height: "100vh", 53 | width: "100vw" 54 | }, 55 | props 56 | ); 57 | 58 | let canvas: HTMLCanvasElement = () as any; 59 | let containerRef: HTMLDivElement = ( 60 |

71 | {canvas} 72 |
73 | ) as any; 74 | 75 | const root = createThreeRoot(canvas, { 76 | events: createPointerEvents, 77 | size: containerRef.getBoundingClientRect(), 78 | camera: props.camera, 79 | shadows: props.shadows, 80 | onPointerMissed: props.onPointerMissed 81 | // TODO: add the rest of the canvas props! 82 | }); 83 | 84 | new ResizeObserver(entries => { 85 | if (entries[0]?.target !== containerRef) return; 86 | root.getState().setSize(entries[0].contentRect.width, entries[0].contentRect.height); 87 | }).observe(containerRef); 88 | 89 | insert( 90 | root.getState().scene as unknown as Instance, 91 | ( 92 | ( 93 | {props.children} 94 | ) as unknown as Accessor 95 | )() 96 | ); 97 | 98 | onCleanup(() => { 99 | log("three", "cleanup"); 100 | threeReconciler.removeRecursive( 101 | root.getState().scene.children as any, 102 | root.getState().scene as any, 103 | true 104 | ); 105 | root.getState().scene.clear(); 106 | }); 107 | 108 | return containerRef; 109 | } 110 | -------------------------------------------------------------------------------- /src/web/events.ts: -------------------------------------------------------------------------------- 1 | import { StoreApi as UseStore } from "zustand/vanilla"; 2 | import { RootState } from "../core/store"; 3 | import { EventManager, Events, createEvents } from "../core/events"; 4 | 5 | const DOM_EVENTS = { 6 | onClick: ["click", false], 7 | onContextMenu: ["contextmenu", false], 8 | onDoubleClick: ["dblclick", false], 9 | onWheel: ["wheel", true], 10 | onPointerDown: ["pointerdown", true], 11 | onPointerUp: ["pointerup", true], 12 | onPointerLeave: ["pointerleave", true], 13 | onPointerMove: ["pointermove", true], 14 | onPointerCancel: ["pointercancel", true], 15 | onLostPointerCapture: ["lostpointercapture", true], 16 | } as const; 17 | 18 | export function createPointerEvents( 19 | store: UseStore 20 | ): EventManager { 21 | const { handlePointer } = createEvents(store); 22 | 23 | return { 24 | connected: false, 25 | handlers: Object.keys(DOM_EVENTS).reduce( 26 | (acc, key) => ({ ...acc, [key]: handlePointer(key) }), 27 | {} 28 | ) as unknown as Events, 29 | connect: (target: HTMLElement) => { 30 | const { set, events } = store.getState(); 31 | events.disconnect?.(); 32 | set((state) => ({ events: { ...state.events, connected: target } })); 33 | Object.entries(events?.handlers ?? []).forEach(([name, event]) => { 34 | const [eventName, passive] = 35 | DOM_EVENTS[name as keyof typeof DOM_EVENTS]; 36 | target.addEventListener(eventName, event, { passive }); 37 | }); 38 | }, 39 | disconnect: () => { 40 | const { set, events } = store.getState(); 41 | if (events.connected) { 42 | Object.entries(events.handlers ?? []).forEach(([name, event]) => { 43 | if (events && events.connected instanceof HTMLElement) { 44 | const [eventName] = DOM_EVENTS[name as keyof typeof DOM_EVENTS]; 45 | events.connected.removeEventListener(eventName, event); 46 | } 47 | }); 48 | set((state) => ({ events: { ...state.events, connected: false } })); 49 | } 50 | }, 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vinxi/tsconfig/solid-library.json", 3 | "include": ["./src"], 4 | "compilerOptions": { 5 | "outDir": "./types", 6 | "rootDir": "./src", 7 | "target": "esnext", 8 | "lib": ["dom", "esnext"] 9 | }, 10 | "exclude": ["dist", "build", "node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import solidPlugin from "vite-plugin-solid"; 3 | import inspect from "vite-plugin-inspect"; 4 | import { DOMElements, SVGElements } from "solid-js/web/dist/dev.cjs"; 5 | export default defineConfig(async (mode) => ({ 6 | build: 7 | process.env.BUILD_MODE === "lib" 8 | ? { 9 | lib: { 10 | entry: "./src/index.tsx", 11 | formats: ["es", "cjs", "umd"], 12 | fileName: "index", 13 | name: "SolidThree", 14 | }, 15 | minify: false, 16 | rollupOptions: { 17 | external: [ 18 | "solid-js", 19 | "solid-js/web", 20 | "solid-js/store", 21 | "three", 22 | "zustand", 23 | "zustand/vanilla", 24 | ], 25 | }, 26 | polyfillDynamicImport: false, 27 | } 28 | : {}, 29 | plugins: [ 30 | // mdx({ 31 | // transformMDX: (code) => { 32 | // return code.replace(/<\!--[a-zA-Z\.\s]+-->/g, ` `); 33 | // }, 34 | // xdm: { 35 | // remarkPlugins: [(await import("remark-gfm")).default], 36 | // }, 37 | // }), 38 | // for the playground, we need to be able to use the renderer from the src itself 39 | solidPlugin({ 40 | solid: { 41 | moduleName: "solid-js/web", 42 | // @ts-ignore 43 | generate: "dynamic", 44 | renderers: [ 45 | { 46 | name: "dom", 47 | moduleName: "solid-js/web", 48 | elements: [...DOMElements.values(), ...SVGElements.values()], 49 | }, 50 | { 51 | name: "universal", 52 | moduleName: "/src/renderer.tsx", 53 | elements: [], 54 | }, 55 | ], 56 | }, 57 | }), 58 | inspect(), 59 | ], 60 | })); 61 | --------------------------------------------------------------------------------