Implement studio.globalToolbar
This commit is contained in:
parent
86547aa4cb
commit
921bc44270
18 changed files with 433 additions and 924 deletions
|
@ -43,6 +43,7 @@
|
||||||
"@theatre/core": "workspace:*",
|
"@theatre/core": "workspace:*",
|
||||||
"@theatre/studio": "workspace:*",
|
"@theatre/studio": "workspace:*",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"polished": "^4.1.3",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-icons": "^4.2.0",
|
"react-icons": "^4.2.0",
|
||||||
|
|
|
@ -6,11 +6,10 @@ import {useEditorStore} from '../store'
|
||||||
import {OrbitControls} from '@react-three/drei'
|
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 Toolbar from './Toolbar/Toolbar'
|
|
||||||
import ProxyManager from './ProxyManager'
|
import ProxyManager from './ProxyManager'
|
||||||
import studio from '@theatre/studio'
|
import studio from '@theatre/studio'
|
||||||
import {useVal} from '@theatre/dataverse-react'
|
import {useVal} from '@theatre/dataverse-react'
|
||||||
import styled, {createGlobalStyle} from 'styled-components'
|
import styled, {createGlobalStyle, StyleSheetManager} from 'styled-components'
|
||||||
|
|
||||||
const GlobalStyle = createGlobalStyle`
|
const GlobalStyle = createGlobalStyle`
|
||||||
:host {
|
:host {
|
||||||
|
@ -52,12 +51,13 @@ const EditorScene = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showGrid && <gridHelper args={[1000, 1000, 0x444444, 0x888888]} />}
|
{showGrid && <gridHelper args={[20, 20, '#6e6e6e', '#4a4b4b']} />}
|
||||||
{showAxes && <axesHelper args={[500]} />}
|
{showAxes && <axesHelper args={[500]} />}
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<OrbitControls ref={orbitControlsRef} enableDamping={false} />
|
<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]} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -106,29 +106,36 @@ const Editor: VFC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<root.div>
|
<root.div>
|
||||||
<GlobalStyle />
|
<StyleSheetManager disableVendorPrefixes>
|
||||||
<Wrapper id="theatre-plugin-r3f-root" visible={true}>
|
<>
|
||||||
<Toolbar />
|
<GlobalStyle />
|
||||||
{sceneSnapshot ? (
|
<Wrapper id="theatre-plugin-r3f-root" visible={true}>
|
||||||
<>
|
{/* <Toolbar /> */}
|
||||||
<CanvasWrapper>
|
{sceneSnapshot ? (
|
||||||
<Canvas
|
<>
|
||||||
// @ts-ignore
|
<CanvasWrapper>
|
||||||
colorManagement
|
<Canvas
|
||||||
camera={initialEditorCamera}
|
// @ts-ignore
|
||||||
onCreated={({gl}) => {
|
colorManagement
|
||||||
gl.setClearColor('white')
|
camera={initialEditorCamera}
|
||||||
}}
|
onCreated={({gl}) => {
|
||||||
shadowMap
|
gl.setClearColor('white')
|
||||||
pixelRatio={window.devicePixelRatio}
|
}}
|
||||||
onPointerMissed={() => studio.__experimental_setSelection([])}
|
shadowMap
|
||||||
>
|
dpr={[1, 2]}
|
||||||
<EditorScene />
|
fog={'red'}
|
||||||
</Canvas>
|
onPointerMissed={() =>
|
||||||
</CanvasWrapper>
|
studio.__experimental_setSelection([])
|
||||||
</>
|
}
|
||||||
) : null}
|
>
|
||||||
</Wrapper>
|
<EditorScene />
|
||||||
|
</Canvas>
|
||||||
|
</CanvasWrapper>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</Wrapper>
|
||||||
|
</>
|
||||||
|
</StyleSheetManager>
|
||||||
</root.div>
|
</root.div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import type {VFC} from 'react'
|
import type {VFC} from 'react'
|
||||||
import {useState} from 'react'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import TransformControlsModeSelect from './TransformControlsModeSelect'
|
import TransformControlsModeSelect from './TransformControlsModeSelect'
|
||||||
import {useEditorStore} from '../../store'
|
import {useEditorStore} from '../../store'
|
||||||
|
@ -13,35 +12,8 @@ import studio from '@theatre/studio'
|
||||||
import {getSelected} from '../useSelected'
|
import {getSelected} from '../useSelected'
|
||||||
import {useVal} from '@theatre/dataverse-react'
|
import {useVal} from '@theatre/dataverse-react'
|
||||||
import IconButton from './utils/IconButton'
|
import IconButton from './utils/IconButton'
|
||||||
import {PortalContext} from 'reakit'
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
z-index: 50;
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
right: 0px;
|
|
||||||
bottom: 0px;
|
|
||||||
left: 0px;
|
|
||||||
pointer-events: none;
|
|
||||||
`
|
|
||||||
|
|
||||||
const TopRow = styled.div`
|
|
||||||
position: relative;
|
|
||||||
margin: 1.25rem;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex: 1 1 0%;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
`
|
|
||||||
|
|
||||||
const Tools = styled.div`
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ToolGroup = styled.div`
|
const ToolGroup = styled.div`
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
`
|
`
|
||||||
|
@ -59,111 +31,102 @@ const Toolbar: VFC = () => {
|
||||||
const viewportShading =
|
const viewportShading =
|
||||||
useVal(editorObject?.props.viewport.shading) ?? 'rendered'
|
useVal(editorObject?.props.viewport.shading) ?? 'rendered'
|
||||||
|
|
||||||
const [wrapper, setWrapper] = useState<null | HTMLDivElement>(null)
|
|
||||||
|
|
||||||
if (!editorObject) return <></>
|
if (!editorObject) return <></>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PortalContext.Provider value={wrapper}>
|
<>
|
||||||
<Container ref={setWrapper}>
|
<ToolGroup>
|
||||||
<TopRow>
|
<TransformControlsModeSelect
|
||||||
<Tools>
|
value={transformControlsMode}
|
||||||
<ToolGroup>
|
onChange={(value) =>
|
||||||
<TransformControlsModeSelect
|
studio.transaction(({set}) =>
|
||||||
value={transformControlsMode}
|
set(editorObject!.props.transformControls.mode, value),
|
||||||
onChange={(value) =>
|
)
|
||||||
studio.transaction(({set}) =>
|
}
|
||||||
set(editorObject!.props.transformControls.mode, value),
|
/>
|
||||||
)
|
</ToolGroup>
|
||||||
}
|
<ToolGroup>
|
||||||
/>
|
<TransformControlsSpaceSelect
|
||||||
</ToolGroup>
|
value={transformControlsSpace}
|
||||||
<ToolGroup>
|
onChange={(space) => {
|
||||||
<TransformControlsSpaceSelect
|
studio.transaction(({set}) => {
|
||||||
value={transformControlsSpace}
|
set(editorObject.props.transformControls.space, space)
|
||||||
onChange={(space) => {
|
})
|
||||||
studio.transaction(({set}) => {
|
}}
|
||||||
set(editorObject.props.transformControls.space, space)
|
/>
|
||||||
})
|
</ToolGroup>
|
||||||
}}
|
<ToolGroup>
|
||||||
/>
|
<ViewportShadingSelect
|
||||||
</ToolGroup>
|
value={viewportShading}
|
||||||
<ToolGroup>
|
onChange={(shading) => {
|
||||||
<ViewportShadingSelect
|
studio.transaction(({set}) => {
|
||||||
value={viewportShading}
|
set(editorObject.props.viewport.shading, shading)
|
||||||
onChange={(shading) => {
|
})
|
||||||
studio.transaction(({set}) => {
|
}}
|
||||||
set(editorObject.props.viewport.shading, shading)
|
/>
|
||||||
})
|
</ToolGroup>
|
||||||
}}
|
<ToolGroup>
|
||||||
/>
|
<IconButton
|
||||||
</ToolGroup>
|
label="Focus on selected"
|
||||||
<ToolGroup>
|
icon={<RiFocus3Line />}
|
||||||
<IconButton
|
onClick={() => {
|
||||||
label="Focus on selected"
|
const orbitControls =
|
||||||
icon={<RiFocus3Line />}
|
useEditorStore.getState().orbitControlsRef?.current
|
||||||
onClick={() => {
|
const selected = getSelected()
|
||||||
const orbitControls =
|
|
||||||
useEditorStore.getState().orbitControlsRef?.current
|
|
||||||
const selected = getSelected()
|
|
||||||
|
|
||||||
let focusObject
|
let focusObject
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
focusObject =
|
focusObject =
|
||||||
useEditorStore.getState().editablesSnapshot![selected]
|
useEditorStore.getState().editablesSnapshot![selected]
|
||||||
.proxyObject
|
.proxyObject
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orbitControls && focusObject) {
|
if (orbitControls && focusObject) {
|
||||||
focusObject.getWorldPosition(
|
focusObject.getWorldPosition(
|
||||||
// @ts-ignore TODO
|
// @ts-ignore TODO
|
||||||
orbitControls.target as Vector3,
|
orbitControls.target as Vector3,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ToolGroup>
|
</ToolGroup>
|
||||||
<ToolGroup>
|
<ToolGroup>
|
||||||
<IconButton
|
<IconButton
|
||||||
label="Align object to view"
|
label="Align object to view"
|
||||||
icon={<GiPocketBow />}
|
icon={<GiPocketBow />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const camera = (
|
const camera = (
|
||||||
useEditorStore.getState().orbitControlsRef
|
useEditorStore.getState().orbitControlsRef?.current as $FixMe
|
||||||
?.current as $FixMe
|
)?.object
|
||||||
)?.object
|
|
||||||
|
|
||||||
const selected = getSelected()
|
const selected = getSelected()
|
||||||
|
|
||||||
let proxyObject
|
let proxyObject
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
proxyObject =
|
proxyObject =
|
||||||
useEditorStore.getState().editablesSnapshot![selected]
|
useEditorStore.getState().editablesSnapshot![selected]
|
||||||
.proxyObject
|
.proxyObject
|
||||||
|
|
||||||
if (proxyObject && camera) {
|
if (proxyObject && camera) {
|
||||||
const direction = new Vector3()
|
const direction = new Vector3()
|
||||||
const position = camera.position.clone()
|
const position = camera.position.clone()
|
||||||
|
|
||||||
camera.getWorldDirection(direction)
|
camera.getWorldDirection(direction)
|
||||||
proxyObject.position.set(0, 0, 0)
|
proxyObject.position.set(0, 0, 0)
|
||||||
proxyObject.lookAt(direction)
|
proxyObject.lookAt(direction)
|
||||||
|
|
||||||
proxyObject.parent!.worldToLocal(position)
|
proxyObject.parent!.worldToLocal(position)
|
||||||
proxyObject.position.copy(position)
|
proxyObject.position.copy(position)
|
||||||
|
|
||||||
proxyObject.updateMatrix()
|
proxyObject.updateMatrix()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ToolGroup>
|
</ToolGroup>
|
||||||
</Tools>
|
</>
|
||||||
</TopRow>
|
|
||||||
</Container>
|
|
||||||
</PortalContext.Provider>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,42 +13,38 @@ interface OptionButtonProps<Option> {
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const _TooltipRef = styled(TooltipReference)<{selected: boolean}>`
|
const TheButton = styled(TooltipReference)<{selected: boolean}>`
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
width: auto;
|
font-size: 11px;
|
||||||
font-size: 0.875rem;
|
line-height: 1.25em;
|
||||||
line-height: 1.25rem;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
height: 1.75rem;
|
height: 24px;
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5em;
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5em;
|
||||||
border: 0 transparent;
|
border: 1px solid #22222238;
|
||||||
|
|
||||||
&:first-child {
|
border-radius: 2px;
|
||||||
border-top-left-radius: 0.25rem;
|
|
||||||
border-bottom-left-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-top-right-radius: 0.25rem;
|
|
||||||
border-bottom-right-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
color: ${({selected}) => (selected ? 'white' : 'rgba(55, 65, 81, 1)')};
|
color: #e6e6e5;
|
||||||
background-color: ${({selected}) =>
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
selected ? 'rgba(6, 95, 70, 1)' : 'rgba(243, 244, 246, 1);'};
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: ${({selected}) =>
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
selected ? 'rgba(6, 78, 59, 1)' : 'rgba(229, 231, 235, 1);'};
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected,
|
||||||
|
&.selected:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -62,15 +58,16 @@ function OptionButton<Option>({
|
||||||
const tooltip = useTooltipState()
|
const tooltip = useTooltipState()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<_TooltipRef
|
<TheButton
|
||||||
{...tooltip}
|
{...tooltip}
|
||||||
forwardedAs={Button}
|
forwardedAs={Button}
|
||||||
selected={option === value}
|
selected={option === value}
|
||||||
|
className={option === value ? 'selected' : undefined}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</_TooltipRef>
|
</TheButton>
|
||||||
<Tooltip {...tooltip}>{label}</Tooltip>
|
<Tooltip {...tooltip}>{label}</Tooltip>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -89,6 +86,7 @@ interface CompactModeSelectProps<Option> {
|
||||||
|
|
||||||
const Container = styled(Group)`
|
const Container = styled(Group)`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const CompactModeSelect = <Option extends string | number>({
|
const CompactModeSelect = <Option extends string | number>({
|
||||||
|
|
|
@ -5,45 +5,48 @@ import {Button} from 'reakit'
|
||||||
import type {IconType} from 'react-icons'
|
import type {IconType} from 'react-icons'
|
||||||
import {Tooltip, TooltipReference, useTooltipState} from './Tooltip'
|
import {Tooltip, TooltipReference, useTooltipState} from './Tooltip'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
import {transparentize} from 'polished'
|
||||||
|
|
||||||
export interface IconButtonProps extends Exclude<ButtonProps, 'children'> {
|
export interface IconButtonProps extends Exclude<ButtonProps, 'children'> {
|
||||||
icon: ReactElement<IconType>
|
icon: ReactElement<IconType>
|
||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const _TooltipRef = styled(TooltipReference)`
|
const TheButton = styled(TooltipReference)`
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
width: auto;
|
font-size: 11px;
|
||||||
font-size: 0.875rem;
|
line-height: 1.25em;
|
||||||
line-height: 1.25rem;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
height: 1.75rem;
|
height: 24px;
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5em;
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5em;
|
||||||
|
color: #e6e6e5;
|
||||||
|
background-color: #313131ba;
|
||||||
|
border: 0 transparent;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
border-top-left-radius: 0.25rem;
|
border-top-left-radius: 3px;
|
||||||
border-bottom-left-radius: 0.25rem;
|
border-bottom-left-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-top-right-radius: 0.25rem;
|
border-top-right-radius: 3px;
|
||||||
border-bottom-right-radius: 0.25rem;
|
border-bottom-right-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
color: rgba(55, 65, 81, 1);
|
color: #e6e6e5;
|
||||||
background-color: rgba(243, 244, 246, 1);
|
background-color: #313131;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(229, 231, 235, 1);
|
background-color: ${transparentize(0.5, '#313131')};
|
||||||
}
|
}
|
||||||
|
|
||||||
border: 0 transparent;
|
border: 0 transparent;
|
||||||
|
@ -53,14 +56,14 @@ const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||||
const tooltip = useTooltipState()
|
const tooltip = useTooltipState()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<_TooltipRef
|
<TheButton
|
||||||
{...props}
|
{...props}
|
||||||
{...tooltip}
|
{...tooltip}
|
||||||
forwardedAs={Button}
|
forwardedAs={Button}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</_TooltipRef>
|
</TheButton>
|
||||||
<Tooltip {...tooltip}>{label}</Tooltip>
|
<Tooltip {...tooltip}>{label}</Tooltip>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
import type {VFC} from 'react'
|
import type {VFC} from 'react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Tooltip as TooltipImpl, TooltipReference, useTooltipState} from 'reakit'
|
import {Tooltip as TooltipImpl, TooltipReference, useTooltipState} from 'reakit'
|
||||||
|
import {transparentize} from 'polished'
|
||||||
import type {TooltipProps} from 'reakit'
|
import type {TooltipProps} from 'reakit'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
export {TooltipReference, useTooltipState}
|
export {TooltipReference, useTooltipState}
|
||||||
|
|
||||||
const Container = styled(TooltipImpl)`
|
const Container = styled(TooltipImpl)`
|
||||||
padding-left: 0.5rem;
|
padding: 3px 5px;
|
||||||
padding-right: 0.5rem;
|
|
||||||
padding-top: 0.25rem;
|
|
||||||
padding-bottom: 0.25rem;
|
|
||||||
|
|
||||||
font-size: 0.875rem;
|
font-size: 11px;
|
||||||
line-height: 1.25rem;
|
line-height: 1.25em;
|
||||||
border-radius: 0.125rem;
|
border-radius: 2px;
|
||||||
background-color: rgba(55, 65, 81, 1);
|
background-color: ${transparentize(0.5, '#313131')};
|
||||||
color: white;
|
color: white;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
font-weight: 500;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const Tooltip: VFC<TooltipProps> = ({className, ...props}) => (
|
export const Tooltip: VFC<TooltipProps> = ({className, ...props}) => (
|
||||||
|
|
|
@ -8,8 +8,16 @@ export type {EditorHelperProps} from './components/EditorHelper'
|
||||||
export {default as editable} from './components/editable'
|
export {default as editable} from './components/editable'
|
||||||
export {bindToCanvas} from './store'
|
export {bindToCanvas} from './store'
|
||||||
export type {EditableState, BindFunction} from './store'
|
export type {EditableState, BindFunction} from './store'
|
||||||
|
import studio from '@theatre/studio'
|
||||||
|
import Toolbar from './components/Toolbar/Toolbar'
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
studio.extend({
|
||||||
|
id: '@theatre/plugin-r3f',
|
||||||
|
globalToolbar: {
|
||||||
|
component: Toolbar,
|
||||||
|
},
|
||||||
|
})
|
||||||
const editorRoot = document.createElement('div')
|
const editorRoot = document.createElement('div')
|
||||||
document.body.appendChild(editorRoot)
|
document.body.appendChild(editorRoot)
|
||||||
|
|
||||||
|
|
|
@ -306,6 +306,23 @@ 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'},
|
||||||
),
|
),
|
||||||
|
|
|
@ -124,6 +124,25 @@ export const compound = <Props extends IValidCompoundProps>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PropTypeConfig_CSSRGBA
|
||||||
|
extends IBasePropType<{r: number; g: number; b: number; a: number}> {
|
||||||
|
type: 'cssrgba'
|
||||||
|
default: {r: number; g: number; b: number; a: number}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rgba = (
|
||||||
|
defaultValue: {r: number; b: number; g: number; a: number},
|
||||||
|
extras?: PropTypeConfigExtras,
|
||||||
|
): PropTypeConfig_CSSRGBA => {
|
||||||
|
return {
|
||||||
|
type: 'cssrgba',
|
||||||
|
valueType: null as $IntentionalAny,
|
||||||
|
[s]: 'TheatrePropType',
|
||||||
|
label: extras?.label,
|
||||||
|
default: defaultValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface PropTypeConfig_Enum extends IBasePropType<{}> {
|
export interface PropTypeConfig_Enum extends IBasePropType<{}> {
|
||||||
type: 'enum'
|
type: 'enum'
|
||||||
cases: Record<string, PropTypeConfig>
|
cases: Record<string, PropTypeConfig>
|
||||||
|
@ -135,6 +154,7 @@ export type PropTypeConfig_AllPrimitives =
|
||||||
| PropTypeConfig_Boolean
|
| PropTypeConfig_Boolean
|
||||||
| PropTypeConfig_String
|
| PropTypeConfig_String
|
||||||
| PropTypeConfig_StringLiteral<$IntentionalAny>
|
| PropTypeConfig_StringLiteral<$IntentionalAny>
|
||||||
|
| PropTypeConfig_CSSRGBA
|
||||||
|
|
||||||
export type PropTypeConfig =
|
export type PropTypeConfig =
|
||||||
| PropTypeConfig_AllPrimitives
|
| PropTypeConfig_AllPrimitives
|
||||||
|
|
|
@ -63,7 +63,6 @@
|
||||||
"json-touch-patch": "^0.11.2",
|
"json-touch-patch": "^0.11.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"microbundle": "^0.13.0",
|
|
||||||
"nanoid": "^3.1.23",
|
"nanoid": "^3.1.23",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"null-loader": "^4.0.1",
|
"null-loader": "^4.0.1",
|
||||||
|
@ -77,6 +76,7 @@
|
||||||
"react-shadow": "^19.0.2",
|
"react-shadow": "^19.0.2",
|
||||||
"react-use": "^17.2.4",
|
"react-use": "^17.2.4",
|
||||||
"react-use-gesture": "^9.1.3",
|
"react-use-gesture": "^9.1.3",
|
||||||
|
"reakit": "^1.3.8",
|
||||||
"redux": "^3.7.2",
|
"redux": "^3.7.2",
|
||||||
"redux-actions": "^2.6.5",
|
"redux-actions": "^2.6.5",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
|
|
@ -9,7 +9,7 @@ import type {
|
||||||
ITransactionPrivateApi,
|
ITransactionPrivateApi,
|
||||||
} from './StudioStore/StudioStore'
|
} from './StudioStore/StudioStore'
|
||||||
import StudioStore from './StudioStore/StudioStore'
|
import StudioStore from './StudioStore/StudioStore'
|
||||||
import type {IStudio} from './TheatreStudio'
|
import type {IExtension, IStudio} from './TheatreStudio'
|
||||||
import TheatreStudio from './TheatreStudio'
|
import TheatreStudio from './TheatreStudio'
|
||||||
import {nanoid} from 'nanoid/non-secure'
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
import type Project from '@theatre/core/projects/Project'
|
import type Project from '@theatre/core/projects/Project'
|
||||||
|
@ -30,6 +30,11 @@ export class Studio {
|
||||||
private readonly _store = new StudioStore()
|
private readonly _store = new StudioStore()
|
||||||
private _corePrivateApi: typeof privateAPI | undefined
|
private _corePrivateApi: typeof privateAPI | undefined
|
||||||
|
|
||||||
|
private _extensions: Atom<{byId: Record<string, IExtension>}> = new Atom({
|
||||||
|
byId: {},
|
||||||
|
})
|
||||||
|
readonly extensionsP = this._extensions.pointer.byId
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.address = {studioId: nanoid(10)}
|
this.address = {studioId: nanoid(10)}
|
||||||
this.publicApi = new TheatreStudio(this)
|
this.publicApi = new TheatreStudio(this)
|
||||||
|
@ -90,4 +95,22 @@ export class Studio {
|
||||||
get corePrivateAPI() {
|
get corePrivateAPI() {
|
||||||
return this._corePrivateApi
|
return this._corePrivateApi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extend(extension: IExtension) {
|
||||||
|
if (!extension || typeof extension !== 'object') {
|
||||||
|
throw new Error(`Extensions must be JS objects`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof extension.id !== 'string') {
|
||||||
|
throw new Error(`extension.id must be a string`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._extensions.getState().byId[extension.id]) {
|
||||||
|
throw new Error(
|
||||||
|
`An extension with the id of ${extension.id} already exists`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this._extensions.setIn(['byId', extension.id], extension)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {VoidFn} from '@theatre/shared/utils/types'
|
import type {$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'
|
||||||
|
@ -11,12 +11,39 @@ import {isSheetObjectPublicAPI} from '@theatre/shared/instanceTypes'
|
||||||
import {getOutlineSelection} from './selectors'
|
import {getOutlineSelection} from './selectors'
|
||||||
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
||||||
import getStudio from './getStudio'
|
import getStudio from './getStudio'
|
||||||
|
import type React from 'react'
|
||||||
|
import type {
|
||||||
|
PropTypeConfig_Boolean,
|
||||||
|
PropTypeConfig_Compound,
|
||||||
|
} from '@theatre/core/propTypes'
|
||||||
|
|
||||||
export interface ITransactionAPI {
|
export interface ITransactionAPI {
|
||||||
set<V>(pointer: Pointer<V>, value: V): void
|
set<V>(pointer: Pointer<V>, value: V): void
|
||||||
unset<V>(pointer: Pointer<V>): void
|
unset<V>(pointer: Pointer<V>): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IPanelType<DataType extends PropTypeConfig_Compound<{}>> {
|
||||||
|
sheetName: string
|
||||||
|
dataType: DataType
|
||||||
|
component: React.ComponentType<{
|
||||||
|
id: string
|
||||||
|
object: ISheetObject<
|
||||||
|
PropTypeConfig_Compound<{
|
||||||
|
visible: PropTypeConfig_Boolean
|
||||||
|
data: DataType
|
||||||
|
}>
|
||||||
|
>
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IExtension = {
|
||||||
|
id: string
|
||||||
|
globalToolbar?: {
|
||||||
|
component: React.ComponentType<{}>
|
||||||
|
}
|
||||||
|
panes?: Record<string, IPanelType<$IntentionalAny>>
|
||||||
|
}
|
||||||
|
|
||||||
export interface IStudio {
|
export interface IStudio {
|
||||||
readonly ui: {
|
readonly ui: {
|
||||||
show(): void
|
show(): void
|
||||||
|
@ -34,6 +61,8 @@ export interface IStudio {
|
||||||
): VoidFunction
|
): VoidFunction
|
||||||
|
|
||||||
readonly selection: Array<ISheetObject>
|
readonly selection: Array<ISheetObject>
|
||||||
|
|
||||||
|
extend(extension: IExtension): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class TheatreStudio implements IStudio {
|
export default class TheatreStudio implements IStudio {
|
||||||
|
@ -62,6 +91,10 @@ export default class TheatreStudio implements IStudio {
|
||||||
*/
|
*/
|
||||||
constructor(internals: Studio) {}
|
constructor(internals: Studio) {}
|
||||||
|
|
||||||
|
extend(extension: IExtension): void {
|
||||||
|
getStudio().extend(extension)
|
||||||
|
}
|
||||||
|
|
||||||
transaction(fn: (api: ITransactionAPI) => void): void {
|
transaction(fn: (api: ITransactionAPI) => void): void {
|
||||||
return getStudio().transaction(({set, unset}) => {
|
return getStudio().transaction(({set, unset}) => {
|
||||||
return fn({set, unset})
|
return fn({set, unset})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import UIRootWrapper from '@theatre/studio/UIRoot/UIRootWrapper'
|
import UIRoot from '@theatre/studio/UIRoot/UIRoot'
|
||||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
|
@ -54,10 +54,7 @@ export default class UI {
|
||||||
this._renderTimeout = undefined
|
this._renderTimeout = undefined
|
||||||
this._documentBodyUIIsRenderedIn = document.body
|
this._documentBodyUIIsRenderedIn = document.body
|
||||||
this._documentBodyUIIsRenderedIn.appendChild(this.containerEl)
|
this._documentBodyUIIsRenderedIn.appendChild(this.containerEl)
|
||||||
ReactDOM.render(
|
ReactDOM.render(React.createElement(UIRoot), this.containerShadow)
|
||||||
React.createElement(UIRootWrapper, {studio: this.studio}),
|
|
||||||
this.containerShadow,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
this._renderTimeout = setTimeout(renderCallback, 10)
|
this._renderTimeout = setTimeout(renderCallback, 10)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
import type {Studio} from '@theatre/studio/Studio'
|
|
||||||
import {usePrism} from '@theatre/dataverse-react'
|
import {usePrism} from '@theatre/dataverse-react'
|
||||||
import {val} from '@theatre/dataverse'
|
import {val} from '@theatre/dataverse'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {createGlobalStyle, StyleSheetManager} from 'styled-components'
|
import styled, {createGlobalStyle, StyleSheetManager} from 'styled-components'
|
||||||
import EnsureProjectsDontHaveErrors from './EnsureProjectsDontHaveErrors'
|
|
||||||
import PanelsRoot from './PanelsRoot'
|
import PanelsRoot from './PanelsRoot'
|
||||||
import ProvideTheme from './ProvideTheme'
|
import ProvideTheme from './ProvideTheme'
|
||||||
import TheTrigger from './TheTrigger'
|
import TheTrigger from './TheTrigger'
|
||||||
|
import GlobalToolbar from '@theatre/studio/toolbars/GlobalToolbar/GlobalToolbar'
|
||||||
|
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
|
import {PortalContext} from 'reakit'
|
||||||
|
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||||
|
|
||||||
const GlobalStyle = createGlobalStyle`
|
const GlobalStyle = createGlobalStyle`
|
||||||
:host {
|
:host {
|
||||||
|
@ -28,13 +30,28 @@ const GlobalStyle = createGlobalStyle`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export default function UIRoot({studio}: {studio: Studio}) {
|
const Container = styled.div`
|
||||||
|
z-index: 50;
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
pointer-events: none;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function UIRoot() {
|
||||||
|
const studio = getStudio()
|
||||||
|
const [containerRef, container] = useRefAndState<HTMLDivElement>(
|
||||||
|
undefined as $IntentionalAny,
|
||||||
|
)
|
||||||
const inside = usePrism(() => {
|
const inside = usePrism(() => {
|
||||||
const visiblityState = val(studio.atomP.ahistoric.visibilityState)
|
const visiblityState = val(studio.atomP.ahistoric.visibilityState)
|
||||||
const initialised = val(studio.atomP.ephemeral.initialised)
|
const initialised = val(studio.atomP.ephemeral.initialised)
|
||||||
|
|
||||||
const shouldShowTrigger = visiblityState === 'onlyTriggerIsVisible'
|
const shouldShowTrigger = visiblityState === 'onlyTriggerIsVisible'
|
||||||
const shouldShowPanels = visiblityState === 'everythingIsVisible'
|
const shouldShowPanels = visiblityState === 'everythingIsVisible'
|
||||||
|
const shouldShowGlobalToolbar = visiblityState !== 'everythingIsHidden'
|
||||||
|
|
||||||
return !initialised ? null : (
|
return !initialised ? null : (
|
||||||
<StyleSheetManager
|
<StyleSheetManager
|
||||||
|
@ -44,13 +61,18 @@ export default function UIRoot({studio}: {studio: Studio}) {
|
||||||
<>
|
<>
|
||||||
<GlobalStyle />
|
<GlobalStyle />
|
||||||
<ProvideTheme>
|
<ProvideTheme>
|
||||||
{shouldShowTrigger && <TheTrigger />}
|
<PortalContext.Provider value={container}>
|
||||||
{shouldShowPanels && <PanelsRoot />}
|
<Container ref={containerRef}>
|
||||||
|
{shouldShowGlobalToolbar && <GlobalToolbar />}
|
||||||
|
{shouldShowTrigger && <TheTrigger />}
|
||||||
|
{shouldShowPanels && <PanelsRoot />}
|
||||||
|
</Container>
|
||||||
|
</PortalContext.Provider>
|
||||||
</ProvideTheme>
|
</ProvideTheme>
|
||||||
</>
|
</>
|
||||||
</StyleSheetManager>
|
</StyleSheetManager>
|
||||||
)
|
)
|
||||||
}, [studio])
|
}, [studio, containerRef, container])
|
||||||
|
|
||||||
return <EnsureProjectsDontHaveErrors>{inside}</EnsureProjectsDontHaveErrors>
|
return inside
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
import type {Studio} from '@theatre/studio/Studio'
|
|
||||||
import React from 'react'
|
|
||||||
import UIRoot from './UIRoot'
|
|
||||||
|
|
||||||
export default class UIRootWrapper extends React.Component<{
|
|
||||||
studio: Studio
|
|
||||||
}> {
|
|
||||||
state = {UIRoot}
|
|
||||||
componentDidMount() {
|
|
||||||
const self = this
|
|
||||||
if (
|
|
||||||
process.env.NODE_ENV !== 'production' &&
|
|
||||||
typeof module === 'object' &&
|
|
||||||
module &&
|
|
||||||
module.hot
|
|
||||||
) {
|
|
||||||
module.hot.accept('./UIRoot', () => {
|
|
||||||
const UIRoot = require('./UIRoot').default
|
|
||||||
self.setState({UIRoot})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
const UIRoot = this.state.UIRoot
|
|
||||||
const rootEl = <UIRoot studio={this.props.studio} />
|
|
||||||
return rootEl
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -68,6 +68,7 @@ const propEditorByPropType: {
|
||||||
enum: () => <></>,
|
enum: () => <></>,
|
||||||
boolean: BooleanPropEditor,
|
boolean: BooleanPropEditor,
|
||||||
stringLiteral: StringLiteralPropEditor,
|
stringLiteral: StringLiteralPropEditor,
|
||||||
|
cssrgba: () => <></>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeterminePropEditor: React.FC<{
|
const DeterminePropEditor: React.FC<{
|
||||||
|
|
35
theatre/studio/src/toolbars/GlobalToolbar/GlobalToolbar.tsx
Normal file
35
theatre/studio/src/toolbars/GlobalToolbar/GlobalToolbar.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import {useVal} from '@theatre/dataverse-react'
|
||||||
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
position: fixed;
|
||||||
|
z-index: 50;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
left: 12px;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const GlobalToolbar: React.FC<{}> = (props) => {
|
||||||
|
const groups: Array<React.ReactNode> = []
|
||||||
|
const extensions = useVal(getStudio().extensionsP)
|
||||||
|
|
||||||
|
for (const [, extension] of Object.entries(extensions)) {
|
||||||
|
if (extension.globalToolbar) {
|
||||||
|
groups.push(
|
||||||
|
<extension.globalToolbar.component
|
||||||
|
key={'extensionToolbar-' + extension.id}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Container>{groups}</Container>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GlobalToolbar
|
Loading…
Reference in a new issue