Skip to content

Selection and picking

Bounds

bounds(shapes) returns the axis-aligned page Box that holds every shape’s frameBounds. A rotated frame contributes the box around its rotated corners, so the result holds the shapes as they appear on the page.

It returns a Result. An empty list gives { kind: "empty-selection" }. A selection that holds nothing has no box, and the function does not invent one.

Picking

pickShape(shapes, point, { tolerance }) returns the shape a click at point selects, or undefined when the click hits nothing. tolerance is the reach of the click around an outline, in page units. The list can be in any order. The function sorts it by z-order and walks from the top down.

A shape is hit near its outline when the outline lies within tolerance of the point. A closed shape is hit inside when the point lies within the outline. The rules, in order:

  1. Closest outline. If the point is near the outline of one or more shapes, the shape with the closest outline wins. A tie goes to the shape on top. This beats any inside hit.
  2. Filled shapes. A filled shape is one whose style.fill.kind is not "none", whatever the kind or colour, and text always counts as filled. A filled shape that is hit, near its outline or inside, ends the walk, because it hides everything below it. Shapes below it are never considered. Shapes above it that are hit only inside, as hollow shapes, lose to it.
  3. Hollow shapes. If no outline is near and no filled shape is hit, the hollow shape with the smallest area wins, whatever its z-order. A tie goes to the shape on top.

Open shapes (line and freehand) have no inside. They are hit only near their stroke, and their fill has no effect on that.

The area is the polygon area of the closed outline in local units, by the shoelace formula. Rotation does not change it. A shape with no area, such as a frame with zero width, never wins an inside hit. It can still be hit near its outline.

docs/examples/pick-nested.ts
import { DEFAULT_STYLE, indexAbove, pagePoint, pickShape, radians, shapeId } from "@draw/shapes";
import type { Shape, ShapeId } from "@draw/shapes";
function push(shapes: readonly Shape[], id: string, size: number): readonly Shape[] {
const offset = (200 - size) / 2;
const shape: Shape = {
id: shapeId(id),
frame: { x: offset, y: offset, width: size, height: size, rotation: radians(0) },
drawable: { kind: "rectangle", cornerRadius: 0 },
index: indexAbove(shapes),
style: DEFAULT_STYLE,
groupIds: [],
};
return [...shapes, shape];
}
const shapes = push(push([], "inner", 100), "outer", 200);
const tolerance = { tolerance: 4 };
export const inside: ShapeId | undefined = pickShape(shapes, pagePoint(100, 100), tolerance)?.id;
export const nearOuter: ShapeId | undefined = pickShape(shapes, pagePoint(2, 100), tolerance)?.id;

The small rectangle sits below the large one and both are hollow. A click at (100, 100) is inside both and away from both outlines, so the smaller one wins even though the larger one is on top. A click at (2, 100) is within 4 units of the large rectangle’s left edge, so the outline rule picks the large one.