UX Improvements
* The cam state of SnapshotEditor cameras now persist * Title bar for the prop editor * Cleaned up the toolbar
This commit is contained in:
parent
d621794280
commit
215cd880f0
13 changed files with 354 additions and 170 deletions
|
@ -1,5 +1,5 @@
|
||||||
import {editable as e, bindToCanvas} from '@theatre/plugin-r3f'
|
import {editable as e, bindToCanvas} from '@theatre/plugin-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} from 'react'
|
import React, {Suspense} from 'react'
|
||||||
import {Canvas} from '@react-three/fiber'
|
import {Canvas} from '@react-three/fiber'
|
||||||
|
@ -86,12 +86,12 @@ function App() {
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<Model url={sceneGLB} />
|
<Model url={sceneGLB} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<OrbitControls
|
{/* <OrbitControls
|
||||||
enablePan={false}
|
enablePan={false}
|
||||||
enableZoom={true}
|
enableZoom={true}
|
||||||
maxPolarAngle={Math.PI / 2}
|
maxPolarAngle={Math.PI / 2}
|
||||||
minPolarAngle={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} />
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
11
packages/plugin-r3f/src/.eslintrc.js
Normal file
11
packages/plugin-r3f/src/.eslintrc.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
module.exports = {
|
||||||
|
rules: {
|
||||||
|
'no-restricted-syntax': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
selector: `ImportDeclaration[source.value=/@theatre\\u002F(studio|core)\\u002F/]`,
|
||||||
|
message: `Importing Theatre's submodules would not work in the production build.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import React, {
|
||||||
import {useEditorStore} 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 '@react-three/drei'
|
import type {OrbitControls} from 'three-stdlib'
|
||||||
import TransformControls from './TransformControls'
|
import TransformControls from './TransformControls'
|
||||||
import shallow from 'zustand/shallow'
|
import shallow from 'zustand/shallow'
|
||||||
import type {Material, Mesh, Object3D} from 'three'
|
import type {Material, Mesh, Object3D} from 'three'
|
||||||
|
@ -21,7 +21,7 @@ import {useSelected} from './useSelected'
|
||||||
import {useVal} from '@theatre/dataverse-react'
|
import {useVal} from '@theatre/dataverse-react'
|
||||||
|
|
||||||
export interface ProxyManagerProps {
|
export interface ProxyManagerProps {
|
||||||
orbitControlsRef: React.MutableRefObject<typeof OrbitControls | undefined>
|
orbitControlsRef: React.MutableRefObject<OrbitControls | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
type IEditableProxy = {
|
type IEditableProxy = {
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import {useState} from 'react'
|
|
||||||
import {useLayoutEffect} from 'react'
|
import {useLayoutEffect} from 'react'
|
||||||
import React, {useEffect, useRef} from 'react'
|
import React from 'react'
|
||||||
import {Canvas} from '@react-three/fiber'
|
import {Canvas} from '@react-three/fiber'
|
||||||
import {useEditorStore} from '../store'
|
import {useEditorStore} from '../store'
|
||||||
import {OrbitControls} from '@react-three/drei'
|
|
||||||
import shallow from 'zustand/shallow'
|
import shallow from 'zustand/shallow'
|
||||||
import root from 'react-shadow/styled-components'
|
import root from 'react-shadow/styled-components'
|
||||||
import ProxyManager from './ProxyManager'
|
import ProxyManager from './ProxyManager'
|
||||||
|
@ -11,6 +9,9 @@ import studio, {ToolbarIconButton} from '@theatre/studio'
|
||||||
import {useVal} from '@theatre/dataverse-react'
|
import {useVal} from '@theatre/dataverse-react'
|
||||||
import styled, {createGlobalStyle, StyleSheetManager} from 'styled-components'
|
import styled, {createGlobalStyle, StyleSheetManager} from 'styled-components'
|
||||||
import {IoCameraReverseOutline} from 'react-icons/all'
|
import {IoCameraReverseOutline} from 'react-icons/all'
|
||||||
|
import type {ISheetObject, ISheet} from '@theatre/core'
|
||||||
|
import type {$FixMe} from '../types'
|
||||||
|
import useSnapshotEditorCamera from './useSnapshotEditorCamera'
|
||||||
|
|
||||||
const GlobalStyle = createGlobalStyle`
|
const GlobalStyle = createGlobalStyle`
|
||||||
:host {
|
:host {
|
||||||
|
@ -31,31 +32,29 @@ const GlobalStyle = createGlobalStyle`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const EditorScene = () => {
|
const EditorScene: React.FC<{snapshotEditorSheet: ISheet; paneId: string}> = ({
|
||||||
const orbitControlsRef = useRef<typeof OrbitControls>()
|
snapshotEditorSheet,
|
||||||
|
paneId,
|
||||||
|
}) => {
|
||||||
|
const [editorCamera, orbitControlsRef] = useSnapshotEditorCamera(
|
||||||
|
snapshotEditorSheet,
|
||||||
|
paneId,
|
||||||
|
)
|
||||||
|
|
||||||
const [editorObject, helpersRoot, setOrbitControlsRef] = useEditorStore(
|
const [editorObject, helpersRoot] = useEditorStore(
|
||||||
(state) => [
|
(state) => [state.editorObject, state.helpersRoot],
|
||||||
state.editorObject,
|
|
||||||
state.helpersRoot,
|
|
||||||
state.setOrbitControlsRef,
|
|
||||||
],
|
|
||||||
shallow,
|
shallow,
|
||||||
)
|
)
|
||||||
|
|
||||||
const showGrid = useVal(editorObject?.props.viewport.showGrid) ?? true
|
const showGrid = useVal(editorObject?.props.viewport.showGrid) ?? true
|
||||||
const showAxes = useVal(editorObject?.props.viewport.showAxes) ?? true
|
const showAxes = useVal(editorObject?.props.viewport.showAxes) ?? true
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setOrbitControlsRef(orbitControlsRef)
|
|
||||||
}, [setOrbitControlsRef])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showGrid && <gridHelper args={[20, 20, '#6e6e6e', '#4a4b4b']} />}
|
{showGrid && <gridHelper args={[20, 20, '#6e6e6e', '#4a4b4b']} />}
|
||||||
{showAxes && <axesHelper args={[500]} />}
|
{showAxes && <axesHelper args={[500]} />}
|
||||||
{/* @ts-ignore */}
|
{editorCamera}
|
||||||
<OrbitControls ref={orbitControlsRef} enableDamping={false} />
|
|
||||||
<primitive object={helpersRoot}></primitive>
|
<primitive object={helpersRoot}></primitive>
|
||||||
<ProxyManager orbitControlsRef={orbitControlsRef} />
|
<ProxyManager orbitControlsRef={orbitControlsRef} />
|
||||||
<color attach="background" args={[0.24, 0.24, 0.24]} />
|
<color attach="background" args={[0.24, 0.24, 0.24]} />
|
||||||
|
@ -96,82 +95,83 @@ const Tools = styled.div`
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
`
|
`
|
||||||
|
|
||||||
const SnapshotEditor: React.FC<{}> = () => {
|
const SnapshotEditor: React.FC<{object: ISheetObject<$FixMe>; paneId: string}> =
|
||||||
const [editorObject, sceneSnapshot, initialEditorCamera, createSnapshot] =
|
(props) => {
|
||||||
useEditorStore(
|
const snapshotEditorSheet = props.object.sheet
|
||||||
|
const paneId = props.paneId
|
||||||
|
|
||||||
|
const [editorObject, sceneSnapshot, createSnapshot] = useEditorStore(
|
||||||
(state) => [
|
(state) => [
|
||||||
state.editorObject,
|
state.editorObject,
|
||||||
state.sceneSnapshot,
|
state.sceneSnapshot,
|
||||||
state.initialEditorCamera,
|
|
||||||
state.createSnapshot,
|
state.createSnapshot,
|
||||||
],
|
],
|
||||||
shallow,
|
shallow,
|
||||||
)
|
)
|
||||||
|
|
||||||
const editorOpen = true
|
const editorOpen = true
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
let timeout: NodeJS.Timeout | undefined
|
let timeout: NodeJS.Timeout | undefined
|
||||||
if (editorOpen) {
|
if (editorOpen) {
|
||||||
// a hack to make sure all the scene's props are
|
// a hack to make sure all the scene's props are
|
||||||
// applied before we take a snapshot
|
// applied before we take a snapshot
|
||||||
timeout = setTimeout(createSnapshot, 100)
|
timeout = setTimeout(createSnapshot, 100)
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
if (timeout !== undefined) {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
}
|
}
|
||||||
}
|
return () => {
|
||||||
}, [editorOpen])
|
if (timeout !== undefined) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [editorOpen])
|
||||||
|
|
||||||
const [overlay, setOverlay] = useState<HTMLDivElement | null>(null)
|
if (!editorObject) return <></>
|
||||||
|
|
||||||
if (!editorObject) return <></>
|
return (
|
||||||
|
<root.div>
|
||||||
|
<StyleSheetManager disableVendorPrefixes>
|
||||||
|
<>
|
||||||
|
<GlobalStyle />
|
||||||
|
<Wrapper>
|
||||||
|
<Overlay>
|
||||||
|
<Tools>
|
||||||
|
<ToolbarIconButton
|
||||||
|
icon={<IoCameraReverseOutline />}
|
||||||
|
label="Refresh Snapshot"
|
||||||
|
onClick={createSnapshot}
|
||||||
|
></ToolbarIconButton>
|
||||||
|
</Tools>
|
||||||
|
</Overlay>
|
||||||
|
|
||||||
return (
|
{sceneSnapshot ? (
|
||||||
<root.div>
|
<>
|
||||||
<StyleSheetManager disableVendorPrefixes>
|
<CanvasWrapper>
|
||||||
<>
|
<Canvas
|
||||||
<GlobalStyle />
|
// @ts-ignore
|
||||||
{/* <PortalContext.Provider value={overlay}> */}
|
colorManagement
|
||||||
<Wrapper>
|
onCreated={({gl}) => {
|
||||||
<Overlay ref={setOverlay}>
|
gl.setClearColor('white')
|
||||||
<Tools>
|
}}
|
||||||
<ToolbarIconButton
|
shadowMap
|
||||||
icon={<IoCameraReverseOutline />}
|
dpr={[1, 2]}
|
||||||
label="Refresh Snapshot"
|
fog={'red'}
|
||||||
onClick={createSnapshot}
|
onPointerMissed={() =>
|
||||||
></ToolbarIconButton>
|
studio.__experimental_setSelection([])
|
||||||
</Tools>
|
}
|
||||||
</Overlay>
|
>
|
||||||
|
<EditorScene
|
||||||
{sceneSnapshot ? (
|
snapshotEditorSheet={snapshotEditorSheet}
|
||||||
<>
|
paneId={paneId}
|
||||||
<CanvasWrapper>
|
/>
|
||||||
<Canvas
|
</Canvas>
|
||||||
// @ts-ignore
|
</CanvasWrapper>
|
||||||
colorManagement
|
</>
|
||||||
camera={initialEditorCamera}
|
) : null}
|
||||||
onCreated={({gl}) => {
|
</Wrapper>
|
||||||
gl.setClearColor('white')
|
{/* </PortalContext.Provider> */}
|
||||||
}}
|
</>
|
||||||
shadowMap
|
</StyleSheetManager>
|
||||||
dpr={[1, 2]}
|
</root.div>
|
||||||
fog={'red'}
|
)
|
||||||
onPointerMissed={() =>
|
}
|
||||||
studio.__experimental_setSelection([])
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<EditorScene />
|
|
||||||
</Canvas>
|
|
||||||
</CanvasWrapper>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</Wrapper>
|
|
||||||
{/* </PortalContext.Provider> */}
|
|
||||||
</>
|
|
||||||
</StyleSheetManager>
|
|
||||||
</root.div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SnapshotEditor
|
export default SnapshotEditor
|
||||||
|
|
|
@ -2,21 +2,13 @@ import type {VFC} from 'react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {useEditorStore} from '../../store'
|
import {useEditorStore} from '../../store'
|
||||||
import shallow from 'zustand/shallow'
|
import shallow from 'zustand/shallow'
|
||||||
import {GiPocketBow, IoCameraOutline, RiFocus3Line} from 'react-icons/all'
|
import {IoCameraOutline} from 'react-icons/all'
|
||||||
import {Vector3} from 'three'
|
|
||||||
import type {$FixMe} from '@theatre/shared/utils/types'
|
|
||||||
import studio, {ToolbarIconButton} from '@theatre/studio'
|
import studio, {ToolbarIconButton} from '@theatre/studio'
|
||||||
import {getSelected} from '../useSelected'
|
|
||||||
import {useVal} from '@theatre/dataverse-react'
|
import {useVal} from '@theatre/dataverse-react'
|
||||||
import styled from 'styled-components'
|
|
||||||
import TransformControlsModeSelect from './TransformControlsModeSelect'
|
import TransformControlsModeSelect from './TransformControlsModeSelect'
|
||||||
import ViewportShadingSelect from './ViewportShadingSelect'
|
import ViewportShadingSelect from './ViewportShadingSelect'
|
||||||
import TransformControlsSpaceSelect from './TransformControlsSpaceSelect'
|
import TransformControlsSpaceSelect from './TransformControlsSpaceSelect'
|
||||||
|
|
||||||
const ToolGroup = styled.div`
|
|
||||||
pointer-events: auto;
|
|
||||||
`
|
|
||||||
|
|
||||||
const Toolbar: VFC = () => {
|
const Toolbar: VFC = () => {
|
||||||
const [editorObject] = useEditorStore(
|
const [editorObject] = useEditorStore(
|
||||||
(state) => [state.editorObject],
|
(state) => [state.editorObject],
|
||||||
|
@ -66,7 +58,7 @@ const Toolbar: VFC = () => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToolbarIconButton
|
{/* <ToolbarIconButton
|
||||||
label="Focus on selected"
|
label="Focus on selected"
|
||||||
icon={<RiFocus3Line />}
|
icon={<RiFocus3Line />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -88,9 +80,9 @@ const Toolbar: VFC = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
<ToolbarIconButton
|
{/* <ToolbarIconButton
|
||||||
label="Align object to view"
|
label="Align object to view"
|
||||||
icon={<GiPocketBow />}
|
icon={<GiPocketBow />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -121,7 +113,7 @@ const Toolbar: VFC = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/> */}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React, {forwardRef, useLayoutEffect, useEffect, useMemo} from 'react'
|
||||||
import type {ReactThreeFiber, Overwrite} from '@react-three/fiber'
|
import type {ReactThreeFiber, Overwrite} from '@react-three/fiber'
|
||||||
import {useThree} from '@react-three/fiber'
|
import {useThree} from '@react-three/fiber'
|
||||||
import {TransformControls as TransformControlsImpl} from 'three/examples/jsm/controls/TransformControls'
|
import {TransformControls as TransformControlsImpl} from 'three/examples/jsm/controls/TransformControls'
|
||||||
import type {OrbitControls} from '@react-three/drei'
|
import type {OrbitControls} from 'three-stdlib'
|
||||||
|
|
||||||
type R3fTransformControls = Overwrite<
|
type R3fTransformControls = Overwrite<
|
||||||
ReactThreeFiber.Object3DNode<
|
ReactThreeFiber.Object3DNode<
|
||||||
|
@ -15,7 +15,7 @@ type R3fTransformControls = Overwrite<
|
||||||
|
|
||||||
export interface TransformControlsProps extends R3fTransformControls {
|
export interface TransformControlsProps extends R3fTransformControls {
|
||||||
object: Object3D
|
object: Object3D
|
||||||
orbitControlsRef?: React.MutableRefObject<typeof OrbitControls | undefined>
|
orbitControlsRef?: React.MutableRefObject<OrbitControls | null>
|
||||||
onObjectChange?: (event: Event) => void
|
onObjectChange?: (event: Event) => void
|
||||||
onDraggingChange?: (event: Event) => void
|
onDraggingChange?: (event: Event) => void
|
||||||
}
|
}
|
||||||
|
|
37
packages/plugin-r3f/src/components/useRefAndState.ts
Normal file
37
packages/plugin-r3f/src/components/useRefAndState.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import type {MutableRefObject} from 'react'
|
||||||
|
import {useMemo, useState} from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines useRef() and useState().
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const [ref, val] = useRefAndState<HTMLDivElement | null>(null)
|
||||||
|
*
|
||||||
|
* useEffect(() => {
|
||||||
|
* val.addEventListener(...)
|
||||||
|
* }, [val])
|
||||||
|
*
|
||||||
|
* return <div ref={ref}></div>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export default function useRefAndState<T>(
|
||||||
|
initialValue: T,
|
||||||
|
): [ref: MutableRefObject<T>, state: T] {
|
||||||
|
const ref = useMemo(() => {
|
||||||
|
let current = initialValue
|
||||||
|
return {
|
||||||
|
get current() {
|
||||||
|
return current
|
||||||
|
},
|
||||||
|
set current(v: T) {
|
||||||
|
current = v
|
||||||
|
setState(v)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const [state, setState] = useState<T>(() => initialValue)
|
||||||
|
|
||||||
|
return [ref, state]
|
||||||
|
}
|
166
packages/plugin-r3f/src/components/useSnapshotEditorCamera.tsx
Normal file
166
packages/plugin-r3f/src/components/useSnapshotEditorCamera.tsx
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
import {OrbitControls, PerspectiveCamera} from '@react-three/drei'
|
||||||
|
import type {OrbitControls as OrbitControlsImpl} from 'three-stdlib'
|
||||||
|
import type {MutableRefObject} from 'react'
|
||||||
|
import {useLayoutEffect, useRef} from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import useRefAndState from './useRefAndState'
|
||||||
|
import {studio} from '@theatre/studio'
|
||||||
|
import type {PerspectiveCamera as PerspectiveCameraImpl} from 'three'
|
||||||
|
import type {ISheet} from '@theatre/core'
|
||||||
|
import {types} from '@theatre/core'
|
||||||
|
import type {ISheetObject} from '@theatre/core'
|
||||||
|
import {useThree} from '@react-three/fiber'
|
||||||
|
|
||||||
|
const camConf = types.compound({
|
||||||
|
transform: types.compound({
|
||||||
|
position: types.compound({
|
||||||
|
x: types.number(0),
|
||||||
|
y: types.number(10),
|
||||||
|
z: types.number(0),
|
||||||
|
}),
|
||||||
|
target: types.compound({
|
||||||
|
x: types.number(0),
|
||||||
|
y: types.number(10),
|
||||||
|
z: types.number(0),
|
||||||
|
}),
|
||||||
|
rotation: types.compound({
|
||||||
|
x: types.number(0),
|
||||||
|
y: types.number(0),
|
||||||
|
z: types.number(0),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function useSnapshotEditorCamera(
|
||||||
|
snapshotEditorSheet: ISheet,
|
||||||
|
paneId: string,
|
||||||
|
): [
|
||||||
|
node: React.ReactNode,
|
||||||
|
orbitControlsRef: MutableRefObject<OrbitControlsImpl | null>,
|
||||||
|
] {
|
||||||
|
// OrbitControls and Cam might change later on, so we use useRefAndState()
|
||||||
|
// instead of useRef() to catch those changes.
|
||||||
|
const [orbitControlsRef, orbitControls] =
|
||||||
|
useRefAndState<OrbitControlsImpl | null>(null)
|
||||||
|
|
||||||
|
const [camRef, cam] = useRefAndState<PerspectiveCameraImpl | undefined>(
|
||||||
|
undefined,
|
||||||
|
)
|
||||||
|
|
||||||
|
const objRef = useRef<ISheetObject<typeof camConf> | null>(null)
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!objRef.current) {
|
||||||
|
objRef.current = snapshotEditorSheet.object(
|
||||||
|
`Editor Camera ${paneId}`,
|
||||||
|
{},
|
||||||
|
camConf,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [paneId])
|
||||||
|
|
||||||
|
usePassValuesFromTheatreToCamera(cam, orbitControls, objRef)
|
||||||
|
usePassValuesFromOrbitControlsToTheatre(cam, orbitControls, objRef)
|
||||||
|
|
||||||
|
const node = (
|
||||||
|
<>
|
||||||
|
<PerspectiveCamera makeDefault ref={camRef} position={[0, 10, 0]} />
|
||||||
|
<OrbitControls
|
||||||
|
makeDefault
|
||||||
|
ref={orbitControlsRef}
|
||||||
|
camera={cam}
|
||||||
|
enableDamping={false}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
return [node, orbitControlsRef]
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePassValuesFromOrbitControlsToTheatre(
|
||||||
|
cam: PerspectiveCameraImpl | undefined,
|
||||||
|
orbitControls: OrbitControlsImpl | null,
|
||||||
|
objRef: MutableRefObject<ISheetObject<typeof camConf> | null>,
|
||||||
|
) {
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!cam || orbitControls == null) return
|
||||||
|
|
||||||
|
let currentScrub: undefined | ReturnType<typeof studio['debouncedScrub']>
|
||||||
|
|
||||||
|
let started = false
|
||||||
|
|
||||||
|
const onStart = () => {
|
||||||
|
started = true
|
||||||
|
if (!currentScrub) {
|
||||||
|
currentScrub = studio.debouncedScrub(600)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const onEnd = () => {
|
||||||
|
started = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = () => {
|
||||||
|
if (!started) return
|
||||||
|
|
||||||
|
const p = cam!.position
|
||||||
|
const position = {x: p.x, y: p.y, z: p.z}
|
||||||
|
|
||||||
|
const r = cam!.rotation
|
||||||
|
const rotation = {x: r.x, y: r.y, z: r.z}
|
||||||
|
|
||||||
|
const t = orbitControls!.target
|
||||||
|
const target = {x: t.x, y: t.y, z: t.z}
|
||||||
|
|
||||||
|
const transform = {
|
||||||
|
position,
|
||||||
|
rotation,
|
||||||
|
target,
|
||||||
|
}
|
||||||
|
|
||||||
|
currentScrub!.capture(({set}) => {
|
||||||
|
set(objRef.current!.props.transform, transform)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
orbitControls.addEventListener('start', onStart)
|
||||||
|
orbitControls.addEventListener('end', onEnd)
|
||||||
|
orbitControls.addEventListener('change', onChange)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
orbitControls.removeEventListener('start', onStart)
|
||||||
|
orbitControls.removeEventListener('end', onEnd)
|
||||||
|
orbitControls.removeEventListener('change', onChange)
|
||||||
|
}
|
||||||
|
}, [cam, orbitControls])
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePassValuesFromTheatreToCamera(
|
||||||
|
cam: PerspectiveCameraImpl | undefined,
|
||||||
|
orbitControls: OrbitControlsImpl | null,
|
||||||
|
objRef: MutableRefObject<ISheetObject<typeof camConf> | null>,
|
||||||
|
) {
|
||||||
|
const invalidate = useThree(({invalidate}) => invalidate)
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!cam || orbitControls === null) return
|
||||||
|
|
||||||
|
const obj = objRef.current!
|
||||||
|
const setFromTheatre = ({
|
||||||
|
transform,
|
||||||
|
}: {
|
||||||
|
transform: typeof camConf['valueType']['transform']
|
||||||
|
}): void => {
|
||||||
|
const {position, rotation, target} = transform
|
||||||
|
cam.position.set(position.x, position.y, position.z)
|
||||||
|
cam.rotation.set(rotation.x, rotation.y, rotation.z)
|
||||||
|
orbitControls.target.set(target.x, target.y, target.z)
|
||||||
|
orbitControls.update()
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsub = obj.onValuesChange(setFromTheatre)
|
||||||
|
setFromTheatre(obj.value)
|
||||||
|
|
||||||
|
return unsub
|
||||||
|
}, [cam, orbitControls, objRef, invalidate])
|
||||||
|
}
|
|
@ -2,10 +2,6 @@ import type {StateCreator} from 'zustand'
|
||||||
import create from 'zustand'
|
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 {MutableRefObject} from 'react'
|
|
||||||
import type {OrbitControls} from '@react-three/drei'
|
|
||||||
// @ts-ignore TODO
|
|
||||||
import type {ContainerProps} from '@react-three/fiber'
|
|
||||||
import type {ISheet, ISheetObject} from '@theatre/core'
|
import type {ISheet, ISheetObject} from '@theatre/core'
|
||||||
import {types, getProject} from '@theatre/core'
|
import {types, getProject} from '@theatre/core'
|
||||||
|
|
||||||
|
@ -138,27 +134,21 @@ export type EditorStore = {
|
||||||
scene: Scene | null
|
scene: Scene | null
|
||||||
gl: WebGLRenderer | null
|
gl: WebGLRenderer | null
|
||||||
allowImplicitInstancing: boolean
|
allowImplicitInstancing: boolean
|
||||||
orbitControlsRef: MutableRefObject<typeof OrbitControls | undefined> | null
|
|
||||||
helpersRoot: Group
|
helpersRoot: Group
|
||||||
editables: Record<string, Editable>
|
editables: Record<string, Editable>
|
||||||
// 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
|
||||||
initialEditorCamera: ContainerProps['camera']
|
|
||||||
|
|
||||||
init: (
|
init: (
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
gl: WebGLRenderer,
|
gl: WebGLRenderer,
|
||||||
allowImplicitInstancing: boolean,
|
allowImplicitInstancing: boolean,
|
||||||
editorCamera: ContainerProps['camera'],
|
|
||||||
sheet: ISheet,
|
sheet: ISheet,
|
||||||
editorObject: null | ISheetObject<typeof editorSheetObjectConfig>,
|
editorObject: null | ISheetObject<typeof editorSheetObjectConfig>,
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
setOrbitControlsRef: (
|
|
||||||
orbitControlsRef: MutableRefObject<typeof OrbitControls | undefined>,
|
|
||||||
) => void
|
|
||||||
addEditable: <T extends EditableType>(type: T, uniqueName: string) => void
|
addEditable: <T extends EditableType>(type: T, uniqueName: string) => void
|
||||||
removeEditable: (uniqueName: string) => void
|
removeEditable: (uniqueName: string) => void
|
||||||
createSnapshot: () => void
|
createSnapshot: () => void
|
||||||
|
@ -177,7 +167,6 @@ const config: StateCreator<EditorStore> = (set, get) => {
|
||||||
scene: null,
|
scene: null,
|
||||||
gl: null,
|
gl: null,
|
||||||
allowImplicitInstancing: false,
|
allowImplicitInstancing: false,
|
||||||
orbitControlsRef: null,
|
|
||||||
helpersRoot: new Group(),
|
helpersRoot: new Group(),
|
||||||
editables: {},
|
editables: {},
|
||||||
canvasName: 'default',
|
canvasName: 'default',
|
||||||
|
@ -185,19 +174,11 @@ const config: StateCreator<EditorStore> = (set, get) => {
|
||||||
editablesSnapshot: null,
|
editablesSnapshot: null,
|
||||||
initialEditorCamera: {},
|
initialEditorCamera: {},
|
||||||
|
|
||||||
init: (
|
init: (scene, gl, allowImplicitInstancing, sheet, editorObject) => {
|
||||||
scene,
|
|
||||||
gl,
|
|
||||||
allowImplicitInstancing,
|
|
||||||
editorCamera,
|
|
||||||
sheet,
|
|
||||||
editorObject,
|
|
||||||
) => {
|
|
||||||
set({
|
set({
|
||||||
scene,
|
scene,
|
||||||
gl,
|
gl,
|
||||||
allowImplicitInstancing,
|
allowImplicitInstancing,
|
||||||
initialEditorCamera: editorCamera,
|
|
||||||
sheet,
|
sheet,
|
||||||
editorObject,
|
editorObject,
|
||||||
})
|
})
|
||||||
|
@ -238,9 +219,7 @@ const config: StateCreator<EditorStore> = (set, get) => {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
setOrbitControlsRef: (camera) => {
|
|
||||||
set({orbitControlsRef: camera})
|
|
||||||
},
|
|
||||||
removeEditable: (name) =>
|
removeEditable: (name) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const {[name]: removed, ...rest} = state.editables
|
const {[name]: removed, ...rest} = state.editables
|
||||||
|
@ -284,7 +263,6 @@ export const useEditorStore = create<EditorStore>(config)
|
||||||
|
|
||||||
export type BindFunction = (options: {
|
export type BindFunction = (options: {
|
||||||
allowImplicitInstancing?: boolean
|
allowImplicitInstancing?: boolean
|
||||||
editorCamera?: ContainerProps['camera']
|
|
||||||
sheet: ISheet
|
sheet: ISheet
|
||||||
}) => (options: {gl: WebGLRenderer; scene: Scene}) => void
|
}) => (options: {gl: WebGLRenderer; scene: Scene}) => void
|
||||||
|
|
||||||
|
@ -309,23 +287,6 @@ const editorSheetObjectConfig = types.compound({
|
||||||
},
|
},
|
||||||
{as: 'menu', label: 'Shading'},
|
{as: 'menu', label: 'Shading'},
|
||||||
),
|
),
|
||||||
mode: types.stringLiteral(
|
|
||||||
'translate',
|
|
||||||
{
|
|
||||||
translate: 'Translate',
|
|
||||||
rotate: 'Rotate',
|
|
||||||
scale: 'Scale',
|
|
||||||
},
|
|
||||||
{as: 'switch', label: 'Mode'},
|
|
||||||
),
|
|
||||||
space: types.stringLiteral(
|
|
||||||
'world',
|
|
||||||
{
|
|
||||||
local: 'Local',
|
|
||||||
world: 'World',
|
|
||||||
},
|
|
||||||
{as: 'switch', label: 'Space'},
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{label: 'Viewport Config'},
|
{label: 'Viewport Config'},
|
||||||
),
|
),
|
||||||
|
@ -355,7 +316,6 @@ const editorSheetObjectConfig = types.compound({
|
||||||
|
|
||||||
export const bindToCanvas: BindFunction = ({
|
export const bindToCanvas: BindFunction = ({
|
||||||
allowImplicitInstancing = false,
|
allowImplicitInstancing = false,
|
||||||
editorCamera = {},
|
|
||||||
sheet,
|
sheet,
|
||||||
}) => {
|
}) => {
|
||||||
const uiSheet: null | ISheet =
|
const uiSheet: null | ISheet =
|
||||||
|
@ -368,13 +328,6 @@ export const bindToCanvas: BindFunction = ({
|
||||||
|
|
||||||
return ({gl, scene}) => {
|
return ({gl, scene}) => {
|
||||||
const init = useEditorStore.getState().init
|
const init = useEditorStore.getState().init
|
||||||
init(
|
init(scene, gl, allowImplicitInstancing, sheet, editorSheetObject)
|
||||||
scene,
|
|
||||||
gl,
|
|
||||||
allowImplicitInstancing,
|
|
||||||
{...{position: [20, 20, 20]}, ...editorCamera},
|
|
||||||
sheet,
|
|
||||||
editorSheetObject,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import studioTicker from '@theatre/studio/studioTicker'
|
||||||
import type {IDerivation, Pointer} from '@theatre/dataverse'
|
import type {IDerivation, Pointer} from '@theatre/dataverse'
|
||||||
import {prism} from '@theatre/dataverse'
|
import {prism} from '@theatre/dataverse'
|
||||||
import SimpleCache from '@theatre/shared/utils/SimpleCache'
|
import SimpleCache from '@theatre/shared/utils/SimpleCache'
|
||||||
import type {$FixMe, VoidFn} from '@theatre/shared/utils/types'
|
import type {$FixMe, $IntentionalAny, VoidFn} from '@theatre/shared/utils/types'
|
||||||
import type {IScrub} from '@theatre/studio/Scrub'
|
import type {IScrub} from '@theatre/studio/Scrub'
|
||||||
|
|
||||||
import type {Studio} from '@theatre/studio/Studio'
|
import type {Studio} from '@theatre/studio/Studio'
|
||||||
|
@ -16,6 +16,7 @@ import type {
|
||||||
PropTypeConfig_Boolean,
|
PropTypeConfig_Boolean,
|
||||||
PropTypeConfig_Compound,
|
PropTypeConfig_Compound,
|
||||||
} from '@theatre/core/propTypes'
|
} from '@theatre/core/propTypes'
|
||||||
|
import {debounce} from 'lodash-es'
|
||||||
|
|
||||||
export interface ITransactionAPI {
|
export interface ITransactionAPI {
|
||||||
set<V>(pointer: Pointer<V>, value: V): void
|
set<V>(pointer: Pointer<V>, value: V): void
|
||||||
|
@ -28,7 +29,7 @@ export interface PaneClassDefinition<
|
||||||
class: string
|
class: string
|
||||||
dataType: DataType
|
dataType: DataType
|
||||||
component: React.ComponentType<{
|
component: React.ComponentType<{
|
||||||
id: string
|
paneId: string
|
||||||
object: ISheetObject<
|
object: ISheetObject<
|
||||||
PropTypeConfig_Compound<{
|
PropTypeConfig_Compound<{
|
||||||
visible: PropTypeConfig_Boolean
|
visible: PropTypeConfig_Boolean
|
||||||
|
@ -65,6 +66,7 @@ export interface IStudio {
|
||||||
|
|
||||||
transaction(fn: (api: ITransactionAPI) => void): void
|
transaction(fn: (api: ITransactionAPI) => void): void
|
||||||
scrub(): IScrub
|
scrub(): IScrub
|
||||||
|
debouncedScrub(threshhold: number): Pick<IScrub, 'capture'>
|
||||||
|
|
||||||
__experimental_setSelection(selection: Array<ISheetObject>): void
|
__experimental_setSelection(selection: Array<ISheetObject>): void
|
||||||
__experimental_onSelectionChange(
|
__experimental_onSelectionChange(
|
||||||
|
@ -158,6 +160,37 @@ export default class TheatreStudio implements IStudio {
|
||||||
return getStudio().scrub()
|
return getStudio().scrub()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debouncedScrub(threshold: number = 1000): Pick<IScrub, 'capture'> {
|
||||||
|
let currentScrub: IScrub | undefined
|
||||||
|
const scheduleCommit = debounce(() => {
|
||||||
|
const s = currentScrub
|
||||||
|
if (!s) return
|
||||||
|
currentScrub = undefined
|
||||||
|
s.commit()
|
||||||
|
}, threshold)
|
||||||
|
|
||||||
|
const capture = (arg: $IntentionalAny) => {
|
||||||
|
if (!currentScrub) {
|
||||||
|
currentScrub = this.scrub()
|
||||||
|
}
|
||||||
|
let errored = true
|
||||||
|
try {
|
||||||
|
currentScrub.capture(arg)
|
||||||
|
errored = false
|
||||||
|
} finally {
|
||||||
|
if (errored) {
|
||||||
|
const s = currentScrub
|
||||||
|
currentScrub = undefined
|
||||||
|
s.discard()
|
||||||
|
} else {
|
||||||
|
scheduleCommit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {capture}
|
||||||
|
}
|
||||||
|
|
||||||
getPanesOfType<PaneClass extends string>(
|
getPanesOfType<PaneClass extends string>(
|
||||||
paneClass: PaneClass,
|
paneClass: PaneClass,
|
||||||
): PaneInstance<PaneClass>[] {
|
): PaneInstance<PaneClass>[] {
|
||||||
|
|
|
@ -132,7 +132,7 @@ const Content: React.FC<{paneInstance: PaneInstance<$FixMe>}> = ({
|
||||||
</PanelDragZone>
|
</PanelDragZone>
|
||||||
<F2>
|
<F2>
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
<Comp id={paneInstance.instanceId} object={paneInstance.object} />
|
<Comp paneId={paneInstance.instanceId} object={paneInstance.object} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</F2>
|
</F2>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
@ -54,6 +54,10 @@ const Title = styled.div`
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
pointer-events: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
`
|
`
|
||||||
|
|
||||||
const headerHeight = `32px`
|
const headerHeight = `32px`
|
||||||
|
@ -106,7 +110,9 @@ const ObjectEditorPanel: React.FC<{}> = (props) => {
|
||||||
<Container>
|
<Container>
|
||||||
<Content>
|
<Content>
|
||||||
<Header>
|
<Header>
|
||||||
<Title>
|
<Title
|
||||||
|
title={`${obj.sheet.address.sheetId}: ${obj.sheet.address.sheetInstanceId} > ${obj.address.objectKey}`}
|
||||||
|
>
|
||||||
<TitleBar_Piece>{obj.sheet.address.sheetId} </TitleBar_Piece>
|
<TitleBar_Piece>{obj.sheet.address.sheetId} </TitleBar_Piece>
|
||||||
|
|
||||||
<TitleBar_Punctuation>{':'} </TitleBar_Punctuation>
|
<TitleBar_Punctuation>{':'} </TitleBar_Punctuation>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import type {PropTypeConfig_Compound} from '@theatre/core/propTypes'
|
import type {PropTypeConfig_Compound} from '@theatre/core/propTypes'
|
||||||
import {isPropConfigComposite} from '@theatre/shared/propTypes/utils'
|
import {isPropConfigComposite} from '@theatre/shared/propTypes/utils'
|
||||||
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
||||||
import {theme} from '@theatre/studio/css'
|
|
||||||
import {usePrism} from '@theatre/dataverse-react'
|
import {usePrism} from '@theatre/dataverse-react'
|
||||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||||
import {getPointerParts} from '@theatre/dataverse'
|
import {getPointerParts} from '@theatre/dataverse'
|
||||||
|
@ -25,10 +24,8 @@ const Container = styled.div`
|
||||||
|
|
||||||
const Header = styled.div`
|
const Header = styled.div`
|
||||||
height: 30px;
|
height: 30px;
|
||||||
/* padding-left: calc(var(--left-pad) + var(--depth) * var(--step)); */
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
/* color: ${theme.panel.body.compoudThing.label.color}; */
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
${rowBg};
|
${rowBg};
|
||||||
|
@ -40,17 +37,6 @@ const Padding = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`
|
`
|
||||||
|
|
||||||
const IconContainer = styled.div`
|
|
||||||
width: 12px;
|
|
||||||
margin-right: -12px;
|
|
||||||
/* margin-left: ${indentationFormula}; */
|
|
||||||
font-size: 9px;
|
|
||||||
text-align: center;
|
|
||||||
transform: rotateZ(90deg);
|
|
||||||
position: relative;
|
|
||||||
left: -18px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const PropName = styled.div`
|
const PropName = styled.div`
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
Loading…
Reference in a new issue