Skip to content

Commit

Permalink
[v9] refactor!: instance descriptors, dynamically map ThreeElements (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
CodyJasonBennett authored Sep 18, 2022
1 parent 8aa3a96 commit 1e7e0a1
Show file tree
Hide file tree
Showing 33 changed files with 1,867 additions and 1,876 deletions.
47 changes: 3 additions & 44 deletions docs/tutorials/typescript.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,35 +34,6 @@ function Box(props) {

The exclamation mark is a non-null assertion that will let TS know that `ref.current` is defined when we access it in effects.

## Typing shorthand props

react-three-fiber accepts short-hand props like scalars, strings, and arrays so you can declaratively set properties without side effects.

Here are the different variations of props:

```tsx
import { Euler, Vector3, Color } from 'three'

rotation: Euler || [x, y, z]
position: Vector3 || [x, y, z] || scalar
color: Color || 'hotpink' || 0xffffff
```

Each property has extended types which you can pull from to type these properties.

```tsx
import { Euler, Vector3, Color } from '@react-three/fiber'
// or
// import { ReactThreeFiber } from '@react-three/fiber'
// ReactThreeFiber.Euler, ReactThreeFiber.Vector3, etc.

rotation: Euler
position: Vector3
color: Color
```

This is particularly useful if you are typing properties outside of components, such as a store or a hook.

## Extend usage

react-three-fiber can also accept third-party elements and extend them into its internal catalogue.
Expand All @@ -89,26 +60,14 @@ You can then declaratively create custom elements with primitives, but TypeScrip
<customElement />
```

### Node Helpers

react-three-fiber exports helpers that you can use to define different types of nodes. These nodes will type an element that we'll attach to the global JSX namespace.

```tsx
Node
Object3DNode
BufferGeometryNode
MaterialNode
LightNode
```

### Extending ThreeElements

Since our custom element is an object, we'll use `Object3DNode` to define it.
To define our element in JSX, we'll use the `ThreeElement` interface to extend `ThreeElements`. This interface describes three.js classes that are available in the R3F catalog and can be used as native elements.

```tsx
import { useRef, useEffect } from 'react'
import { GridHelper } from 'three'
import { extend, Object3DNode } from '@react-three/fiber'
import { extend, ThreeElement } from '@react-three/fiber'

// Create our custom element
class CustomElement extends GridHelper {}
Expand All @@ -119,7 +78,7 @@ extend({ CustomElement })
// Add types to ThreeElements elements so primitives pick up on it
declare module '@react-three/fiber' {
interface ThreeElements {
customElement: Object3DNode<CustomElement, typeof CustomElement>
customElement: ThreeElement<typeof CustomElement>
}
}

Expand Down
2 changes: 1 addition & 1 deletion example/src/demos/Lines.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useRef, useEffect, useState, useCallback, useContext, useMemo } from 'react'
import { extend, Canvas, useThree, ReactThreeFiber } from '@react-three/fiber'
import { extend, Canvas, useThree } from '@react-three/fiber'
import { OrbitControls } from 'three-stdlib'
extend({ OrbitControls })

Expand Down
2 changes: 1 addition & 1 deletion example/src/demos/Pointcloud.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useRef, useEffect, useState, useCallback, useContext, useMemo } from 'react'
import { extend, Canvas, useThree, ReactThreeFiber } from '@react-three/fiber'
import { extend, Canvas, useThree } from '@react-three/fiber'
import * as THREE from 'three'
import { OrbitControls } from 'three-stdlib'
extend({ OrbitControls })
Expand Down
7 changes: 3 additions & 4 deletions example/typings/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { ReactThreeFiber } from '@react-three/fiber'
import { ThreeElement } from '@react-three/fiber'
import { OrbitControls } from 'three-stdlib'

import { DotMaterial } from '../src/demos/Pointcloud'

declare module '@react-three/fiber' {
interface ThreeElements {
orbitControls: ReactThreeFiber.Node<OrbitControls, typeof OrbitControls>
dotMaterial: ReactThreeFiber.MaterialNode<DotMaterial, typeof DotMaterial>
orbitControls: ThreeElement<typeof OrbitControls>
dotMaterial: ThreeElement<typeof DotMaterial>
}
}
17 changes: 10 additions & 7 deletions packages/fiber/src/core/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ export function createEvents(store: UseBoundStore<RootState>) {
function filterPointerEvents(objects: THREE.Object3D[]) {
return objects.filter((obj) =>
['Move', 'Over', 'Enter', 'Out', 'Leave'].some(
(name) => (obj as unknown as Instance).__r3f?.handlers[('onPointer' + name) as keyof EventHandlers],
(name) =>
(obj as Instance<THREE.Object3D>['object']).__r3f?.handlers[('onPointer' + name) as keyof EventHandlers],
),
)
}
Expand Down Expand Up @@ -242,7 +243,8 @@ export function createEvents(store: UseBoundStore<RootState>) {
let eventObject: THREE.Object3D | null = hit.object
// Bubble event up
while (eventObject) {
if ((eventObject as unknown as Instance).__r3f?.eventCount) intersections.push({ ...hit, eventObject })
if ((eventObject as Instance<THREE.Object3D>['object']).__r3f?.eventCount)
intersections.push({ ...hit, eventObject })
eventObject = eventObject.parent
}
}
Expand Down Expand Up @@ -374,10 +376,10 @@ export function createEvents(store: UseBoundStore<RootState>) {
)
) {
const eventObject = hoveredObj.eventObject
const instance = (eventObject as unknown as Instance).__r3f
const handlers = instance?.handlers
const instance = (eventObject as Instance<THREE.Object3D>['object']).__r3f
internal.hovered.delete(makeId(hoveredObj))
if (instance?.eventCount) {
const handlers = instance.handlers
// Clear out intersects, they are outdated by now
const data = { ...hoveredObj, intersections }
handlers.onPointerOut?.(data as ThreeEvent<PointerEvent>)
Expand Down Expand Up @@ -439,10 +441,11 @@ export function createEvents(store: UseBoundStore<RootState>) {

handleIntersects(hits, event, delta, (data: ThreeEvent<DomEvent>) => {
const eventObject = data.eventObject
const instance = (eventObject as unknown as Instance).__r3f
const handlers = instance?.handlers
const instance = (eventObject as Instance<THREE.Object3D>['object']).__r3f

// Check presence of handlers
if (!instance?.eventCount) return
const handlers = instance.handlers

if (isPointerMove) {
// Move event ...
Expand Down Expand Up @@ -493,7 +496,7 @@ export function createEvents(store: UseBoundStore<RootState>) {

function pointerMissed(event: MouseEvent, objects: THREE.Object3D[]) {
objects.forEach((object: THREE.Object3D) =>
(object as unknown as Instance).__r3f?.handlers.onPointerMissed?.(event),
(object as Instance<THREE.Object3D>['object']).__r3f?.handlers.onPointerMissed?.(event),
)
}

Expand Down
13 changes: 8 additions & 5 deletions packages/fiber/src/core/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { context, RootState, RenderCallback, StageTypes } from './store'
import { buildGraph, ObjectMap, is, useMutableCallback, useIsomorphicLayoutEffect } from './utils'
import { Stage, Stages, UpdateCallback } from './stages'
import { LoadingManager } from 'three'
import { LocalState, Instance } from './renderer'
import { Instance } from './renderer'

export interface Loader<T> extends THREE.Loader {
load(
Expand All @@ -24,14 +24,17 @@ export type ConditionalType<Child, Parent, Truthy, Falsy> = Child extends Parent
export type BranchingReturn<T, Parent, Coerced> = ConditionalType<T, Parent, Coerced, T>

/**
* Exposes an object's {@link LocalState}.
* Exposes an object's {@link Instance}.
* @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#useInstanceHandle
*
* **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<LocalState> {
const instance = React.useRef<LocalState>(null!)
useIsomorphicLayoutEffect(() => void (instance.current = (ref.current as unknown as Instance).__r3f), [ref])
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 Instance<O>['object']).__r3f!),
[ref],
)
return instance
}

Expand Down
24 changes: 12 additions & 12 deletions packages/fiber/src/core/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as React from 'react'
import { ConcurrentRoot } from 'react-reconciler/constants'
import create, { StoreApi, UseBoundStore } from 'zustand'

import * as ReactThreeFiber from '../three-types'
import { ThreeElement } from '../three-types'
import {
Renderer,
createStore,
Expand All @@ -19,9 +19,9 @@ import {
FrameloopLegacy,
Frameloop,
} from './store'
import { createRenderer, extend, Root } from './renderer'
import { reconciler, extend, Root } from './renderer'
import { createLoop, addEffect, addAfterEffect, addTail } from './loop'
import { getEventPriority, EventManager, ComputeFunction } from './events'
import { EventManager, ComputeFunction } from './events'
import {
is,
dispose,
Expand All @@ -31,14 +31,14 @@ import {
useIsomorphicLayoutEffect,
Camera,
updateCamera,
applyProps,
} from './utils'
import { useStore } from './hooks'
import { Stage, Lifecycle, Stages } from './stages'
import { OffscreenCanvas } from 'three'

const roots = new Map<Element, Root>()
const { invalidate, advance } = createLoop(roots)
const { reconciler, applyProps } = createRenderer(roots, getEventPriority)
const shallowLoose = { objects: 'shallow', strict: false } as EquConfig

type Properties<T> = Pick<T, { [K in keyof T]: T[K] extends (_: any) => any ? never : K }[keyof T]>
Expand Down Expand Up @@ -88,9 +88,9 @@ export type RenderProps<TCanvas extends Element> = {
camera?: (
| Camera
| Partial<
ReactThreeFiber.Object3DNode<THREE.Camera, typeof THREE.Camera> &
ReactThreeFiber.Object3DNode<THREE.PerspectiveCamera, typeof THREE.PerspectiveCamera> &
ReactThreeFiber.Object3DNode<THREE.OrthographicCamera, typeof THREE.OrthographicCamera>
ThreeElement<typeof THREE.Camera> &
ThreeElement<typeof THREE.PerspectiveCamera> &
ThreeElement<typeof THREE.OrthographicCamera>
>
) & {
/** Flags the camera as manual, putting projection into your own hands */
Expand Down Expand Up @@ -233,9 +233,9 @@ function createRoot<TCanvas extends Element>(canvas: TCanvas): ReconcilerRoot<TC

// Set raycaster options
const { params, ...options } = raycastOptions || {}
if (!is.equ(options, raycaster, shallowLoose)) applyProps(raycaster as any, { ...options })
if (!is.equ(options, raycaster, shallowLoose)) applyProps(raycaster, { ...options } as any)
if (!is.equ(params, raycaster.params, shallowLoose))
applyProps(raycaster as any, { params: { ...raycaster.params, ...params } })
applyProps(raycaster, { params: { ...raycaster.params, ...params } } as any)

// Create default camera (one time only!)
if (!state.camera) {
Expand All @@ -247,7 +247,7 @@ function createRoot<TCanvas extends Element>(canvas: TCanvas): ReconcilerRoot<TC
: new THREE.PerspectiveCamera(75, 0, 0.1, 1000)
if (!isCamera) {
camera.position.z = 5
if (cameraOptions) applyProps(camera as any, cameraOptions as any)
if (cameraOptions) applyProps(camera, cameraOptions as any)
// Always look at center by default
if (!cameraOptions?.rotation) camera.lookAt(0, 0, 0)
}
Expand Down Expand Up @@ -317,7 +317,7 @@ function createRoot<TCanvas extends Element>(canvas: TCanvas): ReconcilerRoot<TC

// Set gl props
if (glConfig && !is.fun(glConfig) && !isRenderer(glConfig) && !is.equ(glConfig, gl, shallowLoose))
applyProps(gl as any, glConfig as any)
applyProps(gl, glConfig as any)
// Store events internally
if (events && !state.events.handlers) state.set({ events: events(store) })
// Check pixelratio
Expand Down Expand Up @@ -413,7 +413,7 @@ function unmountComponentAtNode<TElement extends Element>(canvas: TElement, call
state.gl?.renderLists?.dispose?.()
state.gl?.forceContextLoss?.()
if (state.gl?.xr) state.xr.disconnect()
dispose(state)
dispose(state.scene)
roots.delete(canvas)
if (callback) callback(canvas)
} catch (e) {
Expand Down
Loading

0 comments on commit 1e7e0a1

Please sign in to comment.