Shape Utils — @edv4h/usketch-shape-utils

Common helpers for writing uSketch shape plugins.

@edv4h/usketch-shape-utils provides common functions for writing uSketch shape plugins. It is a set of React-independent pure functions, so that third parties building their own plugins (e.g. @acme/usketch-plugin-shape-basic) can reuse boilerplate such as bounds calculation, hit testing, resizing, editable text, and free-position search.

Install

pnpm add @edv4h/usketch-shape-utils @edv4h/usketch-shared

Bounds

function getBounds(data: ShapeData): BoundingBox;

Returns the shape’s AABB as { x, y, width, height }. Most shapes can pass this straight to ShapeDefinition.getBounds.

Hit Test

function aabbHitTest(data: ShapeData, point: Point): boolean;
function ellipseHitTest(data: ShapeData, point: Point): boolean;
function lineHitTest(data: ShapeData, point: Point, tolerance?: number): boolean;
function pointInPolygon(point: Point, polygon: Point[]): boolean;
FunctionPurpose
aabbHitTestRectangular AABB
ellipseHitTestEllipse
lineHitTestLine segment (true within tolerance px, default 4)
pointInPolygonArbitrary polygon (triangle, hexagon, star, etc.)

For rotation support, wrap the hit test with withRotation(hitTest) from @edv4h/usketch-shared:

import { withRotation } from "@edv4h/usketch-shared";
import { pointInPolygon } from "@edv4h/usketch-shape-utils";

const hitTest = withRotation((data, point) =>
  pointInPolygon(point, getHexagonPoints(data)),
);

Resize

function createResize(minW: number, minH: number): (
  data: ShapeData,
  handle: ResizeHandle,
  delta: Point,
) => ShapeData;

Generates a resize function that handles all 8 resize handles (se / nw / ne / sw / e / w / n / s). Specify the minimum width and height.

import { createResize } from "@edv4h/usketch-shape-utils";

const resize = createResize(10, 10); // minimum 10x10 px

Editable Text

Helpers for wiring contentEditable-based in-place text editing into a shape plugin.

function createEditableTextController(
  ctx: PluginContext,
  options: EditableTextOptions,
): EditableTextController;

function editableTextProps(id: string, text: string): { /* props for a contentEditable div */ };

// Custom event names the controller listens for
const TEXT_INPUT_EVENT: string;   // "usketch:text-input"
const TEXT_BLUR_EVENT: string;    // "usketch:text-blur"
const TEXT_ESCAPE_EVENT: string;  // "usketch:text-escape"

createEditableTextController wires up the shared editable-text state machine plus DOM/canvas listeners, returning a controller with beginEdit / isEditing / teardown. editableTextProps returns props to spread onto the editor div (a plain div for HTML shapes, or a div inside <foreignObject> for SVG shapes) and handles IME composition, Escape, blur, and one-time focus. The EditableTextOptions / EditableTextController types are also exported.

Free Position

Helpers for placing a new shape at a free spot that doesn’t overlap existing shapes.

function findFreePosition(opts: FindFreePositionOptions): BoundingBox;
function overlapsAny(box: BoundingBox, occupied: BoundingBox[]): boolean;

type FreePositionStrategy = "ring" | "push";

findFreePosition searches for a free rectangle from the desired position (desired) and the rectangles to avoid (occupied) using the "ring" (default) or "push" strategy. overlapsAny reports whether a box overlaps any of the occupied rectangles. The FindFreePositionOptions / FreePositionStrategy types are also exported.

A note on GPU Primitives

The GPU primitive helpers (rectGpuPrimitive and friends) are not part of this package. They live in @edv4h/usketch-plugin-shape-basic (plugins/usketch-plugin-shape-basic) — see that package for GPU rendering support. The ShapeDefinition.gpuPrimitive hook itself can be implemented freely by each shape definition.