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 {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 {getProject} from '@theatre/core'
|
||||||
import React, {Suspense, useState} from 'react'
|
import React, {Suspense, useState} from 'react'
|
||||||
import {Canvas} from '@react-three/fiber'
|
import {Canvas} from '@react-three/fiber'
|
||||||
|
@ -85,12 +85,6 @@ function App() {
|
||||||
<RefreshSnapshot />
|
<RefreshSnapshot />
|
||||||
<Model url={sceneGLB} />
|
<Model url={sceneGLB} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<OrbitControls
|
|
||||||
enablePan={false}
|
|
||||||
enableZoom={true}
|
|
||||||
maxPolarAngle={Math.PI / 2}
|
|
||||||
minPolarAngle={Math.PI / 2}
|
|
||||||
/>
|
|
||||||
<Stars radius={500} depth={50} count={1000} factor={10} />
|
<Stars radius={500} depth={50} count={1000} factor={10} />
|
||||||
</SheetProvider>
|
</SheetProvider>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
|
@ -1,44 +1,35 @@
|
||||||
import type {Object3D} from 'three'
|
import type {Object3D} from 'three'
|
||||||
import {
|
import type {VFC} from 'react'
|
||||||
BoxHelper,
|
import React, {useEffect, useLayoutEffect, useMemo, useState} from 'react'
|
||||||
CameraHelper,
|
import {Sphere, Html} from '@react-three/drei'
|
||||||
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 {useEditorStore} from '../store'
|
import {useEditorStore} from '../store'
|
||||||
import shallow from 'zustand/shallow'
|
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 studio from '@theatre/studio'
|
||||||
import {useSelected} from './useSelected'
|
import {useSelected} from './useSelected'
|
||||||
import {useVal} from '@theatre/react'
|
import {useVal} from '@theatre/react'
|
||||||
import {getEditorSheetObject} from './editorStuff'
|
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 {
|
export interface EditableProxyProps {
|
||||||
editableName: string
|
editableName: string
|
||||||
editableType: EditableType
|
|
||||||
object: Object3D
|
object: Object3D
|
||||||
onChange?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditableProxy: VFC<EditableProxyProps> = ({
|
const EditableProxy: VFC<EditableProxyProps> = ({
|
||||||
editableName: uniqueName,
|
editableName: uniqueName,
|
||||||
editableType,
|
|
||||||
object,
|
object,
|
||||||
}) => {
|
}) => {
|
||||||
const editorObject = getEditorSheetObject()
|
const editorObject = getEditorSheetObject()
|
||||||
const setSnapshotProxyObject = useEditorStore(
|
const [setSnapshotProxyObject, editables] = useEditorStore(
|
||||||
(state) => state.setSnapshotProxyObject,
|
(state) => [state.setSnapshotProxyObject, state.editables],
|
||||||
shallow,
|
shallow,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const editable = editables[uniqueName]
|
||||||
|
|
||||||
const selected = useSelected()
|
const selected = useSelected()
|
||||||
const showOverlayIcons =
|
const showOverlayIcons =
|
||||||
useVal(editorObject?.props.viewport.showOverlayIcons) ?? false
|
useVal(editorObject?.props.viewport.showOverlayIcons) ?? false
|
||||||
|
@ -52,107 +43,39 @@ const EditableProxy: VFC<EditableProxyProps> = ({
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const originalVisibility = object.visible
|
const originalVisibility = object.visible
|
||||||
|
|
||||||
if (object.userData.__visibleOnlyInEditor) {
|
if (editable.visibleOnlyInEditor) {
|
||||||
object.visible = true
|
object.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
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.visible = originalVisibility
|
||||||
}
|
}
|
||||||
}, [object.userData.__visibleOnlyInEditor, object.visible])
|
}, [editable.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',
|
|
||||||
]
|
|
||||||
|
|
||||||
const [hovered, setHovered] = useState(false)
|
const [hovered, setHovered] = useState(false)
|
||||||
|
|
||||||
useHelper(
|
// Helpers
|
||||||
objectRef,
|
const scene = useThree((state) => state.scene)
|
||||||
selected === uniqueName || dimensionless.includes(editableType) || hovered
|
const helper = useMemo<Helper>(
|
||||||
? Helper
|
() => editable.objectConfig.createHelper(object),
|
||||||
: null,
|
[object],
|
||||||
...helperArgs,
|
|
||||||
)
|
)
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -162,7 +85,7 @@ const EditableProxy: VFC<EditableProxyProps> = ({
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
const theatreObject =
|
const theatreObject =
|
||||||
useEditorStore.getState().sheetObjects[uniqueName]
|
useEditorStore.getState().editables[uniqueName].sheetObject
|
||||||
|
|
||||||
if (!theatreObject) {
|
if (!theatreObject) {
|
||||||
console.log('no theatre object for', uniqueName)
|
console.log('no theatre object for', uniqueName)
|
||||||
|
@ -181,22 +104,28 @@ const EditableProxy: VFC<EditableProxyProps> = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<primitive object={object}>
|
<primitive object={object}>
|
||||||
{showOverlayIcons && (
|
{(showOverlayIcons ||
|
||||||
|
(editable.objectConfig.dimensionless &&
|
||||||
|
selected !== uniqueName)) && (
|
||||||
<Html
|
<Html
|
||||||
center
|
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>
|
</Html>
|
||||||
)}
|
)}
|
||||||
{dimensionless.includes(editableType) && (
|
{editable.objectConfig.dimensionless && (
|
||||||
<Sphere
|
<Sphere
|
||||||
args={[2, 4, 2]}
|
args={[2, 4, 2]}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.delta < 2) {
|
if (e.delta < 2) {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
const theatreObject =
|
const theatreObject =
|
||||||
useEditorStore.getState().sheetObjects[uniqueName]
|
useEditorStore.getState().editables[uniqueName].sheetObject
|
||||||
|
|
||||||
if (!theatreObject) {
|
if (!theatreObject) {
|
||||||
console.log('no theatre object for', uniqueName)
|
console.log('no theatre object for', uniqueName)
|
||||||
|
|
|
@ -6,7 +6,8 @@ import React, {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import {useEditorStore} from '../store'
|
import type {Editable} from '../store';
|
||||||
|
import { useEditorStore} from '../store'
|
||||||
import {createPortal} from '@react-three/fiber'
|
import {createPortal} from '@react-three/fiber'
|
||||||
import EditableProxy from './EditableProxy'
|
import EditableProxy from './EditableProxy'
|
||||||
import type {OrbitControls} from 'three-stdlib'
|
import type {OrbitControls} from 'three-stdlib'
|
||||||
|
@ -15,8 +16,6 @@ import shallow from 'zustand/shallow'
|
||||||
import type {Material, Mesh, Object3D} from 'three'
|
import type {Material, Mesh, Object3D} from 'three'
|
||||||
import {MeshBasicMaterial, MeshPhongMaterial} from 'three'
|
import {MeshBasicMaterial, MeshPhongMaterial} from 'three'
|
||||||
import studio from '@theatre/studio'
|
import studio from '@theatre/studio'
|
||||||
import type {ISheetObject} from '@theatre/core'
|
|
||||||
import type {$FixMe} from '../types'
|
|
||||||
import {useSelected} from './useSelected'
|
import {useSelected} from './useSelected'
|
||||||
import {useVal} from '@theatre/react'
|
import {useVal} from '@theatre/react'
|
||||||
import useInvalidate from './useInvalidate'
|
import useInvalidate from './useInvalidate'
|
||||||
|
@ -26,17 +25,17 @@ export interface ProxyManagerProps {
|
||||||
orbitControlsRef: React.MutableRefObject<OrbitControls | null>
|
orbitControlsRef: React.MutableRefObject<OrbitControls | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
type IEditableProxy = {
|
type IEditableProxy<T> = {
|
||||||
portal: ReturnType<typeof createPortal>
|
portal: ReturnType<typeof createPortal>
|
||||||
object: Object3D
|
object: Object3D
|
||||||
sheetObject: ISheetObject<$FixMe>
|
editable: Editable<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
|
const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
|
||||||
const isBeingEdited = useRef(false)
|
const isBeingEdited = useRef(false)
|
||||||
const editorObject = getEditorSheetObject()
|
const editorObject = getEditorSheetObject()
|
||||||
const [sceneSnapshot, sheetObjects] = useEditorStore(
|
const [sceneSnapshot, editables] = useEditorStore(
|
||||||
(state) => [state.sceneSnapshot, state.sheetObjects],
|
(state) => [state.sceneSnapshot, state.editables],
|
||||||
shallow,
|
shallow,
|
||||||
)
|
)
|
||||||
const transformControlsMode =
|
const transformControlsMode =
|
||||||
|
@ -51,7 +50,7 @@ const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
|
||||||
const sceneProxy = useMemo(() => sceneSnapshot?.clone(), [sceneSnapshot])
|
const sceneProxy = useMemo(() => sceneSnapshot?.clone(), [sceneSnapshot])
|
||||||
const [editableProxies, setEditableProxies] = useState<
|
const [editableProxies, setEditableProxies] = useState<
|
||||||
{
|
{
|
||||||
[name in string]?: IEditableProxy
|
[name in string]?: IEditableProxy<any>
|
||||||
}
|
}
|
||||||
>({})
|
>({})
|
||||||
|
|
||||||
|
@ -63,7 +62,7 @@ const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const editableProxies: {[name: string]: IEditableProxy} = {}
|
const editableProxies: {[name: string]: IEditableProxy<any>} = {}
|
||||||
|
|
||||||
sceneProxy.traverse((object) => {
|
sceneProxy.traverse((object) => {
|
||||||
if (object.userData.__editable) {
|
if (object.userData.__editable) {
|
||||||
|
@ -77,13 +76,12 @@ const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
|
||||||
portal: createPortal(
|
portal: createPortal(
|
||||||
<EditableProxy
|
<EditableProxy
|
||||||
editableName={object.userData.__editableName}
|
editableName={object.userData.__editableName}
|
||||||
editableType={object.userData.__editableType}
|
|
||||||
object={object}
|
object={object}
|
||||||
/>,
|
/>,
|
||||||
object.parent!,
|
object.parent!,
|
||||||
),
|
),
|
||||||
object: object,
|
object: object,
|
||||||
sheetObject: sheetObjects[uniqueName]!,
|
editable: editables[uniqueName]!,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,25 +92,22 @@ const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
|
||||||
|
|
||||||
const selected = useSelected()
|
const selected = useSelected()
|
||||||
const editableProxyOfSelected = selected && editableProxies[selected]
|
const editableProxyOfSelected = selected && editableProxies[selected]
|
||||||
|
const editable = selected ? editables[selected] : undefined
|
||||||
|
|
||||||
// subscribe to external changes
|
// subscribe to external changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editableProxyOfSelected) return
|
if (!editableProxyOfSelected || !editable) return
|
||||||
const object = editableProxyOfSelected.object
|
const object = editableProxyOfSelected.object
|
||||||
const sheetObject = editableProxyOfSelected.sheetObject
|
const sheetObject = editableProxyOfSelected.editable.sheetObject
|
||||||
|
const objectConfig = editable.objectConfig
|
||||||
|
|
||||||
const setFromTheatre = (newValues: any) => {
|
const setFromTheatre = (newValues: any) => {
|
||||||
object.position.set(
|
// @ts-ignore
|
||||||
newValues.position.x,
|
Object.entries(objectConfig.props).forEach(([key, value]) => {
|
||||||
newValues.position.y,
|
// @ts-ignore
|
||||||
newValues.position.z,
|
return value.apply(newValues[key], object)
|
||||||
)
|
})
|
||||||
object.rotation.set(
|
objectConfig.updateObject?.(object)
|
||||||
newValues.rotation.x,
|
|
||||||
newValues.rotation.y,
|
|
||||||
newValues.rotation.z,
|
|
||||||
)
|
|
||||||
object.scale.set(newValues.scale.x, newValues.scale.y, newValues.scale.z)
|
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,39 +224,43 @@ const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<primitive object={sceneProxy} />
|
<primitive object={sceneProxy} />
|
||||||
{selected && editableProxyOfSelected && (
|
{selected &&
|
||||||
<TransformControls
|
editableProxyOfSelected &&
|
||||||
mode={transformControlsMode}
|
editable &&
|
||||||
space={transformControlsSpace}
|
editable.objectConfig.useTransformControls && (
|
||||||
orbitControlsRef={orbitControlsRef}
|
<TransformControls
|
||||||
object={editableProxyOfSelected.object}
|
mode={transformControlsMode}
|
||||||
onObjectChange={() => {
|
space={transformControlsSpace}
|
||||||
const sheetObject = editableProxyOfSelected.sheetObject
|
orbitControlsRef={orbitControlsRef}
|
||||||
const obj = editableProxyOfSelected.object
|
object={editableProxyOfSelected.object}
|
||||||
|
onObjectChange={() => {
|
||||||
|
const sheetObject = editableProxyOfSelected.editable.sheetObject
|
||||||
|
const obj = editableProxyOfSelected.object
|
||||||
|
|
||||||
scrub.capture(({set}) => {
|
scrub.capture(({set}) => {
|
||||||
set(sheetObject.props, {
|
set(sheetObject.props, {
|
||||||
position: {
|
...sheetObject.value,
|
||||||
x: obj.position.x,
|
position: {
|
||||||
y: obj.position.y,
|
x: obj.position.x,
|
||||||
z: obj.position.z,
|
y: obj.position.y,
|
||||||
},
|
z: obj.position.z,
|
||||||
rotation: {
|
},
|
||||||
x: obj.rotation.x,
|
rotation: {
|
||||||
y: obj.rotation.y,
|
x: obj.rotation.x,
|
||||||
z: obj.rotation.z,
|
y: obj.rotation.y,
|
||||||
},
|
z: obj.rotation.z,
|
||||||
scale: {
|
},
|
||||||
x: obj.scale.x,
|
scale: {
|
||||||
y: obj.scale.y,
|
x: obj.scale.x,
|
||||||
z: obj.scale.z,
|
y: obj.scale.y,
|
||||||
},
|
z: obj.scale.z,
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
}}
|
||||||
}}
|
onDraggingChange={(event) => (isBeingEdited.current = event.value)}
|
||||||
onDraggingChange={(event) => (isBeingEdited.current = event.value)}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
|
||||||
{Object.values(editableProxies).map(
|
{Object.values(editableProxies).map(
|
||||||
(editableProxy) => editableProxy!.portal,
|
(editableProxy) => editableProxy!.portal,
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,222 +1,178 @@
|
||||||
import type {ComponentProps, ComponentType, RefAttributes} from 'react'
|
import type {ComponentProps, ComponentType, RefAttributes} from 'react'
|
||||||
import React, {forwardRef, useLayoutEffect, useRef, useState} from 'react'
|
import React, {forwardRef, useLayoutEffect, useRef, useState} from 'react'
|
||||||
import type {
|
import {allRegisteredObjects, useEditorStore} from '../store'
|
||||||
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 mergeRefs from 'react-merge-refs'
|
import mergeRefs from 'react-merge-refs'
|
||||||
import type {$FixMe} from '@theatre/shared/utils/types'
|
import type {$FixMe} from '@theatre/shared/utils/types'
|
||||||
import type {ISheetObject} from '@theatre/core'
|
import type {ISheetObject} from '@theatre/core'
|
||||||
import useInvalidate from './useInvalidate'
|
import useInvalidate from './useInvalidate'
|
||||||
import {useCurrentSheet} from '../SheetProvider'
|
import {useCurrentSheet} from '../SheetProvider'
|
||||||
|
import defaultEditableFactoryConfig from '../defaultEditableFactoryConfig'
|
||||||
|
import type {EditableFactoryConfig} from '../editableFactoryConfigUtils'
|
||||||
|
|
||||||
interface Elements {
|
const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
|
||||||
group: Group
|
config: EditableFactoryConfig,
|
||||||
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,
|
|
||||||
>(
|
|
||||||
Component: T,
|
|
||||||
type: T extends 'primitive' ? null : U,
|
|
||||||
) => {
|
) => {
|
||||||
type Props = Omit<ComponentProps<T>, 'visible'> & {
|
const editable = <
|
||||||
uniqueName: string
|
T extends ComponentType<any> | Keys | 'primitive',
|
||||||
visible?: boolean | 'editor'
|
U extends T extends Keys ? T : Keys,
|
||||||
additionalProps?: $FixMe
|
>(
|
||||||
objRef?: $FixMe
|
Component: T,
|
||||||
} & (T extends 'primitive'
|
type: T extends 'primitive' ? null : U,
|
||||||
? {
|
) => {
|
||||||
editableType: U
|
type Props = Omit<ComponentProps<T>, 'visible'> & {
|
||||||
}
|
uniqueName: string
|
||||||
: {}) &
|
visible?: boolean | 'editor'
|
||||||
RefAttributes<Elements[U]>
|
additionalProps?: $FixMe
|
||||||
|
objRef?: $FixMe
|
||||||
|
} & (T extends 'primitive'
|
||||||
|
? {
|
||||||
|
editableType: U
|
||||||
|
}
|
||||||
|
: {}) &
|
||||||
|
RefAttributes<JSX.IntrinsicElements[U]>
|
||||||
|
|
||||||
return forwardRef(
|
return forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
uniqueName,
|
uniqueName,
|
||||||
visible,
|
visible,
|
||||||
editableType,
|
editableType,
|
||||||
additionalProps,
|
additionalProps,
|
||||||
objRef,
|
objRef,
|
||||||
...props
|
...props
|
||||||
}: Props,
|
}: Props,
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const objectRef = useRef<Elements[U]>()
|
const actualType = type ?? editableType
|
||||||
|
|
||||||
const sheet = useCurrentSheet()
|
const objectRef = useRef<JSX.IntrinsicElements[U]>()
|
||||||
|
|
||||||
const [sheetObject, setSheetObject] = useState<
|
const sheet = useCurrentSheet()
|
||||||
undefined | ISheetObject<$FixMe>
|
|
||||||
>(undefined)
|
|
||||||
|
|
||||||
const invalidate = useInvalidate()
|
const [sheetObject, setSheetObject] = useState<
|
||||||
|
undefined | ISheetObject<$FixMe>
|
||||||
|
>(undefined)
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
const invalidate = useInvalidate()
|
||||||
if (!sheet) return
|
|
||||||
const sheetObject = sheet.object(uniqueName, {
|
|
||||||
...baseSheetObjectType,
|
|
||||||
...additionalProps,
|
|
||||||
})
|
|
||||||
allRegisteredObjects.add(sheetObject)
|
|
||||||
setSheetObject(sheetObject)
|
|
||||||
|
|
||||||
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
|
if (objRef) objRef!.current = sheetObject
|
||||||
.getState()
|
|
||||||
.setSheetObject(uniqueName, sheetObject as $FixMe)
|
|
||||||
}, [sheet, uniqueName])
|
|
||||||
|
|
||||||
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) => {
|
// store initial values of props
|
||||||
transformDeps.push(
|
useLayoutEffect(() => {
|
||||||
props[`position-${axis}` as any],
|
if (!sheetObject) return
|
||||||
props[`rotation-${axis}` as any],
|
sheetObject!.initialValue = Object.fromEntries(
|
||||||
props[`scale-${axis}` as any],
|
// @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
|
||||||
|
<Component
|
||||||
|
ref={mergeRefs([objectRef, ref])}
|
||||||
|
{...props}
|
||||||
|
visible={visible !== 'editor' && visible}
|
||||||
|
userData={{
|
||||||
|
__editable: true,
|
||||||
|
__editableName: uniqueName,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// store initial values of props
|
const extensions = {
|
||||||
useLayoutEffect(() => {
|
...Object.fromEntries(
|
||||||
if (!sheetObject) return
|
Object.keys(config).map((key) => [
|
||||||
// calculate initial properties before adding the editable
|
key,
|
||||||
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 (
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
<Component
|
editable(key, key),
|
||||||
ref={mergeRefs([objectRef, ref])}
|
]),
|
||||||
{...props}
|
),
|
||||||
visible={visible !== 'editor' && visible}
|
primitive: editable('primitive', null),
|
||||||
userData={{
|
} as unknown as {
|
||||||
__editable: true,
|
[Property in Keys]: React.ForwardRefExoticComponent<
|
||||||
__editableName: uniqueName,
|
React.PropsWithoutRef<
|
||||||
__editableType: type ?? editableType,
|
Omit<JSX.IntrinsicElements[Property], 'visible'> & {
|
||||||
__visibleOnlyInEditor: visible === 'editor',
|
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) =>
|
const editable = createEditable<keyof typeof defaultEditableFactoryConfig>(
|
||||||
// @ts-ignore
|
defaultEditableFactoryConfig,
|
||||||
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')
|
|
||||||
|
|
||||||
export default editable
|
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 type {Object3D, Scene, WebGLRenderer} from 'three'
|
||||||
import {Group} from 'three'
|
import {Group} from 'three'
|
||||||
import type {ISheetObject} from '@theatre/core'
|
import type {ISheetObject} from '@theatre/core'
|
||||||
import {types} from '@theatre/core'
|
import type {ObjectConfig} from './editableFactoryConfigUtils'
|
||||||
|
|
||||||
export type EditableType =
|
|
||||||
| 'group'
|
|
||||||
| 'mesh'
|
|
||||||
| 'spotLight'
|
|
||||||
| 'directionalLight'
|
|
||||||
| 'pointLight'
|
|
||||||
| 'perspectiveCamera'
|
|
||||||
| 'orthographicCamera'
|
|
||||||
|
|
||||||
export type TransformControlsMode = 'translate' | 'rotate' | 'scale'
|
export type TransformControlsMode = 'translate' | 'rotate' | 'scale'
|
||||||
export type TransformControlsSpace = 'world' | 'local'
|
export type TransformControlsSpace = 'world' | 'local'
|
||||||
export type ViewportShading = 'wireframe' | 'flat' | 'solid' | 'rendered'
|
export type ViewportShading = 'wireframe' | 'flat' | 'solid' | 'rendered'
|
||||||
|
|
||||||
const positionComp = types.number(1, {nudgeMultiplier: 0.1})
|
export type BaseSheetObjectType = ISheetObject<any>
|
||||||
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 const allRegisteredObjects = new WeakSet<BaseSheetObjectType>()
|
export const allRegisteredObjects = new WeakSet<BaseSheetObjectType>()
|
||||||
|
|
||||||
export interface AbstractEditable<T extends EditableType> {
|
export interface Editable<T> {
|
||||||
type: T
|
type: string
|
||||||
role: 'active' | 'removed'
|
sheetObject: ISheetObject<any>
|
||||||
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 type EditableSnapshot<T extends Editable<any> = Editable<any>> = {
|
||||||
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> = {
|
|
||||||
proxyObject?: Object3D | null
|
proxyObject?: Object3D | null
|
||||||
} & T
|
} & T
|
||||||
|
|
||||||
export interface AbstractSerializedEditable<T extends EditableType> {
|
export interface SerializedEditable {
|
||||||
type: T
|
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 {
|
export interface EditableState {
|
||||||
editables: Record<string, SerializedEditable>
|
editables: Record<string, SerializedEditable>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EditorStore = {
|
export type EditorStore = {
|
||||||
sheetObjects: {[uniqueName in string]?: BaseSheetObjectType}
|
|
||||||
scene: Scene | null
|
scene: Scene | null
|
||||||
gl: WebGLRenderer | null
|
gl: WebGLRenderer | null
|
||||||
allowImplicitInstancing: boolean
|
|
||||||
helpersRoot: Group
|
helpersRoot: Group
|
||||||
editables: Record<string, Editable>
|
editables: Record<string, Editable<any>>
|
||||||
// this will come in handy when we start supporting multiple canvases
|
// this will come in handy when we start supporting multiple canvases
|
||||||
canvasName: string
|
canvasName: string
|
||||||
sceneSnapshot: Scene | null
|
sceneSnapshot: Scene | null
|
||||||
editablesSnapshot: Record<string, EditableSnapshot> | null
|
editablesSnapshot: Record<string, EditableSnapshot> | null
|
||||||
|
|
||||||
init: (
|
init: (scene: Scene, gl: WebGLRenderer) => void
|
||||||
scene: Scene,
|
|
||||||
gl: WebGLRenderer,
|
|
||||||
allowImplicitInstancing: boolean,
|
|
||||||
) => void
|
|
||||||
|
|
||||||
addEditable: <T extends EditableType>(type: T, uniqueName: string) => void
|
addEditable: (uniqueName: string, editable: Editable<any>) => void
|
||||||
removeEditable: (uniqueName: string) => void
|
|
||||||
createSnapshot: () => void
|
createSnapshot: () => void
|
||||||
setSheetObject: (uniqueName: string, sheetObject: BaseSheetObjectType) => void
|
|
||||||
setSnapshotProxyObject: (
|
setSnapshotProxyObject: (
|
||||||
proxyObject: Object3D | null,
|
proxyObject: Object3D | null,
|
||||||
uniqueName: string,
|
uniqueName: string,
|
||||||
) => void
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const config: StateCreator<EditorStore> = (set, get) => {
|
const config: StateCreator<EditorStore> = (set) => {
|
||||||
return {
|
return {
|
||||||
sheet: null,
|
sheet: null,
|
||||||
editorObject: null,
|
editorObject: null,
|
||||||
sheetObjects: {},
|
|
||||||
scene: null,
|
scene: null,
|
||||||
gl: null,
|
gl: null,
|
||||||
allowImplicitInstancing: false,
|
|
||||||
helpersRoot: new Group(),
|
helpersRoot: new Group(),
|
||||||
editables: {},
|
editables: {},
|
||||||
canvasName: 'default',
|
canvasName: 'default',
|
||||||
|
@ -176,65 +65,18 @@ const config: StateCreator<EditorStore> = (set, get) => {
|
||||||
editablesSnapshot: null,
|
editablesSnapshot: null,
|
||||||
initialEditorCamera: {},
|
initialEditorCamera: {},
|
||||||
|
|
||||||
init: (scene, gl, allowImplicitInstancing) => {
|
init: (scene, gl) => {
|
||||||
set({
|
set({
|
||||||
scene,
|
scene,
|
||||||
gl,
|
gl,
|
||||||
allowImplicitInstancing,
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
addEditable: (type, uniqueName) =>
|
addEditable: (uniqueName, editable) => {
|
||||||
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) => {
|
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
sheetObjects: {
|
editables: {
|
||||||
...state.sheetObjects,
|
...state.editables,
|
||||||
[uniqueName]: sheetObject,
|
[uniqueName]: editable,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
@ -245,6 +87,7 @@ const config: StateCreator<EditorStore> = (set, get) => {
|
||||||
editablesSnapshot: state.editables,
|
editablesSnapshot: state.editables,
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
setSnapshotProxyObject: (proxyObject, uniqueName) => {
|
setSnapshotProxyObject: (proxyObject, uniqueName) => {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
editablesSnapshot: {
|
editablesSnapshot: {
|
||||||
|
@ -267,11 +110,7 @@ export type BindFunction = (options: {
|
||||||
scene: Scene
|
scene: Scene
|
||||||
}) => void
|
}) => void
|
||||||
|
|
||||||
export const bindToCanvas: BindFunction = ({
|
export const bindToCanvas: BindFunction = ({gl, scene}) => {
|
||||||
allowImplicitInstancing = false,
|
|
||||||
gl,
|
|
||||||
scene,
|
|
||||||
}) => {
|
|
||||||
const init = useEditorStore.getState().init
|
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 {ISequence} from '@theatre/core/sequences/TheatreSequence'
|
||||||
export type {ISheetObject} from '@theatre/core/sheetObjects/TheatreSheetObject'
|
export type {ISheetObject} from '@theatre/core/sheetObjects/TheatreSheetObject'
|
||||||
export type {ISheet} from '@theatre/core/sheets/TheatreSheet'
|
export type {ISheet} from '@theatre/core/sheets/TheatreSheet'
|
||||||
|
export type {IShorthandCompoundProps} from '@theatre/core/propTypes'
|
||||||
import * as globalVariableNames from '@theatre/shared/globalVariableNames'
|
import * as globalVariableNames from '@theatre/shared/globalVariableNames'
|
||||||
import type StudioBundle from '@theatre/studio/StudioBundle'
|
import type StudioBundle from '@theatre/studio/StudioBundle'
|
||||||
import CoreBundle from './CoreBundle'
|
import CoreBundle from './CoreBundle'
|
||||||
|
|
|
@ -726,3 +726,5 @@ export type PropTypeConfig =
|
||||||
| PropTypeConfig_AllSimples
|
| PropTypeConfig_AllSimples
|
||||||
| PropTypeConfig_Compound<$IntentionalAny>
|
| PropTypeConfig_Compound<$IntentionalAny>
|
||||||
| PropTypeConfig_Enum
|
| PropTypeConfig_Enum
|
||||||
|
|
||||||
|
export type {IShorthandCompoundProps}
|
||||||
|
|
Loading…
Reference in a new issue