-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
Copy pathstore.ts
366 lines (329 loc) Β· 12.4 KB
/
store.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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
import * as THREE from 'three'
import * as React from 'react'
import create, { GetState, SetState, StoreApi, UseBoundStore } from 'zustand'
import { DomEvent, EventManager, PointerCaptureTarget, ThreeEvent } from './events'
import { _XRFrame, calculateDpr, Camera, isOrthographicCamera, updateCamera } from './utils'
import { Advance, Invalidate } from './loop'
// Keys that shouldn't be copied between R3F stores
export const privateKeys = [
'set',
'get',
'setSize',
'setFrameloop',
'setDpr',
'events',
'invalidate',
'advance',
'size',
'viewport',
] as const
export type PrivateKeys = typeof privateKeys[number]
export interface Intersection extends THREE.Intersection {
eventObject: THREE.Object3D
}
export type Subscription = {
ref: React.MutableRefObject<RenderCallback>
priority: number
store: UseBoundStore<RootState, StoreApi<RootState>>
}
export type Dpr = number | [min: number, max: number]
export type Size = {
width: number
height: number
top: number
left: number
/** @deprecated `updateStyle` is now disabled for OffscreenCanvas and will be removed in v9. */
updateStyle?: boolean
}
export type Viewport = Size & {
/** The initial pixel ratio */
initialDpr: number
/** Current pixel ratio */
dpr: number
/** size.width / viewport.width */
factor: number
/** Camera distance */
distance: number
/** Camera aspect ratio: width / height */
aspect: number
}
export type RenderCallback = (state: RootState, delta: number, frame?: _XRFrame) => void
export type Performance = {
/** Current performance normal, between min and max */
current: number
/** How low the performance can go, between 0 and max */
min: number
/** How high the performance can go, between min and max */
max: number
/** Time until current returns to max in ms */
debounce: number
/** Sets current to min, puts the system in regression */
regress: () => void
}
export type Renderer = { render: (scene: THREE.Scene, camera: THREE.Camera) => any }
export const isRenderer = (def: any) => !!def?.render
export type InternalState = {
active: boolean
priority: number
frames: number
lastEvent: React.MutableRefObject<DomEvent | null>
interaction: THREE.Object3D[]
hovered: Map<string, ThreeEvent<DomEvent>>
subscribers: Subscription[]
capturedMap: Map<number, Map<THREE.Object3D, PointerCaptureTarget>>
initialClick: [x: number, y: number]
initialHits: THREE.Object3D[]
subscribe: (
callback: React.MutableRefObject<RenderCallback>,
priority: number,
store: UseBoundStore<RootState, StoreApi<RootState>>,
) => () => void
}
export type RootState = {
/** Set current state */
set: SetState<RootState>
/** Get current state */
get: GetState<RootState>
/** The instance of the renderer */
gl: THREE.WebGLRenderer
/** Default camera */
camera: Camera & { manual?: boolean }
/** Default scene */
scene: THREE.Scene
/** Default raycaster */
raycaster: THREE.Raycaster
/** Default clock */
clock: THREE.Clock
/** Event layer interface, contains the event handler and the node they're connected to */
events: EventManager<any>
/** XR interface */
xr: { connect: () => void; disconnect: () => void }
/** Currently used controls */
controls: THREE.EventDispatcher | null
/** Normalized event coordinates */
pointer: THREE.Vector2
/** @deprecated Normalized event coordinates, use "pointer" instead! */
mouse: THREE.Vector2
/* Whether to enable r139's THREE.ColorManagement */
legacy: boolean
/** Shortcut to gl.outputColorSpace = THREE.LinearSRGBColorSpace */
linear: boolean
/** Shortcut to gl.toneMapping = NoTonemapping */
flat: boolean
/** Render loop flags */
frameloop: 'always' | 'demand' | 'never'
/** Adaptive performance interface */
performance: Performance
/** Reactive pixel-size of the canvas */
size: Size
/** Reactive size of the viewport in threejs units */
viewport: Viewport & {
getCurrentViewport: (
camera?: Camera,
target?: THREE.Vector3 | Parameters<THREE.Vector3['set']>,
size?: Size,
) => Omit<Viewport, 'dpr' | 'initialDpr'>
}
/** Flags the canvas for render, but doesn't render in itself */
invalidate: (frames?: number) => void
/** Advance (render) one step */
advance: (timestamp: number, runGlobalEffects?: boolean) => void
/** Shortcut to setting the event layer */
setEvents: (events: Partial<EventManager<any>>) => void
/**
* Shortcut to manual sizing
*/
setSize: (
width: number,
height: number,
/** @deprecated `updateStyle` is now disabled for OffscreenCanvas and will be removed in v9. */
updateStyle?: boolean,
top?: number,
left?: number,
) => void
/** Shortcut to manual setting the pixel ratio */
setDpr: (dpr: Dpr) => void
/** Shortcut to frameloop flags */
setFrameloop: (frameloop?: 'always' | 'demand' | 'never') => void
/** When the canvas was clicked but nothing was hit */
onPointerMissed?: (event: MouseEvent) => void
/** If this state model is layered (via createPortal) then this contains the previous layer */
previousRoot?: UseBoundStore<RootState, StoreApi<RootState>>
/** Internals */
internal: InternalState
}
const context = React.createContext<UseBoundStore<RootState>>(null!)
const createStore = (invalidate: Invalidate, advance: Advance): UseBoundStore<RootState> => {
const rootState = create<RootState>((set, get) => {
const position = new THREE.Vector3()
const defaultTarget = new THREE.Vector3()
const tempTarget = new THREE.Vector3()
function getCurrentViewport(
camera: Camera = get().camera,
target: THREE.Vector3 | Parameters<THREE.Vector3['set']> = defaultTarget,
size: Size = get().size,
): Omit<Viewport, 'dpr' | 'initialDpr'> {
const { width, height, top, left } = size
const aspect = width / height
if (target instanceof THREE.Vector3) tempTarget.copy(target)
else tempTarget.set(...target)
const distance = camera.getWorldPosition(position).distanceTo(tempTarget)
if (isOrthographicCamera(camera)) {
return { width: width / camera.zoom, height: height / camera.zoom, top, left, factor: 1, distance, aspect }
} else {
const fov = (camera.fov * Math.PI) / 180 // convert vertical fov to radians
const h = 2 * Math.tan(fov / 2) * distance // visible height
const w = h * (width / height)
return { width: w, height: h, top, left, factor: width / w, distance, aspect }
}
}
let performanceTimeout: ReturnType<typeof setTimeout> | undefined = undefined
const setPerformanceCurrent = (current: number) =>
set((state) => ({ performance: { ...state.performance, current } }))
const pointer = new THREE.Vector2()
const rootState: RootState = {
set,
get,
// Mock objects that have to be configured
gl: null as unknown as THREE.WebGLRenderer,
camera: null as unknown as Camera,
raycaster: null as unknown as THREE.Raycaster,
events: { priority: 1, enabled: true, connected: false },
xr: null as unknown as { connect: () => void; disconnect: () => void },
scene: null as unknown as THREE.Scene,
invalidate: (frames = 1) => invalidate(get(), frames),
advance: (timestamp: number, runGlobalEffects?: boolean) => advance(timestamp, runGlobalEffects, get()),
legacy: false,
linear: false,
flat: false,
controls: null,
clock: new THREE.Clock(),
pointer,
mouse: pointer,
frameloop: 'always',
onPointerMissed: undefined,
performance: {
current: 1,
min: 0.5,
max: 1,
debounce: 200,
regress: () => {
const state = get()
// Clear timeout
if (performanceTimeout) clearTimeout(performanceTimeout)
// Set lower bound performance
if (state.performance.current !== state.performance.min) setPerformanceCurrent(state.performance.min)
// Go back to upper bound performance after a while unless something regresses meanwhile
performanceTimeout = setTimeout(
() => setPerformanceCurrent(get().performance.max),
state.performance.debounce,
)
},
},
size: { width: 0, height: 0, top: 0, left: 0, updateStyle: false },
viewport: {
initialDpr: 0,
dpr: 0,
width: 0,
height: 0,
top: 0,
left: 0,
aspect: 0,
distance: 0,
factor: 0,
getCurrentViewport,
},
setEvents: (events: Partial<EventManager<any>>) =>
set((state) => ({ ...state, events: { ...state.events, ...events } })),
setSize: (width: number, height: number, updateStyle?: boolean, top?: number, left?: number) => {
const camera = get().camera
const size = { width, height, top: top || 0, left: left || 0, updateStyle }
set((state) => ({ size, viewport: { ...state.viewport, ...getCurrentViewport(camera, defaultTarget, size) } }))
},
setDpr: (dpr: Dpr) =>
set((state) => {
const resolved = calculateDpr(dpr)
return { viewport: { ...state.viewport, dpr: resolved, initialDpr: state.viewport.initialDpr || resolved } }
}),
setFrameloop: (frameloop: 'always' | 'demand' | 'never' = 'always') => {
const clock = get().clock
// if frameloop === "never" clock.elapsedTime is updated using advance(timestamp)
clock.stop()
clock.elapsedTime = 0
if (frameloop !== 'never') {
clock.start()
clock.elapsedTime = 0
}
set(() => ({ frameloop }))
},
previousRoot: undefined,
internal: {
active: false,
priority: 0,
frames: 0,
lastEvent: React.createRef(),
interaction: [],
hovered: new Map<string, ThreeEvent<DomEvent>>(),
subscribers: [],
initialClick: [0, 0],
initialHits: [],
capturedMap: new Map(),
subscribe: (
ref: React.MutableRefObject<RenderCallback>,
priority: number,
store: UseBoundStore<RootState, StoreApi<RootState>>,
) => {
const internal = get().internal
// If this subscription was given a priority, it takes rendering into its own hands
// For that reason we switch off automatic rendering and increase the manual flag
// As long as this flag is positive there can be no internal rendering at all
// because there could be multiple render subscriptions
internal.priority = internal.priority + (priority > 0 ? 1 : 0)
internal.subscribers.push({ ref, priority, store })
// Register subscriber and sort layers from lowest to highest, meaning,
// highest priority renders last (on top of the other frames)
internal.subscribers = internal.subscribers.sort((a, b) => a.priority - b.priority)
return () => {
const internal = get().internal
if (internal?.subscribers) {
// Decrease manual flag if this subscription had a priority
internal.priority = internal.priority - (priority > 0 ? 1 : 0)
// Remove subscriber from list
internal.subscribers = internal.subscribers.filter((s) => s.ref !== ref)
}
}
},
},
}
return rootState
})
const state = rootState.getState()
let oldSize = state.size
let oldDpr = state.viewport.dpr
let oldCamera = state.camera
rootState.subscribe(() => {
const { camera, size, viewport, gl, set } = rootState.getState()
// Resize camera and renderer on changes to size and pixelratio
if (size.width !== oldSize.width || size.height !== oldSize.height || viewport.dpr !== oldDpr) {
oldSize = size
oldDpr = viewport.dpr
// Update camera & renderer
updateCamera(camera, size)
gl.setPixelRatio(viewport.dpr)
const updateStyle =
size.updateStyle ?? (typeof HTMLCanvasElement !== 'undefined' && gl.domElement instanceof HTMLCanvasElement)
gl.setSize(size.width, size.height, updateStyle)
}
// Update viewport once the camera changes
if (camera !== oldCamera) {
oldCamera = camera
// Update viewport
set((state) => ({ viewport: { ...state.viewport, ...state.viewport.getCurrentViewport(camera) } }))
}
})
// Invalidate on any change
rootState.subscribe((state) => invalidate(state))
// Return root state
return rootState
}
export { createStore, context }