# react-three

React components and hooks for Three.js scenes, views, and effects.

```typescript
import {
    Three,
    ThreeView,
    ThreeViewControl,
    useThreeEffect,
    useThreeEvent,
    useThreeObject,
    useThreeForceUpdate,
} from '@yaoc1996/js-tools/react-three'
```

---

## Components

---

### `Three`

Root component that owns a `WebGLRenderer` and drives the animation loop. Renders a `<canvas>` and provides `ThreeContext` to all children. One or more `ThreeView` components must be nested inside.

#### Parameters

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | — | Child components (typically `ThreeView`). |
| `autoResize` | `boolean` | `true` | Automatically resize the renderer when the parent element changes size. |
| `canvasRef` | `MutableRefObject<HTMLCanvasElement \| null> \| ((e: HTMLCanvasElement) => void)` | — | Optional ref or callback to access the underlying `<canvas>` element. |

#### Example

```typescript
<Three>
    <ThreeView mode="perspective">
        {/* hooks and objects here */}
    </ThreeView>
</Three>
```

---

### `ThreeView`

Owns a `Scene`, `Camera`, and `EventHandler`. Registers itself with the parent `Three` component and provides `ThreeViewContext` to its children.

#### Parameters

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `mode` | `'perspective' \| 'orthographic'` | — | Camera type. |
| `xOffset` | `number \| ((width: number) => number)` | `0` | Viewport x offset in pixels, or a function of canvas width. |
| `yOffset` | `number \| ((height: number) => number)` | `0` | Viewport y offset in pixels, or a function of canvas height. |
| `width` | `number \| ((width: number) => number)` | identity | Viewport width in pixels, or a function of canvas width. |
| `height` | `number \| ((height: number) => number)` | identity | Viewport height in pixels, or a function of canvas height. |
| `children` | `ReactNode` | — | Hook-consumers and object components. |

#### Example

```typescript
<Three>
    <ThreeView mode="perspective" width={(w) => w / 2}>
        {/* left half of canvas */}
    </ThreeView>
    <ThreeView mode="orthographic" xOffset={(w) => w / 2} width={(w) => w / 2}>
        {/* right half of canvas */}
    </ThreeView>
</Three>
```

---

## `class ThreeViewControl`

Controls viewport layout and tracks camera state to decide whether a re-render is needed.

### `constructor(xOffset, yOffset, width, height)`

| Name | Type | Description |
|------|------|-------------|
| `xOffset` | `number \| ((width: number) => number)` | X offset value or factory. |
| `yOffset` | `number \| ((height: number) => number)` | Y offset value or factory. |
| `width` | `number \| ((width: number) => number)` | Width value or factory. |
| `height` | `number \| ((height: number) => number)` | Height value or factory. |

### `getXOffset(width) → number`

Returns the x offset given the total canvas width.

### `getYOffset(height) → number`

Returns the y offset given the total canvas height.

### `getWidth(width) → number`

Returns the viewport width given the total canvas width.

### `getHeight(height) → number`

Returns the viewport height given the total canvas height.

### `update(camera) → boolean`

Compares the camera's position, rotation, and projection matrix against the last recorded state. Returns `true` if anything changed (frame should be rendered) and records the new state. Also returns `true` if `forceUpdate()` was called since the last `update()`.

### `forceUpdate()`

Forces the next `update()` call to return `true` regardless of camera state.

---

## Hooks

---

### `useThreeEffect<Mode>(effect, deps?)`

Runs `effect` inside the Three.js animation context. Equivalent to `useEffect` but receives `{ renderer, scene, camera }` and re-runs when `renderer`, `scene`, `camera`, `control`, or any element of `deps` changes. Calls `control.forceUpdate()` after the effect and after cleanup.

#### Parameters

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `effect` | `ThreeEffect<Mode>` | — | Callback receiving `{ renderer, scene, camera }`. May return a destructor. |
| `deps` | `DependencyList` | `[]` | Extra dependencies beyond the built-in context values. |

#### Example

```typescript
useThreeEffect(({ scene }) => {
    const light = new AmbientLight(0xffffff, 0.5)
    scene.add(light)
    return () => scene.remove(light)
}, [])
```

---

### `useThreeObject<Mode>(createObject)`

Adds an `Object3D` to the scene and removes it on cleanup. Accepts a plain `Object3D`, a factory function, or `undefined`.

#### Parameters

| Name | Type | Description |
|------|------|-------------|
| `createObject` | `CreateObjectEffect<Mode> \| Object3D \| undefined` | Object, factory `({ renderer, scene, camera }) => Object3D \| [Object3D, destructor] \| void`, or `undefined`. |

#### Example

```typescript
// static object
const mesh = useMemo(() => new Mesh(geo, mat), [])
useThreeObject(mesh)

// factory with cleanup
useThreeObject(({ scene }) => {
    const helper = new GridHelper(10, 10)
    return [helper, () => helper.geometry.dispose()]
})
```

---

### `useThreeEvent<Mode>(name, callback)`

Subscribes to a Three.js render-loop event. The callback is stable-ref-tracked: changing its identity between renders does **not** re-register the listener.

#### Parameters

| Name | Type | Description |
|------|------|-------------|
| `name` | `'beforeupdate' \| 'beforerender' \| 'afterrender' \| 'afterupdate'` | Event name. |
| `callback` | `ThreeEventCallback<Mode, EventName>` | Called with the event payload each time the event fires. |

#### Example

```typescript
useThreeEvent('beforerender', ({ time, camera }) => {
    camera.position.x = Math.sin(time * 0.001)
})
```

#### Notes

- `beforeupdate` / `afterupdate` fire every animation tick regardless of whether a frame is rendered.
- `beforerender` / `afterrender` fire only when `ThreeViewControl.update()` returns `true`.

---

### `useThreeForceUpdate()`

Returns a stable callback that calls `control.forceUpdate()`, scheduling a re-render on the next animation tick.

#### Returns

`() => void` — stable callback (does not change identity across renders).

#### Example

```typescript
const forceUpdate = useThreeForceUpdate()

useEffect(() => {
    // trigger a redraw after an external state change
    forceUpdate()
}, [someExternalValue, forceUpdate])
```

---

## Types

```typescript
type CameraMode = 'perspective' | 'orthographic'

type ThreeEffect<Mode extends CameraMode = CameraMode> = (params: {
    renderer: WebGLRenderer
    scene: Scene
    camera: CameraTypeMap[Mode]
}) => Destructor | void

type CreateObjectEffect<Mode extends CameraMode> = (params: {
    renderer: WebGLRenderer
    scene: Scene
    camera: CameraTypeMap[Mode]
}) => void | Object3D | readonly [Object3D, Destructor]

type ThreeEventCallback<
    Mode extends CameraMode,
    EventName extends keyof ThreeEventMap<Mode>
> = (params: ThreeEventMap<Mode>[EventName]) => void

type ThreeEventMap<Mode extends CameraMode> = {
    beforeupdate: { time: number; scene: Scene; renderer: WebGLRenderer; camera: CameraTypeMap[Mode] }
    beforerender: { time: number; scene: Scene; renderer: WebGLRenderer; camera: CameraTypeMap[Mode] }
    afterrender:  { time: number; scene: Scene; renderer: WebGLRenderer; camera: CameraTypeMap[Mode] }
    afterupdate:  { time: number; scene: Scene; renderer: WebGLRenderer; camera: CameraTypeMap[Mode] }
}
```
