Moved r3f's toolbar out of the snapshot editor
This commit is contained in:
parent
7f30f08bd0
commit
86547aa4cb
8 changed files with 19 additions and 20 deletions
170
packages/plugin-r3f/src/components/Toolbar/Toolbar.tsx
Normal file
170
packages/plugin-r3f/src/components/Toolbar/Toolbar.tsx
Normal file
|
@ -0,0 +1,170 @@
|
|||
import type {VFC} from 'react'
|
||||
import {useState} from 'react'
|
||||
import React from 'react'
|
||||
import TransformControlsModeSelect from './TransformControlsModeSelect'
|
||||
import {useEditorStore} from '../../store'
|
||||
import shallow from 'zustand/shallow'
|
||||
import TransformControlsSpaceSelect from './TransformControlsSpaceSelect'
|
||||
import ViewportShadingSelect from './ViewportShadingSelect'
|
||||
import {GiPocketBow, RiFocus3Line} from 'react-icons/all'
|
||||
import {Vector3} from 'three'
|
||||
import type {$FixMe} from '@theatre/shared/utils/types'
|
||||
import studio from '@theatre/studio'
|
||||
import {getSelected} from '../useSelected'
|
||||
import {useVal} from '@theatre/dataverse-react'
|
||||
import IconButton from './utils/IconButton'
|
||||
import {PortalContext} from 'reakit'
|
||||
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`
|
||||
pointer-events: auto;
|
||||
`
|
||||
|
||||
const Toolbar: VFC = () => {
|
||||
const [editorObject] = useEditorStore(
|
||||
(state) => [state.editorObject],
|
||||
shallow,
|
||||
)
|
||||
|
||||
const transformControlsMode =
|
||||
useVal(editorObject?.props.transformControls.mode) ?? 'translate'
|
||||
const transformControlsSpace =
|
||||
useVal(editorObject?.props.transformControls.space) ?? 'world'
|
||||
const viewportShading =
|
||||
useVal(editorObject?.props.viewport.shading) ?? 'rendered'
|
||||
|
||||
const [wrapper, setWrapper] = useState<null | HTMLDivElement>(null)
|
||||
|
||||
if (!editorObject) return <></>
|
||||
|
||||
return (
|
||||
<PortalContext.Provider value={wrapper}>
|
||||
<Container ref={setWrapper}>
|
||||
<TopRow>
|
||||
<Tools>
|
||||
<ToolGroup>
|
||||
<TransformControlsModeSelect
|
||||
value={transformControlsMode}
|
||||
onChange={(value) =>
|
||||
studio.transaction(({set}) =>
|
||||
set(editorObject!.props.transformControls.mode, value),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</ToolGroup>
|
||||
<ToolGroup>
|
||||
<TransformControlsSpaceSelect
|
||||
value={transformControlsSpace}
|
||||
onChange={(space) => {
|
||||
studio.transaction(({set}) => {
|
||||
set(editorObject.props.transformControls.space, space)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</ToolGroup>
|
||||
<ToolGroup>
|
||||
<ViewportShadingSelect
|
||||
value={viewportShading}
|
||||
onChange={(shading) => {
|
||||
studio.transaction(({set}) => {
|
||||
set(editorObject.props.viewport.shading, shading)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</ToolGroup>
|
||||
<ToolGroup>
|
||||
<IconButton
|
||||
label="Focus on selected"
|
||||
icon={<RiFocus3Line />}
|
||||
onClick={() => {
|
||||
const orbitControls =
|
||||
useEditorStore.getState().orbitControlsRef?.current
|
||||
const selected = getSelected()
|
||||
|
||||
let focusObject
|
||||
|
||||
if (selected) {
|
||||
focusObject =
|
||||
useEditorStore.getState().editablesSnapshot![selected]
|
||||
.proxyObject
|
||||
}
|
||||
|
||||
if (orbitControls && focusObject) {
|
||||
focusObject.getWorldPosition(
|
||||
// @ts-ignore TODO
|
||||
orbitControls.target as Vector3,
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ToolGroup>
|
||||
<ToolGroup>
|
||||
<IconButton
|
||||
label="Align object to view"
|
||||
icon={<GiPocketBow />}
|
||||
onClick={() => {
|
||||
const camera = (
|
||||
useEditorStore.getState().orbitControlsRef
|
||||
?.current as $FixMe
|
||||
)?.object
|
||||
|
||||
const selected = getSelected()
|
||||
|
||||
let proxyObject
|
||||
|
||||
if (selected) {
|
||||
proxyObject =
|
||||
useEditorStore.getState().editablesSnapshot![selected]
|
||||
.proxyObject
|
||||
|
||||
if (proxyObject && camera) {
|
||||
const direction = new Vector3()
|
||||
const position = camera.position.clone()
|
||||
|
||||
camera.getWorldDirection(direction)
|
||||
proxyObject.position.set(0, 0, 0)
|
||||
proxyObject.lookAt(direction)
|
||||
|
||||
proxyObject.parent!.worldToLocal(position)
|
||||
proxyObject.position.copy(position)
|
||||
|
||||
proxyObject.updateMatrix()
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ToolGroup>
|
||||
</Tools>
|
||||
</TopRow>
|
||||
</Container>
|
||||
</PortalContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default Toolbar
|
|
@ -0,0 +1,39 @@
|
|||
import type {VFC} from 'react'
|
||||
import React from 'react'
|
||||
import {GiClockwiseRotation, GiMove, GiResize} from 'react-icons/all'
|
||||
import type {TransformControlsMode} from '../../store'
|
||||
import CompactModeSelect from './utils/CompactModeSelect'
|
||||
|
||||
export interface TransformControlsModeSelectProps {
|
||||
value: TransformControlsMode
|
||||
onChange: (value: TransformControlsMode) => void
|
||||
}
|
||||
|
||||
const TransformControlsModeSelect: VFC<TransformControlsModeSelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => (
|
||||
<CompactModeSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={[
|
||||
{
|
||||
option: 'translate',
|
||||
label: 'Tool: Translate',
|
||||
icon: <GiMove />,
|
||||
},
|
||||
{
|
||||
option: 'rotate',
|
||||
label: 'Tool: Rotate',
|
||||
icon: <GiClockwiseRotation />,
|
||||
},
|
||||
{
|
||||
option: 'scale',
|
||||
label: 'Tool: Scale',
|
||||
icon: <GiResize />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
export default TransformControlsModeSelect
|
|
@ -0,0 +1,34 @@
|
|||
import type {VFC} from 'react'
|
||||
import React from 'react'
|
||||
import type {TransformControlsSpace} from '../../store'
|
||||
import {BiCube, BiGlobe} from 'react-icons/all'
|
||||
import CompactModeSelect from './utils/CompactModeSelect'
|
||||
|
||||
export interface TransformControlsSpaceSelectProps {
|
||||
value: TransformControlsSpace
|
||||
onChange: (value: TransformControlsSpace) => void
|
||||
}
|
||||
|
||||
const TransformControlsSpaceSelect: VFC<TransformControlsSpaceSelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => (
|
||||
<CompactModeSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={[
|
||||
{
|
||||
option: 'world',
|
||||
label: 'Space: World',
|
||||
icon: <BiGlobe />,
|
||||
},
|
||||
{
|
||||
option: 'local',
|
||||
label: 'Space: Local',
|
||||
icon: <BiCube />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
export default TransformControlsSpaceSelect
|
|
@ -0,0 +1,44 @@
|
|||
import type {VFC} from 'react'
|
||||
import React from 'react'
|
||||
import type {ViewportShading} from '../../store'
|
||||
import {FaCube, GiCube, GiIceCube, BiCube} from 'react-icons/all'
|
||||
import CompactModeSelect from './utils/CompactModeSelect'
|
||||
|
||||
export interface ViewportShadingSelectProps {
|
||||
value: ViewportShading
|
||||
onChange: (value: ViewportShading) => void
|
||||
}
|
||||
|
||||
const ViewportShadingSelect: VFC<ViewportShadingSelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => (
|
||||
<CompactModeSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={[
|
||||
{
|
||||
option: 'wireframe',
|
||||
label: 'Display: Wireframe',
|
||||
icon: <BiCube />,
|
||||
},
|
||||
{
|
||||
option: 'flat',
|
||||
label: 'Display: Flat',
|
||||
icon: <GiCube />,
|
||||
},
|
||||
{
|
||||
option: 'solid',
|
||||
label: 'Display: Solid',
|
||||
icon: <FaCube />,
|
||||
},
|
||||
{
|
||||
option: 'rendered',
|
||||
label: 'Display: Rendered',
|
||||
icon: <GiIceCube />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
export default ViewportShadingSelect
|
|
@ -0,0 +1,115 @@
|
|||
import type {ReactElement, ReactNode} from 'react'
|
||||
import React from 'react'
|
||||
import type {IconType} from 'react-icons'
|
||||
import {Group, Button} from 'reakit'
|
||||
import styled from 'styled-components'
|
||||
import {Tooltip, TooltipReference, useTooltipState} from './Tooltip'
|
||||
|
||||
interface OptionButtonProps<Option> {
|
||||
value: Option
|
||||
option: Option
|
||||
label: string
|
||||
icon: ReactElement<IconType>
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const _TooltipRef = styled(TooltipReference)<{selected: boolean}>`
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
width: auto;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 600;
|
||||
height: 1.75rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
border: 0 transparent;
|
||||
|
||||
&:first-child {
|
||||
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 {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
color: ${({selected}) => (selected ? 'white' : 'rgba(55, 65, 81, 1)')};
|
||||
background-color: ${({selected}) =>
|
||||
selected ? 'rgba(6, 95, 70, 1)' : 'rgba(243, 244, 246, 1);'};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({selected}) =>
|
||||
selected ? 'rgba(6, 78, 59, 1)' : 'rgba(229, 231, 235, 1);'};
|
||||
}
|
||||
`
|
||||
|
||||
function OptionButton<Option>({
|
||||
value,
|
||||
option,
|
||||
label,
|
||||
icon,
|
||||
onClick,
|
||||
}: OptionButtonProps<Option>) {
|
||||
const tooltip = useTooltipState()
|
||||
return (
|
||||
<>
|
||||
<_TooltipRef
|
||||
{...tooltip}
|
||||
forwardedAs={Button}
|
||||
selected={option === value}
|
||||
aria-label={label}
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon}
|
||||
</_TooltipRef>
|
||||
<Tooltip {...tooltip}>{label}</Tooltip>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface CompactModeSelectProps<Option> {
|
||||
value: Option
|
||||
onChange: (value: Option) => void
|
||||
options: {
|
||||
option: Option
|
||||
label: string
|
||||
icon: ReactElement<IconType>
|
||||
}[]
|
||||
settingsPanel?: ReactNode
|
||||
}
|
||||
|
||||
const Container = styled(Group)`
|
||||
display: flex;
|
||||
`
|
||||
|
||||
const CompactModeSelect = <Option extends string | number>({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
}: CompactModeSelectProps<Option>) => {
|
||||
return (
|
||||
<Container>
|
||||
{options.map(({label, icon, option}) => (
|
||||
<OptionButton
|
||||
key={option}
|
||||
value={value}
|
||||
option={option}
|
||||
label={label}
|
||||
icon={icon}
|
||||
onClick={() => onChange(option)}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default CompactModeSelect
|
|
@ -0,0 +1,70 @@
|
|||
import type {ReactElement} from 'react'
|
||||
import React, {forwardRef} from 'react'
|
||||
import type {ButtonProps} from 'reakit'
|
||||
import {Button} from 'reakit'
|
||||
import type {IconType} from 'react-icons'
|
||||
import {Tooltip, TooltipReference, useTooltipState} from './Tooltip'
|
||||
import styled from 'styled-components'
|
||||
|
||||
export interface IconButtonProps extends Exclude<ButtonProps, 'children'> {
|
||||
icon: ReactElement<IconType>
|
||||
label: string
|
||||
}
|
||||
|
||||
const _TooltipRef = styled(TooltipReference)`
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
width: auto;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 600;
|
||||
height: 1.75rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
|
||||
&:first-child {
|
||||
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 {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
color: rgba(55, 65, 81, 1);
|
||||
background-color: rgba(243, 244, 246, 1);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(229, 231, 235, 1);
|
||||
}
|
||||
|
||||
border: 0 transparent;
|
||||
`
|
||||
const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
({label, icon, className, ...props}, ref) => {
|
||||
const tooltip = useTooltipState()
|
||||
return (
|
||||
<>
|
||||
<_TooltipRef
|
||||
{...props}
|
||||
{...tooltip}
|
||||
forwardedAs={Button}
|
||||
aria-label={label}
|
||||
>
|
||||
{icon}
|
||||
</_TooltipRef>
|
||||
<Tooltip {...tooltip}>{label}</Tooltip>
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default IconButton
|
26
packages/plugin-r3f/src/components/Toolbar/utils/Tooltip.tsx
Normal file
26
packages/plugin-r3f/src/components/Toolbar/utils/Tooltip.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import type {VFC} from 'react'
|
||||
import React from 'react'
|
||||
import {Tooltip as TooltipImpl, TooltipReference, useTooltipState} from 'reakit'
|
||||
|
||||
import type {TooltipProps} from 'reakit'
|
||||
import styled from 'styled-components'
|
||||
|
||||
export {TooltipReference, useTooltipState}
|
||||
|
||||
const Container = styled(TooltipImpl)`
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
border-radius: 0.125rem;
|
||||
background-color: rgba(55, 65, 81, 1);
|
||||
color: white;
|
||||
pointer-events: none;
|
||||
`
|
||||
|
||||
export const Tooltip: VFC<TooltipProps> = ({className, ...props}) => (
|
||||
<Container {...props} className={className as string} />
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue