03 @dirtytalk/spatial

Spatial

Rects of damage. Repaint just those.

Chapter 03 Watch spatial run, interactively โ†’

@dirtytalk/spatial tracks changes in 2D space. A region is a list of damage entries, each with a Rect and a kind ('data' | 'layout' | 'paint'), so a renderer can redraw only what changed.

pnpm add @dirtytalk/spatial @dirtytalk/engine

Scene

Subclass SceneNode, call markDamaged when something changes, and attach nodes to a SceneRoot with a Renderer2D.

import { SceneNode, SceneRoot } from '@dirtytalk/spatial';
import type { Renderer2D, Rect } from '@dirtytalk/spatial';
import { SyncScheduler } from '@dirtytalk/engine';

const renderer: Renderer2D = {
  beginFrame(regions: readonly Rect[]) {
    console.log('repaint', regions);
  },
  endFrame() {},
};

class Button extends SceneNode {
  label = '';

  setLabel(label: string) {
    if (this.label === label) return;
    this.label = label;
    this.markDamaged('paint');
  }

  paint() {}
}

const root = new SceneRoot(renderer, { scheduler: new SyncScheduler() });
root.bounds = { x: 0, y: 0, w: 800, h: 600 };

const button = new Button();
button.bounds = { x: 10, y: 10, w: 120, h: 40 };
root.adoptChild(button);

button.setLabel('Click me'); // repaint [{ x: 10, y: 10, w: 120, h: 40 }]

SceneRoot owns the channel and runs the data โ†’ layout โ†’ paint pipeline. This package ships the Renderer2D interface, not an implementation.

Pointer routing

PointerRouter hit-tests the scene tree and dispatches framework-agnostic SpatialPointerEvents.

import { PointerRouter } from '@dirtytalk/spatial';

const router = new PointerRouter(root);

canvas.addEventListener('pointerdown', (e) => {
  router.dispatch({
    type: 'down',
    x: e.offsetX,
    y: e.offsetY,
    buttons: e.buttons,
    pointerId: e.pointerId,
  });
});

Rect helpers

rectOverlaps, rectEquals, unionRects, rectClamp and pointInRect, plus RectSpace, the Space the engine uses for damage lists.