Constraints
A sketch is two things: the geometry you draw and the relationships it has to satisfy. The geometry — every line, arc and circle — is a guess: a rough placement that gets the drawing started. Constraints state what must be true ("these ends touch", "this line is horizontal", "this arc is 20 wide"), and the solver moves the geometry until every statement holds. Change one number later and the whole sketch follows.
There are two kinds:
- Positional constraints relate entities without a number: coincident, horizontal, tangent, symmetric, … They shape the drawing.
- Dimensional constraints carry a value: distance, diameter, radius, angle. They size it.
In the viewport
- Open a sketch and click the entities to constrain — a line, an endpoint, an arc, a circle's center. Ctrl/Shift-click adds to the selection.
- The constraint toolbar appears under the sketch toolbar: Coincident, Horizontal, Vertical, Parallel, Perpendicular, Tangent, Equal, Concentric, Collinear, Midpoint, Symmetric, Fix, Dimension, Angle. Only the buttons that make sense for the selection are enabled; hovering a disabled one says what it needs ("pick two lines", "pick a point and a line, or three points").
- Hover a button to preview the solve as a ghost; click it to apply. The statement is written into the sketch body after the geometry, the solver re-solves, and a badge appears on the constrained entity —
Hfor horizontal,Vvertical,Ttangent,∥parallel,⊥perpendicular,=equal,◎concentric,≡collinear,Mmidpoint,Ssymmetric,Ffix. Coincident points share one dot; a point held on a line or circle shows⊙. - Drag any geometry that is still free. The solver re-solves live, keeping every constraint.

- 1Coincidenttwo points, or a point and a line / arc / circle
- 2Horizontalone line, or two or more points
- 3Verticalone line, or two or more points
- 4Paralleltwo or more lines
- 5Perpendiculartwo lines
- 6Tangenta line and an arc / circle, or two arcs / circles
- 7Equaltwo or more lines, or two or more arcs / circles
- 8Concentrictwo arcs / circles
- 9Collineartwo lines
- 10Midpointa point and a line, or three points
- 11Symmetrictwo points and the mirror line
- 12Fixone point
- 13Dimensiontwo points or entities, one line, or one circle / arc. Always enabled: with nothing picked it arms and waits for the picks
- 14Angletwo lines
- 15Delete constrainta constraint badge picked in the viewport
Right-click a badge or a dimension label for its row menu; a constraint row in the timeline (the sketch's N constraints group) can be removed there.
Constraints while you draw
The sketch dialog has an Auto-constraints → Infer while drawing toggle (on by default). With it on, the drawing tools write constraints as you go: snapping a new endpoint onto an existing vertex emits coincident, and a line drawn close to an axis emits horizontal or vertical. Hold Ctrl while committing a point to skip the inference once. The Show constraints toggles — Dimensional and Positional — hide either family of badges when a dense sketch gets busy.
The canonical rectangle
Four sloppy lines become an exact 100 × 50 rectangle. Select two line ends and click Coincident for each corner; select a side and click Horizontal or Vertical; select the bottom-left end and click Fix; then Dimension the bottom and the right side.

The code behind it
Geometry first, then constraints — the layout the UI writes. Each coincident pins a corner, horizontal / vertical square the sides, fix anchors the profile on the plane, and the two distance dimensions set the size.
import { sketch, line } from 'fluidcad/core';
import { coincident, horizontal, vertical, fix, distance } from "fluidcad/constraints";
sketch("xy", () => {
// The guesses are rough on purpose — the constraints do the work.
const b = line([1, -2], [99, 3]);
const r = line([99, 3], [101, 52]);
const t = line([101, 52], [-2, 48]);
const l = line([-2, 48], [1, -2]);
coincident(b.end(), r.start());
coincident(r.end(), t.start());
coincident(t.end(), l.start());
coincident(l.end(), b.start());
horizontal(b);
vertical(r);
horizontal(t);
vertical(l);
fix(b.start(), [0, 0]);
distance(b.start(), b.end(), 100);
distance(r.start(), r.end(), 50);
})
The result is fully constrained — zero degrees of freedom — and renders green.
The catalog
| Statement | Meaning | Removes |
|---|---|---|
fix(p [, [x, y]]) | Pins a point, optionally at explicit coordinates | 2 DOF |
coincident(a, b) | Two points coincide, or a point lies on a line / arc / circle | 2 / 1 |
horizontal(l) · horizontal(p1, p2, …) | The line is horizontal, or the points share a y | 1 per pair |
vertical(l) · vertical(p1, p2, …) | The line is vertical, or the points share an x | 1 per pair |
parallel(a, b, …) | Two or more lines are parallel | 1 per pair |
perpendicular(a, b) | Two lines meet at 90° | 1 |
symmetric(a, b, l) | Two points mirror across a line or axis | 2 |
midpoint(p, l) · midpoint(p, a, b) | A point at a line's midpoint, or halfway between two points | 2 |
tangent(a, b) | A line and a circle/arc, or two circles/arcs, touch smoothly | 1 |
equal(a, b, …) | Equal lengths (lines) or equal radii (arcs / circles) | 1 per pair |
concentric(a, b) | Two arcs / circles share a center | 2 |
collinear(a, b) | A line lies on another line's infinite extension (or on an axis) | 2 |
distance(a, b, value [, axis]) | Distance between points, a point and a line, two lines, or to a circumference | 1 |
diameter(c, value) | Diameter of a circle or arc | 1 |
radius(a, value) | Radius of an arc or circle | 1 |
angle(a, b, degrees) | Counter-clockwise angle from one line to another | 1 |
All sixteen come from one module:
import { coincident, horizontal, vertical, parallel, perpendicular,
tangent, equal, concentric, collinear, midpoint, symmetric,
fix, distance, angle, radius, diameter } from 'fluidcad/constraints';
A statement targets geometry either as a whole (horizontal(l)) or through a point accessor: l.start(), l.end(), l.mid() (coincident only), a.center(), c.center(), or a point() statement itself.
Sketch datums
Every sketch carries three fixed entities that cost nothing and never move: origin(), xAxis() and yAxis(), importable from fluidcad/core. In the viewport they are the origin dot and the two axis lines; click them like any entity. Constrain against them instead of fixing arbitrary points — a profile centred on the origin stays centred when its size changes.

The code behind it
import { sketch, circle, origin, xAxis, yAxis } from 'fluidcad/core';
import { coincident, symmetric, diameter, distance, equal } from "fluidcad/constraints";
sketch("xy", () => {
const hub = circle([2, 3], 30);
const left = circle([-42, 2], 16);
const right = circle([38, -1], 16);
coincident(hub.center(), origin());
coincident(left.center(), xAxis());
symmetric(left.center(), right.center(), yAxis());
equal(left, right);
diameter(hub, 30);
diameter(left, 16);
distance(left.center(), right.center(), 80);
})
Common uses:
coincident(c.center(), origin()) // center on the sketch origin
coincident(p, xAxis()) // point on the X axis
collinear(xAxis(), l) // line along the X axis
symmetric(a, b, yAxis()) // mirror pair across the Y axis
distance(origin(), c.center(), 40) // dimension from the origin
angle(xAxis(), ramp, 30) // angle against the horizontal
A constraint whose every target is fixed (two datums, say) has nothing to solve and is refused.
The two axis datums are also the directions the in-sketch transforms take — mirror(yAxis(), …), copy("linear", xAxis(), …) — where a bare "x" would mean the world axis. See Mirror.
Fixed references
project() brings existing 3D edges and faces into the sketch as fixed reference geometry: the solver never moves it, it renders locked, and your geometry constrains against it — tangent(bore, l), concentric(c, rim), coincident(p, outline.ref(2).start()). That is how a feature follows the shape it sits on.
Reading the solver's verdict
The viewport tells you the state of the solve at all times:
- Green geometry is fully constrained — the solver has locked it.
- Default-coloured geometry still has freedom. It rests at the guess and can be dragged; the solver re-solves live, honouring every constraint.
- Red geometry is in conflict: the constraints cannot all hold. The status readout names the conflicting statements by file and line — remove or change one.
- Redundant constraints (satisfied, but adding nothing) are named too. A tidy sketch keeps zero.
The status readout also counts the remaining degrees of freedom; "Fully constrained" means zero. See Distance → Degrees of freedom for how the count works.
A habit that keeps sketches predictable: constrain the shape first (coincident, horizontal, tangent, …), then the size (dimensions), then the position (fix or a datum), and watch the DOF counter fall to zero.
Multiple solutions: the guess picks the branch
Many constraint systems have more than one valid answer — a circle tangent to a line can sit on either side of it. The solver converges to the solution nearest the guesses, so the guess coordinates choose the branch. Draw roughly on the side you mean; exactness does not matter, the side does. The Tangent page shows both branches from identical constraints.