Add instant editable cameras to the r3f extension (#315)
Add spruced up editable orthographic/perspective camera
This commit is contained in:
parent
25cf1d7fe8
commit
6497bf2097
5 changed files with 182 additions and 13 deletions
87
packages/r3f/src/drei/OrthographicCamera.tsx
Normal file
87
packages/r3f/src/drei/OrthographicCamera.tsx
Normal 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',
|
||||||
|
)
|
75
packages/r3f/src/drei/PerspectiveCamera.tsx
Normal file
75
packages/r3f/src/drei/PerspectiveCamera.tsx
Normal 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',
|
||||||
|
)
|
2
packages/r3f/src/drei/index.ts
Normal file
2
packages/r3f/src/drei/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './PerspectiveCamera'
|
||||||
|
export * from './OrthographicCamera'
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue