2D parametric sketch editor. Rust + Bevy + egui.
- Draw segments, circles, arcs with snapping
- Geometric constraint solver (GCS) — 15 constraint types
- DOF analysis, redundancy detection
- Undo/redo, soft-delete
- Rectangle tool intentionally provides a central snap point; the visible construction centerline exists to make that center point work reliably and support midpoint workflow
| Constraint | Selection |
|---|---|
| Horizontal | segment(s) |
| Vertical | segment(s) |
| Perpendicular ⊥ | 2 segments |
| Parallel ∥ | 2 segments |
| Tangent | segment + circle/arc |
| Coincident | 2+ points |
| Distance | 2 points |
| Equal length = | 2 segments |
| Angle ∠ | 2 segments |
| Fix | point(s) |
| Point on line | point(s) + segment |
| Point on circle | point(s) + circle |
| Midpoint | point + segment |
| Symmetric | 2 points + segment |
Notes:
- Dimension constraints stay point-based. We do not attach a dimension to a
segmentas a first-class reference, because a segment dimension is just the same relation between its two endpoint points. - This avoids duplicating the model and keeps solver/serialization/undo logic simpler. If we ever improve this area, it should be only as selection UX that expands a clicked segment into its endpoints, not as a new constraint representation.
- Constraint model is canonical:
Coincidentis point-to-point only. Curve membership must usePointOnLine,PointOnCircle, orPointOnArc. - There is no separate store-level
Collinear: two segments useTangent, which the solver maps to internal collinearity.
Gauss-Newton GCS with forward-mode autodiff (Pwd / PwdVec).
Newton step via nalgebra SVD — handles under/over/exactly-determined systems.
DOF and redundancy computed via SVD rank.
See SOLVER.md for details and roadmap.
cargo run
Requires a Wayland compositor. Key bindings: 1 line, 2 circle, 3 arc, Esc select, M move, Ctrl+Z/Y undo/redo.
See HOTKEYS.md for the full, maintained shortcut reference.
NO_COLOR=false trunk serve
If your shell exports NO_COLOR=1, trunk 0.21.x may fail to parse it; overriding with NO_COLOR=false keeps the web build working.
- The sketch scene renders to the full canvas.
- egui panels are treated as overlays on top of the scene instead of shrinking the scene viewport.
- The top toolbar uses a fixed overlay height.
- Scene input is blocked while the pointer is over egui via
EguiBlocksInput. - We keep separate world and egui cameras so UI layout does not drag the scene camera with it.
- Our local
vendor/eguipatch must only clampinteract_rectfor panel hit-testing. - Do not clamp
response.rectin top-level panels: egui uses it for subsequent panel allocation and shrinking it causes panel overlap/regressions. - Historical upstream context for mouse input in
eguilives in two oldenomadoPRs:#1605"Feature Mouse Lock API" explored cursor lock/grab for drag-style widgets, and#1614"Mouse delta from DeviceEvent" explored feeding raw/native mouse motion separately from pointer position. - The useful takeaway from those threads is architectural: raw motion / native event handling belongs in the integration/backend layer,
while core
eguishould receive documented, backend-agnostic input semantics. - Current de-vendoring direction:
keep the
eguipatch as small as possible and upstream only theDragValue::use_raw_motion(true)style behavior (raw motion preferred over pointer delta, normalized bypixels_per_point, with a normal fallback). bevy_eguishould own the integration part: feedMouseMotion/raw device deltas intoeguiinput, and translateViewportCommandcursor requests into Bevy window cursor state.merkashould own cursor-lock policy: deciding when numeric drags hide/lock/unlock the cursor is app behavior and should stay in local state machines/systems, not inside vendoredegui.- In other words, the path to removing vendored
eguichanges is: upstream raw-motion support toegui, upstream/backend the Bevy integration pieces tobevy_egui, then keep onlymerka's local UX policy on top. - Panel hit-testing note:
the local
vendor/eguipanel fix is still active and still only clampsresponse.interact_rect. If scene input starts feeling blocked outside the visible panel again, first check for integration-level pointer absorption before suspectingpanel.rsregressions; enablingbevy_egui's global absorb-input path can bypass our local rect-based scene gating. - Links: emilk/egui#1605 emilk/egui#1614
External projects we read for comparison. Per-primitive canonical-representation comparison against FreeCAD / LibreCAD / QCAD / SolveSpace lives in CANONICAL_GEOMETRY.md.
https://github.com/xorza/CatCad — Rust parametric CAD (GPL-3.0-or-later, so it is a
reading reference only, not a source of code). Its silverpoint crate is the closest
thing to our sketch_core + mm_solver: 2D sketch geometry, constraints and solver in
one library, with the app on top of its own GUI framework (palantir) and a wgpu
renderer (aperture3d).
Where it lines up with us:
- same shape of core — sketch arena with generational ids, constraints as residuals, DOF and redundancy from the rank of the Jacobian, undo as a whole-sketch snapshot;
- drag is a pull, not a written position: the cursor target enters the solve as a weak
least-squares term (their
Pull, ourWeakPointTarget), so geometry is never teleported off the constraint set and cleaned up afterwards; Coincidentis point-to-point only, curve membership is its own constraint (PointOnSegment/PointOnCircle) — the same anti-ambiguity rule we hold.
Where it is ahead of us:
- profiles and 3D.
arrangementcuts every curve at every crossing, walks the loops and reads signed area, so it answers "what regions does this drawing enclose" and feedsprism(extrude) and triangulation. Our enclave graph answers a different question — connected components for the solver and for selection — and we have no face extraction and no 3D at all. - feature history. A timeline of steps with sketches hanging off planes, i.e. actual parametric modelling; we have a flat sketch plus undo.
- per-entity freedom. The solve reports
Determined/Partly/Freeper point, segment and circle out of the null space of the Jacobian, and a drag asks the rank before running whether the pull can move anything at all instead of grinding through a hundred refused iterations. We only report a global DOF number. - Adaptive damping with step acceptance. Both of us form the same damped normal
equations
(JᵀJ + λI) δ = -Jᵀr— the difference is what λ does. Ours is a fixed Tikhonov1e-6and every step is taken unconditionally; theirs is a real Levenberg-Marquardt trust region: λ scaled by the largest diagonal ofJᵀJ(so it is relative to the sketch's scale, where our absolute constant is not), a trial assembly, the step kept only if the residual norm fell, and λ multiplied by 8 on rejection and by 0.3 on acceptance. Plus a stall test on the relative improvement, which stops a run against a system that has no solution — we only stop onmax_err < 1e-6or 100 iterations. That accept/reject loop is the robustness near degeneracies, not the presence of λ. - Linear algebra and Jacobians. They factor the lower triangle with a hand-written
Cholesky and answer a non-positive pivot by damping harder; we run an
nalgebraSVD of the normal matrix every iteration — rank-tolerant, but far more flops, paid back by our subsystem decomposition making eachnsmaller. Their partials are written by hand through aJacobianRow; ours come from forward-mode autodiff per (equation, variable), which is what makes a new constraint kind cheap to add for us and a piece of calculus for them. Their tolerance is1e-10onf64throughout; ours is1e-6because the sketch boundary isf32. - allocation budget tests (
dhat) and golden-image visual tests as first-class gates.
Where we are ahead of it:
- primitives. They have points, segments and circles — no arc entity at all (arcs
exist only as pieces of a circle inside
arrangement), no conics. We have circular arcs in canonicalstart_angle + sweep_angleform plus arcs of ellipse / hyperbola / parabola with internal curve constraints. - constraint coverage. 14 variants against our 28 (three of ours are weak drag targets): they have no angle dimension, no
symmetry, no midpoint, no diameter, no circle-circle tangency, no equal-radius across
arcs, no axis constraints. They do have
Spacing(parallel edge-to-edge dimension), which we do not. - tools and editing. Rectangle, trim, split, fillet, chamfer, snapping with priorities
(endpoint / midpoint / center / quadrant / on-curve / axis) against their point, line,
circle and dimension. Their equivalent of snap is auto-constraint on click (
Anchor): a click on an edge or a rim builds aPointOnSegment/PointOnCircleinstead of merely landing there. - solver decomposition. We partition the parameter graph into independent subsystems
(
mm_solver::partition, our enclave track); they solve one dense system for the whole sketch. - deterministic input-trace replay (DST) for interaction testing.
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
vendor/egui and vendor/bevy_egui are patched copies of upstream crates and stay
under their own upstream licenses. Bundled fonts in assets/fonts/ are third-party:
DejaVu Sans (Bitstream Vera / DejaVu license) and Anonymous Pro (SIL Open Font License 1.1).