Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: container effects, recursive dispose, portal state #72

Merged
merged 7 commits into from
Sep 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 91 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,14 @@
- [Canvas](#canvas)
- [Canvas Props](#canvas-props)
- [Custom Canvas](#custom-canvas)
- [Testing](#testing)
- [Root State](#root-state)
- [Creating Elements](#creating-elements)
- [JSX, properties, and shortcuts](#jsx-properties-and-shortcuts)
- [Setting constructor arguments via `args`](#setting-constructor-arguments-via-args)
- [Attaching into element properties via `attach`](#attaching-into-element-properties-via-attach)
- [Creating custom elements via `extend`](#creating-custom-elements-via-extend)
- [Adding third-party objects via `<primitive />`](#adding-third-party-objects-via-primitive-)
- [Hooks](#hooks)
- [Root State](#root-state)
- [Accessing state via `useOGL`](#accessing-state-via-useogl)
- [Frameloop subscriptions via `useFrame`](#frameloop-subscriptions-via-useframe)
- [Loading assets via `useLoader`](#loading-assets-via-useloader)
Expand All @@ -39,6 +38,8 @@
- [Access internals via `useInstanceHandle`](#access-internals-via-useinstancehandle)
- [Events](#events)
- [Custom Events](#custom-events)
- [Portals](#portals)
- [Testing](#testing)

## Installation

Expand All @@ -61,6 +62,11 @@ react-ogl itself is super minimal, but you can use the familiar [@react-three/fi

This example uses [`create-react-app`](https://reactjs.org/docs/create-a-new-react-app.html#create-react-app) for the sake of simplicity, but you can use your own environment or [create a codesandbox](https://react.new).

<details>
<summary>Show full example</summary>

<br />

```bash
# Create app
npx create-react-app my-app
Expand Down Expand Up @@ -143,10 +149,17 @@ createRoot(document.getElementById('root')).render(
)
```

</details>

### react-native

This example uses [`expo-cli`](https://docs.expo.dev/get-started/create-a-new-app) but you can create a bare app with `react-native` CLI as well.

<details>
<summary>Show full example</summary>

<br />

```bash
# Create app and cd into it
npx expo init my-app # or npx react-native init my-app
Expand Down Expand Up @@ -244,6 +257,8 @@ export default () => (
)
```

</details>

## Canvas

react-ogl provides an x-platform `<Canvas />` component for web and native that serves as the entrypoint for your OGL scenes. It is a real DOM canvas or native view that accepts OGL elements as children (see [creating elements](#creating-elements)).
Expand Down Expand Up @@ -327,65 +342,6 @@ function CustomCanvas({ children }) {
}
```

### Testing

In addition to `createRoot` (see [custom canvas](#custom-canvas)), react-ogl exports an internal `reconciler` which can be used to safely flush async effects in tests via `reconciler#act`. The following emulates a legacy root and asserts against `RootState` (see [root state](#root-state)).

```tsx
import * as React from 'react'
import * as OGL from 'ogl'
import { type RootState, createRoot, act } from 'react-ogl'

it('tests against a react-ogl component or scene', async () => {
const transform = React.createRef<OGL.Transform>()
let state: RootState = null!

await act(async () => {
const root = createRoot(document.createElement('canvas'))
state = root.render(<transform ref={transform} />).getState()
})

expect(transform.current).toBeInstanceOf(OGL.Transform)
expect(state.scene.children.length).toBe(1)
expect(state.scene.children[0]).toBe(transform.current)
})
```

### Root State

Each `<Canvas />` or `Root` encapsulates its own OGL state via [React context](https://reactjs.org/docs/context.html) and a [Zustand](https://github.com/pmndrs/zustand) store, as defined by `RootState`. This can be accessed and modified with the `onCreated` canvas prop, and with hooks like `useOGL` (see [hooks](#hooks)).

```tsx
interface RootState {
// Zustand setter and getter for live state manipulation.
// See https://github.com/pmndrs/zustand
get(): RootState
set(fn: (previous: RootState) => (next: Partial<RootState>)): void
// Canvas layout information
size: { width: number; height: number }
// OGL scene internals
renderer: OGL.Renderer
gl: OGL.OGLRenderingContext
scene: OGL.Transform
camera: OGL.Camera
// OGL perspective and frameloop preferences
orthographic: boolean
frameloop: 'always' | 'never'
// Internal XR manager to enable WebXR features
xr: XRManager
// Frameloop internals for custom render loops
priority: number
subscribed: React.MutableRefObject<Subscription>[]
subscribe: (refCallback: React.MutableRefObject<Subscription>, renderPriority?: number) => void
unsubscribe: (refCallback: React.MutableRefObject<Subscription>, renderPriority?: number) => void
// Optional canvas event manager and its state
events?: EventManager
mouse: OGL.Vec2
raycaster: OGL.Raycast
hovered: Map<number, Instance<OGL.Mesh>['object']>
}
```

## Creating elements

react-ogl renders React components into an OGL scene-graph, and can be used on top of other renderers like [react-dom](https://npmjs.com/react-dom) and [react-native](https://npmjs.com/react-native) that render for web and native, respectively. react-ogl components are defined by primitives or lower-case elements native to the OGL namespace (for custom elements, see [extend](#creating-custom-elements-via-extend)).
Expand Down Expand Up @@ -564,6 +520,41 @@ const object = new OGL.Transform()

react-ogl ships with hooks that allow you to tie or request information to your components. These are called within the body of `<Canvas />` and contain imperative and possibly stateful code.

### Root State

Each `<Canvas />` or `Root` encapsulates its own OGL state via [React context](https://reactjs.org/docs/context.html) and a [Zustand](https://github.com/pmndrs/zustand) store, as defined by `RootState`. This can be accessed and modified with the `onCreated` canvas prop, and with hooks like `useOGL`.

```tsx
interface RootState {
// Zustand setter and getter for live state manipulation.
// See https://github.com/pmndrs/zustand
get(): RootState
set(fn: (previous: RootState) => (next: Partial<RootState>)): void
// Canvas layout information
size: { width: number; height: number }
// OGL scene internals
renderer: OGL.Renderer
gl: OGL.OGLRenderingContext
scene: OGL.Transform
camera: OGL.Camera
// OGL perspective and frameloop preferences
orthographic: boolean
frameloop: 'always' | 'never'
// Internal XR manager to enable WebXR features
xr: XRManager
// Frameloop internals for custom render loops
priority: number
subscribed: React.MutableRefObject<Subscription>[]
subscribe: (refCallback: React.MutableRefObject<Subscription>, renderPriority?: number) => void
unsubscribe: (refCallback: React.MutableRefObject<Subscription>, renderPriority?: number) => void
// Optional canvas event manager and its state
events?: EventManager
mouse: OGL.Vec2
raycaster: OGL.Raycast
hovered: Map<number, Instance<OGL.Mesh>['object']>
}
```

### Accessing state via `useOGL`

Returns the current canvas' `RootState`, describing react-ogl state and OGL rendering internals (see [root state](#root-state)).
Expand Down Expand Up @@ -779,3 +770,42 @@ const events = {
```

</details>

## Portals

Portal children into a foreign OGL element via `createPortal`, which can modify children's `RootState`. This is particularly useful for postprocessing and complex render effects.

```tsx
function Component {
// scene & camera are inherited from portal parameters
const { scene, camera, ... } = useOGL()
}

const scene = new OGL.Transform()
const camera = new OGL.Camera()

<transform>
{createPortal(<Component />, scene, { camera })
</transform>
```

## Testing

In addition to `createRoot` (see [custom canvas](#custom-canvas)), react-ogl exports an internal `act` which can be used to safely flush async effects in tests. The following emulates a legacy root and asserts against `RootState` (see [root state](#root-state)).

```tsx
import * as React from 'react'
import * as OGL from 'ogl'
import { type Root, type RootStore, type RootState, createRoot, act } from 'react-ogl'

it('tests against a react-ogl component or scene', async () => {
const transform = React.createRef<OGL.Transform>()

const root: Root = createRoot(document.createElement('canvas'))
const store: RootStore = await act(async () => root.render(<transform ref={transform} />))
const state: RootState = store.getState()

expect(transform.current).toBeInstanceOf(OGL.Transform)
expect(state.scene.children).toStrictEqual([transform.current])
})
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@types/webxr": "^0.5.0",
"react-reconciler": "^0.27.0",
"react-use-measure": "^2.1.1",
"scheduler": "^0.23.0",
"suspend-react": "^0.0.8",
"zustand": "^3.7.1"
},
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const POINTER_EVENTS = [
/**
* React internal props.
*/
export const RESERVED_PROPS = ['children', 'key', 'ref', '__self', '__source'] as const
export const RESERVED_PROPS = ['children', 'key', 'ref', '__self', '__source']

/**
* react-ogl instance-specific props.
Expand Down
5 changes: 4 additions & 1 deletion src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ export const useIsomorphicLayoutEffect =
*/
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])
useIsomorphicLayoutEffect(
() => void (instance.current = (ref.current as unknown as Instance<O>['object']).__ogl!),
[ref],
)
return instance
}

Expand Down
Loading