Points drawables
The box drawables take their shape from the frame. The points family is different. A line keeps a list of points, and the frame is the box around them.
The fit invariant
Points are stored in frame-local units. After any edit, the bounding box of the points is exactly [0, width] × [0, height] of the frame. Every operation on a points drawable ends by refitting. It measures the points, builds a frame of that size with the same rotation, and shifts the points so the box starts at the local origin. The new frame is placed so each point keeps its page position, including on a rotated frame.
The refit is why resize and dragHandle return a Fitted pair. See Drawables and geometry.
Local units are used instead of normalized 0 to 1 values. A horizontal line has a height of 0, so normalized values would divide by zero.
Line
A line is { kind: "line", points }. LinePoints is a tuple of at least two LocalPoints, so a line with fewer than two points cannot be built.
create(frame)returns two points,(0, 0)and(width, height).geometryreturns the points as an open outline, withclosed: false. There is no edge from the last point back to the first.handlesreturns onevertexhandle for each point. The handle’sindexis the point’s index and itspointis the point in page space.resize(drawable, change)maps each point by the change’soriginand scale, then refits againstchange.frame.dragHandle(drawable, drag)replaces the point atdrag.handle.indexwith the pointer in the local space ofdrag.frame, then refits. An index outside the points leaves them unchanged, and so does a handle that is not avertex.
Resizing and flips
A flip is not stored. It mirrors the points. Dragging a handle past its anchor gives a negative scale, and the points land on the other side of the anchor.
import { adapterFor, pagePoint, radians, resizeFrame } from "@draw/shapes";import type { AdapterEnv, Fitted, Frame } from "@draw/shapes";
const env: AdapterEnv = { measureText: (line, font) => line.length * font.size * 0.6 };
const frame: Frame = { x: 100, y: 100, width: 200, height: 100, rotation: radians(0) };
const line = adapterFor("line").create(frame, env);
const change = resizeFrame(frame, "right", pagePoint(50, 150));
export const flipped: Fitted<"line"> = adapterFor("line").resize(line, change, env);The right handle moves to x = 50, past the left edge. The result is a 50 by 100 frame at x = 50. The points (0, 0) and (200, 100) become (50, 0) and (0, 100). The first point stays on the anchor.
Flat lines
A horizontal line has a height of 0. Its scale on Y is 1 whichever of the top or bottom handles moves, so the scale cannot say which side the content stays on. The change’s origin says it. After the refit the line stays horizontal on its page y with a height of 0. A vertical line behaves the same on X. A frame with no size holds two coincident points and never produces NaN.
Vertex drags
handles gives the grab points. dragHandle moves one of them and refits around the result.
import { adapterFor, pagePoint, radians } from "@draw/shapes";import type { AdapterEnv, Fitted, Frame, Handle } from "@draw/shapes";
const env: AdapterEnv = { measureText: (line, font) => line.length * font.size * 0.6 };
const frame: Frame = { x: 100, y: 100, width: 200, height: 100, rotation: radians(0) };
const adapter = adapterFor("line");
const line = adapter.create(frame, env);
export const handles: readonly Handle[] = adapter.handles(frame, line);
export const endDragged: readonly Fitted<"line">[] = handles .filter((handle) => handle.kind === "vertex" && handle.index === 1) .map((handle) => adapter.dragHandle(line, { frame, handle, pointer: pagePoint(400, 50) }, env));The end vertex moves to (400, 50), above the frame. The frame grows upward to a 300 by 50 box at y = 50. The start vertex keeps its page position at (100, 100), which is now local (0, 50).
Freehand
A freehand stroke is { kind: "freehand", samples }. FreehandSamples is a non-empty tuple of FreehandSamples. Each sample is a point in frame-local units and a pressure. A one-sample stroke is a dot.
Points and pressures live in one sample, not in two parallel arrays. Two arrays could end up with different lengths, and the type system could not rule that out. A list of samples cannot.
create(frame)returns two samples,(0, 0)and(width, height), both with a pressure of0.5.geometryreturns the sample points as an open outline. Pressure does not enter the outline. A one-sample stroke is a dot thathitTestfinds within the tolerance.resize(drawable, change)maps the points the same way a line does and refits againstchange.frame. Pressures do not change.handlesis empty. A stroke has no vertices to grab.dragHandle(drawable, drag)returnsdrag.frameand the drawable as they were.
Freehand and line share one mapping and one refit, so a flip, a rotated frame and a flat stroke behave as they do for a line.