-
-
Notifications
You must be signed in to change notification settings - Fork 9
/
hooks.ts
139 lines (119 loc) · 4.31 KB
/
hooks.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import * as React from 'react'
import * as OGL from 'ogl'
import { suspend } from 'suspend-react'
import type { StateSelector, EqualityChecker } from 'zustand'
import type { Instance, RootState, RootStore, Subscription } from './types'
import { classExtends } from './utils'
/**
* An SSR-friendly useLayoutEffect.
*
* React currently throws a warning when using useLayoutEffect on the server.
* To get around it, we can conditionally useEffect on the server (no-op) and
* useLayoutEffect elsewhere.
*
* @see https://github.com/facebook/react/issues/14927
*/
export const useIsomorphicLayoutEffect =
typeof window !== 'undefined' && (window.document?.createElement || window.navigator?.product === 'ReactNative')
? React.useLayoutEffect
: React.useEffect
/**
* Exposes an object's {@link Instance}.
*
* **Note**: this is an escape hatch to react-internal fields. Expect this to change significantly between versions.
*/
export function useInstanceHandle<O>(ref: React.MutableRefObject<O>): React.MutableRefObject<Instance> {
const instance = React.useRef<Instance>(null!)
useIsomorphicLayoutEffect(() => void (instance.current = (ref.current as unknown as any).__ogl), [ref])
return instance
}
/**
* Internal OGL context.
*/
export const OGLContext = React.createContext<RootStore>(null!)
/**
* Returns the internal OGL store.
*/
export function useStore() {
const store = React.useContext(OGLContext)
if (!store) throw `react-ogl hooks can only used inside a canvas or OGLContext provider!`
return store
}
/**
* Returns the internal OGL state.
*/
export function useOGL<T = RootState>(
selector: StateSelector<RootState, T> = (state) => state as unknown as T,
equalityFn?: EqualityChecker<T>,
) {
return useStore()(selector, equalityFn)
}
export interface ObjectMap {
nodes: Record<string, OGL.Mesh>
programs: Record<string, OGL.Program>
}
/**
* Creates an `ObjectMap` from an object.
*/
export function useGraph(object: OGL.Transform) {
return React.useMemo(() => {
const data: ObjectMap = { nodes: {}, programs: {} }
object.traverse((obj: OGL.Transform | OGL.Mesh) => {
if (!(obj instanceof OGL.Mesh)) return
if (obj.name) data.nodes[obj.name] = obj
if (obj.program.gltfMaterial && !data.programs[obj.program.gltfMaterial.name]) {
data.programs[obj.program.gltfMaterial.name] = obj.program
}
})
return data
}, [object])
}
/**
* Subscribe an element into a shared render loop.
*/
export function useFrame(callback: Subscription, renderPriority = 0) {
const subscribe = useOGL((state) => state.subscribe)
const unsubscribe = useOGL((state) => state.unsubscribe)
// Store frame callback in a ref so we can pass a mutable reference.
// This allows the callback to dynamically update without blocking
// the render loop.
const ref = React.useRef(callback)
useIsomorphicLayoutEffect(() => void (ref.current = callback), [callback])
// Subscribe on mount and unsubscribe on unmount
useIsomorphicLayoutEffect(() => {
subscribe(ref, renderPriority)
return () => void unsubscribe(ref, renderPriority)
}, [subscribe, unsubscribe, renderPriority])
}
export type LoaderRepresentation =
| { load(gl: OGL.OGLRenderingContext, url: string): Promise<any> }
| Pick<typeof OGL.TextureLoader, 'load'>
export type LoaderResult<L extends LoaderRepresentation> = Awaited<ReturnType<L['load']>>
/**
* Loads assets suspensefully.
*/
export function useLoader<L extends LoaderRepresentation, I extends string | string[], R = LoaderResult<L>>(
loader: L,
input: I,
extensions?: (loader: L) => void,
): I extends any[] ? R[] : R {
const gl = useOGL((state) => state.gl)
// Put keys into an array so their contents are spread and cached with suspend
const keys = Array.isArray(input) ? input : [input]
return suspend(
async (gl, loader, ...urls) => {
// Call extensions
extensions?.(loader)
const result = await Promise.all(
urls.map(async (url: string) => {
// @ts-ignore OGL's loaders don't have a consistent signature
if (classExtends(loader, OGL.TextureLoader)) return loader.load(gl, { url })
return await loader.load(gl, url)
}),
)
// Return result | result[], mirroring input | input[]
return Array.isArray(input) ? result : result[0]
},
[gl, loader, ...keys],
)
}