From 6497bf2097f20bbc77f752189374c4ba63b48043 Mon Sep 17 00:00:00 2001 From: Andrew Prifer <2991360+AndrewPrifer@users.noreply.github.com> Date: Thu, 13 Oct 2022 20:14:53 +0200 Subject: [PATCH] Add instant editable cameras to the r3f extension (#315) Add spruced up editable orthographic/perspective camera --- packages/r3f/src/drei/OrthographicCamera.tsx | 87 ++++++++++++++++++++ packages/r3f/src/drei/PerspectiveCamera.tsx | 75 +++++++++++++++++ packages/r3f/src/drei/index.ts | 2 + packages/r3f/src/index.ts | 1 + packages/r3f/src/main/editable.tsx | 30 ++++--- 5 files changed, 182 insertions(+), 13 deletions(-) create mode 100644 packages/r3f/src/drei/OrthographicCamera.tsx create mode 100644 packages/r3f/src/drei/PerspectiveCamera.tsx create mode 100644 packages/r3f/src/drei/index.ts diff --git a/packages/r3f/src/drei/OrthographicCamera.tsx b/packages/r3f/src/drei/OrthographicCamera.tsx new file mode 100644 index 0000000..0c55d24 --- /dev/null +++ b/packages/r3f/src/drei/OrthographicCamera.tsx @@ -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 + 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(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).current + ? (lookAt as MutableRefObject).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', +) diff --git a/packages/r3f/src/drei/PerspectiveCamera.tsx b/packages/r3f/src/drei/PerspectiveCamera.tsx new file mode 100644 index 0000000..830d67e --- /dev/null +++ b/packages/r3f/src/drei/PerspectiveCamera.tsx @@ -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 + 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(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).current + ? (lookAt as MutableRefObject).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', +) diff --git a/packages/r3f/src/drei/index.ts b/packages/r3f/src/drei/index.ts new file mode 100644 index 0000000..97b2c8a --- /dev/null +++ b/packages/r3f/src/drei/index.ts @@ -0,0 +1,2 @@ +export * from './PerspectiveCamera' +export * from './OrthographicCamera' diff --git a/packages/r3f/src/index.ts b/packages/r3f/src/index.ts index f030548..ff6e4de 100644 --- a/packages/r3f/src/index.ts +++ b/packages/r3f/src/index.ts @@ -23,3 +23,4 @@ export {makeStoreKey as __private_makeStoreKey} from './main/utils' export {default as SheetProvider, useCurrentSheet} from './main/SheetProvider' export {refreshSnapshot} from './main/utils' export {default as RefreshSnapshot} from './main/RefreshSnapshot' +export * from './drei' diff --git a/packages/r3f/src/main/editable.tsx b/packages/r3f/src/main/editable.tsx index 9b2b7bf..ae1dae3 100644 --- a/packages/r3f/src/main/editable.tsx +++ b/packages/r3f/src/main/editable.tsx @@ -1,4 +1,4 @@ -import type {ComponentProps, ComponentType, RefAttributes} from 'react' +import type {ComponentProps, ComponentType, Ref, RefAttributes} from 'react' import React, { forwardRef, useEffect, @@ -64,6 +64,7 @@ const createEditable = ( const invalidate = useInvalidate() + // warn about cameras in r3f useEffect(() => { if ( Component === 'perspectiveCamera' || @@ -95,6 +96,7 @@ Then you can use it in your JSX like any other editable component. Note the make } }, [Component, theatreKey]) + // create sheet object and add editable to store useLayoutEffect(() => { if (!sheet) return 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), } as unknown as { [Property in Keys]: React.ForwardRefExoticComponent< - React.PropsWithoutRef< + React.PropsWithRef< Omit & { theatreKey: string visible?: boolean | 'editor' additionalProps?: $FixMe objRef?: $FixMe - } & React.RefAttributes + // not exactly sure how to get the type of the threejs object itself + ref?: Ref + } > > } & { primitive: React.ForwardRefExoticComponent< - React.PropsWithoutRef< - { - object: any - theatreKey: string - visible?: boolean | 'editor' - additionalProps?: $FixMe - objRef?: $FixMe - editableType: keyof JSX.IntrinsicElements - } & React.RefAttributes - > & { + React.PropsWithRef<{ + object: any + theatreKey: string + visible?: boolean | 'editor' + additionalProps?: $FixMe + objRef?: $FixMe + editableType: keyof JSX.IntrinsicElements + // not exactly sure how to get the type of the threejs object itself + ref?: Ref + }> & { // 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 [props: string]: any