From dceb3965d65716323b2f6b38a38fa1f8fabfad41 Mon Sep 17 00:00:00 2001 From: Andrew Prifer <2991360+AndrewPrifer@users.noreply.github.com> Date: Wed, 4 May 2022 16:43:44 +0200 Subject: [PATCH] Make `editable` schema-based and add default schemas (#139) * Make editable schema based and fix a couple UX issues * Refactor the icons a bit * Add support for points, lines, line segments, and line loops * Adjust nudge multipliers * Fix types * Fix helpers not showing on hover in some cases --- .../playground/src/shared/r3f-rocket/App.tsx | 8 +- packages/r3f/src/components/EditableProxy.tsx | 163 +++----- packages/r3f/src/components/ProxyManager.tsx | 109 +++--- packages/r3f/src/components/editable.tsx | 350 ++++++++---------- .../r3f/src/defaultEditableFactoryConfig.ts | 109 ++++++ .../r3f/src/editableFactoryConfigUtils.ts | 98 +++++ packages/r3f/src/icons.tsx | 17 + packages/r3f/src/store.ts | 205 ++-------- theatre/core/src/index.ts | 1 + theatre/core/src/propTypes/index.ts | 2 + 10 files changed, 503 insertions(+), 559 deletions(-) create mode 100644 packages/r3f/src/defaultEditableFactoryConfig.ts create mode 100644 packages/r3f/src/editableFactoryConfigUtils.ts create mode 100644 packages/r3f/src/icons.tsx diff --git a/packages/playground/src/shared/r3f-rocket/App.tsx b/packages/playground/src/shared/r3f-rocket/App.tsx index e8cb0e7..88cee73 100644 --- a/packages/playground/src/shared/r3f-rocket/App.tsx +++ b/packages/playground/src/shared/r3f-rocket/App.tsx @@ -1,5 +1,5 @@ import {editable as e, RefreshSnapshot, SheetProvider} from '@theatre/r3f' -import {OrbitControls, Stars} from '@react-three/drei' +import { Stars} from '@react-three/drei' import {getProject} from '@theatre/core' import React, {Suspense, useState} from 'react' import {Canvas} from '@react-three/fiber' @@ -85,12 +85,6 @@ function App() { - diff --git a/packages/r3f/src/components/EditableProxy.tsx b/packages/r3f/src/components/EditableProxy.tsx index 049885b..bc817a7 100644 --- a/packages/r3f/src/components/EditableProxy.tsx +++ b/packages/r3f/src/components/EditableProxy.tsx @@ -1,44 +1,35 @@ 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 type {VFC} from 'react' +import React, {useEffect, useLayoutEffect, useMemo, useState} from 'react' +import {Sphere, Html} from '@react-three/drei' import {useEditorStore} from '../store' import shallow from 'zustand/shallow' -import {GiCube, GiLightBulb, GiLightProjector} from 'react-icons/gi' -import {BsCameraVideoFill, BsFillCollectionFill} from 'react-icons/bs' -import {BiSun} from 'react-icons/bi' -import type {IconType} from 'react-icons' import studio from '@theatre/studio' import {useSelected} from './useSelected' import {useVal} from '@theatre/react' import {getEditorSheetObject} from './editorStuff' +import type {IconID} from '../icons' +import icons from '../icons' +import type {Helper} from '../editableFactoryConfigUtils' +import {invalidate, useFrame, useThree} from '@react-three/fiber' export interface EditableProxyProps { editableName: string - editableType: EditableType object: Object3D - onChange?: () => void } const EditableProxy: VFC = ({ editableName: uniqueName, - editableType, object, }) => { const editorObject = getEditorSheetObject() - const setSnapshotProxyObject = useEditorStore( - (state) => state.setSnapshotProxyObject, + const [setSnapshotProxyObject, editables] = useEditorStore( + (state) => [state.setSnapshotProxyObject, state.editables], shallow, ) + const editable = editables[uniqueName] + const selected = useSelected() const showOverlayIcons = useVal(editorObject?.props.viewport.showOverlayIcons) ?? false @@ -52,107 +43,39 @@ const EditableProxy: VFC = ({ useLayoutEffect(() => { const originalVisibility = object.visible - if (object.userData.__visibleOnlyInEditor) { + if (editable.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 - switch (editableType) { - case 'group': - icon = - break - case 'mesh': - icon = - break - case 'pointLight': - icon = - break - case 'spotLight': - icon = - break - case 'directionalLight': - icon = - break - case 'perspectiveCamera': - case 'orthographicCamera': - icon = - } - - const objectRef = useRef(object) - - useLayoutEffect(() => { - objectRef.current = object - }, [object]) - - const dimensionless = [ - 'spotLight', - 'pointLight', - 'directionalLight', - 'perspectiveCamera', - 'orthographicCamera', - ] + }, [editable.visibleOnlyInEditor, object.visible]) const [hovered, setHovered] = useState(false) - useHelper( - objectRef, - selected === uniqueName || dimensionless.includes(editableType) || hovered - ? Helper - : null, - ...helperArgs, + // Helpers + const scene = useThree((state) => state.scene) + const helper = useMemo( + () => editable.objectConfig.createHelper(object), + [object], ) + useEffect(() => { + if (selected === uniqueName || hovered) { + scene.add(helper) + invalidate() + } + + return () => { + scene.remove(helper) + invalidate() + } + }, [selected, hovered, helper, scene]) + useFrame(() => { + if (helper.update) { + helper.update() + } + }) return ( <> @@ -162,7 +85,7 @@ const EditableProxy: VFC = ({ e.stopPropagation() const theatreObject = - useEditorStore.getState().sheetObjects[uniqueName] + useEditorStore.getState().editables[uniqueName].sheetObject if (!theatreObject) { console.log('no theatre object for', uniqueName) @@ -181,22 +104,28 @@ const EditableProxy: VFC = ({ }} > - {showOverlayIcons && ( + {(showOverlayIcons || + (editable.objectConfig.dimensionless && + selected !== uniqueName)) && ( - {icon} +
{icons[editable.objectConfig.icon as IconID]}
)} - {dimensionless.includes(editableType) && ( + {editable.objectConfig.dimensionless && ( { if (e.delta < 2) { e.stopPropagation() const theatreObject = - useEditorStore.getState().sheetObjects[uniqueName] + useEditorStore.getState().editables[uniqueName].sheetObject if (!theatreObject) { console.log('no theatre object for', uniqueName) diff --git a/packages/r3f/src/components/ProxyManager.tsx b/packages/r3f/src/components/ProxyManager.tsx index d86ba75..578fcae 100644 --- a/packages/r3f/src/components/ProxyManager.tsx +++ b/packages/r3f/src/components/ProxyManager.tsx @@ -6,7 +6,8 @@ import React, { useRef, useState, } from 'react' -import {useEditorStore} from '../store' +import type {Editable} from '../store'; +import { useEditorStore} from '../store' import {createPortal} from '@react-three/fiber' import EditableProxy from './EditableProxy' import type {OrbitControls} from 'three-stdlib' @@ -15,8 +16,6 @@ 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' @@ -26,17 +25,17 @@ export interface ProxyManagerProps { orbitControlsRef: React.MutableRefObject } -type IEditableProxy = { +type IEditableProxy = { portal: ReturnType object: Object3D - sheetObject: ISheetObject<$FixMe> + editable: Editable } const ProxyManager: VFC = ({orbitControlsRef}) => { const isBeingEdited = useRef(false) const editorObject = getEditorSheetObject() - const [sceneSnapshot, sheetObjects] = useEditorStore( - (state) => [state.sceneSnapshot, state.sheetObjects], + const [sceneSnapshot, editables] = useEditorStore( + (state) => [state.sceneSnapshot, state.editables], shallow, ) const transformControlsMode = @@ -51,7 +50,7 @@ const ProxyManager: VFC = ({orbitControlsRef}) => { const sceneProxy = useMemo(() => sceneSnapshot?.clone(), [sceneSnapshot]) const [editableProxies, setEditableProxies] = useState< { - [name in string]?: IEditableProxy + [name in string]?: IEditableProxy } >({}) @@ -63,7 +62,7 @@ const ProxyManager: VFC = ({orbitControlsRef}) => { return } - const editableProxies: {[name: string]: IEditableProxy} = {} + const editableProxies: {[name: string]: IEditableProxy} = {} sceneProxy.traverse((object) => { if (object.userData.__editable) { @@ -77,13 +76,12 @@ const ProxyManager: VFC = ({orbitControlsRef}) => { portal: createPortal( , object.parent!, ), object: object, - sheetObject: sheetObjects[uniqueName]!, + editable: editables[uniqueName]!, } } } @@ -94,25 +92,22 @@ const ProxyManager: VFC = ({orbitControlsRef}) => { const selected = useSelected() const editableProxyOfSelected = selected && editableProxies[selected] + const editable = selected ? editables[selected] : undefined // subscribe to external changes useEffect(() => { - if (!editableProxyOfSelected) return + if (!editableProxyOfSelected || !editable) return const object = editableProxyOfSelected.object - const sheetObject = editableProxyOfSelected.sheetObject + const sheetObject = editableProxyOfSelected.editable.sheetObject + const objectConfig = editable.objectConfig 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) + // @ts-ignore + Object.entries(objectConfig.props).forEach(([key, value]) => { + // @ts-ignore + return value.apply(newValues[key], object) + }) + objectConfig.updateObject?.(object) invalidate() } @@ -229,39 +224,43 @@ const ProxyManager: VFC = ({orbitControlsRef}) => { return ( <> - {selected && editableProxyOfSelected && ( - { - const sheetObject = editableProxyOfSelected.sheetObject - const obj = editableProxyOfSelected.object + {selected && + editableProxyOfSelected && + editable && + editable.objectConfig.useTransformControls && ( + { + const sheetObject = editableProxyOfSelected.editable.sheetObject + const obj = editableProxyOfSelected.object - scrub.capture(({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, - }, + scrub.capture(({set}) => { + set(sheetObject.props, { + ...sheetObject.value, + 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)} - /> - )} + }} + onDraggingChange={(event) => (isBeingEdited.current = event.value)} + /> + )} {Object.values(editableProxies).map( (editableProxy) => editableProxy!.portal, )} diff --git a/packages/r3f/src/components/editable.tsx b/packages/r3f/src/components/editable.tsx index a25cb8d..7c9e4e9 100644 --- a/packages/r3f/src/components/editable.tsx +++ b/packages/r3f/src/components/editable.tsx @@ -1,222 +1,178 @@ 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 {allRegisteredObjects, 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' +import defaultEditableFactoryConfig from '../defaultEditableFactoryConfig' +import type {EditableFactoryConfig} from '../editableFactoryConfigUtils' -interface Elements { - group: Group - mesh: Mesh - spotLight: SpotLight - directionalLight: DirectionalLight - perspectiveCamera: PerspectiveCamera - orthographicCamera: OrthographicCamera - pointLight: PointLight -} - -const editable = < - T extends ComponentType | EditableType | 'primitive', - U extends T extends EditableType ? T : EditableType, ->( - Component: T, - type: T extends 'primitive' ? null : U, +const createEditable = ( + config: EditableFactoryConfig, ) => { - type Props = Omit, 'visible'> & { - uniqueName: string - visible?: boolean | 'editor' - additionalProps?: $FixMe - objRef?: $FixMe - } & (T extends 'primitive' - ? { - editableType: U - } - : {}) & - RefAttributes + const editable = < + T extends ComponentType | Keys | 'primitive', + U extends T extends Keys ? T : Keys, + >( + Component: T, + type: T extends 'primitive' ? null : U, + ) => { + type Props = Omit, 'visible'> & { + uniqueName: string + visible?: boolean | 'editor' + additionalProps?: $FixMe + objRef?: $FixMe + } & (T extends 'primitive' + ? { + editableType: U + } + : {}) & + RefAttributes - return forwardRef( - ( - { - uniqueName, - visible, - editableType, - additionalProps, - objRef, - ...props - }: Props, - ref, - ) => { - const objectRef = useRef() + return forwardRef( + ( + { + uniqueName, + visible, + editableType, + additionalProps, + objRef, + ...props + }: Props, + ref, + ) => { + const actualType = type ?? editableType - const sheet = useCurrentSheet() + const objectRef = useRef() - const [sheetObject, setSheetObject] = useState< - undefined | ISheetObject<$FixMe> - >(undefined) + const sheet = useCurrentSheet() - const invalidate = useInvalidate() + const [sheetObject, setSheetObject] = useState< + undefined | ISheetObject<$FixMe> + >(undefined) - useLayoutEffect(() => { - if (!sheet) return - const sheetObject = sheet.object(uniqueName, { - ...baseSheetObjectType, - ...additionalProps, - }) - allRegisteredObjects.add(sheetObject) - setSheetObject(sheetObject) + const invalidate = useInvalidate() - if (objRef) objRef!.current = sheetObject + useLayoutEffect(() => { + if (!sheet) return + const sheetObject = sheet.object( + uniqueName, + Object.assign( + { + ...additionalProps, + }, + // @ts-ignore + ...Object.values(config[actualType].props).map( + // @ts-ignore + (value) => value.type, + ), + ), + ) + allRegisteredObjects.add(sheetObject) + setSheetObject(sheetObject) - useEditorStore - .getState() - .setSheetObject(uniqueName, sheetObject as $FixMe) - }, [sheet, uniqueName]) + if (objRef) objRef!.current = sheetObject - const transformDeps: string[] = [] + useEditorStore.getState().addEditable(uniqueName, { + type: actualType, + sheetObject, + visibleOnlyInEditor: visible === 'editor', + // @ts-ignore + objectConfig: config[actualType], + }) + }, [sheet, uniqueName]) - ;['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 + sheetObject!.initialValue = Object.fromEntries( + // @ts-ignore + Object.entries(config[actualType].props).map( + // @ts-ignore + ([key, value]) => [key, value.parse(props)], + ), + ) + }, [ + uniqueName, + sheetObject, + // @ts-ignore + ...Object.keys(config[actualType].props).map( + // @ts-ignore + (key) => props[key], + ), + ]) + + // subscribe to prop changes from theatre + useLayoutEffect(() => { + if (!sheetObject) return + + const object = objectRef.current! + + const setFromTheatre = (newValues: any) => { + // @ts-ignore + Object.entries(config[actualType].props).forEach( + // @ts-ignore + ([key, value]) => value.apply(newValues[key], object), + ) + // @ts-ignore + config[actualType].updateObject?.(object) + invalidate() + } + + setFromTheatre(sheetObject.value) + + const untap = sheetObject.onValuesChange(setFromTheatre) + + return () => { + untap() + } + }, [sheetObject]) + + return ( + // @ts-ignore + ) - }) + }, + ) + } - // 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 ( + const extensions = { + ...Object.fromEntries( + Object.keys(config).map((key) => [ + key, // @ts-ignore - - ) - }, - ) + editable(key, key), + ]), + ), + primitive: editable('primitive', null), + } as unknown as { + [Property in Keys]: React.ForwardRefExoticComponent< + React.PropsWithoutRef< + Omit & { + uniqueName: string + visible?: boolean | 'editor' + additionalProps?: $FixMe + objRef?: $FixMe + } & React.RefAttributes + > + > + } + + return Object.assign(editable, extensions) } -const createEditable = (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') +const editable = createEditable( + defaultEditableFactoryConfig, +) export default editable diff --git a/packages/r3f/src/defaultEditableFactoryConfig.ts b/packages/r3f/src/defaultEditableFactoryConfig.ts new file mode 100644 index 0000000..cdabff9 --- /dev/null +++ b/packages/r3f/src/defaultEditableFactoryConfig.ts @@ -0,0 +1,109 @@ +import type {EditableFactoryConfig} from './editableFactoryConfigUtils' +import { + createNumberPropConfig, + createVector, + createVectorPropConfig, + extendObjectProps, +} from './editableFactoryConfigUtils' +import type { + DirectionalLight, + Object3D, + OrthographicCamera, + PerspectiveCamera, + PointLight, + SpotLight, +} from 'three' +import { + BoxHelper, + CameraHelper, + DirectionalLightHelper, + PointLightHelper, + SpotLightHelper, +} from 'three' + +const baseObjectConfig = { + props: { + position: createVectorPropConfig('position'), + rotation: createVectorPropConfig('rotation'), + scale: createVectorPropConfig('scale', createVector([1, 1, 1])), + }, + useTransformControls: true, + icon: 'cube' as const, + createHelper: (object: Object3D) => new BoxHelper(object, selectionColor), +} + +const baseLightConfig = { + ...extendObjectProps(baseObjectConfig, { + intensity: createNumberPropConfig('intensity', 1), + distance: createNumberPropConfig('distance'), + decay: createNumberPropConfig('decay'), + }), + dimensionless: true, +} + +const baseCameraConfig = { + ...extendObjectProps(baseObjectConfig, { + near: createNumberPropConfig('near', 0.1, {nudgeMultiplier: 0.1}), + far: createNumberPropConfig('far', 2000, {nudgeMultiplier: 0.1}), + }), + updateObject: (camera: PerspectiveCamera | OrthographicCamera) => { + camera.updateProjectionMatrix() + }, + icon: 'camera' as const, + dimensionless: true, + createHelper: (camera: PerspectiveCamera) => new CameraHelper(camera), +} + +const selectionColor = '#40AAA4' + +const defaultEditableFactoryConfig = { + group: { + ...baseObjectConfig, + icon: 'collection' as const, + createHelper: (object: Object3D) => new BoxHelper(object, selectionColor), + }, + mesh: { + ...baseObjectConfig, + icon: 'cube' as const, + createHelper: (object: Object3D) => new BoxHelper(object, selectionColor), + }, + spotLight: { + ...extendObjectProps(baseLightConfig, { + angle: createNumberPropConfig('angle', 0, {nudgeMultiplier: 0.001}), + penumbra: createNumberPropConfig('penumbra', 0, {nudgeMultiplier: 0.001}), + }), + icon: 'spotLight' as const, + createHelper: (light: SpotLight) => + new SpotLightHelper(light, selectionColor), + }, + directionalLight: { + ...extendObjectProps(baseObjectConfig, { + intensity: createNumberPropConfig('intensity', 1), + }), + icon: 'sun' as const, + dimensionless: true, + createHelper: (light: DirectionalLight) => + new DirectionalLightHelper(light, 1, selectionColor), + }, + pointLight: { + ...baseLightConfig, + icon: 'lightBulb' as const, + createHelper: (light: PointLight) => + new PointLightHelper(light, 1, selectionColor), + }, + perspectiveCamera: extendObjectProps(baseCameraConfig, { + fov: createNumberPropConfig('fov', 50, {nudgeMultiplier: 0.1}), + zoom: createNumberPropConfig('zoom', 1), + }), + orthographicCamera: baseCameraConfig, + points: baseObjectConfig, + line: baseObjectConfig, + lineLoop: baseObjectConfig, + lineSegments: baseObjectConfig, +} + +// Assert that the config is indeed of EditableFactoryConfig without actually +// forcing it to that type so that we can pass the real type to the editable factory +defaultEditableFactoryConfig as EditableFactoryConfig + +export default defaultEditableFactoryConfig diff --git a/packages/r3f/src/editableFactoryConfigUtils.ts b/packages/r3f/src/editableFactoryConfigUtils.ts new file mode 100644 index 0000000..9a0a2b3 --- /dev/null +++ b/packages/r3f/src/editableFactoryConfigUtils.ts @@ -0,0 +1,98 @@ +import type {IShorthandCompoundProps} from '@theatre/core' +import {types} from '@theatre/core' +import type {Object3D} from 'three' +import type {IconID} from './icons' + +export type Helper = Object3D & { + update?: () => void +} +type PropConfig = { + parse: (props: Record) => T + apply: (value: T, object: any) => void + type: IShorthandCompoundProps +} +type Props = Record> +type Meta = { + useTransformControls: boolean + updateObject?: (object: T) => void + icon: IconID + dimensionless?: boolean + createHelper: (object: T) => Helper +} +export type ObjectConfig = {props: Props} & Meta +export type EditableFactoryConfig = Partial< + Record> +> + +type Vector3 = { + x: number + y: number + z: number +} + +export const createVector = (components?: [number, number, number]) => { + return components + ? {x: components[0], y: components[1], z: components[2]} + : { + x: 0, + y: 0, + z: 0, + } +} + +export const createVectorPropConfig = ( + key: string, + defaultValue = createVector(), + {nudgeMultiplier = 0.01} = {}, +): PropConfig => ({ + parse: (props) => { + const vector = props[key] + ? Array.isArray(props[key]) + ? createVector(props[key] as any) + : { + x: props[key].x, + y: props[key].y, + z: props[key].z, + } + : defaultValue + ;(['x', 'y', 'z'] as const).forEach((axis) => { + if (props[`${key}-${axis}` as any]) + vector[axis] = props[`${key}-${axis}` as any] + }) + return vector + }, + apply: (value, object) => { + object[key].set(value.x, value.y, value.z) + }, + type: { + [key]: { + x: types.number(defaultValue.x, {nudgeMultiplier}), + y: types.number(defaultValue.y, {nudgeMultiplier}), + z: types.number(defaultValue.z, {nudgeMultiplier}), + }, + }, +}) + +export const createNumberPropConfig = ( + key: string, + defaultValue: number = 0, + {nudgeMultiplier = 0.01} = {}, +): PropConfig => ({ + parse: (props) => { + return props[key] ?? defaultValue ?? 0 + }, + apply: (value, object) => { + object[key] = value + }, + type: { + [key]: types.number(defaultValue, {nudgeMultiplier}), + }, +}) + +export const extendObjectProps = ( + objectConfig: T, + extension: Props, +) => ({ + ...objectConfig, + props: {...objectConfig.props, ...extension}, +}) diff --git a/packages/r3f/src/icons.tsx b/packages/r3f/src/icons.tsx new file mode 100644 index 0000000..1a54e96 --- /dev/null +++ b/packages/r3f/src/icons.tsx @@ -0,0 +1,17 @@ +import {BsCameraVideoFill, BsFillCollectionFill} from 'react-icons/bs' +import {GiCube, GiLightBulb, GiLightProjector} from 'react-icons/gi' +import {BiSun} from 'react-icons/bi' +import React from 'react' + +const icons = { + collection: , + cube: , + lightBulb: , + spotLight: , + sun: , + camera: , +} + +export type IconID = keyof typeof icons + +export default icons diff --git a/packages/r3f/src/store.ts b/packages/r3f/src/store.ts index 0de458b..ca97b09 100644 --- a/packages/r3f/src/store.ts +++ b/packages/r3f/src/store.ts @@ -3,172 +3,61 @@ 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' +import type {ObjectConfig} from './editableFactoryConfigUtils' 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 = { - position: { - x: positionComp, - y: positionComp, - z: positionComp, - }, - rotation: { - x: rotationComp, - y: rotationComp, - z: rotationComp, - }, - scale: { - x: scaleComp, - y: scaleComp, - z: scaleComp, - }, -} - -export type BaseSheetObjectType = ISheetObject +export type BaseSheetObjectType = ISheetObject export const allRegisteredObjects = new WeakSet() -export interface AbstractEditable { - type: T - role: 'active' | 'removed' - sheetObject?: ISheetObject +export interface Editable { + type: string + sheetObject: ISheetObject + objectConfig: ObjectConfig + visibleOnlyInEditor: boolean } -// 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 = { +export type EditableSnapshot = Editable> = { proxyObject?: Object3D | null } & T -export interface AbstractSerializedEditable { - type: T +export interface SerializedEditable { + type: string } -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 } export type EditorStore = { - sheetObjects: {[uniqueName in string]?: BaseSheetObjectType} scene: Scene | null gl: WebGLRenderer | null - allowImplicitInstancing: boolean helpersRoot: Group - editables: Record + editables: Record> // this will come in handy when we start supporting multiple canvases canvasName: string sceneSnapshot: Scene | null editablesSnapshot: Record | null - init: ( - scene: Scene, - gl: WebGLRenderer, - allowImplicitInstancing: boolean, - ) => void + init: (scene: Scene, gl: WebGLRenderer) => void - addEditable: (type: T, uniqueName: string) => void - removeEditable: (uniqueName: string) => void + addEditable: (uniqueName: string, editable: Editable) => void createSnapshot: () => void - setSheetObject: (uniqueName: string, sheetObject: BaseSheetObjectType) => void setSnapshotProxyObject: ( proxyObject: Object3D | null, uniqueName: string, ) => void } -const config: StateCreator = (set, get) => { +const config: StateCreator = (set) => { return { sheet: null, editorObject: null, - sheetObjects: {}, scene: null, gl: null, - allowImplicitInstancing: false, helpersRoot: new Group(), editables: {}, canvasName: 'default', @@ -176,65 +65,18 @@ const config: StateCreator = (set, get) => { editablesSnapshot: null, initialEditorCamera: {}, - init: (scene, gl, allowImplicitInstancing) => { + init: (scene, gl) => { 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) => { + addEditable: (uniqueName, editable) => { set((state) => ({ - sheetObjects: { - ...state.sheetObjects, - [uniqueName]: sheetObject, + editables: { + ...state.editables, + [uniqueName]: editable, }, })) }, @@ -245,6 +87,7 @@ const config: StateCreator = (set, get) => { editablesSnapshot: state.editables, })) }, + setSnapshotProxyObject: (proxyObject, uniqueName) => { set((state) => ({ editablesSnapshot: { @@ -267,11 +110,7 @@ export type BindFunction = (options: { scene: Scene }) => void -export const bindToCanvas: BindFunction = ({ - allowImplicitInstancing = false, - gl, - scene, -}) => { +export const bindToCanvas: BindFunction = ({gl, scene}) => { const init = useEditorStore.getState().init - init(scene, gl, allowImplicitInstancing) + init(scene, gl) } diff --git a/theatre/core/src/index.ts b/theatre/core/src/index.ts index 2faf3ca..5b36ad2 100644 --- a/theatre/core/src/index.ts +++ b/theatre/core/src/index.ts @@ -12,6 +12,7 @@ export type { export type {ISequence} from '@theatre/core/sequences/TheatreSequence' export type {ISheetObject} from '@theatre/core/sheetObjects/TheatreSheetObject' export type {ISheet} from '@theatre/core/sheets/TheatreSheet' +export type {IShorthandCompoundProps} from '@theatre/core/propTypes' import * as globalVariableNames from '@theatre/shared/globalVariableNames' import type StudioBundle from '@theatre/studio/StudioBundle' import CoreBundle from './CoreBundle' diff --git a/theatre/core/src/propTypes/index.ts b/theatre/core/src/propTypes/index.ts index 80b5a8b..b3a1cdf 100644 --- a/theatre/core/src/propTypes/index.ts +++ b/theatre/core/src/propTypes/index.ts @@ -726,3 +726,5 @@ export type PropTypeConfig = | PropTypeConfig_AllSimples | PropTypeConfig_Compound<$IntentionalAny> | PropTypeConfig_Enum + +export type {IShorthandCompoundProps}