Add instant editable cameras to the r3f extension (#315)

Add spruced up editable orthographic/perspective camera
This commit is contained in:
Andrew Prifer 2022-10-13 20:14:53 +02:00 committed by GitHub
parent 25cf1d7fe8
commit 6497bf2097
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 182 additions and 13 deletions

View file

@ -0,0 +1,87 @@
import * as React from 'react'
import type {
OrthographicCamera as OrthographicCameraImpl,
Object3D,
} from 'three'
import {useFrame, useThree} from '@react-three/fiber'
import mergeRefs from 'react-merge-refs'
import {editable} from '../index'
import {Vector3} from 'three'
import type {MutableRefObject} from 'react'
import {editorStore} from '../main/store'
export type OrthographicCameraProps = Omit<
JSX.IntrinsicElements['orthographicCamera'],
'lookAt'
> & {
lookAt?:
| [number, number, number]
| Vector3
| MutableRefObject<Object3D | null | undefined>
makeDefault?: boolean
manual?: boolean
children?: React.ReactNode
}
export const OrthographicCamera = editable(
React.forwardRef(
({makeDefault, lookAt, ...props}: OrthographicCameraProps, ref) => {
const set = useThree(({set}) => set)
const camera = useThree(({camera}) => camera)
const size = useThree(({size}) => size)
const cameraRef = React.useRef<OrthographicCameraImpl>(null!)
React.useLayoutEffect(() => {
if (!props.manual) {
cameraRef.current.updateProjectionMatrix()
}
}, [size, props])
React.useLayoutEffect(() => {
cameraRef.current.updateProjectionMatrix()
})
React.useLayoutEffect(() => {
if (makeDefault) {
const oldCam = camera
set(() => ({camera: cameraRef.current!}))
return () => set(() => ({camera: oldCam}))
}
// The camera should not be part of the dependency list because this components camera is a stable reference
// that must exchange the default, and clean up after itself on unmount.
}, [cameraRef, makeDefault, set])
useFrame(() => {
if (lookAt && cameraRef.current) {
cameraRef.current.lookAt(
Array.isArray(lookAt)
? new Vector3(...lookAt)
: (lookAt as MutableRefObject<Object3D>).current
? (lookAt as MutableRefObject<Object3D>).current.position
: (lookAt as Vector3),
)
// how could we make it possible for users to do something like this too?
const snapshot = editorStore.getState().editablesSnapshot
if (snapshot) {
snapshot[
cameraRef.current.userData.__storeKey
].proxyObject?.rotation.copy(cameraRef.current.rotation)
}
}
})
return (
<orthographicCamera
left={size.width / -2}
right={size.width / 2}
top={size.height / 2}
bottom={size.height / -2}
ref={mergeRefs([cameraRef, ref])}
{...props}
/>
)
},
),
'orthographicCamera',
)

View file

@ -0,0 +1,75 @@
import * as React from 'react'
import type {PerspectiveCamera as PerspectiveCameraImpl, Object3D} from 'three'
import {useFrame, useThree} from '@react-three/fiber'
import mergeRefs from 'react-merge-refs'
import {editable} from '../index'
import {Vector3} from 'three'
import {editorStore} from '../main/store'
import type {MutableRefObject} from 'react'
export type PerspectiveCameraProps = Omit<
JSX.IntrinsicElements['perspectiveCamera'],
'lookAt'
> & {
lookAt?:
| [number, number, number]
| Vector3
| MutableRefObject<Object3D | null | undefined>
makeDefault?: boolean
manual?: boolean
children?: React.ReactNode
}
export const PerspectiveCamera = editable(
React.forwardRef(
({makeDefault, lookAt, ...props}: PerspectiveCameraProps, ref) => {
const set = useThree(({set}) => set)
const camera = useThree(({camera}) => camera)
const size = useThree(({size}) => size)
const cameraRef = React.useRef<PerspectiveCameraImpl>(null!)
React.useLayoutEffect(() => {
if (!props.manual) {
cameraRef.current.aspect = size.width / size.height
}
}, [size, props])
React.useLayoutEffect(() => {
cameraRef.current.updateProjectionMatrix()
})
React.useLayoutEffect(() => {
if (makeDefault) {
const oldCam = camera
set(() => ({camera: cameraRef.current!}))
return () => set(() => ({camera: oldCam}))
}
// The camera should not be part of the dependency list because this components camera is a stable reference
// that must exchange the default, and clean up after itself on unmount.
}, [cameraRef, makeDefault, set])
useFrame(() => {
if (lookAt && cameraRef.current) {
cameraRef.current.lookAt(
Array.isArray(lookAt)
? new Vector3(...lookAt)
: (lookAt as MutableRefObject<Object3D>).current
? (lookAt as MutableRefObject<Object3D>).current.position
: (lookAt as Vector3),
)
// how could we make it possible for users to do something like this too?
const snapshot = editorStore.getState().editablesSnapshot
if (snapshot) {
snapshot[
cameraRef.current.userData.__storeKey
].proxyObject?.rotation.copy(cameraRef.current.rotation)
}
}
})
return <perspectiveCamera ref={mergeRefs([cameraRef, ref])} {...props} />
},
),
'perspectiveCamera',
)

View file

@ -0,0 +1,2 @@
export * from './PerspectiveCamera'
export * from './OrthographicCamera'

View file

@ -23,3 +23,4 @@ export {makeStoreKey as __private_makeStoreKey} from './main/utils'
export {default as SheetProvider, useCurrentSheet} from './main/SheetProvider' export {default as SheetProvider, useCurrentSheet} from './main/SheetProvider'
export {refreshSnapshot} from './main/utils' export {refreshSnapshot} from './main/utils'
export {default as RefreshSnapshot} from './main/RefreshSnapshot' export {default as RefreshSnapshot} from './main/RefreshSnapshot'
export * from './drei'

View file

@ -1,4 +1,4 @@
import type {ComponentProps, ComponentType, RefAttributes} from 'react' import type {ComponentProps, ComponentType, Ref, RefAttributes} from 'react'
import React, { import React, {
forwardRef, forwardRef,
useEffect, useEffect,
@ -64,6 +64,7 @@ const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
const invalidate = useInvalidate() const invalidate = useInvalidate()
// warn about cameras in r3f
useEffect(() => { useEffect(() => {
if ( if (
Component === 'perspectiveCamera' || Component === 'perspectiveCamera' ||
@ -95,6 +96,7 @@ Then you can use it in your JSX like any other editable component. Note the make
} }
}, [Component, theatreKey]) }, [Component, theatreKey])
// create sheet object and add editable to store
useLayoutEffect(() => { useLayoutEffect(() => {
if (!sheet) return if (!sheet) return
const sheetObject = sheet.object( const sheetObject = sheet.object(
@ -199,27 +201,29 @@ Then you can use it in your JSX like any other editable component. Note the make
primitive: editable('primitive', null), primitive: editable('primitive', null),
} as unknown as { } as unknown as {
[Property in Keys]: React.ForwardRefExoticComponent< [Property in Keys]: React.ForwardRefExoticComponent<
React.PropsWithoutRef< React.PropsWithRef<
Omit<JSX.IntrinsicElements[Property], 'visible'> & { Omit<JSX.IntrinsicElements[Property], 'visible'> & {
theatreKey: string theatreKey: string
visible?: boolean | 'editor' visible?: boolean | 'editor'
additionalProps?: $FixMe additionalProps?: $FixMe
objRef?: $FixMe objRef?: $FixMe
} & React.RefAttributes<JSX.IntrinsicElements[Property]> // not exactly sure how to get the type of the threejs object itself
ref?: Ref<unknown>
}
> >
> >
} & { } & {
primitive: React.ForwardRefExoticComponent< primitive: React.ForwardRefExoticComponent<
React.PropsWithoutRef< React.PropsWithRef<{
{
object: any object: any
theatreKey: string theatreKey: string
visible?: boolean | 'editor' visible?: boolean | 'editor'
additionalProps?: $FixMe additionalProps?: $FixMe
objRef?: $FixMe objRef?: $FixMe
editableType: keyof JSX.IntrinsicElements editableType: keyof JSX.IntrinsicElements
} & React.RefAttributes<JSX.IntrinsicElements['primitive']> // not exactly sure how to get the type of the threejs object itself
> & { ref?: Ref<unknown>
}> & {
// Have to reproduce the primitive component's props here because we need to // Have to reproduce the primitive component's props here because we need to
// lift this index type here to the outside to make auto-complete work // lift this index type here to the outside to make auto-complete work
[props: string]: any [props: string]: any