# API reference

> Every public export of @dirtytalk/spatial with exact TypeScript signatures, parameters, return values, errors and examples.

Source: https://dirtytalk.42.dog/docs/spatial/api/

All exports come from `@dirtytalk/spatial`. For guided explanations see [Scene graph](/docs/spatial/scene-graph/), [Rendering](/docs/spatial/rendering/) and [Pointer events](/docs/spatial/pointer-events/).

## Types

### Rect

```ts
interface Rect {
  x: number;
  y: number;
  w: number;
  h: number;
}
```

A 2D axis-aligned rectangle in CSS pixels.

```ts
import type { Rect } from '@dirtytalk/spatial';

const r: Rect = { x: 10, y: 10, w: 120, h: 40 };
```

### DamageKind

```ts
type DamageKind = 'paint' | 'layout' | 'data';
```

Classifies damage and determines which pipeline stages run: `'data'` runs `rebuildData`, `doLayout` and paint; `'layout'` runs `doLayout` and paint; `'paint'` runs paint only.

```ts
import type { DamageKind } from '@dirtytalk/spatial';

const kind: DamageKind = 'layout';
```

### Damage

```ts
interface Damage {
  rect: Rect;
  kind: DamageKind;
  node?: unknown;
}
```

A single damage entry. `node` is the `SceneNode` that raised it; root-level damage may omit it.

```ts
import type { Damage } from '@dirtytalk/spatial';

const d: Damage = { rect: { x: 0, y: 0, w: 10, h: 10 }, kind: 'paint' };
```

### DirtyRegion

```ts
type DirtyRegion = readonly Damage[];
```

The Region type of the spatial `DirtyChannel`.

```ts
import type { DirtyRegion } from '@dirtytalk/spatial';

const region: DirtyRegion = [
  { rect: { x: 0, y: 0, w: 10, h: 10 }, kind: 'paint' },
];
```

## Rect helpers

### rectOverlaps

```ts
function rectOverlaps(a: Rect, b: Rect): boolean;
```

Returns `true` if the rects share area. Rects that only touch, and rects with zero or negative `w` or `h`, do not overlap.

```ts
import { rectOverlaps } from '@dirtytalk/spatial';

rectOverlaps({ x: 0, y: 0, w: 10, h: 10 }, { x: 10, y: 0, w: 10, h: 10 }); // false
```

### rectEquals

```ts
function rectEquals(a: Rect, b: Rect): boolean;
```

Returns `true` if `x`, `y`, `w` and `h` are all strictly equal.

```ts
import { rectEquals } from '@dirtytalk/spatial';

rectEquals({ x: 0, y: 0, w: 1, h: 1 }, { x: 0, y: 0, w: 1, h: 1 }); // true
```

### unionRects

```ts
function unionRects(rects: readonly Rect[]): Rect;
```

Returns the bounding rect of `rects`, or `{ x: 0, y: 0, w: 0, h: 0 }` for an empty array.

```ts
import { unionRects } from '@dirtytalk/spatial';

unionRects([
  { x: 0, y: 0, w: 10, h: 10 },
  { x: 20, y: 20, w: 10, h: 10 },
]); // { x: 0, y: 0, w: 30, h: 30 }
```

### rectClamp

```ts
function rectClamp(inner: Rect, outer: Rect): Rect;
```

Returns the intersection of `inner` and `outer`. When they do not overlap, the result has `w` and `h` of `0` (its `x` and `y` are not meaningful).

```ts
import { rectClamp } from '@dirtytalk/spatial';

rectClamp({ x: -5, y: 0, w: 20, h: 10 }, { x: 0, y: 0, w: 10, h: 10 });
// { x: 0, y: 0, w: 10, h: 10 }
```

### pointInRect

```ts
function pointInRect(x: number, y: number, r: Rect): boolean;
```

Half-open containment test over `[x, x + w) x [y, y + h)`: the top-left edge is inside, the bottom-right edge is outside.

```ts
import { pointInRect } from '@dirtytalk/spatial';

pointInRect(10, 10, { x: 10, y: 10, w: 5, h: 5 }); // true
pointInRect(15, 15, { x: 10, y: 10, w: 5, h: 5 }); // false
```

## Scene

### RectSpace

```ts
const RectSpace: Space<DirtyRegion>;
```

The `Space` (from `@dirtytalk/engine`) over `DirtyRegion`. `intersects(interest, dirty)` is `true` for any `'data'` or `'layout'` entry when both sides are non-empty, and for `'paint'` entries only when their rect overlaps an interest rect. Also implements the optional in-place `unionInto`. See [Rendering](/docs/spatial/rendering/#rectspace).

```ts
import { RectSpace } from '@dirtytalk/spatial';
import { DirtyChannel, SyncScheduler } from '@dirtytalk/engine';
import type { DirtyRegion } from '@dirtytalk/spatial';

const channel = new DirtyChannel<DirtyRegion>(RectSpace, new SyncScheduler());
```

### SceneNodeOptions

```ts
interface SceneNodeOptions {
  bounds?: Rect;
  clipsOverflow?: boolean;
}
```

| Option          | Default                      | Meaning                                         |
| --------------- | ---------------------------- | ----------------------------------------------- |
| `bounds`        | `{ x: 0, y: 0, w: 0, h: 0 }` | Initial bounds.                                 |
| `clipsOverflow` | `false`                      | Clip descendants' damage to this node's bounds. |

```ts
import type { SceneNodeOptions } from '@dirtytalk/spatial';

const options: SceneNodeOptions = { bounds: { x: 0, y: 0, w: 50, h: 50 } };
```

### SceneNode

```ts
abstract class SceneNode {
  bounds: Rect;
  parent: SceneNode | null;
  children: SceneNode[];
  clipsOverflow: boolean;

  constructor(options?: SceneNodeOptions);

  abstract paint(layer: unknown): void;
  rebuildData?(): void;
  doLayout?(): void;

  protected markDamaged(kind: DamageKind, rect?: Rect): void;
  protected batch(fn: () => void): void;

  setBounds(next: Rect): void;
  adoptChild(child: SceneNode): void;
  removeChild(child: SceneNode): void;
}
```

Abstract base for painted objects. It owns its bounds and parent/children, and reports damage up the parent chain to the `SceneRoot`.

**Members**

- `paint(layer)`: required. Draw the node. `SceneRoot` passes `undefined` as `layer`.
- `rebuildData()`: optional. Called once per frame for `'data'` damage raised by this node.
- `doLayout()`: optional. Called once per frame for `'data'` or `'layout'` damage targeting this node.
- `markDamaged(kind, rect?)`: protected. Reports damage; `rect` defaults to `this.bounds` and is clamped by `clipsOverflow` ancestors. No-op when the node has no `SceneRoot` ancestor.
- `batch(fn)`: protected. Runs `fn`, buffering this node's damage, then emits one entry per kind and target node with the union rect. Nested calls are absorbed by the outer batch. Damage is emitted even if `fn` throws, and the error propagates.
- `setBounds(next)`: no-op if equal to current bounds. Otherwise emits `paint` damage for the old and new rects, and `layout` damage for the new rect targeting the parent (if any).
- `adoptChild(child)`: detaches `child` from its previous parent, attaches it, emits `paint` damage for `child.bounds`. Throws `Error('SceneNode.adoptChild: a node cannot adopt itself or an ancestor')` if `child` is this node or an ancestor.
- `removeChild(child)`: no-op if `child` is not a child. Otherwise removes it, emits `paint` damage for its bounds and sets `child.parent = null`.

```ts
import { SceneNode } from '@dirtytalk/spatial';

class Dot extends SceneNode {
  color = 'black';

  setColor(color: string) {
    this.color = color;
    this.markDamaged('paint');
  }

  paint() {}
}

const parent = new Dot({ bounds: { x: 0, y: 0, w: 100, h: 100 } });
parent.adoptChild(new Dot({ bounds: { x: 10, y: 10, w: 5, h: 5 } }));
```

### SceneRoot

```ts
class SceneRoot extends SceneNode {
  readonly channel: DirtyChannel<DirtyRegion>;
  readonly renderer: Renderer2D;
  fullFrame: boolean;

  constructor(renderer: Renderer2D, options?: SceneRootOptions);

  dispose(): void;
  hitTest(x: number, y: number): SceneNode | null;
  paint(layer: unknown): void;
  protected paintFrame(regions: readonly Rect[]): number;
  _emitDamage(damage: Damage): void;
}
```

Root of a scene graph. Owns the `DirtyChannel`, the scheduler and the renderer, and runs the data, layout, paint pipeline on each flush.

**Parameters**

- `renderer: Renderer2D`: receives frames.
- `options?: SceneRootOptions`: see [SceneRootOptions](#scenerootoptions).

**Members**

- `channel`: the underlying `DirtyChannel`. `renderer`: the renderer passed to the constructor.
- `fullFrame`: default `false`. When `true`, every frame repaints the whole bounds and visits all children without culling. A cost-comparison baseline; leave `false` in production.
- `dispose()`: cancels any pending frame; later damage is ignored.
- `hitTest(x, y)`: returns the topmost, deepest node containing the point, or `null`. See [Pointer events](/docs/spatial/pointer-events/#hit-testing).
- `paint(layer)`: calls `paint(layer)` on every child in adoption order. The render pipeline does not use it; it paints through `paintFrame`.
- `paintFrame(regions)`: protected stage-3 hook. Default: `beginFrame(regions)`, paint children overlapping the regions, `endFrame()`; returns the number of nodes painted. `regions` is never empty.
- `_emitDamage(damage)`: internal, called by `SceneNode`. Do not call it directly.

```ts
import { SceneRoot } from '@dirtytalk/spatial';
import type { Renderer2D } from '@dirtytalk/spatial';
import { ManualScheduler } from '@dirtytalk/engine';

const renderer: Renderer2D = {
  beginFrame(regions) {
    console.log(regions);
  },
  endFrame() {},
};
const scheduler = new ManualScheduler();
const root = new SceneRoot(renderer, {
  scheduler,
  bounds: { x: 0, y: 0, w: 800, h: 600 },
});

scheduler.pump(); // flush pending damage into a frame
root.dispose();
```

## Rendering

### Renderer2D

```ts
interface Renderer2D {
  beginFrame(regions: readonly Rect[]): void;
  endFrame(): void;
}
```

The contract a renderer implements. `beginFrame` receives the frame's individual damage rects (never empty), not their union. `endFrame` closes the frame. Frames with no region left after clamping are skipped entirely.

```ts
import type { Renderer2D } from '@dirtytalk/spatial';

const renderer: Renderer2D = {
  beginFrame(regions) {
    console.log('scissor to', regions.length, 'rects');
  },
  endFrame() {},
};
```

### SceneRootOptions

```ts
interface SceneRootOptions {
  scheduler?: Scheduler;
  bounds?: Rect;
  onFrameTiming?: (timing: FrameTiming) => void;
}
```

| Option          | Default              | Meaning                                                               |
| --------------- | -------------------- | --------------------------------------------------------------------- |
| `scheduler`     | `new RAFScheduler()` | Decides when pending damage becomes a frame.                          |
| `bounds`        | zero rect            | Root bounds; the default interest region and clamp for frame regions. |
| `onFrameTiming` | none                 | Called after each painted frame with a `FrameTiming`.                 |

```ts
import type { SceneRootOptions } from '@dirtytalk/spatial';
import { MicrotaskScheduler } from '@dirtytalk/engine';

const options: SceneRootOptions = {
  scheduler: new MicrotaskScheduler(),
  bounds: { x: 0, y: 0, w: 640, h: 480 },
};
```

### FrameTiming

```ts
interface FrameTiming {
  layoutMs: number;
  paintMs: number;
  paintedNodes: number;
}
```

- `layoutMs`: ms in the data and layout stages.
- `paintMs`: ms in the paint stage (`beginFrame`, paint walk, `endFrame`).
- `paintedNodes`: number of top-level nodes whose `paint()` ran.

```ts
import type { FrameTiming } from '@dirtytalk/spatial';

const log = (t: FrameTiming) => console.log(`${t.paintedNodes} nodes`);
```

## Pointer

### SpatialPointerEvent

```ts
interface SpatialPointerEvent {
  type: 'down' | 'move' | 'up' | 'cancel';
  x: number;
  y: number;
  buttons: number;
  pointerId: number;
}
```

A minimal, surface-agnostic pointer event. Compatible with values taken from a DOM `PointerEvent`.

```ts
import type { SpatialPointerEvent } from '@dirtytalk/spatial';

const e: SpatialPointerEvent = {
  type: 'down',
  x: 20,
  y: 20,
  buttons: 1,
  pointerId: 1,
};
```

### PointerHandler

```ts
interface PointerHandler {
  onPointerDown?(e: SpatialPointerEvent): void;
  onPointerMove?(e: SpatialPointerEvent): void;
  onPointerUp?(e: SpatialPointerEvent): void;
  onPointerCancel?(e: SpatialPointerEvent): void;
}
```

Optional methods a `SceneNode` subclass defines to receive events from `PointerRouter`. Missing methods are skipped.

```ts
import { SceneNode } from '@dirtytalk/spatial';
import type { PointerHandler, SpatialPointerEvent } from '@dirtytalk/spatial';

class Handle extends SceneNode implements PointerHandler {
  onPointerUp(e: SpatialPointerEvent) {
    console.log('released at', e.x, e.y);
  }

  paint() {}
}
```

### PointerRouter

```ts
class PointerRouter {
  constructor(root: SceneRoot);
  dispatch(e: SpatialPointerEvent): SceneNode | null;
}
```

Hit-tests the scene and dispatches events to a single node, with per-pointer capture. There is no bubbling.

**Parameters**

- `root: SceneRoot`: the scene to hit-test.
- `dispatch(e)`: the event to route.

**Returns** the node that received the event, or `null` if none did.

**Behaviour**

- `'down'`: hit-test; if hit, capture the node for `e.pointerId` and call `onPointerDown`.
- `'move'`, `'up'`, `'cancel'`: delivered to the captured node for that pointer (`'up'` and `'cancel'` release it). Without a captured node, `'move'` is delivered to the hit node and `'up'` and `'cancel'` are ignored.

See [Pointer events](/docs/spatial/pointer-events/) for details and DOM wiring.

```ts
import { PointerRouter } from '@dirtytalk/spatial';
import type { SceneRoot } from '@dirtytalk/spatial';

declare const root: SceneRoot;

const router = new PointerRouter(root);
const target = router.dispatch({
  type: 'down',
  x: 20,
  y: 20,
  buttons: 1,
  pointerId: 1,
});
```
