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
This commit is contained in:
parent
6caf8267c5
commit
dceb3965d6
10 changed files with 503 additions and 559 deletions
|
@ -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() {
|
|||
<RefreshSnapshot />
|
||||
<Model url={sceneGLB} />
|
||||
</Suspense>
|
||||
<OrbitControls
|
||||
enablePan={false}
|
||||
enableZoom={true}
|
||||
maxPolarAngle={Math.PI / 2}
|
||||
minPolarAngle={Math.PI / 2}
|
||||
/>
|
||||
<Stars radius={500} depth={50} count={1000} factor={10} />
|
||||
</SheetProvider>
|
||||
</Canvas>
|
||||
|
|
|
@ -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<EditableProxyProps> = ({
|
||||
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<EditableProxyProps> = ({
|
|||
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<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',
|
||||
]
|
||||
}, [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<Helper>(
|
||||
() => 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<EditableProxyProps> = ({
|
|||
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<EditableProxyProps> = ({
|
|||
}}
|
||||
>
|
||||
<primitive object={object}>
|
||||
{showOverlayIcons && (
|
||||
{(showOverlayIcons ||
|
||||
(editable.objectConfig.dimensionless &&
|
||||
selected !== uniqueName)) && (
|
||||
<Html
|
||||
center
|
||||
className="pointer-events-none p-1 rounded bg-white bg-opacity-70 shadow text-gray-700"
|
||||
style={{
|
||||
pointerEvents: 'none',
|
||||
transform: 'scale(2)',
|
||||
opacity: hovered ? 0.3 : 1,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
<div>{icons[editable.objectConfig.icon as IconID]}</div>
|
||||
</Html>
|
||||
)}
|
||||
{dimensionless.includes(editableType) && (
|
||||
{editable.objectConfig.dimensionless && (
|
||||
<Sphere
|
||||
args={[2, 4, 2]}
|
||||
onClick={(e) => {
|
||||
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)
|
||||
|
|
|
@ -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<OrbitControls | null>
|
||||
}
|
||||
|
||||
type IEditableProxy = {
|
||||
type IEditableProxy<T> = {
|
||||
portal: ReturnType<typeof createPortal>
|
||||
object: Object3D
|
||||
sheetObject: ISheetObject<$FixMe>
|
||||
editable: Editable<T>
|
||||
}
|
||||
|
||||
const ProxyManager: VFC<ProxyManagerProps> = ({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<ProxyManagerProps> = ({orbitControlsRef}) => {
|
|||
const sceneProxy = useMemo(() => sceneSnapshot?.clone(), [sceneSnapshot])
|
||||
const [editableProxies, setEditableProxies] = useState<
|
||||
{
|
||||
[name in string]?: IEditableProxy
|
||||
[name in string]?: IEditableProxy<any>
|
||||
}
|
||||
>({})
|
||||
|
||||
|
@ -63,7 +62,7 @@ const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
|
|||
return
|
||||
}
|
||||
|
||||
const editableProxies: {[name: string]: IEditableProxy} = {}
|
||||
const editableProxies: {[name: string]: IEditableProxy<any>} = {}
|
||||
|
||||
sceneProxy.traverse((object) => {
|
||||
if (object.userData.__editable) {
|
||||
|
@ -77,13 +76,12 @@ const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
|
|||
portal: createPortal(
|
||||
<EditableProxy
|
||||
editableName={object.userData.__editableName}
|
||||
editableType={object.userData.__editableType}
|
||||
object={object}
|
||||
/>,
|
||||
object.parent!,
|
||||
),
|
||||
object: object,
|
||||
sheetObject: sheetObjects[uniqueName]!,
|
||||
editable: editables[uniqueName]!,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -94,25 +92,22 @@ const ProxyManager: VFC<ProxyManagerProps> = ({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,18 +224,22 @@ const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
|
|||
return (
|
||||
<>
|
||||
<primitive object={sceneProxy} />
|
||||
{selected && editableProxyOfSelected && (
|
||||
{selected &&
|
||||
editableProxyOfSelected &&
|
||||
editable &&
|
||||
editable.objectConfig.useTransformControls && (
|
||||
<TransformControls
|
||||
mode={transformControlsMode}
|
||||
space={transformControlsSpace}
|
||||
orbitControlsRef={orbitControlsRef}
|
||||
object={editableProxyOfSelected.object}
|
||||
onObjectChange={() => {
|
||||
const sheetObject = editableProxyOfSelected.sheetObject
|
||||
const sheetObject = editableProxyOfSelected.editable.sheetObject
|
||||
const obj = editableProxyOfSelected.object
|
||||
|
||||
scrub.capture(({set}) => {
|
||||
set(sheetObject.props, {
|
||||
...sheetObject.value,
|
||||
position: {
|
||||
x: obj.position.x,
|
||||
y: obj.position.y,
|
||||
|
|
|
@ -1,42 +1,24 @@
|
|||
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<any> | EditableType | 'primitive',
|
||||
U extends T extends EditableType ? T : EditableType,
|
||||
>(
|
||||
const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
|
||||
config: EditableFactoryConfig,
|
||||
) => {
|
||||
const editable = <
|
||||
T extends ComponentType<any> | Keys | 'primitive',
|
||||
U extends T extends Keys ? T : Keys,
|
||||
>(
|
||||
Component: T,
|
||||
type: T extends 'primitive' ? null : U,
|
||||
) => {
|
||||
) => {
|
||||
type Props = Omit<ComponentProps<T>, 'visible'> & {
|
||||
uniqueName: string
|
||||
visible?: boolean | 'editor'
|
||||
|
@ -47,7 +29,7 @@ const editable = <
|
|||
editableType: U
|
||||
}
|
||||
: {}) &
|
||||
RefAttributes<Elements[U]>
|
||||
RefAttributes<JSX.IntrinsicElements[U]>
|
||||
|
||||
return forwardRef(
|
||||
(
|
||||
|
@ -61,7 +43,9 @@ const editable = <
|
|||
}: Props,
|
||||
ref,
|
||||
) => {
|
||||
const objectRef = useRef<Elements[U]>()
|
||||
const actualType = type ?? editableType
|
||||
|
||||
const objectRef = useRef<JSX.IntrinsicElements[U]>()
|
||||
|
||||
const sheet = useCurrentSheet()
|
||||
|
||||
|
@ -73,85 +57,51 @@ const editable = <
|
|||
|
||||
useLayoutEffect(() => {
|
||||
if (!sheet) return
|
||||
const sheetObject = sheet.object(uniqueName, {
|
||||
...baseSheetObjectType,
|
||||
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)
|
||||
|
||||
if (objRef) objRef!.current = 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],
|
||||
)
|
||||
useEditorStore.getState().addEditable(uniqueName, {
|
||||
type: actualType,
|
||||
sheetObject,
|
||||
visibleOnlyInEditor: visible === 'editor',
|
||||
// @ts-ignore
|
||||
objectConfig: config[actualType],
|
||||
})
|
||||
}, [sheet, uniqueName])
|
||||
|
||||
// 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
|
||||
sheetObject!.initialValue = Object.fromEntries(
|
||||
// @ts-ignore
|
||||
Object.entries(config[actualType].props).map(
|
||||
// @ts-ignore
|
||||
([key, value]) => [key, value.parse(props)],
|
||||
),
|
||||
)
|
||||
}, [
|
||||
uniqueName,
|
||||
sheetObject,
|
||||
props.position,
|
||||
props.rotation,
|
||||
props.scale,
|
||||
|
||||
...transformDeps,
|
||||
// @ts-ignore
|
||||
...Object.keys(config[actualType].props).map(
|
||||
// @ts-ignore
|
||||
(key) => props[key],
|
||||
),
|
||||
])
|
||||
|
||||
// subscribe to prop changes from theatre
|
||||
|
@ -161,21 +111,13 @@ const editable = <
|
|||
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,
|
||||
// @ts-ignore
|
||||
Object.entries(config[actualType].props).forEach(
|
||||
// @ts-ignore
|
||||
([key, value]) => value.apply(newValues[key], object),
|
||||
)
|
||||
// @ts-ignore
|
||||
config[actualType].updateObject?.(object)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
|
@ -197,26 +139,40 @@ const editable = <
|
|||
userData={{
|
||||
__editable: true,
|
||||
__editableName: uniqueName,
|
||||
__editableType: type ?? editableType,
|
||||
__visibleOnlyInEditor: visible === 'editor',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
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<JSX.IntrinsicElements[Property], 'visible'> & {
|
||||
uniqueName: string
|
||||
visible?: boolean | 'editor'
|
||||
additionalProps?: $FixMe
|
||||
objRef?: $FixMe
|
||||
} & React.RefAttributes<JSX.IntrinsicElements[Property]>
|
||||
>
|
||||
>
|
||||
}
|
||||
|
||||
return Object.assign(editable, extensions)
|
||||
}
|
||||
|
||||
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')
|
||||
const editable = createEditable<keyof typeof defaultEditableFactoryConfig>(
|
||||
defaultEditableFactoryConfig,
|
||||
)
|
||||
|
||||
export default editable
|
||||
|
|
109
packages/r3f/src/defaultEditableFactoryConfig.ts
Normal file
109
packages/r3f/src/defaultEditableFactoryConfig.ts
Normal file
|
@ -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
|
98
packages/r3f/src/editableFactoryConfigUtils.ts
Normal file
98
packages/r3f/src/editableFactoryConfigUtils.ts
Normal file
|
@ -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<T> = {
|
||||
parse: (props: Record<string, any>) => T
|
||||
apply: (value: T, object: any) => void
|
||||
type: IShorthandCompoundProps
|
||||
}
|
||||
type Props = Record<string, PropConfig<any>>
|
||||
type Meta<T> = {
|
||||
useTransformControls: boolean
|
||||
updateObject?: (object: T) => void
|
||||
icon: IconID
|
||||
dimensionless?: boolean
|
||||
createHelper: (object: T) => Helper
|
||||
}
|
||||
export type ObjectConfig<T> = {props: Props} & Meta<T>
|
||||
export type EditableFactoryConfig = Partial<
|
||||
Record<keyof JSX.IntrinsicElements, ObjectConfig<any>>
|
||||
>
|
||||
|
||||
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<Vector3> => ({
|
||||
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<number> => ({
|
||||
parse: (props) => {
|
||||
return props[key] ?? defaultValue ?? 0
|
||||
},
|
||||
apply: (value, object) => {
|
||||
object[key] = value
|
||||
},
|
||||
type: {
|
||||
[key]: types.number(defaultValue, {nudgeMultiplier}),
|
||||
},
|
||||
})
|
||||
|
||||
export const extendObjectProps = <T extends {props: {}}>(
|
||||
objectConfig: T,
|
||||
extension: Props,
|
||||
) => ({
|
||||
...objectConfig,
|
||||
props: {...objectConfig.props, ...extension},
|
||||
})
|
17
packages/r3f/src/icons.tsx
Normal file
17
packages/r3f/src/icons.tsx
Normal file
|
@ -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: <BsFillCollectionFill />,
|
||||
cube: <GiCube />,
|
||||
lightBulb: <GiLightBulb />,
|
||||
spotLight: <GiLightProjector />,
|
||||
sun: <BiSun />,
|
||||
camera: <BsCameraVideoFill />,
|
||||
}
|
||||
|
||||
export type IconID = keyof typeof icons
|
||||
|
||||
export default icons
|
|
@ -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<typeof baseSheetObjectType>
|
||||
export type BaseSheetObjectType = ISheetObject<any>
|
||||
|
||||
export const allRegisteredObjects = new WeakSet<BaseSheetObjectType>()
|
||||
|
||||
export interface AbstractEditable<T extends EditableType> {
|
||||
type: T
|
||||
role: 'active' | 'removed'
|
||||
sheetObject?: ISheetObject<any>
|
||||
export interface Editable<T> {
|
||||
type: string
|
||||
sheetObject: ISheetObject<any>
|
||||
objectConfig: ObjectConfig<T>
|
||||
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<T extends Editable = Editable> = {
|
||||
export type EditableSnapshot<T extends Editable<any> = Editable<any>> = {
|
||||
proxyObject?: Object3D | null
|
||||
} & T
|
||||
|
||||
export interface AbstractSerializedEditable<T extends EditableType> {
|
||||
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<string, SerializedEditable>
|
||||
}
|
||||
|
||||
export type EditorStore = {
|
||||
sheetObjects: {[uniqueName in string]?: BaseSheetObjectType}
|
||||
scene: Scene | null
|
||||
gl: WebGLRenderer | null
|
||||
allowImplicitInstancing: boolean
|
||||
helpersRoot: Group
|
||||
editables: Record<string, Editable>
|
||||
editables: Record<string, Editable<any>>
|
||||
// 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
|
||||
init: (scene: Scene, gl: WebGLRenderer) => void
|
||||
|
||||
addEditable: <T extends EditableType>(type: T, uniqueName: string) => void
|
||||
removeEditable: (uniqueName: string) => void
|
||||
addEditable: (uniqueName: string, editable: Editable<any>) => void
|
||||
createSnapshot: () => void
|
||||
setSheetObject: (uniqueName: string, sheetObject: BaseSheetObjectType) => void
|
||||
setSnapshotProxyObject: (
|
||||
proxyObject: Object3D | null,
|
||||
uniqueName: string,
|
||||
) => void
|
||||
}
|
||||
|
||||
const config: StateCreator<EditorStore> = (set, get) => {
|
||||
const config: StateCreator<EditorStore> = (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<EditorStore> = (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 {
|
||||
addEditable: (uniqueName, editable) => {
|
||||
set((state) => ({
|
||||
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,
|
||||
[uniqueName]: editable,
|
||||
},
|
||||
}))
|
||||
},
|
||||
|
@ -245,6 +87,7 @@ const config: StateCreator<EditorStore> = (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)
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -726,3 +726,5 @@ export type PropTypeConfig =
|
|||
| PropTypeConfig_AllSimples
|
||||
| PropTypeConfig_Compound<$IntentionalAny>
|
||||
| PropTypeConfig_Enum
|
||||
|
||||
export type {IShorthandCompoundProps}
|
||||
|
|
Loading…
Reference in a new issue