Skip to content

Bounding-Box Annotation

The classic annotation loop: in draw mode, drag out a new Rect on the background; in select mode, hand off to a SelectionController to move, resize, and delete boxes (with undo). Drawing reads event.worldPoint from each pointer event and resizes a draft rect live.

import { Stage, Rect, History, SelectionController } from '@veyrajs/core'
import type { SceneEvent } from '@veyrajs/core'
const container = document.querySelector('#app') as HTMLElement
const stage = new Stage({ container, width: 800, height: 480, background: '#0b1220' })
const layer = stage.createLayer()
const history = new History()
let controller: SelectionController | null = null
let draft: Rect | null = null
let start = { x: 0, y: 0 }
// --- Draw mode: drag out a new box on the background ---
stage.on('pointerdown', (e: SceneEvent) => {
if (controller) return // select mode owns the pointer
start = e.worldPoint
draft = new Rect({
x: start.x, y: start.y, width: 0, height: 0,
fill: 'rgba(56,189,248,0.25)', stroke: '#38bdf8', strokeWidth: 2,
})
layer.add(draft)
})
stage.on('pointermove', (e: SceneEvent) => {
if (!draft) return
const p = e.worldPoint // normalize so any drag direction works
draft.x = Math.min(start.x, p.x)
draft.y = Math.min(start.y, p.y)
draft.width = Math.abs(p.x - start.x)
draft.height = Math.abs(p.y - start.y)
})
stage.on('pointerup', () => {
if (draft && (draft.width < 4 || draft.height < 4)) draft.remove() // drop tiny boxes
draft = null
})
// --- Switch to select mode to move / resize / delete the boxes ---
function enableEditing() {
controller = new SelectionController(stage, { history })
}

The draw handlers bail with if (controller) return because the controller intercepts pointer events in the capture phase once editing is on — the two modes never fight over the same drag. Each finished box is a normal Rect, so the controller selects and transforms it for free; pass history and its moves/resizes are undoable. Wrap the layer.add(draft) in an AddNodeCommand run through the history if you want box creation itself to be undoable.