Skip to main content

Snapping

@oh-just-another/scene ships a pluggable snap engine. The SnapEngine runs a list of contributors against a probe point and returns the best candidate within a threshold. Three built-in contributors cover the common cases: gridSnapper, anchorSnapper, and outlineSnapper.

Running the engineโ€‹

import { SnapEngine, gridSnapper, anchorSnapper, outlineSnapper } from "@oh-just-another/scene";

const engine = new SnapEngine([gridSnapper, anchorSnapper, outlineSnapper]);

const result = engine.snap({
scene,
probe: { x: 103, y: 48 },
threshold: 8,
gesture: "draw-edge",
});
if (result.best) console.log(result.best.snapped, result.best.kind);

The SnapContext carries the scene, the probe point, a world-unit threshold, the current gesture ("draw-edge", "edit-edge-endpoint", "move-shape", or "draw-shape"), and an optional excludeElementIds set (e.g. the shape being dragged). snap returns { best, all }: every in-threshold SnapCandidate sorted by distance, with best the closest (or null). A candidate's distance is the squared distance from the probe.

The anchor and outline snappers contribute only for edge gestures ("draw-edge", "edit-edge-endpoint"); the grid snapper applies to all gestures.

Snap-to-grid configurationโ€‹

Grid snapping uses the fixed world-unit spacing from resolveSnapSpacing() (which returns DEFAULT_GRID_SPACING, 20). gridSnapper itself no-ops when the grid is hidden (gridEnabled: false). The separate snapToGrid viewport flag is a host-level opt-out read via isSnapToGridEnabled โ€” undefined counts as enabled; the Editor in @oh-just-another/state snaps only when both gridEnabled and isSnapToGridEnabled(viewport) hold.

import { isSnapToGridEnabled, resolveSnapSpacing } from "@oh-just-another/scene";

const on = isSnapToGridEnabled(scene.viewport);
const spacing = resolveSnapSpacing();

The spacing default lives in the package's constants.ts (DEFAULT_GRID_SPACING), so hosts that want a different grid can tune it there.

Object snapping and size assistsโ€‹

The Editor in @oh-just-another/state also snaps move / resize gestures to other shapes, independent of the grid: the dragged shape's edges and centres align with the edges / centres of visible nearby shapes within OBJECT_SNAP_THRESHOLD_PX (6 screen px). While moving, an edge meets an edge and a centre meets a centre (never edge-to-centre); while resizing, the dragged edge may land on any line. On a tie an edge beats a centre. Object snapping wins over grid snapping when it lands; Cmd / Ctrl suppresses both for the gesture.

Who takes part: every visible on-screen shape is a target except brush strokes, groups, shapes rotated off the 90ยฐ grid, shapes smaller than OBJECT_SNAP_MIN_SIZE_PX on screen, and the selection itself; nothing snaps on a view with more than OBJECT_SNAP_MAX_CANDIDATES shapes. Brush strokes never snap as movers either. A multi-selection snaps by its frame edges only (no centre line); a single shape snaps by its world AABB.

The overlay draws a dashed alignment guide through the aligned lines, overshooting both shapes by SNAP_GUIDE_OVERSHOOT_PX; for an edge snap the gap between the two shapes along the guide is measured with a solid ticked distance segment, labelled with the rounded distance when showObjectSize is on (SnapGuide.kind, moving, other; gapIntervals computes the stretches).

While resizing, the dragged edge can also land where the width / height equals a nearby shape's (SIZE_SUGGEST_THRESHOLD_PX); the matched size is measured with the same ticked segment on both shapes (SizeMatch.axis), and a W ร— H pill shows the live size under the shape.

All three are per-user preferences (see EditorPreferences):

editor.setPreferences({ snapObjects: true, suggestObjectSize: true, showObjectSize: true });
editor.snapGuides; // SnapGuide[] of the current gesture tick (overlay input)
editor.sizeReadout; // { bounds, width, height } | null while resizing
editor.sizeMatch; // bounds of the size-matched shape | null

The pure math lives in snapMoveDeltaToObjects / snapResizeDeltaToObjects; thresholds and guide colours are in the package's constants.ts.

Custom contributorsโ€‹

A SnapContributor is an object with an id and a contribute(ctx: SnapContext): readonly SnapCandidate[] method. Add your own to the engine's list to snap to guides, rulers, or domain-specific geometry; the "guideline" candidate kind is reserved for alignment-guide contributors.