react-three-fiber
Version:
React-fiber renderer for THREE.js
144 lines (128 loc) • 4.26 kB
text/typescript
import * as THREE from 'three'
import { useRef, useContext, useEffect, useLayoutEffect, useMemo, useState } from 'react'
import { SharedCanvasContext, RenderCallback, stateContext } from './canvas'
//@ts-ignore
import usePromise from 'react-promise-suspense'
// helper type for omitting properties from types
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
export function useFrame(callback: RenderCallback, renderPriority: number = 0): void {
const { subscribe } = useContext(stateContext)
// Update ref
const ref = useRef<RenderCallback>(callback)
useLayoutEffect(() => void (ref.current = callback), [callback])
// Subscribe/unsub
useEffect(() => {
const unsubscribe = subscribe(ref, renderPriority)
return () => unsubscribe()
}, [renderPriority])
}
export function useRender(callback: RenderCallback, takeOver: boolean) {
return useFrame(callback, takeOver ? 1 : 0)
}
export function useThree(): SharedCanvasContext {
return useContext(stateContext)
}
export function useUpdate<T>(
callback: (props: T) => void,
dependents: any[],
optionalRef?: React.MutableRefObject<T>
): React.MutableRefObject<any> {
const { invalidate } = useContext(stateContext)
const localRef = useRef()
const ref = optionalRef ? optionalRef : localRef
useEffect(() => {
if (ref.current) {
callback(ref.current)
invalidate()
}
}, dependents)
return ref
}
export function useResource<T>(optionalRef?: React.MutableRefObject<T>): [React.MutableRefObject<T>, T] {
const [_, forceUpdate] = useState(false)
const localRef = useRef<T>((undefined as unknown) as T)
const ref = optionalRef ? optionalRef : localRef
useEffect(() => void forceUpdate(i => !i), [ref.current])
return [ref, ref.current]
}
type Content = {
geometry: THREE.Geometry | THREE.BufferGeometry
material: THREE.Material | THREE.Material[]
}
type Extensions = (loader: THREE.Loader) => void
type LoaderData = {
data: any
objects: any[]
}
const blackList = [
'id',
'uuid',
'type',
'children',
'parent',
'matrix',
'matrixWorld',
'matrixWorldNeedsUpdate',
'modelViewMatrix',
'normalMatrix',
]
function prune(props: any) {
const reducedProps = { ...props }
// Remove black listed props
blackList.forEach(name => delete reducedProps[name])
// Remove functions
Object.keys(reducedProps).forEach(name => typeof reducedProps[name] === 'function' && delete reducedProps[name])
// Prune materials and geometries
if (reducedProps.material) reducedProps.material = prune(reducedProps.material)
if (reducedProps.geometry) reducedProps.geometry = prune(reducedProps.geometry)
// Return cleansed object
return reducedProps
}
export function useLoader<T>(Proto: THREE.Loader, url: string | string[], extensions?: Extensions): T {
const loader = useMemo(() => {
// Construct new loader
const temp = new (Proto as any)()
// Run loader extensions
if (extensions) extensions(temp)
return temp
}, [Proto])
// Use suspense to load async assets
let results = usePromise<LoaderData>(
(Proto: THREE.Loader, url: string | string[]) => {
const urlArray = Array.isArray(url) ? url : [url]
return Promise.all(
urlArray.map(
url =>
new Promise(res =>
loader.load(url, (data: any) => {
const objects: any[] = []
if (data.scene) data.scene.traverse((props: any) => objects.push(prune(props)))
data.__$ = objects
res(data)
})
)
)
)
},
[Proto, url]
)
// Dispose objects on unmount
useEffect(() => () =>
results.forEach((data: any) => {
if (data.dispose) data.dispose()
if (data.scene) data.scene.dispose()
}, [])
)
// Temporary hack to make the new api backwards compatible for a while ...
const isArray = Array.isArray(url)
if (!isArray) {
Object.assign(results[0], {
[Symbol.iterator]() {
console.warn('[value]=useLoader(...) is deprecated, please use value=useLoader(...) instead!')
return [results[0]][Symbol.iterator]()
},
})
}
// Return the object itself and a list of pruned props
return isArray ? results : results[0]
}