RasmahRasmah

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 LinearAlgebra

Extended 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).

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