Arael Sketch -- a parametric 2D sketcher built on arael
The arael-sketch* crates are an interactive constraint-based 2D
sketch editor built on the arael optimization
framework. You draw geometry (points, lines, arcs, circles), apply
geometric constraints (horizontal, coincident, tangent, parallel,
perpendicular, ...) and parametric dimensions (lengths, radii,
angles, optionally expressions like d0 * 2 + 3), and the
Levenberg-Marquardt solver keeps everything consistent in real time.
Treat arael-sketch as the big example for arael: a real-world, end-user application that exercises nearly every feature of the framework. If you want to see how arael's macros, Hessian blocks, Jacobian analysis, and both differentiation modes fit together on a non-toy problem, this is the codebase to read.
Why it is a good showcase for arael
The sketch solver combines both differentiation modes arael offers:
- Geometric constraints (horizontal, coincident, parallel,
tangent, etc.) use compile-time differentiation -- each
constraint is a
#[arael(constraint(...))]attribute on a struct inarael-sketch-solver. The macro symbolically differentiates every residual, applies common-subexpression elimination, and emits compiled Gauss-Newton code inside the usualcalc_cost/calc_grad_hessianpair. - Parametric dimensions use runtime differentiation -- the
user types an expression such as
d0 * 2 + 3as a dimension value. At set-up time the expression is parsed witharael_sym::parse, differentiated symbolically (E::diff(...)), and plugged into the solver viaExtendedModeland aTripletBlockon the root. Dimensions can reference each other, entity properties (L0.length,A0.radius), and arithmetic expressions; broken references (deleted entities) are detected and the dimension falls back to its last computed value.
This makes arael-sketch a fully parametric constraint solver where changing one dimension propagates through all dependent expressions.
Beyond the two differentiation modes, the sketch uses:
SelfBlock/CrossBlockfor the dense per-entity Hessian blocks (seePoint,Line,Arc, and the various coincidence / parallel / tangent structs).TripletBlockon the root for the runtime expression-dimension rows -- 3+-entity constraints written at runtime.#[arael(root, jacobian)]for DOF analysis, conflict detection, and the "blocker" diagnostics surfaced when a new constraint is rejected as redundant.Ref<T>/refs::Vec<T>handles that survive insertion and deletion across long edit sessions.- WASM/browser support -- the GUI crate compiles unchanged to
wasm32-unknown-unknownvia eframe.
Three-crate layout
After a recent split the editor is three independent crates, all under the arael workspace. Pull in only what you need:
| Crate | Depends on | What lives there | Has egui? |
|---|---|---|---|
arael-sketch-solver |
arael |
Sketch, entities (Point, Line, Arc), constraints, dimensions, Jacobian, DOF analysis, blocker report |
No |
arael-sketch-backend |
arael-sketch-solver |
CommandContext, Action, History, text-command parser, conflict detection, MCP server, geometry helpers |
No |
arael-sketch |
arael-sketch-backend |
EditorApp and eframe/egui GUI: drawing, tools, drag, selection, theme, params panel |
Yes |
The boundary is hard: arael-sketch-backend has zero dependency on
eframe, egui, egui_extras, egui_commonmark, or rfd. Every
feature the GUI exposes -- drawing, constraining, dimensioning,
scripting, MCP -- is available to headless consumers through the
backend crate.
If you want to embed the solver itself (say, in a different GUI
or a batch tool), depend on arael-sketch-solver. If you want the
command language, undo/redo, and the MCP server without the GUI,
depend on arael-sketch-backend. The full editor including the
egui canvas is arael-sketch.
Backend API quick tour
arael-sketch-backend is the layer most programmatic users will
touch. The public surface (from arael-sketch-backend/src/lib.rs):
pub use ;
pub use ;
pub use ;
pub use check_constraint_conflict;
pub use ;
// arc/line math, hit-testing
// elliptic arc from tangents + bulge
// (not wasm) MCP HTTP server behind a wake callback
Three layers, one Action alphabet
The sketch is always mutated through an Action. Three independent
layers build sequences of actions, but they all emit variants from
the same enum, which is what makes History's snapshot model work
uniformly across GUI edits, scripted batches, and MCP tool calls:
-
Raw
Action::apply. The lowest layer.Action::AddLine { p1, p2 }is literallysketch.add_line(p1, p2)-- no endpoint snapping, no coincidence, no solve. Stable semantics for programmatic callers. Actions are deliberately dumb; anything cleverer belongs in a higher layer. (Seerectangle_actions-- the rectangle's four corner coincidences are pushed explicitly because no layer below is doing it for you.) -
Command parser (
cmd_add_lineand siblings inarael-sketch-backend/src/commands.rs). Walks the parsed text arguments, snaps endpoints against existing entities within a tolerance, and emits extra coincidence actions (ApplyCoincidentPP,ApplyCoincidentLL21,ApplyCoincidentArcStart, etc.) alongside the bareAddLine. The[connected: L1.p1=L0.p2]messages and the "Coincident constraint already exists" dedup you see in therectangle_commandsexample come from this layer. -
GUI tools (
EditorApp::apply_snap_coincidentand friends inarael-sketch/src/main.rs). The mouse resolves to a richerSnapTargetenum than a parser can concisely name -- line body, line midpoint, arc body, arc midpoint, arc start/end/center -- so the GUI dispatches the full ten-variant snap taxonomy into the matching actions (ApplyLineP1OnLine,ApplyMidpointLP1,ApplyMidpointLP1Arc,ApplyLineP1OnArc, ...). It also layers in auto-perpendicular (ApplyPerpendicular) when the drawn line crosses a host line at a right angle, gated byhas_perp_conflictso a redundant perp is never pushed. All of it goes into onebegin_group()frame so a single Ctrl+Z undoes the line and its auto-snaps and any auto-perps as one unit.
The command parser and the GUI are independent pipelines and do
duplicate the "add line then coincident-where-needed" shape --
deliberately, because each sees different input -- but they
converge on the same low-level Action alphabet. That is the key
invariant the whole backend leans on.
Dispatch paths
Two dispatch paths into the sketch, both supported by the same backend:
-
Direct
Action::apply. Construct anAction::Add.../Action::Apply...enum variant and callaction.apply(&mut sketch)to mutate the sketch and solve. EachActionisserde-encodable and round-trips throughHistory, so you get undo/redo for free. This is what the GUI toolbar uses for single-click operations. -
Text commands through
CommandContext. Build aCommandContextaround your sketch + history + session-var state, then feed it a command string. The parser handles coord literals, entity references (L0.p2,A1.center), geometric functions (midpoint(L0),intersect(L0, L1)), vector arithmetic, and per-session variables. Under the hood each command still produces one or moreActions, so the history stack stays coherent whether you drove the sketch from the GUI, a script, or the MCP server.use ; let mut ctx = new; execute; execute; execute; execute; execute;
Full command reference (40+ commands for geometry, constraints,
dimensions, parameters, introspection, view control, explain / DOF
diagnostics): see
arael-sketch-backend/docs/COMMANDS.md.
Additional backend facilities worth knowing about:
-
History+CursorState. Snapshot/restore of the whole sketch via bincode, with groupable actions so a multi-step operation (for instance, a rectangle tool pushing fourAddLines, four coincidences, and four flags in one go) undoes as one unit. The GUI's Ctrl+Z and the MCP's implicit atomicity both ride on this.Historyexists for four reasons:- Undo/redo. Every GUI click, drag, and command line funnels
through
history.push(action, sketch, cursor); Ctrl+Z and Ctrl+Shift+Z are the undo/redo methods on this struct. - Atomicity.
begin_group()tags subsequent pushes with the same group id so a multi-step operation undoes as one frame.Action::Drag { snapshot }wraps a whole drag trajectory in a single reversible unit. - Cursor restoration. Each frame stores where the
command-panel cursor was and which tangent it pointed along,
so undoing a
moveputs the typing cursor back where it was. - Snapshot storage, not replay. Every push serialises the
post-apply sketch via bincode; undo/redo deserialises the
snapshot and calls
sketch.solve()once. The action log is not replayed forward, becauseAction::applyis non-deterministic in general (drag trajectories, solver starting points, helper-point ordering) and a forward replay could diverge. Snapshots make undo/redo exact.
Only
Actions are tracked. Any mutation you want to be reversible must go through anAction. Raw mutations likesketch.lines[r].p1 = Param::fixed(..)or pushing constraint structs directly intosketch.*collections bypassHistoryentirely, are invisible to undo/redo, and will not come back on a redo. The rawSketchAPI is there for headless batch work where history does not matter (seerectangle_solver); the moment you want undo/redo, always go through theActionenum (seerectangle_actions, which usesAction::LockLineP1instead of a directParam::fixedassignment precisely so the pin survives undo/redo). - Undo/redo. Every GUI click, drag, and command line funnels
through
-
check_constraint_conflict(sketch, action). Cheap pre-apply validation -- catches self-reference, impossible orientations (horizontal and vertical on the same line), and transitive redundancy before the solver sees the new constraint. Used by theexplaincommand and by the interactive validation loop. -
Blocker analysis (DOF rejection). When a constraint is rejected because it does not reduce DOF, the solver's
Sketch::analyze_blockersreports which existing constraints are the minimum set responsible. The GUI flashes those constraints in pink; headless consumers get the names in the error string. -
MCP server.
mcp_server::start(addr, verbose, allow_all, wake)spawns an axum + tokio server on a background thread, hosting the Streamable HTTP MCP protocol (2025-03-26 spec). Thewake: Arc<dyn Fn() + Send + Sync>callback is invoked on each inbound request so the host can drain the command channel (the GUI passesctx.request_repaint; a bare-metal host could pass a no-op). The server exposesexecute_command,execute_script,get_sketch_state, andget_helptools, and servesCOMMANDS.mdplus the live sketch as MCP resources.
Running (native)
Common flags: --empty (start blank instead of with a demo),
--nogui --stdout --script path/to/script.cmd (headless script
run), --mcp --mcp-allow-all (start the MCP server), --dark (dark
theme).
Running (browser)
The editor compiles to WebAssembly and runs in the browser unchanged
via eframe. Requires trunk
(cargo install trunk) and the wasm32-unknown-unknown target
(rustup target add wasm32-unknown-unknown):
# Open http://localhost:8080
Tools
- Line (L), Circle (O), Arc (A), Point (P) -- draw geometry with auto-snap to nearby points, endpoints, and curves.
- Dimension (D) -- length, distance, radius, angle, and
point-to-line distance dimensions with draggable annotations.
Numeric values and parametric expressions (
d0 * 2,L0.length + 3) are both accepted. - Select (S) -- click to select, drag to move entities, Backspace/Delete to remove.
- Dark/Light mode toggle, Save/Load (JSON), Undo/Redo (Ctrl+Z / Ctrl+Shift+Z).
Constraints
Horizontal (H), Vertical (V), Coincident (C), Parallel, Perpendicular, Equal length/radius, Tangent (T), Collinear, Midpoint (M), Symmetry (lines or points about a mirror line), Lock (K), Line style (X). Constraints are visualised as symbols on the geometry and can be selected and deleted.
Internally these are plain Rust structs in arael-sketch-solver
with a #[arael(constraint(...))] attribute describing the residual,
e.g. Parallel, Perpendicular, CoincidentLL21, TangentLA,
Midpoint, etc. Each one owns a CrossBlock (for two-entity
constraints) or SelfBlock (for single-entity flag-style
constraints). Some constraints internally introduce a helper
point (named Pc<n>) to bridge into entity centres or midpoints;
helper points are invisible to the user and the command interface.
Runnable examples
Three end-to-end rectangle examples, one per crate, demonstrating each layer of the API:
| Example | Crate | Shows |
|---|---|---|
rectangle_solver |
arael-sketch-solver |
Raw Sketch API: add lines, flag H/V, push CoincidentLL21 structs, set length flags, call sketch.solve(). |
rectangle_actions |
arael-sketch-backend |
Action enum + History: same rectangle built through Action::AddLine / ApplyHorizontal / ApplyCoincidentLL21 / AddDimension / LockLineP1, then undo+redo. |
rectangle_commands |
arael-sketch-backend |
CommandContext + text commands: same rectangle driven by the command parser, exactly the syntax the / panel and MCP agents use. |
Run them with:
Sketch Solver API in detail
The solver crate can be used directly, independently of the command
layer or the GUI. Good for unit tests, batch tooling, or embedding in
another application. This is the code inside
rectangle_solver:
use CrossBlock;
use vect2d;
use *;
let mut sketch = new;
// Create a rectangle from 4 lines
let bottom = sketch.add_line;
let right = sketch.add_line;
let top = sketch.add_line;
let left = sketch.add_line;
// Horizontal/vertical constraints
sketch.lines.constraints.horizontal = true;
sketch.lines.constraints.horizontal = true;
sketch.lines.constraints.vertical = true;
sketch.lines.constraints.vertical = true;
// Connect corners (a.p2 == b.p1)
sketch.coincident_ll21.push;
sketch.coincident_ll21.push;
sketch.coincident_ll21.push;
sketch.coincident_ll21.push;
// Fix bottom-left corner and set dimensions
sketch.lines.p1 = fixed;
sketch.lines.constraints.has_length = true;
sketch.lines.constraints.length = 4.0;
sketch.lines.constraints.has_length = true;
sketch.lines.constraints.length = 2.0;
// Solve -- all constraints satisfied simultaneously
sketch.solve;
// bottom: (0,0)->(4,0), right: (4,0)->(4,2), top: (4,2)->(0,2), left: (0,2)->(0,0)
The sketch solver uses Levenberg-Marquardt optimization with drift
regularization and robust drag constraints. Geometric constraints
are differentiated at compile time; parametric expression dimensions
use runtime differentiation via ExtendedModel and a root-mounted
TripletBlock.
Command panel & scripting
Press / in the GUI to open the command panel. Full scripting
support with 40+ commands for geometry creation, constraints,
dimensions, parameters, introspection, and view control. Commands
support expressions, coordinate references (L0.p2, @dx,dy),
geometric functions (midpoint(L0), intersect(L0,L1)), and vector
arithmetic (L0.p2 + normal(L0) * 3).
See arael-sketch-backend/docs/COMMANDS.md for the full command reference.
Scripts can also be run headless:
This path boots the command context, runs every line, prints results to stdout, and exits. No GUI is initialised at all. Ideal for regression tests and CI-side sketch generation.
AI agent integration (MCP)
arael-sketch-backend embeds an MCP (Model Context Protocol)
server, enabling AI agents like Claude Code to create and modify
sketches programmatically. The agent sends sketch commands and reads
state through the standard MCP tool interface.

Dark mode with parameters panel, command history showing MCP agent connection, and geometry drawn by Claude Code.
Run the native editor with:
The MCP server is decoupled from egui: it takes a wake callback
(Arc<dyn Fn() + Send + Sync>) instead of a handle to the GUI. The
editor supplies ctx.request_repaint; a headless host (for example,
a non-GUI MCP bridge) would supply whatever drains its own command
channel. That decoupling is what lets the entire backend live in a
crate with zero GUI dependencies.
Further reading
- arael README -- the framework behind it all: macros, solvers, Jacobian, Starship method, SLAM and localization demos.
- docs/SLAM.md -- a large-scale end-to-end example of compile-time differentiation.
- docs/SOLVERS.md -- Levenberg-Marquardt,
LmConfig, and backend selection. - arael-sketch-backend/docs/COMMANDS.md -- full command reference.
