Create reference window for r3f (#169)
* Add reference window (and fix tooltips) * Replace image with data url so the build doesn't fail
This commit is contained in:
parent
8520c74116
commit
95d4e1d315
7 changed files with 247 additions and 40 deletions
|
@ -53,7 +53,13 @@ function App() {
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Canvas dpr={[1.5, 2]} linear shadows frameloop="demand">
|
<Canvas
|
||||||
|
dpr={[1.5, 2]}
|
||||||
|
linear
|
||||||
|
shadows
|
||||||
|
gl={{preserveDrawingBuffer: true}}
|
||||||
|
frameloop="demand"
|
||||||
|
>
|
||||||
<SheetProvider sheet={getProject('Space').sheet('Scene')}>
|
<SheetProvider sheet={getProject('Space').sheet('Scene')}>
|
||||||
<fog attach="fog" args={[bg, 16, 30]} />
|
<fog attach="fog" args={[bg, 16, 30]} />
|
||||||
<color attach="background" args={[bg]} />
|
<color attach="background" args={[bg]} />
|
||||||
|
|
149
packages/r3f/src/components/ReferenceWindow/ReferenceWindow.tsx
Normal file
149
packages/r3f/src/components/ReferenceWindow/ReferenceWindow.tsx
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
import type {VFC} from 'react'
|
||||||
|
import React, {useEffect, useLayoutEffect, useRef} from 'react'
|
||||||
|
import {useEditorStore} from '../../store'
|
||||||
|
import shallow from 'zustand/shallow'
|
||||||
|
import type {WebGLRenderer} from 'three'
|
||||||
|
import useMeasure from 'react-use-measure'
|
||||||
|
import styled, {keyframes} from 'styled-components'
|
||||||
|
import {TiWarningOutline} from 'react-icons/ti'
|
||||||
|
// This is ugly, but pure TS doesn't let you do bundler-stuff
|
||||||
|
import noiseImageUrl from './noiseImage'
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
position: relative;
|
||||||
|
width: fit-content;
|
||||||
|
height: fit-content;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Canvas = styled.canvas`
|
||||||
|
display: block;
|
||||||
|
`
|
||||||
|
|
||||||
|
const staticAnimation = keyframes`
|
||||||
|
0% { transform: translate(0,0) }
|
||||||
|
10% { transform: translate(-5%,-5%) }
|
||||||
|
20% { transform: translate(-10%,5%) }
|
||||||
|
30% { transform: translate(5%,-10%) }
|
||||||
|
40% { transform: translate(-5%,15%) }
|
||||||
|
50% { transform: translate(-10%,5%) }
|
||||||
|
60% { transform: translate(15%,0) }
|
||||||
|
70% { transform: translate(0,10%) }
|
||||||
|
80% { transform: translate(-15%,0) }
|
||||||
|
90% { transform: translate(10%,5%) }
|
||||||
|
100% { transform: translate(5%,0) }
|
||||||
|
`
|
||||||
|
|
||||||
|
const Static = styled.div`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
width: 200px;
|
||||||
|
height: 120px;
|
||||||
|
padding: 18px;
|
||||||
|
|
||||||
|
::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
right: -50%;
|
||||||
|
bottom: -50%;
|
||||||
|
background: #2f2f2f url(${noiseImageUrl}) repeat 0 0;
|
||||||
|
animation: ${staticAnimation} 0.2s infinite;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Warning = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.8;
|
||||||
|
`
|
||||||
|
|
||||||
|
interface ReferenceWindowProps {
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReferenceWindow: VFC<ReferenceWindowProps> = ({height}) => {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
|
|
||||||
|
const [gl] = useEditorStore((state) => [state.gl], shallow)
|
||||||
|
const [ref, bounds] = useMeasure()
|
||||||
|
|
||||||
|
const preserveDrawingBuffer =
|
||||||
|
(
|
||||||
|
gl?.domElement.getContext('webgl') ?? gl?.domElement.getContext('webgl2')
|
||||||
|
)?.getContextAttributes()?.preserveDrawingBuffer ?? false
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (gl) {
|
||||||
|
ref(gl?.domElement)
|
||||||
|
}
|
||||||
|
}, [gl, ref])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let animationHandle: number
|
||||||
|
const draw = (gl: WebGLRenderer) => () => {
|
||||||
|
animationHandle = requestAnimationFrame(draw(gl))
|
||||||
|
|
||||||
|
if (!gl.domElement || !preserveDrawingBuffer) {
|
||||||
|
cancelAnimationFrame(animationHandle)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = (gl.domElement.width / gl.domElement.height) * height
|
||||||
|
|
||||||
|
const ctx = canvasRef.current!.getContext('2d')!
|
||||||
|
|
||||||
|
console.log(gl.domElement.getContext('webgl2')!.getContextAttributes())
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/17861447/html5-canvas-drawimage-how-to-apply-antialiasing
|
||||||
|
ctx.imageSmoothingQuality = 'high'
|
||||||
|
|
||||||
|
ctx.fillStyle = 'white'
|
||||||
|
ctx.fillRect(0, 0, width, height)
|
||||||
|
|
||||||
|
ctx.drawImage(gl.domElement, 0, 0, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gl) {
|
||||||
|
draw(gl)()
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(animationHandle)
|
||||||
|
}
|
||||||
|
}, [gl, height, preserveDrawingBuffer])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
{gl?.domElement && preserveDrawingBuffer ? (
|
||||||
|
<Canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
width={
|
||||||
|
((bounds.width || gl.domElement.width) /
|
||||||
|
(bounds.height || gl.domElement.height)) *
|
||||||
|
height
|
||||||
|
}
|
||||||
|
height={height}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Static>
|
||||||
|
<Warning>
|
||||||
|
<TiWarningOutline size="3em" />
|
||||||
|
<code>
|
||||||
|
Please set <pre>{`gl={{ preserveDrawingBuffer: true }}`}</pre> on
|
||||||
|
the r3f Canvas for the reference window to work.
|
||||||
|
</code>
|
||||||
|
</Warning>
|
||||||
|
</Static>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReferenceWindow
|
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
File diff suppressed because one or more lines are too long
|
@ -9,13 +9,15 @@ import ProxyManager from './ProxyManager'
|
||||||
import studio, {ToolbarIconButton} from '@theatre/studio'
|
import studio, {ToolbarIconButton} from '@theatre/studio'
|
||||||
import {useVal} from '@theatre/react'
|
import {useVal} from '@theatre/react'
|
||||||
import styled, {createGlobalStyle, StyleSheetManager} from 'styled-components'
|
import styled, {createGlobalStyle, StyleSheetManager} from 'styled-components'
|
||||||
import {IoCameraReverseOutline} from 'react-icons/io5'
|
|
||||||
import type {ISheet} from '@theatre/core'
|
import type {ISheet} from '@theatre/core'
|
||||||
import useSnapshotEditorCamera from './useSnapshotEditorCamera'
|
import useSnapshotEditorCamera from './useSnapshotEditorCamera'
|
||||||
import {getEditorSheet, getEditorSheetObject} from './editorStuff'
|
import {getEditorSheet, getEditorSheetObject} from './editorStuff'
|
||||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||||
import {InfiniteGridHelper} from '../InfiniteGridHelper'
|
import {InfiniteGridHelper} from '../InfiniteGridHelper'
|
||||||
import {DragDetectorProvider} from './DragDetector'
|
import {DragDetectorProvider} from './DragDetector'
|
||||||
|
import TooltipPortalProvider from './TooltipPortalProvider'
|
||||||
|
import {FiRefreshCw} from 'react-icons/fi'
|
||||||
|
import ReferenceWindow from './ReferenceWindow/ReferenceWindow'
|
||||||
|
|
||||||
const GlobalStyle = createGlobalStyle`
|
const GlobalStyle = createGlobalStyle`
|
||||||
:host {
|
:host {
|
||||||
|
@ -97,16 +99,26 @@ const Overlay = styled.div`
|
||||||
|
|
||||||
const Tools = styled.div`
|
const Tools = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 8px;
|
left: 12px;
|
||||||
top: 6px;
|
top: 12px;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const ReferenceWindowContainer = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
`
|
||||||
|
|
||||||
const SnapshotEditor: React.FC<{paneId: string}> = (props) => {
|
const SnapshotEditor: React.FC<{paneId: string}> = (props) => {
|
||||||
const snapshotEditorSheet = getEditorSheet()
|
const snapshotEditorSheet = getEditorSheet()
|
||||||
const paneId = props.paneId
|
const paneId = props.paneId
|
||||||
const editorObject = getEditorSheetObject()
|
const editorObject = getEditorSheetObject()
|
||||||
|
|
||||||
|
const showReferenceWindow =
|
||||||
|
useVal(editorObject?.props.viewport.showReferenceWindow) ?? true
|
||||||
|
|
||||||
const [sceneSnapshot, createSnapshot] = useEditorStore(
|
const [sceneSnapshot, createSnapshot] = useEditorStore(
|
||||||
(state) => [state.sceneSnapshot, state.createSnapshot],
|
(state) => [state.sceneSnapshot, state.createSnapshot],
|
||||||
shallow,
|
shallow,
|
||||||
|
@ -148,6 +160,7 @@ const SnapshotEditor: React.FC<{paneId: string}> = (props) => {
|
||||||
<StyleSheetManager disableVendorPrefixes>
|
<StyleSheetManager disableVendorPrefixes>
|
||||||
<>
|
<>
|
||||||
<GlobalStyle />
|
<GlobalStyle />
|
||||||
|
<TooltipPortalProvider>
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Overlay>
|
<Overlay>
|
||||||
<Tools>
|
<Tools>
|
||||||
|
@ -155,9 +168,14 @@ const SnapshotEditor: React.FC<{paneId: string}> = (props) => {
|
||||||
title="Refresh Snapshot"
|
title="Refresh Snapshot"
|
||||||
onClick={createSnapshot}
|
onClick={createSnapshot}
|
||||||
>
|
>
|
||||||
<IoCameraReverseOutline />
|
<FiRefreshCw />
|
||||||
</ToolbarIconButton>
|
</ToolbarIconButton>
|
||||||
</Tools>
|
</Tools>
|
||||||
|
{showReferenceWindow && (
|
||||||
|
<ReferenceWindowContainer>
|
||||||
|
<ReferenceWindow height={120} />
|
||||||
|
</ReferenceWindowContainer>
|
||||||
|
)}
|
||||||
</Overlay>
|
</Overlay>
|
||||||
|
|
||||||
{sceneSnapshot ? (
|
{sceneSnapshot ? (
|
||||||
|
@ -184,7 +202,7 @@ const SnapshotEditor: React.FC<{paneId: string}> = (props) => {
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
{/* </PortalContext.Provider> */}
|
</TooltipPortalProvider>
|
||||||
</>
|
</>
|
||||||
</StyleSheetManager>
|
</StyleSheetManager>
|
||||||
</root.div>
|
</root.div>
|
||||||
|
|
27
packages/r3f/src/components/TooltipPortalProvider.tsx
Normal file
27
packages/r3f/src/components/TooltipPortalProvider.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import type {ReactNode} from 'react';
|
||||||
|
import React, { useState} from 'react'
|
||||||
|
import {PortalContext} from 'reakit'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const PortalHost = styled.div`
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
`
|
||||||
|
|
||||||
|
export interface PortalManagerProps {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const TooltipPortalProvider: React.VFC<PortalManagerProps> = ({children}) => {
|
||||||
|
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PortalContext.Provider value={wrapper}>
|
||||||
|
{children}
|
||||||
|
<PortalHost ref={setWrapper} />
|
||||||
|
</PortalContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TooltipPortalProvider
|
|
@ -22,6 +22,9 @@ const editorSheetObjectConfig = {
|
||||||
},
|
},
|
||||||
{as: 'menu', label: 'Shading'},
|
{as: 'menu', label: 'Shading'},
|
||||||
),
|
),
|
||||||
|
showReferenceWindow: types.boolean(true, {
|
||||||
|
label: 'Reference',
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{label: 'Viewport Config'},
|
{label: 'Viewport Config'},
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue