Sketching
Every solid starts as a flat drawing on a plane. A sketch holds that drawing: lines, arcs, circles and the other primitives, plus the constraints that say how they relate. A solver reads both and moves the geometry until every relationship holds. The solved profile is what Extrude, Revolve and the other 3D operations consume.
Starting a sketch
- Click Sketch on the toolbar. The Sketch dialog docks on the right with one Face / Plane slot.
- Click where the drawing should go: one of the three origin plane quads shown in the viewport, a planar face of an existing solid, or a plane row in the timeline. The first pick is the action — the
sketch()statement is written and the sketch opens, with the camera looking straight at the plane. - Draw with the sketch toolbar that appears above the viewport, then click the green Finish sketch button. It offers the follow-up feature (Extrude, Revolve, …) so the next dialog opens with this sketch already in its profile slot.
The chip in the dialog names the picked target. Its ✕ leaves the sketch view so a different face or plane can be picked; the statement moves onto the new target in place. Cancel deletes the sketch statement — only if this dialog wrote it.

The code behind it
Picking the XY plane wrote sketch("xy", () => { … }). Everything drawn while the sketch is open lands inside the callback: geometry statements first, then constraint statements.
import { sketch, line } from 'fluidcad/core';
import { coincident, horizontal, vertical, fix, distance } from "fluidcad/constraints";
sketch("xy", () => {
// An L-bracket outline: six lines drawn corner to corner. The
// coordinates are guesses; nothing joins consecutive lines until a
// constraint says so.
const base = line([0, 0], [80, 0]);
const toe = line([80, 0], [80, 15]);
const shelf = line([80, 15], [15, 15]);
const inner = line([15, 15], [15, 60]);
const top = line([15, 60], [0, 60]);
const back = line([0, 60], [0, 0]);
// The Polyline tool writes these when each new line starts on the
// previous end (Auto-constraints); by hand you write them yourself.
coincident(base.end(), toe.start());
coincident(toe.end(), shelf.start());
coincident(shelf.end(), inner.start());
coincident(inner.end(), top.start());
coincident(top.end(), back.start());
coincident(back.end(), base.start());
// Square the bracket up and give it its two leg lengths and thickness.
horizontal(base);
vertical(toe);
horizontal(shelf);
vertical(inner);
horizontal(top);
vertical(back);
fix(base.start(), [0, 0]);
distance(base.start(), base.end(), 80);
distance(back.start(), back.end(), 60);
distance(toe.start(), toe.end(), 15);
distance(top.start(), top.end(), 15);
})
Standard planes
Picking an origin plane writes its name. Each plane has an alias and a negative version whose normal points the other way:
| String | Alias | Negative | Negative alias |
|---|---|---|---|
"xy" | "top" | "-xy" | "bottom" |
"xz" | "front" | "-xz" | "back" |
"yz" | "right" | "-yz" | "left" |
sketch("xy", () => { ... }) // horizontal, looking down — same as "top"
sketch("xz", () => { ... }) // vertical, seen from the front — same as "front"
sketch("yz", () => { ... }) // vertical, seen from the right — same as "right"
Custom planes
A plane made with the Plane tool (offset, mid plane, or from an edge — see Planes) is a pick target like any origin plane. In code it is a plane() object:
import { plane } from 'fluidcad/core';
const p = plane("xy", { offset: 50 }) // XY shifted 50 up
const p2 = plane("xz", { rotateX: 45 }) // XZ tilted 45° about X
sketch(p, () => { ... })
Sketching on a face
Clicking a planar face of a solid sketches on that face. The statement references the face through the feature that made it:
const e = extrude(30)
sketch(e.endFaces(), () => {
circle([0, 0], 30)
})
Coordinates inside a face sketch are 2D coordinates on the face's plane. The origin is the world origin projected onto that plane, so [0, 0] only lands on the face when the face sits over the world origin. To place geometry against the existing shape, project its edges into the sketch and constrain against them — a projected outline is fixed reference geometry the solver never moves.
The face keeps working as a sketch plane even if a later feature reshapes or removes it: the plane's position and orientation are remembered.
Planes from an edge
A plane can also sit on an edge, normal to it — the setup for a profile that will be swept along that edge:
const e = extrude(30)
plane(e.sideEdges(0), 'start') // at the edge start, facing outward
plane(e.sideEdges(0), 0.5) // halfway along (same as 'middle')
plane(e.sideEdges(0), 1) // at the end
The sketch dialog
While a sketch is open the dialog shows its options. None of these change the model — they change how you draw and what you see:

| Toggle | What it does |
|---|---|
| Section view | Clips away everything in front of the sketch plane, so a sketch on an inner face is not hidden by the walls around it. |
| Lock camera | Keeps the view flat on the plane: pan and zoom only, no orbit. On by default. |
| Snap to vertices | Clicks and drags snap to existing sketch vertices. |
| Snap to grid | Clicks and drags snap to the grid. |
| Show constraints → Dimensional | Shows or hides the distance, angle, radius and diameter labels. |
| Show constraints → Positional | Shows or hides the coincident, tangent, horizontal/vertical and other relation glyphs. |
The Auto-constraints group belongs to the constraints story — what gets inferred while you draw is explained in the constraints introduction.
Drawing

The sketch toolbar holds the drawing tools (Line, Polyline, Bezier, Circle, Polygon, Rectangle, arcs, Slot, Text) and the tools that work on drawn geometry (Fillet, Offset, Project, Copy, Mirror). A few conventions apply to all of them:
- Coordinate pill. Every tool's anchor point shows an X / Y pill. Type a value to place the point exactly, Tab between the fields; expressions and
param()names are accepted. A typed coordinate is written literally into the statement and does not constrain the point — add afixor a dimension if it must stay there. A body point carries the shape's own dimension instead (a circle's diameter, a line's length); only a typed value becomes a dimension constraint, a click merely places it. - Guide latch. With the Guide button latched, everything you draw gets
.guide()— construction geometry that is excluded from the profile. See Guides. - Esc leaves the active tool. Typed coordinates clear first, then the tool.
- Dragging. Geometry that still has freedom can be dragged; the solver re-solves live while you drag, honouring every constraint.
Keyboard shortcuts
Every drawing tool and every constraint button has a letter shortcut, shown in its tooltip. Press the letters with nothing focused; a two-letter chord is typed one key after the other. A single letter that also starts a chord (c is Circle, cc Coincident) fires after a short pause or as soon as the next key rules the chord out.
| Tool | Keys | Tool | Keys |
|---|---|---|---|
| Line | l | Polyline | ll |
| Circle | c | Center arc | ca |
| Three-point arc | a | Rectangle | r |
| Polygon | p | Bezier | b |
| Text | x | Fillet | f |
| Offset | o | Project | pj |
| Copy | cp | Mirror | m |
| Guide latch | g | Look along the sketch normal | n |
A constraint shortcut only works while its button would accept a click — pick the geometry first, exactly as for the button. With an illegal pick set the key does nothing.
| Constraint | Keys | Constraint | Keys |
|---|---|---|---|
| Coincident | cc | Concentric | cn |
| Horizontal | h | Collinear | cl |
| Vertical | v | Midpoint | mp |
| Parallel | pa | Symmetric | s |
| Perpendicular | pe | Fix | fx |
| Tangent | t | Dimension | d |
| Equal | e | Angle | da |
Delete removes a picked constraint glyph, and Ctrl+Z / Ctrl+Shift+Z undo and redo any operation the sketcher wrote (see Undo and redo).
Reading the viewport
- Green geometry is fully constrained — the solver has locked it.
- Default-coloured geometry still has degrees of freedom. It rests at the coordinates you drew and can be dragged.
- Red geometry is in conflict: the constraints cannot all hold.
- Constraints show as small glyphs on their geometry; dimensions render as labels you can double-click to edit.
The status readout gives the remaining degrees of freedom. "Fully constrained" means zero. The constraints introduction covers what to do with the number.
How a sketch becomes a face
When an operation consumes the sketch, the 2D geometry is turned into faces:
- Overlapping closed shapes fuse. Two closed shapes that cross each other become one combined outline.
- Closed shapes inside closed shapes are holes. This is how rings, bores and pockets are drawn: an inner loop is subtracted from the outer region.
- Open or loose geometry is ignored. Lines and arcs that do not close a loop contribute nothing to the face — but they can still serve as guides or constraint targets.
sketch("xy", () => {
circle([0, 0], 60) // outer circle
circle([0, 0], 30) // inner circle → a hole
})
extrude(20) // the ring between the two circles
To keep an inner shape solid instead of drilling it, chain .drill(false) on the operation:
extrude(20).drill(false) // the inner circle is NOT treated as a hole
When a sketch produces several separate regions, the operation dialogs let you pick the ones you want — see region picking.
Writing a sketch by hand
Everything the toolbar writes can be typed. The layout convention that keeps a sketch readable, and that the tools follow:
- Geometry statements —
line,arc,circle, … drawn at rough guess coordinates. - Constraint statements —
coincident,horizontal,distance, … imported fromfluidcad/constraints. - Derived operations last —
offset,fillet,mirror,copy— because they consume solved geometry.
import { sketch, line } from 'fluidcad/core';
import { coincident, horizontal, vertical, fix, distance } from 'fluidcad/constraints';
sketch("xy", () => {
const b = line([0, 0], [100, 0]); // guesses
const r = line([100, 0], [100, 60]);
const t = line([100, 60], [0, 60]);
const l = line([0, 60], [0, 0]);
coincident(b.end(), r.start()); // relationships
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); // sizes
distance(r.start(), r.end(), 60);
})
Where no constraint applies, geometry stays exactly where you drew it. Where one does, the guess only chooses which of several valid solutions the solver lands on.