Skip to main content

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.