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.