diff --git a/packages/playground/src/space-exploration/App.tsx b/packages/playground/src/space-exploration/App.tsx index 392d4f0..75be028 100644 --- a/packages/playground/src/space-exploration/App.tsx +++ b/packages/playground/src/space-exploration/App.tsx @@ -1,5 +1,5 @@ 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 React, {Suspense} from 'react' import {Canvas} from '@react-three/fiber' @@ -86,12 +86,12 @@ function App() { - + /> */} diff --git a/packages/plugin-r3f/src/.eslintrc.js b/packages/plugin-r3f/src/.eslintrc.js new file mode 100644 index 0000000..183a838 --- /dev/null +++ b/packages/plugin-r3f/src/.eslintrc.js @@ -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.`, + }, + ], + }, +} diff --git a/packages/plugin-r3f/src/components/ProxyManager.tsx b/packages/plugin-r3f/src/components/ProxyManager.tsx index e794266..9395e29 100644 --- a/packages/plugin-r3f/src/components/ProxyManager.tsx +++ b/packages/plugin-r3f/src/components/ProxyManager.tsx @@ -9,7 +9,7 @@ import React, { import {useEditorStore} from '../store' import {createPortal} from '@react-three/fiber' import EditableProxy from './EditableProxy' -import type {OrbitControls} from '@react-three/drei' +import type {OrbitControls} from 'three-stdlib' import TransformControls from './TransformControls' import shallow from 'zustand/shallow' import type {Material, Mesh, Object3D} from 'three' @@ -21,7 +21,7 @@ import {useSelected} from './useSelected' import {useVal} from '@theatre/dataverse-react' export interface ProxyManagerProps { - orbitControlsRef: React.MutableRefObject + orbitControlsRef: React.MutableRefObject } type IEditableProxy = { diff --git a/packages/plugin-r3f/src/components/SnapshotEditor.tsx b/packages/plugin-r3f/src/components/SnapshotEditor.tsx index 0ea57e1..e8b6ec6 100644 --- a/packages/plugin-r3f/src/components/SnapshotEditor.tsx +++ b/packages/plugin-r3f/src/components/SnapshotEditor.tsx @@ -1,9 +1,7 @@ -import {useState} from 'react' import {useLayoutEffect} from 'react' -import React, {useEffect, useRef} from 'react' +import React from 'react' import {Canvas} from '@react-three/fiber' import {useEditorStore} from '../store' -import {OrbitControls} from '@react-three/drei' import shallow from 'zustand/shallow' import root from 'react-shadow/styled-components' import ProxyManager from './ProxyManager' @@ -11,6 +9,9 @@ import studio, {ToolbarIconButton} from '@theatre/studio' import {useVal} from '@theatre/dataverse-react' import styled, {createGlobalStyle, StyleSheetManager} from 'styled-components' 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` :host { @@ -31,31 +32,29 @@ const GlobalStyle = createGlobalStyle` } ` -const EditorScene = () => { - const orbitControlsRef = useRef() +const EditorScene: React.FC<{snapshotEditorSheet: ISheet; paneId: string}> = ({ + snapshotEditorSheet, + paneId, +}) => { + const [editorCamera, orbitControlsRef] = useSnapshotEditorCamera( + snapshotEditorSheet, + paneId, + ) - const [editorObject, helpersRoot, setOrbitControlsRef] = useEditorStore( - (state) => [ - state.editorObject, - state.helpersRoot, - state.setOrbitControlsRef, - ], + const [editorObject, helpersRoot] = useEditorStore( + (state) => [state.editorObject, state.helpersRoot], shallow, ) const showGrid = useVal(editorObject?.props.viewport.showGrid) ?? true const showAxes = useVal(editorObject?.props.viewport.showAxes) ?? true - useEffect(() => { - setOrbitControlsRef(orbitControlsRef) - }, [setOrbitControlsRef]) - return ( <> {showGrid && } {showAxes && } - {/* @ts-ignore */} - + {editorCamera} + @@ -96,82 +95,83 @@ const Tools = styled.div` pointer-events: auto; ` -const SnapshotEditor: React.FC<{}> = () => { - const [editorObject, sceneSnapshot, initialEditorCamera, createSnapshot] = - useEditorStore( +const SnapshotEditor: React.FC<{object: ISheetObject<$FixMe>; paneId: string}> = + (props) => { + const snapshotEditorSheet = props.object.sheet + const paneId = props.paneId + + const [editorObject, sceneSnapshot, createSnapshot] = useEditorStore( (state) => [ state.editorObject, state.sceneSnapshot, - state.initialEditorCamera, state.createSnapshot, ], shallow, ) - const editorOpen = true - useLayoutEffect(() => { - let timeout: NodeJS.Timeout | undefined - if (editorOpen) { - // a hack to make sure all the scene's props are - // applied before we take a snapshot - timeout = setTimeout(createSnapshot, 100) - } - return () => { - if (timeout !== undefined) { - clearTimeout(timeout) + const editorOpen = true + useLayoutEffect(() => { + let timeout: NodeJS.Timeout | undefined + if (editorOpen) { + // a hack to make sure all the scene's props are + // applied before we take a snapshot + timeout = setTimeout(createSnapshot, 100) } - } - }, [editorOpen]) + return () => { + if (timeout !== undefined) { + clearTimeout(timeout) + } + } + }, [editorOpen]) - const [overlay, setOverlay] = useState(null) + if (!editorObject) return <> - if (!editorObject) return <> + return ( + + + <> + + + + + } + label="Refresh Snapshot" + onClick={createSnapshot} + > + + - return ( - - - <> - - {/* */} - - - - } - label="Refresh Snapshot" - onClick={createSnapshot} - > - - - - {sceneSnapshot ? ( - <> - - { - gl.setClearColor('white') - }} - shadowMap - dpr={[1, 2]} - fog={'red'} - onPointerMissed={() => - studio.__experimental_setSelection([]) - } - > - - - - - ) : null} - - {/* */} - - - - ) -} + {sceneSnapshot ? ( + <> + + { + gl.setClearColor('white') + }} + shadowMap + dpr={[1, 2]} + fog={'red'} + onPointerMissed={() => + studio.__experimental_setSelection([]) + } + > + + + + + ) : null} + + {/* */} + + + + ) + } export default SnapshotEditor diff --git a/packages/plugin-r3f/src/components/Toolbar/Toolbar.tsx b/packages/plugin-r3f/src/components/Toolbar/Toolbar.tsx index b562077..f221fc0 100644 --- a/packages/plugin-r3f/src/components/Toolbar/Toolbar.tsx +++ b/packages/plugin-r3f/src/components/Toolbar/Toolbar.tsx @@ -2,21 +2,13 @@ import type {VFC} from 'react' import React from 'react' import {useEditorStore} from '../../store' import shallow from 'zustand/shallow' -import {GiPocketBow, IoCameraOutline, RiFocus3Line} from 'react-icons/all' -import {Vector3} from 'three' -import type {$FixMe} from '@theatre/shared/utils/types' +import {IoCameraOutline} from 'react-icons/all' import studio, {ToolbarIconButton} from '@theatre/studio' -import {getSelected} from '../useSelected' import {useVal} from '@theatre/dataverse-react' -import styled from 'styled-components' import TransformControlsModeSelect from './TransformControlsModeSelect' import ViewportShadingSelect from './ViewportShadingSelect' import TransformControlsSpaceSelect from './TransformControlsSpaceSelect' -const ToolGroup = styled.div` - pointer-events: auto; -` - const Toolbar: VFC = () => { const [editorObject] = useEditorStore( (state) => [state.editorObject], @@ -66,7 +58,7 @@ const Toolbar: VFC = () => { }} /> - } onClick={() => { @@ -88,9 +80,9 @@ const Toolbar: VFC = () => { ) } }} - /> + /> */} - } onClick={() => { @@ -121,7 +113,7 @@ const Toolbar: VFC = () => { } } }} - /> + /> */} ) } diff --git a/packages/plugin-r3f/src/components/TransformControls.tsx b/packages/plugin-r3f/src/components/TransformControls.tsx index cf4c830..8f45553 100644 --- a/packages/plugin-r3f/src/components/TransformControls.tsx +++ b/packages/plugin-r3f/src/components/TransformControls.tsx @@ -3,7 +3,7 @@ import React, {forwardRef, useLayoutEffect, useEffect, useMemo} from 'react' import type {ReactThreeFiber, Overwrite} from '@react-three/fiber' import {useThree} from '@react-three/fiber' 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< ReactThreeFiber.Object3DNode< @@ -15,7 +15,7 @@ type R3fTransformControls = Overwrite< export interface TransformControlsProps extends R3fTransformControls { object: Object3D - orbitControlsRef?: React.MutableRefObject + orbitControlsRef?: React.MutableRefObject onObjectChange?: (event: Event) => void onDraggingChange?: (event: Event) => void } diff --git a/packages/plugin-r3f/src/components/useRefAndState.ts b/packages/plugin-r3f/src/components/useRefAndState.ts new file mode 100644 index 0000000..c567c6a --- /dev/null +++ b/packages/plugin-r3f/src/components/useRefAndState.ts @@ -0,0 +1,37 @@ +import type {MutableRefObject} from 'react' +import {useMemo, useState} from 'react' + +/** + * Combines useRef() and useState(). + * + * @example + * ```typescript + * const [ref, val] = useRefAndState(null) + * + * useEffect(() => { + * val.addEventListener(...) + * }, [val]) + * + * return
+ * ``` + */ +export default function useRefAndState( + initialValue: T, +): [ref: MutableRefObject, 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(() => initialValue) + + return [ref, state] +} diff --git a/packages/plugin-r3f/src/components/useSnapshotEditorCamera.tsx b/packages/plugin-r3f/src/components/useSnapshotEditorCamera.tsx new file mode 100644 index 0000000..0ac8fef --- /dev/null +++ b/packages/plugin-r3f/src/components/useSnapshotEditorCamera.tsx @@ -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, +] { + // OrbitControls and Cam might change later on, so we use useRefAndState() + // instead of useRef() to catch those changes. + const [orbitControlsRef, orbitControls] = + useRefAndState(null) + + const [camRef, cam] = useRefAndState( + undefined, + ) + + const objRef = useRef | 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 = ( + <> + + + + ) + + return [node, orbitControlsRef] +} + +function usePassValuesFromOrbitControlsToTheatre( + cam: PerspectiveCameraImpl | undefined, + orbitControls: OrbitControlsImpl | null, + objRef: MutableRefObject | null>, +) { + useLayoutEffect(() => { + if (!cam || orbitControls == null) return + + let currentScrub: undefined | ReturnType + + 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 | 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]) +} diff --git a/packages/plugin-r3f/src/store.ts b/packages/plugin-r3f/src/store.ts index 5f8c923..8465ba2 100644 --- a/packages/plugin-r3f/src/store.ts +++ b/packages/plugin-r3f/src/store.ts @@ -2,10 +2,6 @@ import type {StateCreator} from 'zustand' import create from 'zustand' import type {Object3D, Scene, WebGLRenderer} 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 {types, getProject} from '@theatre/core' @@ -138,27 +134,21 @@ export type EditorStore = { scene: Scene | null gl: WebGLRenderer | null allowImplicitInstancing: boolean - orbitControlsRef: MutableRefObject | null helpersRoot: Group editables: Record // this will come in handy when we start supporting multiple canvases canvasName: string sceneSnapshot: Scene | null editablesSnapshot: Record | null - initialEditorCamera: ContainerProps['camera'] init: ( scene: Scene, gl: WebGLRenderer, allowImplicitInstancing: boolean, - editorCamera: ContainerProps['camera'], sheet: ISheet, editorObject: null | ISheetObject, ) => void - setOrbitControlsRef: ( - orbitControlsRef: MutableRefObject, - ) => void addEditable: (type: T, uniqueName: string) => void removeEditable: (uniqueName: string) => void createSnapshot: () => void @@ -177,7 +167,6 @@ const config: StateCreator = (set, get) => { scene: null, gl: null, allowImplicitInstancing: false, - orbitControlsRef: null, helpersRoot: new Group(), editables: {}, canvasName: 'default', @@ -185,19 +174,11 @@ const config: StateCreator = (set, get) => { editablesSnapshot: null, initialEditorCamera: {}, - init: ( - scene, - gl, - allowImplicitInstancing, - editorCamera, - sheet, - editorObject, - ) => { + init: (scene, gl, allowImplicitInstancing, sheet, editorObject) => { set({ scene, gl, allowImplicitInstancing, - initialEditorCamera: editorCamera, sheet, editorObject, }) @@ -238,9 +219,7 @@ const config: StateCreator = (set, get) => { }, } }), - setOrbitControlsRef: (camera) => { - set({orbitControlsRef: camera}) - }, + removeEditable: (name) => set((state) => { const {[name]: removed, ...rest} = state.editables @@ -284,7 +263,6 @@ export const useEditorStore = create(config) export type BindFunction = (options: { allowImplicitInstancing?: boolean - editorCamera?: ContainerProps['camera'] sheet: ISheet }) => (options: {gl: WebGLRenderer; scene: Scene}) => void @@ -309,23 +287,6 @@ const editorSheetObjectConfig = types.compound({ }, {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'}, ), @@ -355,7 +316,6 @@ const editorSheetObjectConfig = types.compound({ export const bindToCanvas: BindFunction = ({ allowImplicitInstancing = false, - editorCamera = {}, sheet, }) => { const uiSheet: null | ISheet = @@ -368,13 +328,6 @@ export const bindToCanvas: BindFunction = ({ return ({gl, scene}) => { const init = useEditorStore.getState().init - init( - scene, - gl, - allowImplicitInstancing, - {...{position: [20, 20, 20]}, ...editorCamera}, - sheet, - editorSheetObject, - ) + init(scene, gl, allowImplicitInstancing, sheet, editorSheetObject) } } diff --git a/theatre/studio/src/TheatreStudio.ts b/theatre/studio/src/TheatreStudio.ts index 357dc9f..379de14 100644 --- a/theatre/studio/src/TheatreStudio.ts +++ b/theatre/studio/src/TheatreStudio.ts @@ -3,7 +3,7 @@ import studioTicker from '@theatre/studio/studioTicker' import type {IDerivation, Pointer} from '@theatre/dataverse' import {prism} from '@theatre/dataverse' 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 {Studio} from '@theatre/studio/Studio' @@ -16,6 +16,7 @@ import type { PropTypeConfig_Boolean, PropTypeConfig_Compound, } from '@theatre/core/propTypes' +import {debounce} from 'lodash-es' export interface ITransactionAPI { set(pointer: Pointer, value: V): void @@ -28,7 +29,7 @@ export interface PaneClassDefinition< class: string dataType: DataType component: React.ComponentType<{ - id: string + paneId: string object: ISheetObject< PropTypeConfig_Compound<{ visible: PropTypeConfig_Boolean @@ -65,6 +66,7 @@ export interface IStudio { transaction(fn: (api: ITransactionAPI) => void): void scrub(): IScrub + debouncedScrub(threshhold: number): Pick __experimental_setSelection(selection: Array): void __experimental_onSelectionChange( @@ -158,6 +160,37 @@ export default class TheatreStudio implements IStudio { return getStudio().scrub() } + debouncedScrub(threshold: number = 1000): Pick { + 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: PaneClass, ): PaneInstance[] { diff --git a/theatre/studio/src/panels/BasePanel/PaneWrapper.tsx b/theatre/studio/src/panels/BasePanel/PaneWrapper.tsx index d916b8d..25897d7 100644 --- a/theatre/studio/src/panels/BasePanel/PaneWrapper.tsx +++ b/theatre/studio/src/panels/BasePanel/PaneWrapper.tsx @@ -132,7 +132,7 @@ const Content: React.FC<{paneInstance: PaneInstance<$FixMe>}> = ({ - + diff --git a/theatre/studio/src/panels/ObjectEditorPanel/ObjectEditorPanel.tsx b/theatre/studio/src/panels/ObjectEditorPanel/ObjectEditorPanel.tsx index 04dfa62..fd55625 100644 --- a/theatre/studio/src/panels/ObjectEditorPanel/ObjectEditorPanel.tsx +++ b/theatre/studio/src/panels/ObjectEditorPanel/ObjectEditorPanel.tsx @@ -54,6 +54,10 @@ const Title = styled.div` font-weight: 500; font-size: 10px; user-select: none; + pointer-events: auto; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; ` const headerHeight = `32px` @@ -106,7 +110,9 @@ const ObjectEditorPanel: React.FC<{}> = (props) => {
- + <Title + title={`${obj.sheet.address.sheetId}: ${obj.sheet.address.sheetInstanceId} > ${obj.address.objectKey}`} + > <TitleBar_Piece>{obj.sheet.address.sheetId} </TitleBar_Piece> <TitleBar_Punctuation>{':'} </TitleBar_Punctuation> diff --git a/theatre/studio/src/panels/ObjectEditorPanel/propEditors/CompoundPropEditor.tsx b/theatre/studio/src/panels/ObjectEditorPanel/propEditors/CompoundPropEditor.tsx index ad61959..cf08a88 100644 --- a/theatre/studio/src/panels/ObjectEditorPanel/propEditors/CompoundPropEditor.tsx +++ b/theatre/studio/src/panels/ObjectEditorPanel/propEditors/CompoundPropEditor.tsx @@ -1,7 +1,6 @@ import type {PropTypeConfig_Compound} from '@theatre/core/propTypes' import {isPropConfigComposite} from '@theatre/shared/propTypes/utils' import type SheetObject from '@theatre/core/sheetObjects/SheetObject' -import {theme} from '@theatre/studio/css' import {usePrism} from '@theatre/dataverse-react' import type {$IntentionalAny} from '@theatre/shared/utils/types' import {getPointerParts} from '@theatre/dataverse' @@ -25,10 +24,8 @@ const Container = styled.div` const Header = styled.div` height: 30px; - /* padding-left: calc(var(--left-pad) + var(--depth) * var(--step)); */ display: flex; align-items: stretch; - /* color: ${theme.panel.body.compoudThing.label.color}; */ position: relative; ${rowBg}; @@ -40,17 +37,6 @@ const Padding = styled.div` 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` margin-left: 4px; cursor: default;