Sketch Constraints: Arcs, Ellipses, and the Full Solver
Overview
The parametric 2D sketch solver expresses a sketch as a flat parameter vector θ (two (x, y) slots per point, plus scalar slots for radii/angles) and a set of SketchConstraints, then drives the concatenated residual to zero with the shared Gauss–Newton / Levenberg–Marquardt solver. This tutorial covers the extended pieces — ellipse/slot/arc/polygon sketches, the arc/ellipse/spline entities, and the remaining geometric and dimensional constraints. (The core loop — solve_sketch, coincident, parallel, tangent, fixed, … — is in Sketches, extrusion, and revolution.)
using Rasmah
using LinearAlgebraExtended sketch primitives
Beyond circle_2d / rect_2d / polygon_2d / spline_2d, Rasmah provides ellipse_2d(rx, ry), slot_2d(width, height, radius) (a rounded rectangle), arc_2d(radius, θ1, θ2) (a circular-arc sector), and regular_polygon_2d(n, r) (a circumscribed n-gon). Each has an analytic area and an exact signed field. These four are the outlines of standard hardware: a 60 × 40 mm elliptical access cover, a 40 × 12 mm slotted hole (6 mm radius), a R30 quarter-disk gusset, and the hexagon of an M8 nut (8 mm across flats, circumradius 8/√3 mm):
e = ellipse_2d(0.030, 0.020) # 60 × 40 access cover
println(e isa Ellipse2D, " area = ", area(e) ≈ π * 0.030 * 0.020)
s = slot_2d(0.040, 0.012, 0.006) # 40 × 12 slot, r6
println(s isa Slot2D, " area = ", area(s)) # w·h − (4−π) r²
a = arc_2d(0.030, 0.0, π / 2) # R30 quarter gusset
println(a isa Arc2D, " quarter disk area = ", area(a) ≈ π * 0.030^2 / 4)
p = regular_polygon_2d(6, 0.008 / sqrt(3)) # M8 nut hexagon
println(p isa Polygon2D, " hexagon area = ", area(p) ≈ 3 * sin(π / 3) * (0.008 / sqrt(3))^2)true area = true
true area = 0.00044909733552923257 m^2
true quarter disk area = true
true hexagon area = true
They feed extrude / revolve exactly like any other sketch (volume = area · height for an extrusion) — the slotted hole as a 2 mm plate:
println(volume(extrude(slot_2d(0.040, 0.012, 0.006), 0.002)) ≈
0.002u"m" * area(slot_2d(0.040, 0.012, 0.006)))true
Arc, ellipse, and spline entities
The solver’s richer entities are SketchArc (centre point + radius + start/end angle slots), SketchEllipse (centre + two radii + rotation angle slots), and SketchSpline (control points). arc_center / arc_radius / ellipse_center read them back from the parameter vector:
# SketchArc(sketch_point(center), r_slot, a0_slot, a1_slot)
arc = SketchArc(sketch_point(1), 7, 8, 9)
θ = [0.0, 2.0, 0.0, 0.0, 4.0, 0.0, 2.0, 0.0, 0.0]
println(arc_center(arc, θ)) # [0.0, 2.0]
println(arc_radius(arc, θ)) # 2.0
ell = SketchEllipse(sketch_point(1), 7, 8, 9)
println(ellipse_center(ell, θ)) # [0.0, 2.0]
sp = SketchSpline([sketch_point(1), sketch_point(3), sketch_point(5)])
println(length(sp.points))[0.0, 2.0]
2.0
[0.0, 2.0]
3
All constraints are subtypes of SketchConstraint, and every entity dispatches to the matching residual.
Extended geometric constraints
The remaining geometric constraints — collinear, concentric, equal, midpoint, and symmetric — plus coincident over the new entity kinds:
p1, p2 = sketch_point(1), sketch_point(3)
p3, p4 = sketch_point(5), sketch_point(7)
l1 = SketchLine(p1, p2)
l2 = SketchLine(p3, p4)
c1 = SketchCircle(sketch_point(1), 7)
c2 = SketchCircle(sketch_point(3), 9)
println(collinear(l1, l2) isa SketchConstraint)
println(concentric(c1, c2) isa SketchConstraint)
println(equal(c1, c2) isa SketchConstraint)
println(midpoint(sketch_point(5), l1) isa SketchConstraint)
println(symmetric(p1, p3, l1) isa SketchConstraint)
println(coincident(sketch_point(1), arc) isa SketchConstraint)
println(tangent(arc, l1) isa SketchConstraint)true
true
true
true
true
true
true
Solve a combined example — a line pinned at both ends, kept horizontal, with a second collinear line whose points are midpoint- and symmetry-constrained to the first:
pa, pb = sketch_point(1), sketch_point(3)
pc, pd = sketch_point(5), sketch_point(7)
pm = sketch_point(9)
ll = SketchLine(pa, pb)
sk = Sketch(
[0.0, 0.0, 4.0, 0.0, 1.0, 2.0, 3.0, 2.0, 2.0, 0.0],
[
fixed(pa, 0.0, 0.0),
fixed(pb, 4.0, 0.0),
horizontal(ll),
collinear(ll, SketchLine(pc, pd)),
midpoint(pm, ll),
symmetric(pc, pd, ll),
],
)
θ = solve_sketch(sk)
println(norm(residuals(sk, θ)) < 1e-8)
println(point_pos(pm, θ)) # midpoint → [2.0, 0.0]true
[2.0, 2.891653601120339e-12]
Dimensional constraints
The remaining dimensional constraints — diameter, angle_constraint, horizontal_distance, and vertical_distance — complement distance, length, and radius:
lq = SketchLine(sketch_point(5), sketch_point(7))
lp = SketchLine(sketch_point(1), sketch_point(3))
println(diameter(c1, 4.0) isa SketchConstraint)
println(angle_constraint(lp, lq, π / 2) isa SketchConstraint)
println(horizontal_distance(sketch_point(1), sketch_point(3), 4.0) isa SketchConstraint)
println(vertical_distance(sketch_point(1), sketch_point(3), 4.0) isa SketchConstraint)true
true
true
true
Arc-specific variants dispatch automatically: radius(arc, d), diameter(arc, d), equal(arc1, arc2), concentric(arc1, arc2), and tangent(arc, line).
A real part: a rod-end link from the solver
The constraints above assemble into a standard part. A rod-end link — the flat tie-bar of a linkage — is two Ø8 bosses joined by two straight tangent edges. A draughtsman draws exactly this: pin the left boss, set the 40 mm centre distance, make the bosses equal Ø8, and make the top and bottom edges tangent to both bosses. The solver does the rest:
mm(v) = round(uconvert(u"mm", v); digits = 1)
mm2(v) = round(uconvert(u"mm^2", v); digits = 1)
mm3(v) = round(uconvert(u"mm^3", v); digits = 0)
p1 = sketch_point(1); p2 = sketch_point(3)
c1 = SketchCircle(p1, 5); c2 = SketchCircle(p2, 6)
l1 = SketchLine(sketch_point(7), sketch_point(9)) # top tangent edge
l2 = SketchLine(sketch_point(11), sketch_point(13)) # bottom tangent edge
sk = Sketch(
[0.0, 0.0, 0.045, 0.0, 0.006, 0.006, 0.0, 0.008, 0.05, 0.008, 0.0, -0.008, 0.05, -0.008],
[
fixed(p1, 0.0, 0.0), # pin the left boss centre
distance(p1, p2, 0.040), # bosses 40 mm apart
radius(c1, 0.004), equal(c1, c2), # both bosses Ø8
tangent(l1, c1), tangent(l1, c2), # top edge touches both bosses
tangent(l2, c1), tangent(l2, c2), # bottom edge touches both bosses
],
)
θ = solve_sketch(sk)
println("residual : ", round(maximum(abs.(residuals(sk, θ))); digits = 2))
println("boss radii : ", round(θ[5] * 1000; digits = 2), " mm, ", round(θ[6] * 1000; digits = 2), " mm")
println("centres : ", mm(point_pos(p1, θ)[1]), ", ", mm(point_pos(p2, θ)[1]), " mm")residual : 0.0
boss radii : 4.0 mm, 4.0 mm
centres : 0.0, 40.0 mm
With the bosses solved, the four tangent points are one perpendicular step from the centre line — geometry the solver already determined. Connecting them gives the straight-edged bar, whose area is exactly 2 r L:
x1 = point_pos(p1, θ); x2 = point_pos(p2, θ)
r1, r2 = θ[5], θ[6]
u = (x2 - x1) ./ norm(x2 - x1) # unit centre line
n = [-u[2], u[1]] # perpendicular
t1 = x1 + n .* r1; t2 = x2 + n .* r2 # top tangent points
b2 = x2 - n .* r2; b1 = x1 - n .* r1 # bottom tangent points
bar = polygon_2d([t1, t2, b2, b1])
println("bar area = ", mm2(area(bar)), " mm² (analytic 2 r L = 320 mm²)")bar area = 320.0 mm² (analytic 2 r L = 320 mm²)
Rounding the two ends turns the bar into the standard link — slot_2d(L + 2r, 2r, r) — and adds exactly two semicircles of area π r². Drilling the Ø5 clearance bores and extruding the 4 mm plate finishes the part, whose volume and mass follow from the closed-form area:
link = slot_2d(0.040 + 2 * 0.004, 2 * 0.004, 0.004)
println("racetrack = ", mm2(area(link)), " mm² (analytic π r² + 2 r L = ",
round((π * 0.004^2 + 2 * 0.004 * 0.040) * 1e6; digits = 1), " mm²)")
bored = difference(difference(link, circle_2d(0.0025)),
translate(circle_2d(0.0025), 0.040, 0.0))
link3d = extrude(bored, 0.004) # 4 mm thick
println("volume = ", mm3(volume(link3d)), " mm³")
println("mass (steel)= ", round(ustrip(uconvert(u"kg", 7850u"kg/m^3" * volume(link3d))) * 1000;
digits = 1), " g")racetrack = 370.3 mm² (analytic π r² + 2 r L = 370.3 mm²)
volume = 1402.0 mm³
mass (steel)= 11.0 g
vtk(link3d; n = 64)Diagnostics with solve_constraints
solve_sketch returns the solved θ; solve_constraints(sketch) returns the full ConstraintSolveResult — the DOF count, the Jacobian rank, and the redundant-row analysis — with under_constrained / over_constrained predicates:
res = solve_constraints(sk)
println(res.converged, " rank=", res.rank, " n_dof=", res.n_dof)
println("under-constrained: ", under_constrained(res), " over-constrained: ", over_constrained(res))true rank=9 n_dof=5
under-constrained: true over-constrained: false
under_constrained is n_dof > 0 and over_constrained is n_constraints > rank — both can hold at once when a sketch has redundant equations and still leaves a degree of freedom free (this sketch has one free DOF, and some equations are linearly dependent).
Then hand the solved points to polygon_2d (via polygon_from_points) for extrude / revolve.
A drawn part: a gusset plate
The same four constraints draw a real workshop part. A gusset plate — the triangular stiffener welded into a steel joint — is a right triangle of 120 mm × 90 mm, cut from 8 mm plate. The draughtsman places three points only roughly, then declares what they should be: one corner fixed, the base horizontal, the back edge vertical, and the two leg lengths 120 and 90:
p1 = sketch_point(1); p2 = sketch_point(3); p3 = sketch_point(5)
l12 = SketchLine(p1, p2); l23 = SketchLine(p2, p3); l31 = SketchLine(p3, p1)
sketch = Sketch(
[0.0, 0.0, 118.0, 3.0, 1.0, 88.0], # a deliberately skew triangle
[
fixed(p1, 0.0, 0.0), # anchor the heel
horizontal(l12), # base along x
distance(p1, p2, 120.0), # base length
vertical(l31), # back edge along y
distance(p1, p3, 90.0), # leg height
],
)
θ = solve_sketch(sketch)
println("corners: ", [point_pos(p, θ) for p in (p1, p2, p3)])corners: [[4.187344725265013e-15, 6.279694847132979e-16], [120.0, 7.850774099212687e-16], [5.2341722968718604e-15, 90.0]]
The solver corrects the skew to a perfect right triangle, and the exact area matches \tfrac12 \cdot 120 \cdot 90 = 5400 mm²:
profile = polygon_from_points([p1, p2, p3], θ)
println("gusset area = ", round(ustrip(area(profile)); digits = 1), " mm²")
println("handbook = ", round(0.5 * 120.0 * 90.0; digits = 1), " mm²")gusset area = 5400.0 mm²
handbook = 5400.0 mm²
Extruded 8 mm, it is a cut list item with an exact volume and a steel mass:
gusset = extrude(profile, 8.0)
V = 0.5 * 120.0 * 90.0 * 8.0
println("gusset volume = ", round(ustrip(volume(gusset)); digits = 0), " mm³ (= ", round(V; digits = 0), ")")
println("mass (steel) = ", round(V * 7.85e-3; digits = 0), " g")gusset volume = 43200.0 mm³ (= 43200.0)
mass (steel) = 339.0 g
vtk(surface_mesh(gusset; n = 48))Change distance(p1, p2, 120.0) to 150 and the gusset — area, volume, mass — re-derives itself. That is the point of drawing with constraints. This same plate appears welded into the column base of the Structural sections tutorial.
Next steps
- Sketches, extrusion, and revolution — the core sketch loop and the basic constraints.
- Snapping — infer coincidences to feed into these constraints.
- Assembly mates — the same shared solver applied to rigid-body mating.
