Renamed @theatre/plugin-r3f to @theatre/r3f
This commit is contained in:
parent
03a2f26686
commit
4f66d57cf8
42 changed files with 19 additions and 19 deletions
1
packages/r3f/.gitignore
vendored
Normal file
1
packages/r3f/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/dist
|
203
packages/r3f/LICENSE
Normal file
203
packages/r3f/LICENSE
Normal file
|
@ -0,0 +1,203 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
7
packages/r3f/README.md
Normal file
7
packages/r3f/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# @theatre/r3f
|
||||
|
||||
A [Theatre.js](https://github.com/AriaMinaei/theatre) extension for [THREE.js](https://threejs.org/) with [React Three Fiber](https://github.com/pmndrs/react-three-fiber).
|
||||
|
||||
## Documentation
|
||||
|
||||
Docs and video tutorials are [here](https://docs.theatrejs.com/r3f/).
|
77
packages/r3f/devEnv/build.ts
Normal file
77
packages/r3f/devEnv/build.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
import * as path from 'path'
|
||||
import {build} from 'esbuild'
|
||||
import type {Plugin} from 'esbuild'
|
||||
import {existsSync, mkdirSync, writeFileSync} from 'fs'
|
||||
|
||||
const externalPlugin = (patterns: RegExp[]): Plugin => {
|
||||
return {
|
||||
name: `external`,
|
||||
|
||||
setup(build) {
|
||||
build.onResolve({filter: /.*/}, (args) => {
|
||||
const external = patterns.some((p) => {
|
||||
return p.test(args.path)
|
||||
})
|
||||
|
||||
if (external) {
|
||||
return {path: args.path, external}
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const definedGlobals = {
|
||||
global: 'window',
|
||||
}
|
||||
|
||||
function createBundles(watch: boolean) {
|
||||
const pathToPackage = path.join(__dirname, '../')
|
||||
const esbuildConfig: Parameters<typeof build>[0] = {
|
||||
entryPoints: [path.join(pathToPackage, 'src/index.tsx')],
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
define: definedGlobals,
|
||||
watch,
|
||||
platform: 'neutral',
|
||||
mainFields: ['browser', 'module', 'main'],
|
||||
target: ['firefox57', 'chrome58'],
|
||||
conditions: ['browser', 'node'],
|
||||
// every dependency is considered external
|
||||
plugins: [externalPlugin([/^[\@a-zA-Z]+/])],
|
||||
}
|
||||
|
||||
build({
|
||||
...esbuildConfig,
|
||||
define: {...definedGlobals, 'process.env.NODE_ENV': '"production"'},
|
||||
outfile: path.join(pathToPackage, 'dist/index.production.js'),
|
||||
format: 'cjs',
|
||||
})
|
||||
|
||||
build({
|
||||
...esbuildConfig,
|
||||
define: {...definedGlobals, 'process.env.NODE_ENV': '"development"'},
|
||||
outfile: path.join(pathToPackage, 'dist/index.development.js'),
|
||||
format: 'cjs',
|
||||
})
|
||||
|
||||
if (!existsSync(path.join(pathToPackage, 'dist')))
|
||||
mkdirSync(path.join(pathToPackage, 'dist'))
|
||||
|
||||
writeFileSync(
|
||||
path.join(pathToPackage, 'dist/index.js'),
|
||||
`module.exports =
|
||||
process.env.NODE_ENV === "production"
|
||||
? require("./index.production.js")
|
||||
: require("./index.development.js")`,
|
||||
{encoding: 'utf-8'},
|
||||
)
|
||||
|
||||
// build({
|
||||
// ...esbuildConfig,
|
||||
// outfile: path.join(pathToPackage, 'dist/index.mjs'),
|
||||
// format: 'esm',
|
||||
// })
|
||||
}
|
||||
|
||||
createBundles(false)
|
3
packages/r3f/devEnv/tsconfig.json
Normal file
3
packages/r3f/devEnv/tsconfig.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
|
||||
}
|
66
packages/r3f/package.json
Normal file
66
packages/r3f/package.json
Normal file
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"name": "@theatre/r3f",
|
||||
"version": "0.4.0-dev.14",
|
||||
"license": "Apache-2.0",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Andrew Prifer",
|
||||
"email": "andrew.prifer@gmail.com",
|
||||
"url": "https://github.com/AndrewPrifer"
|
||||
},
|
||||
{
|
||||
"name": "Aria Minaei",
|
||||
"email": "aria@theatrejs.com",
|
||||
"url": "https://github.com/AriaMinaei"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/AriaMinaei/theatre",
|
||||
"directory": "packages/r3f"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"scripts": {
|
||||
"prepack": "node ../../devEnv/ensurePublishing.js",
|
||||
"typecheck": "yarn run build",
|
||||
"build": "run-s build:ts build:js",
|
||||
"build:ts": "tsc --build ./tsconfig.json",
|
||||
"build:js": "node -r esbuild-register ./devEnv/build.ts",
|
||||
"prepublish": "node ../../devEnv/ensurePublishing.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^26.0.23",
|
||||
"@types/lodash-es": "^4.17.4",
|
||||
"@types/node": "^15.6.2",
|
||||
"@types/react": "^17.0.9",
|
||||
"@types/styled-components": "^5.1.9",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"typescript": "^4.4.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-three/drei": "^7.3.1",
|
||||
"@theatre/react": "workspace:*",
|
||||
"lodash-es": "^4.17.21",
|
||||
"polished": "^4.1.3",
|
||||
"react-icons": "^4.2.0",
|
||||
"react-merge-refs": "^1.1.0",
|
||||
"react-shadow": "^19.0.2",
|
||||
"react-use-measure": "^2.0.4",
|
||||
"reakit": "^1.3.8",
|
||||
"styled-components": "^5.3.0",
|
||||
"zustand": "^3.5.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-three/fiber": "^7.0.6",
|
||||
"@theatre/core": "*",
|
||||
"@theatre/studio": "*",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"three": "^0.131.3"
|
||||
}
|
||||
}
|
11
packages/r3f/src/.eslintrc.js
Normal file
11
packages/r3f/src/.eslintrc.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
module.exports = {
|
||||
rules: {
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: `ImportDeclaration[source.value=/@theatre\\u002F(studio|core)\\u002F/]`,
|
||||
message: `Importing Theatre's submodules would not work in the production build.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
47
packages/r3f/src/SheetProvider.tsx
Normal file
47
packages/r3f/src/SheetProvider.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {useThree} from '@react-three/fiber'
|
||||
import type {ISheet} from '@theatre/core'
|
||||
import {bindToCanvas} from './store'
|
||||
|
||||
const ctx = createContext<{sheet: ISheet | undefined} | undefined>(undefined)
|
||||
|
||||
const useWrapperContext = (): {sheet: ISheet | undefined} => {
|
||||
const val = useContext(ctx)
|
||||
if (!val) {
|
||||
throw new Error(
|
||||
`No sheet found. You need to add a <SheetProvider> higher up in the tree. https://docs.theatrejs.com/r3f.html#sheetprovider`,
|
||||
)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
export const useCurrentSheet = (): ISheet | undefined => {
|
||||
return useWrapperContext().sheet
|
||||
}
|
||||
|
||||
const SheetProvider: React.FC<{
|
||||
getSheet: () => ISheet
|
||||
}> = (props) => {
|
||||
const {scene, gl} = useThree((s) => ({scene: s.scene, gl: s.gl}))
|
||||
const [sheet, setSheet] = useState<ISheet | undefined>(undefined)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const sheet = props.getSheet()
|
||||
if (!sheet || sheet.type !== 'Theatre_Sheet_PublicAPI') {
|
||||
throw new Error(
|
||||
`getSheet() in <Wrapper getSheet={getSheet}> has returned an invalid value`,
|
||||
)
|
||||
}
|
||||
setSheet(sheet)
|
||||
bindToCanvas({gl, scene})
|
||||
}, [scene, gl])
|
||||
|
||||
return <ctx.Provider value={{sheet}}>{props.children}</ctx.Provider>
|
||||
}
|
||||
|
||||
export default SheetProvider
|
224
packages/r3f/src/components/EditableProxy.tsx
Normal file
224
packages/r3f/src/components/EditableProxy.tsx
Normal file
|
@ -0,0 +1,224 @@
|
|||
import type {Object3D} from 'three'
|
||||
import {
|
||||
BoxHelper,
|
||||
CameraHelper,
|
||||
DirectionalLightHelper,
|
||||
PointLightHelper,
|
||||
SpotLightHelper,
|
||||
} from 'three'
|
||||
import type {ReactElement, VFC} from 'react'
|
||||
import React, {useEffect, useLayoutEffect, useRef, useState} from 'react'
|
||||
import {useHelper, Sphere, Html} from '@react-three/drei'
|
||||
import type {EditableType} from '../store'
|
||||
import {useEditorStore} from '../store'
|
||||
import shallow from 'zustand/shallow'
|
||||
import {
|
||||
BiSun,
|
||||
BsCameraVideoFill,
|
||||
BsFillCollectionFill,
|
||||
GiCube,
|
||||
GiLightBulb,
|
||||
GiLightProjector,
|
||||
} from 'react-icons/all'
|
||||
import type {IconType} from 'react-icons'
|
||||
import studio from '@theatre/studio'
|
||||
import {useSelected} from './useSelected'
|
||||
import {useVal} from '@theatre/react'
|
||||
import {getEditorSheetObject} from './editorStuff'
|
||||
|
||||
export interface EditableProxyProps {
|
||||
editableName: string
|
||||
editableType: EditableType
|
||||
object: Object3D
|
||||
onChange?: () => void
|
||||
}
|
||||
|
||||
const EditableProxy: VFC<EditableProxyProps> = ({
|
||||
editableName: uniqueName,
|
||||
editableType,
|
||||
object,
|
||||
}) => {
|
||||
const editorObject = getEditorSheetObject()
|
||||
const setSnapshotProxyObject = useEditorStore(
|
||||
(state) => state.setSnapshotProxyObject,
|
||||
shallow,
|
||||
)
|
||||
|
||||
const selected = useSelected()
|
||||
const showOverlayIcons =
|
||||
useVal(editorObject?.props.viewport.showOverlayIcons) ?? false
|
||||
|
||||
useEffect(() => {
|
||||
setSnapshotProxyObject(object, uniqueName)
|
||||
|
||||
return () => setSnapshotProxyObject(null, uniqueName)
|
||||
}, [uniqueName, object, setSnapshotProxyObject])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const originalVisibility = object.visible
|
||||
|
||||
if (object.userData.__visibleOnlyInEditor) {
|
||||
object.visible = true
|
||||
}
|
||||
|
||||
return () => {
|
||||
// this has absolutely no effect, __visibleOnlyInEditor of the snapshot never changes, I'm just doing it because it looks right 🤷️
|
||||
object.visible = originalVisibility
|
||||
}
|
||||
}, [object.userData.__visibleOnlyInEditor, object.visible])
|
||||
|
||||
// set up helper
|
||||
let Helper:
|
||||
| typeof SpotLightHelper
|
||||
| typeof DirectionalLightHelper
|
||||
| typeof PointLightHelper
|
||||
| typeof BoxHelper
|
||||
| typeof CameraHelper
|
||||
|
||||
switch (editableType) {
|
||||
case 'spotLight':
|
||||
Helper = SpotLightHelper
|
||||
break
|
||||
case 'directionalLight':
|
||||
Helper = DirectionalLightHelper
|
||||
break
|
||||
case 'pointLight':
|
||||
Helper = PointLightHelper
|
||||
break
|
||||
case 'perspectiveCamera':
|
||||
case 'orthographicCamera':
|
||||
Helper = CameraHelper
|
||||
break
|
||||
case 'group':
|
||||
case 'mesh':
|
||||
Helper = BoxHelper
|
||||
}
|
||||
|
||||
let helperArgs: [string] | [number, string] | []
|
||||
const size = 1
|
||||
const color = 'darkblue'
|
||||
|
||||
switch (editableType) {
|
||||
case 'directionalLight':
|
||||
case 'pointLight':
|
||||
helperArgs = [size, color]
|
||||
break
|
||||
case 'group':
|
||||
case 'mesh':
|
||||
case 'spotLight':
|
||||
helperArgs = [color]
|
||||
break
|
||||
case 'perspectiveCamera':
|
||||
case 'orthographicCamera':
|
||||
helperArgs = []
|
||||
}
|
||||
|
||||
let icon: ReactElement<IconType>
|
||||
switch (editableType) {
|
||||
case 'group':
|
||||
icon = <BsFillCollectionFill />
|
||||
break
|
||||
case 'mesh':
|
||||
icon = <GiCube />
|
||||
break
|
||||
case 'pointLight':
|
||||
icon = <GiLightBulb />
|
||||
break
|
||||
case 'spotLight':
|
||||
icon = <GiLightProjector />
|
||||
break
|
||||
case 'directionalLight':
|
||||
icon = <BiSun />
|
||||
break
|
||||
case 'perspectiveCamera':
|
||||
case 'orthographicCamera':
|
||||
icon = <BsCameraVideoFill />
|
||||
}
|
||||
|
||||
const objectRef = useRef(object)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
objectRef.current = object
|
||||
}, [object])
|
||||
|
||||
const dimensionless = [
|
||||
'spotLight',
|
||||
'pointLight',
|
||||
'directionalLight',
|
||||
'perspectiveCamera',
|
||||
'orthographicCamera',
|
||||
]
|
||||
|
||||
const [hovered, setHovered] = useState(false)
|
||||
|
||||
useHelper(
|
||||
objectRef,
|
||||
selected === uniqueName || dimensionless.includes(editableType) || hovered
|
||||
? Helper
|
||||
: null,
|
||||
...helperArgs,
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<group
|
||||
onClick={(e) => {
|
||||
if (e.delta < 2) {
|
||||
e.stopPropagation()
|
||||
|
||||
const theatreObject =
|
||||
useEditorStore.getState().sheetObjects[uniqueName]
|
||||
|
||||
if (!theatreObject) {
|
||||
console.log('no theatre object for', uniqueName)
|
||||
} else {
|
||||
studio.setSelection([theatreObject])
|
||||
}
|
||||
}
|
||||
}}
|
||||
onPointerOver={(e) => {
|
||||
e.stopPropagation()
|
||||
setHovered(true)
|
||||
}}
|
||||
onPointerOut={(e) => {
|
||||
e.stopPropagation()
|
||||
setHovered(false)
|
||||
}}
|
||||
>
|
||||
<primitive object={object}>
|
||||
{showOverlayIcons && (
|
||||
<Html
|
||||
center
|
||||
className="pointer-events-none p-1 rounded bg-white bg-opacity-70 shadow text-gray-700"
|
||||
>
|
||||
{icon}
|
||||
</Html>
|
||||
)}
|
||||
{dimensionless.includes(editableType) && (
|
||||
<Sphere
|
||||
args={[2, 4, 2]}
|
||||
onClick={(e) => {
|
||||
if (e.delta < 2) {
|
||||
e.stopPropagation()
|
||||
const theatreObject =
|
||||
useEditorStore.getState().sheetObjects[uniqueName]
|
||||
|
||||
if (!theatreObject) {
|
||||
console.log('no theatre object for', uniqueName)
|
||||
} else {
|
||||
studio.setSelection([theatreObject])
|
||||
}
|
||||
}
|
||||
}}
|
||||
userData={{helper: true}}
|
||||
>
|
||||
<meshBasicMaterial visible={false} />
|
||||
</Sphere>
|
||||
)}
|
||||
</primitive>
|
||||
</group>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditableProxy
|
23
packages/r3f/src/components/EditorHelper.tsx
Normal file
23
packages/r3f/src/components/EditorHelper.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import type {ComponentProps, ElementType} from 'react'
|
||||
import React from 'react'
|
||||
import {useEditorStore} from '../store'
|
||||
import {createPortal} from '@react-three/fiber'
|
||||
|
||||
export type EditorHelperProps<T extends ElementType> = {
|
||||
component: T
|
||||
} & ComponentProps<T>
|
||||
|
||||
const EditorHelper = <T extends ElementType>({
|
||||
component: Component,
|
||||
...props
|
||||
}: EditorHelperProps<T>) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const helpersRoot = useEditorStore((state) => state.helpersRoot)
|
||||
|
||||
return <>{createPortal(<Component {...props} />, helpersRoot)}</>
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default EditorHelper
|
268
packages/r3f/src/components/ProxyManager.tsx
Normal file
268
packages/r3f/src/components/ProxyManager.tsx
Normal file
|
@ -0,0 +1,268 @@
|
|||
import type {VFC} from 'react'
|
||||
import React, {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {useEditorStore} from '../store'
|
||||
import {createPortal} from '@react-three/fiber'
|
||||
import EditableProxy from './EditableProxy'
|
||||
import type {OrbitControls} from 'three-stdlib'
|
||||
import TransformControls from './TransformControls'
|
||||
import shallow from 'zustand/shallow'
|
||||
import type {Material, Mesh, Object3D} from 'three'
|
||||
import {MeshBasicMaterial, MeshPhongMaterial} from 'three'
|
||||
import studio from '@theatre/studio'
|
||||
import type {ISheetObject} from '@theatre/core'
|
||||
import type {$FixMe} from '../types'
|
||||
import {useSelected} from './useSelected'
|
||||
import {useVal} from '@theatre/react'
|
||||
import useInvalidate from './useInvalidate'
|
||||
import {getEditorSheetObject} from './editorStuff'
|
||||
|
||||
export interface ProxyManagerProps {
|
||||
orbitControlsRef: React.MutableRefObject<OrbitControls | null>
|
||||
}
|
||||
|
||||
type IEditableProxy = {
|
||||
portal: ReturnType<typeof createPortal>
|
||||
object: Object3D
|
||||
sheetObject: ISheetObject<$FixMe>
|
||||
}
|
||||
|
||||
const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
|
||||
const isBeingEdited = useRef(false)
|
||||
const editorObject = getEditorSheetObject()
|
||||
const [sceneSnapshot, sheetObjects] = useEditorStore(
|
||||
(state) => [state.sceneSnapshot, state.sheetObjects],
|
||||
shallow,
|
||||
)
|
||||
const transformControlsMode =
|
||||
useVal(editorObject?.props.transformControls.mode) ?? 'translate'
|
||||
|
||||
const transformControlsSpace =
|
||||
useVal(editorObject?.props.transformControls.space) ?? 'world'
|
||||
|
||||
const viewportShading =
|
||||
useVal(editorObject?.props.viewport.shading) ?? 'rendered'
|
||||
|
||||
const sceneProxy = useMemo(() => sceneSnapshot?.clone(), [sceneSnapshot])
|
||||
const [editableProxies, setEditableProxies] = useState<
|
||||
{
|
||||
[name in string]?: IEditableProxy
|
||||
}
|
||||
>({})
|
||||
|
||||
const invalidate = useInvalidate()
|
||||
|
||||
// set up scene proxies
|
||||
useLayoutEffect(() => {
|
||||
if (!sceneProxy) {
|
||||
return
|
||||
}
|
||||
|
||||
const editableProxies: {[name: string]: IEditableProxy} = {}
|
||||
|
||||
sceneProxy.traverse((object) => {
|
||||
if (object.userData.__editable) {
|
||||
// there are duplicate uniqueNames in the scene, only display one instance in the editor
|
||||
if (editableProxies[object.userData.__editableName]) {
|
||||
object.parent!.remove(object)
|
||||
} else {
|
||||
const uniqueName = object.userData.__editableName
|
||||
|
||||
editableProxies[uniqueName] = {
|
||||
portal: createPortal(
|
||||
<EditableProxy
|
||||
editableName={object.userData.__editableName}
|
||||
editableType={object.userData.__editableType}
|
||||
object={object}
|
||||
/>,
|
||||
object.parent!,
|
||||
),
|
||||
object: object,
|
||||
sheetObject: sheetObjects[uniqueName]!,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setEditableProxies(editableProxies)
|
||||
}, [orbitControlsRef, sceneProxy])
|
||||
|
||||
const selected = useSelected()
|
||||
const editableProxyOfSelected = selected && editableProxies[selected]
|
||||
|
||||
// subscribe to external changes
|
||||
useEffect(() => {
|
||||
if (!editableProxyOfSelected) return
|
||||
const object = editableProxyOfSelected.object
|
||||
const sheetObject = editableProxyOfSelected.sheetObject
|
||||
|
||||
const setFromTheatre = (newValues: any) => {
|
||||
object.position.set(
|
||||
newValues.position.x,
|
||||
newValues.position.y,
|
||||
newValues.position.z,
|
||||
)
|
||||
object.rotation.set(
|
||||
newValues.rotation.x,
|
||||
newValues.rotation.y,
|
||||
newValues.rotation.z,
|
||||
)
|
||||
object.scale.set(newValues.scale.x, newValues.scale.y, newValues.scale.z)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
setFromTheatre(sheetObject.value)
|
||||
|
||||
const untap = sheetObject.onValuesChange(setFromTheatre)
|
||||
|
||||
return () => {
|
||||
untap()
|
||||
}
|
||||
}, [editableProxyOfSelected, selected])
|
||||
|
||||
// set up viewport shading modes
|
||||
const [renderMaterials, setRenderMaterials] = useState<{
|
||||
[id: string]: Material | Material[]
|
||||
}>({})
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!sceneProxy) {
|
||||
return
|
||||
}
|
||||
|
||||
const renderMaterials: {
|
||||
[id: string]: Material | Material[]
|
||||
} = {}
|
||||
|
||||
sceneProxy.traverse((object) => {
|
||||
const mesh = object as Mesh
|
||||
if (mesh.isMesh && !mesh.userData.helper) {
|
||||
renderMaterials[mesh.id] = mesh.material
|
||||
}
|
||||
})
|
||||
|
||||
setRenderMaterials(renderMaterials)
|
||||
|
||||
return () => {
|
||||
// @todo do we need this cleanup?
|
||||
// Object.entries(renderMaterials).forEach(([id, material]) => {
|
||||
// ;(sceneProxy.getObjectById(Number.parseInt(id)) as Mesh).material =
|
||||
// material
|
||||
// })
|
||||
}
|
||||
}, [sceneProxy])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!sceneProxy) {
|
||||
return
|
||||
}
|
||||
|
||||
sceneProxy.traverse((object) => {
|
||||
const mesh = object as Mesh
|
||||
if (mesh.isMesh && !mesh.userData.helper) {
|
||||
let material
|
||||
switch (viewportShading) {
|
||||
case 'wireframe':
|
||||
mesh.material = new MeshBasicMaterial({
|
||||
wireframe: true,
|
||||
color: 'black',
|
||||
})
|
||||
break
|
||||
case 'flat':
|
||||
// it is possible that renderMaterials hasn't updated yet
|
||||
if (!renderMaterials[mesh.id]) {
|
||||
return
|
||||
}
|
||||
material = new MeshBasicMaterial()
|
||||
if (renderMaterials[mesh.id].hasOwnProperty('color')) {
|
||||
material.color = (renderMaterials[mesh.id] as any).color
|
||||
}
|
||||
if (renderMaterials[mesh.id].hasOwnProperty('map')) {
|
||||
material.map = (renderMaterials[mesh.id] as any).map
|
||||
}
|
||||
if (renderMaterials[mesh.id].hasOwnProperty('vertexColors')) {
|
||||
material.vertexColors = (
|
||||
renderMaterials[mesh.id] as any
|
||||
).vertexColors
|
||||
}
|
||||
mesh.material = material
|
||||
break
|
||||
case 'solid':
|
||||
// it is possible that renderMaterials hasn't updated yet
|
||||
if (!renderMaterials[mesh.id]) {
|
||||
return
|
||||
}
|
||||
material = new MeshPhongMaterial()
|
||||
if (renderMaterials[mesh.id].hasOwnProperty('color')) {
|
||||
material.color = (renderMaterials[mesh.id] as any).color
|
||||
}
|
||||
if (renderMaterials[mesh.id].hasOwnProperty('map')) {
|
||||
material.map = (renderMaterials[mesh.id] as any).map
|
||||
}
|
||||
if (renderMaterials[mesh.id].hasOwnProperty('vertexColors')) {
|
||||
material.vertexColors = (
|
||||
renderMaterials[mesh.id] as any
|
||||
).vertexColors
|
||||
}
|
||||
mesh.material = material
|
||||
break
|
||||
case 'rendered':
|
||||
mesh.material = renderMaterials[mesh.id]
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [viewportShading, renderMaterials, sceneProxy])
|
||||
|
||||
if (!sceneProxy) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<primitive object={sceneProxy} />
|
||||
{selected && editableProxyOfSelected && (
|
||||
<TransformControls
|
||||
mode={transformControlsMode}
|
||||
space={transformControlsSpace}
|
||||
orbitControlsRef={orbitControlsRef}
|
||||
object={editableProxyOfSelected.object}
|
||||
onObjectChange={() => {
|
||||
const sheetObject = editableProxyOfSelected.sheetObject
|
||||
const obj = editableProxyOfSelected.object
|
||||
|
||||
studio.transaction(({set}) => {
|
||||
set(sheetObject.props, {
|
||||
position: {
|
||||
x: obj.position.x,
|
||||
y: obj.position.y,
|
||||
z: obj.position.z,
|
||||
},
|
||||
rotation: {
|
||||
x: obj.rotation.x,
|
||||
y: obj.rotation.y,
|
||||
z: obj.rotation.z,
|
||||
},
|
||||
scale: {
|
||||
x: obj.scale.x,
|
||||
y: obj.scale.y,
|
||||
z: obj.scale.z,
|
||||
},
|
||||
})
|
||||
})
|
||||
}}
|
||||
onDraggingChange={(event) => (isBeingEdited.current = event.value)}
|
||||
/>
|
||||
)}
|
||||
{Object.values(editableProxies).map(
|
||||
(editableProxy) => editableProxy!.portal,
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProxyManager
|
27
packages/r3f/src/components/RefreshSnapshot.tsx
Normal file
27
packages/r3f/src/components/RefreshSnapshot.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import React, {useEffect} from 'react'
|
||||
import useRefreshSnapshot from './useRefreshSnapshot'
|
||||
|
||||
/**
|
||||
* Putting this element in a suspense tree makes sure the snapshot editor
|
||||
* gets refreshed once the tree renders.
|
||||
*
|
||||
* Alternatively you can use
|
||||
* @link useRefreshSnapshot()
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <Suspense fallback={null}>
|
||||
* <RefreshSnapshot />
|
||||
* <Model url={sceneGLB} />
|
||||
* </Suspense>
|
||||
* ```
|
||||
*/
|
||||
const RefreshSnapshot: React.FC<{}> = (props) => {
|
||||
const refreshSnapshot = useRefreshSnapshot()
|
||||
useEffect(() => {
|
||||
refreshSnapshot()
|
||||
}, [])
|
||||
return <></>
|
||||
}
|
||||
|
||||
export default RefreshSnapshot
|
188
packages/r3f/src/components/SnapshotEditor.tsx
Normal file
188
packages/r3f/src/components/SnapshotEditor.tsx
Normal file
|
@ -0,0 +1,188 @@
|
|||
import {useCallback, useLayoutEffect} from 'react'
|
||||
import React from 'react'
|
||||
import {Canvas} from '@react-three/fiber'
|
||||
import type {BaseSheetObjectType} from '../store'
|
||||
import {allRegisteredObjects, useEditorStore} from '../store'
|
||||
import shallow from 'zustand/shallow'
|
||||
import root from 'react-shadow/styled-components'
|
||||
import ProxyManager from './ProxyManager'
|
||||
import studio, {ToolbarIconButton} from '@theatre/studio'
|
||||
import {useVal} from '@theatre/react'
|
||||
import styled, {createGlobalStyle, StyleSheetManager} from 'styled-components'
|
||||
import {IoCameraReverseOutline} from 'react-icons/all'
|
||||
import type {ISheet} from '@theatre/core'
|
||||
import useSnapshotEditorCamera from './useSnapshotEditorCamera'
|
||||
import {getEditorSheet, getEditorSheetObject} from './editorStuff'
|
||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
:host {
|
||||
contain: strict;
|
||||
all: initial;
|
||||
color: white;
|
||||
font: 11px -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe Editor,
|
||||
HelveticaNeue-Light, Ubuntu, Droid Sans, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
list-style: none;
|
||||
}
|
||||
`
|
||||
|
||||
const EditorScene: React.FC<{snapshotEditorSheet: ISheet; paneId: string}> = ({
|
||||
snapshotEditorSheet,
|
||||
paneId,
|
||||
}) => {
|
||||
const [editorCamera, orbitControlsRef] = useSnapshotEditorCamera(
|
||||
snapshotEditorSheet,
|
||||
paneId,
|
||||
)
|
||||
|
||||
const editorObject = getEditorSheetObject()
|
||||
|
||||
const helpersRoot = useEditorStore((state) => state.helpersRoot, shallow)
|
||||
|
||||
const showGrid = useVal(editorObject?.props.viewport.showGrid) ?? true
|
||||
const showAxes = useVal(editorObject?.props.viewport.showAxes) ?? true
|
||||
|
||||
return (
|
||||
<>
|
||||
{showGrid && <gridHelper args={[20, 20, '#6e6e6e', '#4a4b4b']} />}
|
||||
{showAxes && <axesHelper args={[500]} />}
|
||||
{editorCamera}
|
||||
|
||||
<primitive object={helpersRoot}></primitive>
|
||||
<ProxyManager orbitControlsRef={orbitControlsRef} />
|
||||
<color attach="background" args={[0.24, 0.24, 0.24]} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
tab-size: 4;
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
margin: 0;
|
||||
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
`
|
||||
|
||||
const CanvasWrapper = styled.div`
|
||||
display: relative;
|
||||
z-index: 0;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const Overlay = styled.div`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
`
|
||||
|
||||
const Tools = styled.div`
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 6px;
|
||||
pointer-events: auto;
|
||||
`
|
||||
|
||||
const SnapshotEditor: React.FC<{paneId: string}> = (props) => {
|
||||
const snapshotEditorSheet = getEditorSheet()
|
||||
const paneId = props.paneId
|
||||
const editorObject = getEditorSheetObject()
|
||||
|
||||
const [sceneSnapshot, createSnapshot] = useEditorStore(
|
||||
(state) => [state.sceneSnapshot, state.createSnapshot],
|
||||
shallow,
|
||||
)
|
||||
|
||||
const editorOpen = true
|
||||
useLayoutEffect(() => {
|
||||
let timeout: NodeJS.Timeout | undefined
|
||||
if (editorOpen) {
|
||||
// a hack to make sure all the scene's props are
|
||||
// applied before we take a snapshot
|
||||
timeout = setTimeout(createSnapshot, 100)
|
||||
}
|
||||
return () => {
|
||||
if (timeout !== undefined) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
}, [editorOpen])
|
||||
|
||||
const onPointerMissed = useCallback(() => {
|
||||
// This callback runs when the user clicks in an empty space inside a SnapshotEditor.
|
||||
// We'll try to set the current selection to the nearest sheet _if_ at least one object
|
||||
// belonging to R3F was selected previously.
|
||||
const obj: undefined | BaseSheetObjectType = studio.selection.find(
|
||||
(sheetOrObject) =>
|
||||
allRegisteredObjects.has(sheetOrObject as $IntentionalAny),
|
||||
) as $IntentionalAny
|
||||
|
||||
if (obj) {
|
||||
studio.setSelection([obj.sheet])
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!editorObject) return <></>
|
||||
|
||||
return (
|
||||
<root.div>
|
||||
<StyleSheetManager disableVendorPrefixes>
|
||||
<>
|
||||
<GlobalStyle />
|
||||
<Wrapper>
|
||||
<Overlay>
|
||||
<Tools>
|
||||
<ToolbarIconButton
|
||||
title="Refresh Snapshot"
|
||||
onClick={createSnapshot}
|
||||
>
|
||||
<IoCameraReverseOutline />
|
||||
</ToolbarIconButton>
|
||||
</Tools>
|
||||
</Overlay>
|
||||
|
||||
{sceneSnapshot ? (
|
||||
<>
|
||||
<CanvasWrapper>
|
||||
<Canvas
|
||||
// @ts-ignore
|
||||
colorManagement
|
||||
onCreated={({gl}) => {
|
||||
gl.setClearColor('white')
|
||||
}}
|
||||
shadowMap
|
||||
dpr={[1, 2]}
|
||||
fog={'red'}
|
||||
frameloop="demand"
|
||||
onPointerMissed={onPointerMissed}
|
||||
>
|
||||
<EditorScene
|
||||
snapshotEditorSheet={snapshotEditorSheet}
|
||||
paneId={paneId}
|
||||
/>
|
||||
</Canvas>
|
||||
</CanvasWrapper>
|
||||
</>
|
||||
) : null}
|
||||
</Wrapper>
|
||||
{/* </PortalContext.Provider> */}
|
||||
</>
|
||||
</StyleSheetManager>
|
||||
</root.div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SnapshotEditor
|
118
packages/r3f/src/components/Toolbar/Toolbar.tsx
Normal file
118
packages/r3f/src/components/Toolbar/Toolbar.tsx
Normal file
|
@ -0,0 +1,118 @@
|
|||
import type {VFC} from 'react'
|
||||
import React from 'react'
|
||||
import {IoCameraOutline} from 'react-icons/all'
|
||||
import studio, {ToolbarIconButton} from '@theatre/studio'
|
||||
import {useVal} from '@theatre/react'
|
||||
import TransformControlsModeSelect from './TransformControlsModeSelect'
|
||||
import ViewportShadingSelect from './ViewportShadingSelect'
|
||||
import TransformControlsSpaceSelect from './TransformControlsSpaceSelect'
|
||||
import {getEditorSheetObject} from '../editorStuff'
|
||||
|
||||
const Toolbar: VFC = () => {
|
||||
const editorObject = getEditorSheetObject()
|
||||
|
||||
const transformControlsMode =
|
||||
useVal(editorObject?.props.transformControls.mode) ?? 'translate'
|
||||
const transformControlsSpace =
|
||||
useVal(editorObject?.props.transformControls.space) ?? 'world'
|
||||
const viewportShading =
|
||||
useVal(editorObject?.props.viewport.shading) ?? 'rendered'
|
||||
|
||||
if (!editorObject) return <></>
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToolbarIconButton
|
||||
onClick={() => {
|
||||
studio.createPane('snapshot')
|
||||
}}
|
||||
title="Create snapshot"
|
||||
>
|
||||
<IoCameraOutline />
|
||||
</ToolbarIconButton>
|
||||
<TransformControlsModeSelect
|
||||
value={transformControlsMode}
|
||||
onChange={(value) =>
|
||||
studio.transaction(({set}) =>
|
||||
set(editorObject!.props.transformControls.mode, value),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<TransformControlsSpaceSelect
|
||||
value={transformControlsSpace}
|
||||
onChange={(space) => {
|
||||
studio.transaction(({set}) => {
|
||||
set(editorObject.props.transformControls.space, space)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ViewportShadingSelect
|
||||
value={viewportShading}
|
||||
onChange={(shading) => {
|
||||
studio.transaction(({set}) => {
|
||||
set(editorObject.props.viewport.shading, shading)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* <ToolbarIconButton
|
||||
label="Focus on selected"
|
||||
icon={<RiFocus3Line />}
|
||||
onClick={() => {
|
||||
const orbitControls =
|
||||
useEditorStore.getState().orbitControlsRef?.current
|
||||
const selected = getSelected()
|
||||
|
||||
let focusObject
|
||||
|
||||
if (selected) {
|
||||
focusObject =
|
||||
useEditorStore.getState().editablesSnapshot![selected].proxyObject
|
||||
}
|
||||
|
||||
if (orbitControls && focusObject) {
|
||||
focusObject.getWorldPosition(
|
||||
// @ts-ignore TODO
|
||||
orbitControls.target as Vector3,
|
||||
)
|
||||
}
|
||||
}}
|
||||
/> */}
|
||||
|
||||
{/* <ToolbarIconButton
|
||||
label="Align object to view"
|
||||
icon={<GiPocketBow />}
|
||||
onClick={() => {
|
||||
const camera = (
|
||||
useEditorStore.getState().orbitControlsRef?.current as $FixMe
|
||||
)?.object
|
||||
|
||||
const selected = getSelected()
|
||||
|
||||
let proxyObject
|
||||
|
||||
if (selected) {
|
||||
proxyObject =
|
||||
useEditorStore.getState().editablesSnapshot![selected].proxyObject
|
||||
|
||||
if (proxyObject && camera) {
|
||||
const direction = new Vector3()
|
||||
const position = camera.position.clone()
|
||||
|
||||
camera.getWorldDirection(direction)
|
||||
proxyObject.position.set(0, 0, 0)
|
||||
proxyObject.lookAt(direction)
|
||||
|
||||
proxyObject.parent!.worldToLocal(position)
|
||||
proxyObject.position.copy(position)
|
||||
|
||||
proxyObject.updateMatrix()
|
||||
}
|
||||
}
|
||||
}}
|
||||
/> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Toolbar
|
|
@ -0,0 +1,41 @@
|
|||
import {ToolbarSwitchSelect} from '@theatre/studio'
|
||||
import type {VFC} from 'react'
|
||||
import React from 'react'
|
||||
import {GiClockwiseRotation, GiMove, GiResize} from 'react-icons/all'
|
||||
import type {TransformControlsMode} from '../../store'
|
||||
|
||||
export interface TransformControlsModeSelectProps {
|
||||
value: TransformControlsMode
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
const TransformControlsModeSelect: VFC<TransformControlsModeSelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
return (
|
||||
<ToolbarSwitchSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={[
|
||||
{
|
||||
value: 'translate',
|
||||
label: 'Tool: Translate',
|
||||
icon: <GiMove />,
|
||||
},
|
||||
{
|
||||
value: 'rotate',
|
||||
label: 'Tool: Rotate',
|
||||
icon: <GiClockwiseRotation />,
|
||||
},
|
||||
{
|
||||
value: 'scale',
|
||||
label: 'Tool: Scale',
|
||||
icon: <GiResize />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default TransformControlsModeSelect
|
|
@ -0,0 +1,34 @@
|
|||
import type {VFC} from 'react'
|
||||
import React from 'react'
|
||||
import type {TransformControlsSpace} from '../../store'
|
||||
import {BiCube, BiGlobe} from 'react-icons/all'
|
||||
import {ToolbarSwitchSelect} from '@theatre/studio'
|
||||
|
||||
export interface TransformControlsSpaceSelectProps {
|
||||
value: TransformControlsSpace
|
||||
onChange: (value: TransformControlsSpace) => void
|
||||
}
|
||||
|
||||
const TransformControlsSpaceSelect: VFC<TransformControlsSpaceSelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => (
|
||||
<ToolbarSwitchSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={[
|
||||
{
|
||||
value: 'world',
|
||||
label: 'Space: World',
|
||||
icon: <BiGlobe />,
|
||||
},
|
||||
{
|
||||
value: 'local',
|
||||
label: 'Space: Local',
|
||||
icon: <BiCube />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
export default TransformControlsSpaceSelect
|
|
@ -0,0 +1,44 @@
|
|||
import type {VFC} from 'react'
|
||||
import React from 'react'
|
||||
import type {ViewportShading} from '../../store'
|
||||
import {FaCube, GiCube, GiIceCube, BiCube} from 'react-icons/all'
|
||||
import {ToolbarSwitchSelect} from '@theatre/studio'
|
||||
|
||||
export interface ViewportShadingSelectProps {
|
||||
value: ViewportShading
|
||||
onChange: (value: ViewportShading) => void
|
||||
}
|
||||
|
||||
const ViewportShadingSelect: VFC<ViewportShadingSelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => (
|
||||
<ToolbarSwitchSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={[
|
||||
{
|
||||
value: 'wireframe',
|
||||
label: 'Display: Wireframe',
|
||||
icon: <BiCube />,
|
||||
},
|
||||
{
|
||||
value: 'flat',
|
||||
label: 'Display: Flat',
|
||||
icon: <GiCube />,
|
||||
},
|
||||
{
|
||||
value: 'solid',
|
||||
label: 'Display: Solid',
|
||||
icon: <FaCube />,
|
||||
},
|
||||
{
|
||||
value: 'rendered',
|
||||
label: 'Display: Rendered',
|
||||
icon: <GiIceCube />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
export default ViewportShadingSelect
|
97
packages/r3f/src/components/TransformControls.tsx
Normal file
97
packages/r3f/src/components/TransformControls.tsx
Normal file
|
@ -0,0 +1,97 @@
|
|||
import type {Object3D, Event} from 'three'
|
||||
import React, {forwardRef, useLayoutEffect, useEffect, useMemo} from 'react'
|
||||
import type {ReactThreeFiber, Overwrite} from '@react-three/fiber'
|
||||
import {useThree} from '@react-three/fiber'
|
||||
import {TransformControls as TransformControlsImpl} from 'three/examples/jsm/controls/TransformControls'
|
||||
import type {OrbitControls} from 'three-stdlib'
|
||||
|
||||
type R3fTransformControls = Overwrite<
|
||||
ReactThreeFiber.Object3DNode<
|
||||
TransformControlsImpl,
|
||||
typeof TransformControlsImpl
|
||||
>,
|
||||
{target?: ReactThreeFiber.Vector3}
|
||||
>
|
||||
|
||||
export interface TransformControlsProps extends R3fTransformControls {
|
||||
object: Object3D
|
||||
orbitControlsRef?: React.MutableRefObject<OrbitControls | null>
|
||||
onObjectChange?: (event: Event) => void
|
||||
onDraggingChange?: (event: Event) => void
|
||||
}
|
||||
|
||||
const TransformControls = forwardRef(
|
||||
(
|
||||
{
|
||||
children,
|
||||
object,
|
||||
orbitControlsRef,
|
||||
onObjectChange,
|
||||
onDraggingChange,
|
||||
...props
|
||||
}: TransformControlsProps,
|
||||
ref,
|
||||
) => {
|
||||
const {camera, gl, invalidate} = useThree()
|
||||
const controls = useMemo(
|
||||
() => new TransformControlsImpl(camera, gl.domElement),
|
||||
[camera, gl.domElement],
|
||||
)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
controls.attach(object)
|
||||
|
||||
return () => void controls.detach()
|
||||
}, [object, controls])
|
||||
|
||||
useEffect(() => {
|
||||
controls?.addEventListener?.('change', invalidate)
|
||||
return () => controls?.removeEventListener?.('change', invalidate)
|
||||
}, [controls, invalidate])
|
||||
|
||||
useEffect(() => {
|
||||
const callback = (event: Event) => {
|
||||
if (orbitControlsRef && orbitControlsRef.current) {
|
||||
// @ts-ignore TODO
|
||||
orbitControlsRef.current.enabled = !event.value
|
||||
}
|
||||
}
|
||||
|
||||
if (controls) {
|
||||
controls.addEventListener!('dragging-changed', callback)
|
||||
}
|
||||
|
||||
return () => {
|
||||
controls.removeEventListener!('dragging-changed', callback)
|
||||
}
|
||||
}, [controls, orbitControlsRef])
|
||||
|
||||
useEffect(() => {
|
||||
if (onObjectChange) {
|
||||
controls.addEventListener('objectChange', onObjectChange)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (onObjectChange) {
|
||||
controls.removeEventListener('objectChange', onObjectChange)
|
||||
}
|
||||
}
|
||||
}, [onObjectChange, controls])
|
||||
|
||||
useEffect(() => {
|
||||
if (onDraggingChange) {
|
||||
controls.addEventListener('dragging-changed', onDraggingChange)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (onDraggingChange) {
|
||||
controls.removeEventListener('dragging-changed', onDraggingChange)
|
||||
}
|
||||
}
|
||||
}, [controls, onDraggingChange])
|
||||
|
||||
return <primitive dispose={null} object={controls} ref={ref} {...props} />
|
||||
},
|
||||
)
|
||||
|
||||
export default TransformControls
|
205
packages/r3f/src/components/editable.tsx
Normal file
205
packages/r3f/src/components/editable.tsx
Normal file
|
@ -0,0 +1,205 @@
|
|||
import type {ComponentProps, ComponentType, RefAttributes} from 'react'
|
||||
import React, {forwardRef, useLayoutEffect, useRef, useState} from 'react'
|
||||
import type {
|
||||
DirectionalLight,
|
||||
Group,
|
||||
Mesh,
|
||||
OrthographicCamera,
|
||||
PerspectiveCamera,
|
||||
PointLight,
|
||||
SpotLight,
|
||||
} from 'three'
|
||||
import {Vector3} from 'three'
|
||||
import type {EditableType} from '../store'
|
||||
import {allRegisteredObjects} from '../store'
|
||||
import {baseSheetObjectType} from '../store'
|
||||
import {useEditorStore} from '../store'
|
||||
import mergeRefs from 'react-merge-refs'
|
||||
import type {$FixMe} from '@theatre/shared/utils/types'
|
||||
import type {ISheetObject} from '@theatre/core'
|
||||
import useInvalidate from './useInvalidate'
|
||||
import {useCurrentSheet} from '../SheetProvider'
|
||||
|
||||
interface Elements {
|
||||
group: Group
|
||||
mesh: Mesh
|
||||
spotLight: SpotLight
|
||||
directionalLight: DirectionalLight
|
||||
perspectiveCamera: PerspectiveCamera
|
||||
orthographicCamera: OrthographicCamera
|
||||
pointLight: PointLight
|
||||
}
|
||||
|
||||
const editable = <
|
||||
T extends ComponentType<any> | EditableType | 'primitive',
|
||||
U extends T extends EditableType ? T : EditableType,
|
||||
>(
|
||||
Component: T,
|
||||
type: T extends 'primitive' ? null : U,
|
||||
) => {
|
||||
type Props = Omit<ComponentProps<T>, 'visible'> & {
|
||||
uniqueName: string
|
||||
visible?: boolean | 'editor'
|
||||
} & (T extends 'primitive'
|
||||
? {
|
||||
editableType: U
|
||||
}
|
||||
: {}) &
|
||||
RefAttributes<Elements[U]>
|
||||
|
||||
return forwardRef(
|
||||
({uniqueName, visible, editableType, ...props}: Props, ref) => {
|
||||
const objectRef = useRef<Elements[U]>()
|
||||
|
||||
const sheet = useCurrentSheet()
|
||||
|
||||
const [sheetObject, setSheetObject] = useState<
|
||||
undefined | ISheetObject<$FixMe>
|
||||
>(undefined)
|
||||
|
||||
const invalidate = useInvalidate()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!sheet) return
|
||||
const sheetObject = sheet.object(uniqueName, baseSheetObjectType)
|
||||
allRegisteredObjects.add(sheetObject)
|
||||
setSheetObject(sheetObject)
|
||||
|
||||
useEditorStore
|
||||
.getState()
|
||||
.setSheetObject(uniqueName, sheetObject as $FixMe)
|
||||
}, [sheet, uniqueName])
|
||||
|
||||
const transformDeps: string[] = []
|
||||
|
||||
;['x', 'y', 'z'].forEach((axis) => {
|
||||
transformDeps.push(
|
||||
props[`position-${axis}` as any],
|
||||
props[`rotation-${axis}` as any],
|
||||
props[`scale-${axis}` as any],
|
||||
)
|
||||
})
|
||||
|
||||
// store initial values of props
|
||||
useLayoutEffect(() => {
|
||||
if (!sheetObject) return
|
||||
// calculate initial properties before adding the editable
|
||||
const position: Vector3 = props.position
|
||||
? Array.isArray(props.position)
|
||||
? new Vector3(...(props.position as any))
|
||||
: props.position
|
||||
: new Vector3()
|
||||
const rotation: Vector3 = props.rotation
|
||||
? Array.isArray(props.rotation)
|
||||
? new Vector3(...(props.rotation as any))
|
||||
: props.rotation
|
||||
: new Vector3()
|
||||
const scale: Vector3 = props.scale
|
||||
? Array.isArray(props.scale)
|
||||
? new Vector3(...(props.scale as any))
|
||||
: props.scale
|
||||
: new Vector3(1, 1, 1)
|
||||
|
||||
;['x', 'y', 'z'].forEach((axis, index) => {
|
||||
if (props[`position-${axis}` as any])
|
||||
position.setComponent(index, props[`position-${axis}` as any])
|
||||
if (props[`rotation-${axis}` as any])
|
||||
rotation.setComponent(index, props[`rotation-${axis}` as any])
|
||||
if (props[`scale-${axis}` as any])
|
||||
scale.setComponent(index, props[`scale-${axis}` as any])
|
||||
})
|
||||
|
||||
const initial = {
|
||||
position: {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
z: position.z,
|
||||
},
|
||||
rotation: {
|
||||
x: rotation.x,
|
||||
y: rotation.y,
|
||||
z: rotation.z,
|
||||
},
|
||||
scale: {
|
||||
x: scale.x,
|
||||
y: scale.y,
|
||||
z: scale.z,
|
||||
},
|
||||
}
|
||||
sheetObject!.initialValue = initial
|
||||
}, [
|
||||
uniqueName,
|
||||
sheetObject,
|
||||
props.position,
|
||||
props.rotation,
|
||||
props.scale,
|
||||
|
||||
...transformDeps,
|
||||
])
|
||||
|
||||
// subscribe to prop changes from theatre
|
||||
useLayoutEffect(() => {
|
||||
if (!sheetObject) return
|
||||
|
||||
const object = objectRef.current!
|
||||
|
||||
const setFromTheatre = (newValues: any) => {
|
||||
object.position.set(
|
||||
newValues.position.x,
|
||||
newValues.position.y,
|
||||
newValues.position.z,
|
||||
)
|
||||
object.rotation.set(
|
||||
newValues.rotation.x,
|
||||
newValues.rotation.y,
|
||||
newValues.rotation.z,
|
||||
)
|
||||
object.scale.set(
|
||||
newValues.scale.x,
|
||||
newValues.scale.y,
|
||||
newValues.scale.z,
|
||||
)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
setFromTheatre(sheetObject.value)
|
||||
|
||||
const untap = sheetObject.onValuesChange(setFromTheatre)
|
||||
|
||||
return () => {
|
||||
untap()
|
||||
}
|
||||
}, [sheetObject])
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Component
|
||||
ref={mergeRefs([objectRef, ref])}
|
||||
{...props}
|
||||
visible={visible !== 'editor' && visible}
|
||||
userData={{
|
||||
__editable: true,
|
||||
__editableName: uniqueName,
|
||||
__editableType: type ?? editableType,
|
||||
__visibleOnlyInEditor: visible === 'editor',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const createEditable = <T extends EditableType>(type: T) =>
|
||||
// @ts-ignore
|
||||
editable(type, type)
|
||||
|
||||
editable.primitive = editable('primitive', null)
|
||||
editable.group = createEditable('group')
|
||||
editable.mesh = createEditable('mesh')
|
||||
editable.spotLight = createEditable('spotLight')
|
||||
editable.directionalLight = createEditable('directionalLight')
|
||||
editable.pointLight = createEditable('pointLight')
|
||||
editable.perspectiveCamera = createEditable('perspectiveCamera')
|
||||
editable.orthographicCamera = createEditable('orthographicCamera')
|
||||
|
||||
export default editable
|
67
packages/r3f/src/components/editorStuff.ts
Normal file
67
packages/r3f/src/components/editorStuff.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import type {ISheet, ISheetObject} from '@theatre/core'
|
||||
import {types} from '@theatre/core'
|
||||
import studio from '@theatre/studio'
|
||||
|
||||
let sheet: ISheet | undefined = undefined
|
||||
let sheetObject: ISheetObject<typeof editorSheetObjectConfig> | undefined =
|
||||
undefined
|
||||
|
||||
const editorSheetObjectConfig = types.compound({
|
||||
viewport: types.compound(
|
||||
{
|
||||
showAxes: types.boolean(true, {label: 'Axes'}),
|
||||
showGrid: types.boolean(true, {label: 'Grid'}),
|
||||
showOverlayIcons: types.boolean(false, {label: 'Overlay Icons'}),
|
||||
shading: types.stringLiteral(
|
||||
'rendered',
|
||||
{
|
||||
flat: 'Flat',
|
||||
rendered: 'Rendered',
|
||||
solid: 'Solid',
|
||||
wireframe: 'Wireframe',
|
||||
},
|
||||
{as: 'menu', label: 'Shading'},
|
||||
),
|
||||
},
|
||||
{label: 'Viewport Config'},
|
||||
),
|
||||
transformControls: types.compound(
|
||||
{
|
||||
mode: types.stringLiteral(
|
||||
'translate',
|
||||
{
|
||||
translate: 'Translate',
|
||||
rotate: 'Rotate',
|
||||
scale: 'Scale',
|
||||
},
|
||||
{as: 'switch', label: 'Mode'},
|
||||
),
|
||||
space: types.stringLiteral(
|
||||
'world',
|
||||
{
|
||||
local: 'Local',
|
||||
world: 'World',
|
||||
},
|
||||
{as: 'switch', label: 'Space'},
|
||||
),
|
||||
},
|
||||
{label: 'Transform Controls'},
|
||||
),
|
||||
})
|
||||
|
||||
export function getEditorSheet(): ISheet {
|
||||
if (!sheet) {
|
||||
sheet = studio.getStudioProject().sheet('R3F UI')
|
||||
}
|
||||
return sheet
|
||||
}
|
||||
|
||||
export function getEditorSheetObject(): ISheetObject<
|
||||
typeof editorSheetObjectConfig
|
||||
> | null {
|
||||
if (!sheetObject) {
|
||||
sheetObject =
|
||||
getEditorSheet().object('Editor', editorSheetObjectConfig) || null
|
||||
}
|
||||
return sheetObject
|
||||
}
|
5
packages/r3f/src/components/useInvalidate.ts
Normal file
5
packages/r3f/src/components/useInvalidate.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import {useThree} from '@react-three/fiber'
|
||||
|
||||
export default function useInvalidate() {
|
||||
return useThree(({invalidate}) => invalidate)
|
||||
}
|
37
packages/r3f/src/components/useRefAndState.ts
Normal file
37
packages/r3f/src/components/useRefAndState.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import type {MutableRefObject} from 'react'
|
||||
import {useMemo, useState} from 'react'
|
||||
|
||||
/**
|
||||
* Combines useRef() and useState().
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const [ref, val] = useRefAndState<HTMLDivElement | null>(null)
|
||||
*
|
||||
* useEffect(() => {
|
||||
* val.addEventListener(...)
|
||||
* }, [val])
|
||||
*
|
||||
* return <div ref={ref}></div>
|
||||
* ```
|
||||
*/
|
||||
export default function useRefAndState<T>(
|
||||
initialValue: T,
|
||||
): [ref: MutableRefObject<T>, state: T] {
|
||||
const ref = useMemo(() => {
|
||||
let current = initialValue
|
||||
return {
|
||||
get current() {
|
||||
return current
|
||||
},
|
||||
set current(v: T) {
|
||||
current = v
|
||||
setState(v)
|
||||
},
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [state, setState] = useState<T>(() => initialValue)
|
||||
|
||||
return [ref, state]
|
||||
}
|
11
packages/r3f/src/components/useRefreshSnapshot.ts
Normal file
11
packages/r3f/src/components/useRefreshSnapshot.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import {useCallback} from 'react'
|
||||
import {useEditorStore} from '../store'
|
||||
|
||||
/**
|
||||
* Returns a function that can be called to refresh the snapshot in the snapshot editor.
|
||||
*/
|
||||
export default function useRefreshSnapshot() {
|
||||
return useCallback(() => {
|
||||
useEditorStore.getState().createSnapshot()
|
||||
}, [])
|
||||
}
|
41
packages/r3f/src/components/useSelected.tsx
Normal file
41
packages/r3f/src/components/useSelected.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import {useLayoutEffect, useRef, useState} from 'react'
|
||||
import {allRegisteredObjects} from '../store'
|
||||
import studio from '@theatre/studio'
|
||||
import type {ISheetObject} from '@theatre/core'
|
||||
|
||||
export function useSelected(): undefined | string {
|
||||
const [state, set] = useState<string | undefined>(undefined)
|
||||
const stateRef = useRef(state)
|
||||
stateRef.current = state
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const setFromStudio = (selection: typeof studio.selection) => {
|
||||
const item = selection.find(
|
||||
(s): s is ISheetObject =>
|
||||
s.type === 'Theatre_SheetObject_PublicAPI' &&
|
||||
allRegisteredObjects.has(s),
|
||||
)
|
||||
if (!item) {
|
||||
set(undefined)
|
||||
} else {
|
||||
set(item.address.objectKey)
|
||||
}
|
||||
}
|
||||
setFromStudio(studio.selection)
|
||||
return studio.onSelectionChange(setFromStudio)
|
||||
}, [])
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
export function getSelected(): undefined | string {
|
||||
const item = studio.selection.find(
|
||||
(s): s is ISheetObject =>
|
||||
s.type === 'Theatre_SheetObject_PublicAPI' && allRegisteredObjects.has(s),
|
||||
)
|
||||
if (!item) {
|
||||
return undefined
|
||||
} else {
|
||||
return item.address.objectKey
|
||||
}
|
||||
}
|
168
packages/r3f/src/components/useSnapshotEditorCamera.tsx
Normal file
168
packages/r3f/src/components/useSnapshotEditorCamera.tsx
Normal file
|
@ -0,0 +1,168 @@
|
|||
import {OrbitControls, PerspectiveCamera} from '@react-three/drei'
|
||||
import type {OrbitControls as OrbitControlsImpl} from 'three-stdlib'
|
||||
import type {MutableRefObject} from 'react'
|
||||
import {useLayoutEffect, useRef} from 'react'
|
||||
import React from 'react'
|
||||
import useRefAndState from './useRefAndState'
|
||||
import studio from '@theatre/studio'
|
||||
import type {PerspectiveCamera as PerspectiveCameraImpl} from 'three'
|
||||
import type {ISheet} from '@theatre/core'
|
||||
import {types} from '@theatre/core'
|
||||
import type {ISheetObject} from '@theatre/core'
|
||||
import {useThree} from '@react-three/fiber'
|
||||
|
||||
const camConf = types.compound({
|
||||
transform: types.compound({
|
||||
position: types.compound({
|
||||
x: types.number(10),
|
||||
y: types.number(10),
|
||||
z: types.number(0),
|
||||
}),
|
||||
target: types.compound({
|
||||
x: types.number(0),
|
||||
y: types.number(0),
|
||||
z: types.number(0),
|
||||
}),
|
||||
}),
|
||||
lens: types.compound({
|
||||
zoom: types.number(1, {range: [0.0001, 10]}),
|
||||
fov: types.number(50, {range: [1, 1000]}),
|
||||
near: types.number(0.1, {range: [0, Infinity]}),
|
||||
far: types.number(2000, {range: [0, Infinity]}),
|
||||
focus: types.number(10, {range: [0, Infinity]}),
|
||||
filmGauge: types.number(35, {range: [0, Infinity]}),
|
||||
filmOffset: types.number(0, {range: [0, Infinity]}),
|
||||
}),
|
||||
})
|
||||
|
||||
export default function useSnapshotEditorCamera(
|
||||
snapshotEditorSheet: ISheet,
|
||||
paneId: string,
|
||||
): [
|
||||
node: React.ReactNode,
|
||||
orbitControlsRef: MutableRefObject<OrbitControlsImpl | null>,
|
||||
] {
|
||||
// OrbitControls and Cam might change later on, so we use useRefAndState()
|
||||
// instead of useRef() to catch those changes.
|
||||
const [orbitControlsRef, orbitControls] =
|
||||
useRefAndState<OrbitControlsImpl | null>(null)
|
||||
|
||||
const [camRef, cam] = useRefAndState<PerspectiveCameraImpl | undefined>(
|
||||
undefined,
|
||||
)
|
||||
|
||||
const objRef = useRef<ISheetObject<typeof camConf> | null>(null)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!objRef.current) {
|
||||
objRef.current = snapshotEditorSheet.object(
|
||||
`Editor Camera ${paneId}`,
|
||||
camConf,
|
||||
)
|
||||
}
|
||||
}, [paneId])
|
||||
|
||||
usePassValuesFromTheatreToCamera(cam, orbitControls, objRef)
|
||||
usePassValuesFromOrbitControlsToTheatre(cam, orbitControls, objRef)
|
||||
|
||||
const node = (
|
||||
<>
|
||||
<PerspectiveCamera makeDefault ref={camRef} position={[0, 102, 0]} />
|
||||
<OrbitControls
|
||||
makeDefault
|
||||
ref={orbitControlsRef}
|
||||
camera={cam}
|
||||
enableDamping={false}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
return [node, orbitControlsRef]
|
||||
}
|
||||
|
||||
function usePassValuesFromOrbitControlsToTheatre(
|
||||
cam: PerspectiveCameraImpl | undefined,
|
||||
orbitControls: OrbitControlsImpl | null,
|
||||
objRef: MutableRefObject<ISheetObject<typeof camConf> | null>,
|
||||
) {
|
||||
useLayoutEffect(() => {
|
||||
if (!cam || orbitControls == null) return
|
||||
|
||||
let currentScrub: undefined | ReturnType<typeof studio['debouncedScrub']>
|
||||
|
||||
let started = false
|
||||
|
||||
const onStart = () => {
|
||||
started = true
|
||||
if (!currentScrub) {
|
||||
currentScrub = studio.debouncedScrub(600)
|
||||
}
|
||||
}
|
||||
const onEnd = () => {
|
||||
started = false
|
||||
}
|
||||
|
||||
const onChange = () => {
|
||||
if (!started) return
|
||||
|
||||
const p = cam!.position
|
||||
const position = {x: p.x, y: p.y, z: p.z}
|
||||
|
||||
const t = orbitControls!.target
|
||||
const target = {x: t.x, y: t.y, z: t.z}
|
||||
|
||||
const transform = {
|
||||
position,
|
||||
target,
|
||||
}
|
||||
|
||||
currentScrub!.capture(({set}) => {
|
||||
set(objRef.current!.props.transform, transform)
|
||||
})
|
||||
}
|
||||
|
||||
orbitControls.addEventListener('start', onStart)
|
||||
orbitControls.addEventListener('end', onEnd)
|
||||
orbitControls.addEventListener('change', onChange)
|
||||
|
||||
return () => {
|
||||
orbitControls.removeEventListener('start', onStart)
|
||||
orbitControls.removeEventListener('end', onEnd)
|
||||
orbitControls.removeEventListener('change', onChange)
|
||||
}
|
||||
}, [cam, orbitControls])
|
||||
}
|
||||
|
||||
function usePassValuesFromTheatreToCamera(
|
||||
cam: PerspectiveCameraImpl | undefined,
|
||||
orbitControls: OrbitControlsImpl | null,
|
||||
objRef: MutableRefObject<ISheetObject<typeof camConf> | null>,
|
||||
) {
|
||||
const invalidate = useThree(({invalidate}) => invalidate)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!cam || orbitControls === null) return
|
||||
|
||||
const obj = objRef.current!
|
||||
const setFromTheatre = (props: typeof camConf['valueType']): void => {
|
||||
const {position, target} = props.transform
|
||||
cam.zoom = props.lens.zoom
|
||||
cam.fov = props.lens.fov
|
||||
cam.near = props.lens.near
|
||||
cam.far = props.lens.far
|
||||
cam.focus = props.lens.focus
|
||||
cam.filmGauge = props.lens.filmGauge
|
||||
cam.filmOffset = props.lens.filmOffset
|
||||
cam.position.set(position.x, position.y, position.z)
|
||||
cam.updateProjectionMatrix()
|
||||
orbitControls.target.set(target.x, target.y, target.z)
|
||||
orbitControls.update()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
const unsub = obj.onValuesChange(setFromTheatre)
|
||||
setFromTheatre(obj.value)
|
||||
|
||||
return unsub
|
||||
}, [cam, orbitControls, objRef, invalidate])
|
||||
}
|
18
packages/r3f/src/extension.ts
Normal file
18
packages/r3f/src/extension.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import SnapshotEditor from './components/SnapshotEditor'
|
||||
import type {IExtension} from '@theatre/studio'
|
||||
import Toolbar from './components/Toolbar/Toolbar'
|
||||
|
||||
const r3fExtension: IExtension = {
|
||||
id: '@theatre/r3f',
|
||||
globalToolbar: {
|
||||
component: Toolbar,
|
||||
},
|
||||
panes: [
|
||||
{
|
||||
class: 'snapshot',
|
||||
component: SnapshotEditor,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default r3fExtension
|
3
packages/r3f/src/globals.d.ts
vendored
Normal file
3
packages/r3f/src/globals.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
declare module '*.txt' {
|
||||
export default string
|
||||
}
|
8
packages/r3f/src/index.tsx
Normal file
8
packages/r3f/src/index.tsx
Normal file
|
@ -0,0 +1,8 @@
|
|||
export {default as extension} from './extension'
|
||||
export {default as EditorHelper} from './components/EditorHelper'
|
||||
export type {EditorHelperProps} from './components/EditorHelper'
|
||||
export {default as editable} from './components/editable'
|
||||
export type {EditableState, BindFunction} from './store'
|
||||
export {default as SheetProvider, useCurrentSheet} from './SheetProvider'
|
||||
export {default as useRefreshSnapshot} from './components/useRefreshSnapshot'
|
||||
export {default as RefreshSnapshot} from './components/RefreshSnapshot'
|
277
packages/r3f/src/store.ts
Normal file
277
packages/r3f/src/store.ts
Normal file
|
@ -0,0 +1,277 @@
|
|||
import type {StateCreator} from 'zustand'
|
||||
import create from 'zustand'
|
||||
import type {Object3D, Scene, WebGLRenderer} from 'three'
|
||||
import {Group} from 'three'
|
||||
import type {ISheetObject} from '@theatre/core'
|
||||
import {types} from '@theatre/core'
|
||||
|
||||
export type EditableType =
|
||||
| 'group'
|
||||
| 'mesh'
|
||||
| 'spotLight'
|
||||
| 'directionalLight'
|
||||
| 'pointLight'
|
||||
| 'perspectiveCamera'
|
||||
| 'orthographicCamera'
|
||||
|
||||
export type TransformControlsMode = 'translate' | 'rotate' | 'scale'
|
||||
export type TransformControlsSpace = 'world' | 'local'
|
||||
export type ViewportShading = 'wireframe' | 'flat' | 'solid' | 'rendered'
|
||||
|
||||
const positionComp = types.number(1, {nudgeMultiplier: 0.1})
|
||||
const rotationComp = types.number(1, {nudgeMultiplier: 0.02})
|
||||
const scaleComp = types.number(1, {nudgeMultiplier: 0.1})
|
||||
|
||||
export const baseSheetObjectType = types.compound({
|
||||
position: types.compound({
|
||||
x: positionComp,
|
||||
y: positionComp,
|
||||
z: positionComp,
|
||||
}),
|
||||
rotation: types.compound({
|
||||
x: rotationComp,
|
||||
y: rotationComp,
|
||||
z: rotationComp,
|
||||
}),
|
||||
scale: types.compound({
|
||||
x: scaleComp,
|
||||
y: scaleComp,
|
||||
z: scaleComp,
|
||||
}),
|
||||
})
|
||||
|
||||
export type BaseSheetObjectType = ISheetObject<typeof baseSheetObjectType>
|
||||
|
||||
export const allRegisteredObjects = new WeakSet<BaseSheetObjectType>()
|
||||
|
||||
export interface AbstractEditable<T extends EditableType> {
|
||||
type: T
|
||||
role: 'active' | 'removed'
|
||||
sheetObject?: ISheetObject<any>
|
||||
}
|
||||
|
||||
// all these identical types are to prepare for a future in which different object types have different properties
|
||||
export interface EditableGroup extends AbstractEditable<'group'> {
|
||||
sheetObject?: BaseSheetObjectType
|
||||
}
|
||||
|
||||
export interface EditableMesh extends AbstractEditable<'mesh'> {
|
||||
sheetObject?: BaseSheetObjectType
|
||||
}
|
||||
|
||||
export interface EditableSpotLight extends AbstractEditable<'spotLight'> {
|
||||
sheetObject?: BaseSheetObjectType
|
||||
}
|
||||
|
||||
export interface EditableDirectionalLight
|
||||
extends AbstractEditable<'directionalLight'> {
|
||||
sheetObject?: BaseSheetObjectType
|
||||
}
|
||||
|
||||
export interface EditablePointLight extends AbstractEditable<'pointLight'> {
|
||||
sheetObject?: BaseSheetObjectType
|
||||
}
|
||||
|
||||
export interface EditablePerspectiveCamera
|
||||
extends AbstractEditable<'perspectiveCamera'> {
|
||||
sheetObject?: BaseSheetObjectType
|
||||
}
|
||||
|
||||
export interface EditableOrthographicCamera
|
||||
extends AbstractEditable<'orthographicCamera'> {
|
||||
sheetObject?: BaseSheetObjectType
|
||||
}
|
||||
|
||||
export type Editable =
|
||||
| EditableGroup
|
||||
| EditableMesh
|
||||
| EditableSpotLight
|
||||
| EditableDirectionalLight
|
||||
| EditablePointLight
|
||||
| EditablePerspectiveCamera
|
||||
| EditableOrthographicCamera
|
||||
|
||||
export type EditableSnapshot<T extends Editable = Editable> = {
|
||||
proxyObject?: Object3D | null
|
||||
} & T
|
||||
|
||||
export interface AbstractSerializedEditable<T extends EditableType> {
|
||||
type: T
|
||||
}
|
||||
|
||||
export interface SerializedEditableGroup
|
||||
extends AbstractSerializedEditable<'group'> {}
|
||||
|
||||
export interface SerializedEditableMesh
|
||||
extends AbstractSerializedEditable<'mesh'> {}
|
||||
|
||||
export interface SerializedEditableSpotLight
|
||||
extends AbstractSerializedEditable<'spotLight'> {}
|
||||
|
||||
export interface SerializedEditableDirectionalLight
|
||||
extends AbstractSerializedEditable<'directionalLight'> {}
|
||||
|
||||
export interface SerializedEditablePointLight
|
||||
extends AbstractSerializedEditable<'pointLight'> {}
|
||||
|
||||
export interface SerializedEditablePerspectiveCamera
|
||||
extends AbstractSerializedEditable<'perspectiveCamera'> {}
|
||||
|
||||
export interface SerializedEditableOrthographicCamera
|
||||
extends AbstractSerializedEditable<'orthographicCamera'> {}
|
||||
|
||||
export type SerializedEditable =
|
||||
| SerializedEditableGroup
|
||||
| SerializedEditableMesh
|
||||
| SerializedEditableSpotLight
|
||||
| SerializedEditableDirectionalLight
|
||||
| SerializedEditablePointLight
|
||||
| SerializedEditablePerspectiveCamera
|
||||
| SerializedEditableOrthographicCamera
|
||||
|
||||
export interface EditableState {
|
||||
editables: Record<string, SerializedEditable>
|
||||
}
|
||||
|
||||
export type EditorStore = {
|
||||
sheetObjects: {[uniqueName in string]?: BaseSheetObjectType}
|
||||
scene: Scene | null
|
||||
gl: WebGLRenderer | null
|
||||
allowImplicitInstancing: boolean
|
||||
helpersRoot: Group
|
||||
editables: Record<string, Editable>
|
||||
// this will come in handy when we start supporting multiple canvases
|
||||
canvasName: string
|
||||
sceneSnapshot: Scene | null
|
||||
editablesSnapshot: Record<string, EditableSnapshot> | null
|
||||
|
||||
init: (
|
||||
scene: Scene,
|
||||
gl: WebGLRenderer,
|
||||
allowImplicitInstancing: boolean,
|
||||
) => void
|
||||
|
||||
addEditable: <T extends EditableType>(type: T, uniqueName: string) => void
|
||||
removeEditable: (uniqueName: string) => void
|
||||
createSnapshot: () => void
|
||||
setSheetObject: (uniqueName: string, sheetObject: BaseSheetObjectType) => void
|
||||
setSnapshotProxyObject: (
|
||||
proxyObject: Object3D | null,
|
||||
uniqueName: string,
|
||||
) => void
|
||||
}
|
||||
|
||||
const config: StateCreator<EditorStore> = (set, get) => {
|
||||
return {
|
||||
sheet: null,
|
||||
editorObject: null,
|
||||
sheetObjects: {},
|
||||
scene: null,
|
||||
gl: null,
|
||||
allowImplicitInstancing: false,
|
||||
helpersRoot: new Group(),
|
||||
editables: {},
|
||||
canvasName: 'default',
|
||||
sceneSnapshot: null,
|
||||
editablesSnapshot: null,
|
||||
initialEditorCamera: {},
|
||||
|
||||
init: (scene, gl, allowImplicitInstancing) => {
|
||||
set({
|
||||
scene,
|
||||
gl,
|
||||
allowImplicitInstancing,
|
||||
})
|
||||
},
|
||||
|
||||
addEditable: (type, uniqueName) =>
|
||||
set((state) => {
|
||||
if (state.editables[uniqueName]) {
|
||||
if (
|
||||
state.editables[uniqueName].type !== type &&
|
||||
process.env.NODE_ENV === 'development'
|
||||
) {
|
||||
console.error(`Warning: There is a mismatch between the serialized type of ${uniqueName} and the one set when adding it to the scene.
|
||||
Serialized: ${state.editables[uniqueName].type}.
|
||||
Current: ${type}.
|
||||
|
||||
This might have happened either because you changed the type of an object, in which case a re-export will solve the issue, or because you re-used the uniqueName for an object of a different type, which is an error.`)
|
||||
}
|
||||
if (
|
||||
state.editables[uniqueName].role === 'active' &&
|
||||
!state.allowImplicitInstancing
|
||||
) {
|
||||
throw Error(
|
||||
`Scene already has an editable object named ${uniqueName}.
|
||||
If this is intentional, please set the allowImplicitInstancing prop of EditableManager to true.`,
|
||||
)
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
editables: {
|
||||
...state.editables,
|
||||
[uniqueName]: {
|
||||
type: type as EditableType,
|
||||
role: 'active',
|
||||
},
|
||||
},
|
||||
}
|
||||
}),
|
||||
|
||||
removeEditable: (name) =>
|
||||
set((state) => {
|
||||
const {[name]: removed, ...rest} = state.editables
|
||||
return {
|
||||
editables: {
|
||||
...rest,
|
||||
[name]: {...removed, role: 'removed'},
|
||||
},
|
||||
}
|
||||
}),
|
||||
setSheetObject: (uniqueName, sheetObject) => {
|
||||
set((state) => ({
|
||||
sheetObjects: {
|
||||
...state.sheetObjects,
|
||||
[uniqueName]: sheetObject,
|
||||
},
|
||||
}))
|
||||
},
|
||||
|
||||
createSnapshot: () => {
|
||||
set((state) => ({
|
||||
sceneSnapshot: state.scene?.clone() ?? null,
|
||||
editablesSnapshot: state.editables,
|
||||
}))
|
||||
},
|
||||
setSnapshotProxyObject: (proxyObject, uniqueName) => {
|
||||
set((state) => ({
|
||||
editablesSnapshot: {
|
||||
...state.editablesSnapshot,
|
||||
[uniqueName]: {
|
||||
...state.editablesSnapshot![uniqueName],
|
||||
proxyObject,
|
||||
},
|
||||
},
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const useEditorStore = create<EditorStore>(config)
|
||||
|
||||
export type BindFunction = (options: {
|
||||
allowImplicitInstancing?: boolean
|
||||
gl: WebGLRenderer
|
||||
scene: Scene
|
||||
}) => void
|
||||
|
||||
export const bindToCanvas: BindFunction = ({
|
||||
allowImplicitInstancing = false,
|
||||
gl,
|
||||
scene,
|
||||
}) => {
|
||||
const init = useEditorStore.getState().init
|
||||
init(scene, gl, allowImplicitInstancing)
|
||||
}
|
2
packages/r3f/src/types.ts
Normal file
2
packages/r3f/src/types.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export type $FixMe = any
|
||||
export type $IntentionalAny = any
|
13
packages/r3f/tsconfig.json
Normal file
13
packages/r3f/tsconfig.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"rootDir": "src",
|
||||
"types": ["jest", "node"],
|
||||
"emitDeclarationOnly": true,
|
||||
"composite": true
|
||||
},
|
||||
"references": [{"path": "../../theatre"}],
|
||||
"include": ["./src/**/*"]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue