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:
Andrew Prifer 2022-05-04 16:43:44 +02:00 committed by GitHub
parent 6caf8267c5
commit dceb3965d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 503 additions and 559 deletions

View file

@ -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>

View file

@ -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)

View file

@ -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,39 +224,43 @@ const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
return (
<>
<primitive object={sceneProxy} />
{selected && editableProxyOfSelected && (
<TransformControls
mode={transformControlsMode}
space={transformControlsSpace}
orbitControlsRef={orbitControlsRef}
object={editableProxyOfSelected.object}
onObjectChange={() => {
const sheetObject = editableProxyOfSelected.sheetObject
const obj = editableProxyOfSelected.object
{selected &&
editableProxyOfSelected &&
editable &&
editable.objectConfig.useTransformControls && (
<TransformControls
mode={transformControlsMode}
space={transformControlsSpace}
orbitControlsRef={orbitControlsRef}
object={editableProxyOfSelected.object}
onObjectChange={() => {
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,
)}

View file

@ -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<any> | EditableType | 'primitive',
U extends T extends EditableType ? T : EditableType,
>(
Component: T,
type: T extends 'primitive' ? null : U,
const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
config: EditableFactoryConfig,
) => {
type Props = Omit<ComponentProps<T>, 'visible'> & {
uniqueName: string
visible?: boolean | 'editor'
additionalProps?: $FixMe
objRef?: $FixMe
} & (T extends 'primitive'
? {
editableType: U
}
: {}) &
RefAttributes<Elements[U]>
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'
additionalProps?: $FixMe
objRef?: $FixMe
} & (T extends 'primitive'
? {
editableType: U
}
: {}) &
RefAttributes<JSX.IntrinsicElements[U]>
return forwardRef(
(
{
uniqueName,
visible,
editableType,
additionalProps,
objRef,
...props
}: Props,
ref,
) => {
const objectRef = useRef<Elements[U]>()
return forwardRef(
(
{
uniqueName,
visible,
editableType,
additionalProps,
objRef,
...props
}: Props,
ref,
) => {
const actualType = type ?? editableType
const sheet = useCurrentSheet()
const objectRef = useRef<JSX.IntrinsicElements[U]>()
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
<Component
ref={mergeRefs([objectRef, ref])}
{...props}
visible={visible !== 'editor' && visible}
userData={{
__editable: true,
__editableName: 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
}, [
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
<Component
ref={mergeRefs([objectRef, ref])}
{...props}
visible={visible !== 'editor' && visible}
userData={{
__editable: true,
__editableName: uniqueName,
__editableType: type ?? editableType,
__visibleOnlyInEditor: visible === 'editor',
}}
/>
)
},
)
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

View 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

View 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},
})

View 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

View file

@ -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 {
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<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)
}

View file

@ -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'

View file

@ -726,3 +726,5 @@ export type PropTypeConfig =
| PropTypeConfig_AllSimples
| PropTypeConfig_Compound<$IntentionalAny>
| PropTypeConfig_Enum
export type {IShorthandCompoundProps}