Handles
Handles are the eight resize grips (plus rotation anchor) drawn around a
selection. Their layout, hit-testing, and resize math live in
@oh-just-another/state so they can be reused by the renderer, hit-test, and any
custom overlay.
Handle identifiersโ
There are eight handle positions โ four corners and four edge midpoints โ
addressed by HandleId. ALL_HANDLES lists them.
import { ALL_HANDLES, HANDLE_SIZE, handlePosition, type HandleId } from "@oh-just-another/state";
// HandleId = "nw" | "n" | "ne" | "e" | "se" | "s" | "sw" | "w"
const worldPos = handlePosition("se", bounds, zoom);
handlePosition returns the world-space point for a handle given the selection
bounds and current zoom, so the drawn dot stays a constant screen size.
Hit-testing a pointerโ
hitHandle maps a pointer position to the handle under it (or null), applying
a screen-space slop band so handles stay grabbable at any zoom. Optional trailing
arguments override the slop half-size (touch hosts pass a bigger one) and restrict
the tested handle set (e.g. corners only for aspect-locked resize).
import { hitHandle } from "@oh-just-another/state";
const grabbed = hitHandle(pointerWorld, bounds, zoom);
if (grabbed) {
// begin resize drag for `grabbed`
}
Resizingโ
resizeBounds(bounds, handle, delta) applies a world-space drag delta to the
bounds, keeping the opposite corner anchored; the result may have negative
width/height mid-drag (normalized on commit). Rotation uses the registered
rotate anchor (registerRotateAnchor / getRotateAnchor).
import { resizeBounds } from "@oh-just-another/state";
const next = resizeBounds(bounds, grabbed, dragDeltaWorld);
HANDLE_SIZE is exported so hosts can match the visual size of the drawn
handles to the hit area.