Drawing a Combination Spanner from Scratch
using RasmahOverview
A combination spanner (wrench) is the tool every machine shop hangs on the wall: an open-end jaw on one side for speed, a box end on the other for grip, and a handle between them. It is also the classic “draw this in CAD” exercise — the part where every drawing feature has an obvious job: two circles and a strip form the jaw, a boolean difference carves the slot, a cut opens the mouth, a regular polygon makes the hex opening, and an extrusion gives it thickness.
This tutorial draws a 153 mm spanner for an M10 nut — which, by ISO 4032, measures 17 mm across the flats. The finished part is then checked the way an inspector would: the jaw opening and the hex opening must clear those flats, the overall length must match the drawing, and the mass must be plausible. Every dimension is in millimetres; the code writes them as metres.
mm(x) = x * 1e-3 # millimetres -> metres (SI)
mmv(v) = round(ustrip(uconvert(u"mm", v)); digits = 1)
mm2(v) = round(ustrip(uconvert(u"mm^2", v)); digits = 1)
AF = 17.0 # ISO 4032 M10 nut: across-flats
jaw = AF + 0.5 # jaw opening: 0.5 mm clearance17.5
The open-end jaw
The jaw is built from a capsule — two circles joined by a strip — a shape that appears constantly in machine design (slots, rod ends, tabs). The outer capsule is the jaw’s outline; a second, smaller capsule subtracted from it carves the slot the nut sits in; a final cut removes the front wall, opening the mouth to the side:
capsule(r) = union(
union(circle_2d(mm(r)), translate(circle_2d(mm(r)), mm(24), 0.0)), # the two ends
translate(rect_2d(mm(24), mm(2r)), mm(12), 0.0)) # the joining strip
# 1. outline, 2. minus the slot capsule, 3. mouth opened to +x
jaw_sketch = difference(
difference(capsule(14.0), capsule(jaw / 2)),
translate(rect_2d(mm(21), mm(jaw)), mm(22.5), 0.0))Difference2D{Difference2D{Union2D{Union2D{Circle2D{Float64}, Translate2D{Circle2D{Float64}, Float64, Float64}}, Translate2D{Rect2D{Float64, Float64}, Float64, Float64}}, Union2D{Union2D{Circle2D{Float64}, Translate2D{Circle2D{Float64}, Float64, Float64}}, Translate2D{Rect2D{Float64, Float64}, Float64, Float64}}}, Translate2D{Rect2D{Float64, Float64}, Float64, Float64}}(Difference2D{Union2D{Union2D{Circle2D{Float64}, Translate2D{Circle2D{Float64}, Float64, Float64}}, Translate2D{Rect2D{Float64, Float64}, Float64, Float64}}, Union2D{Union2D{Circle2D{Float64}, Translate2D{Circle2D{Float64}, Float64, Float64}}, Translate2D{Rect2D{Float64, Float64}, Float64, Float64}}}(Union2D{Union2D{Circle2D{Float64}, Translate2D{Circle2D{Float64}, Float64, Float64}}, Translate2D{Rect2D{Float64, Float64}, Float64, Float64}}(Union2D{Circle2D{Float64}, Translate2D{Circle2D{Float64}, Float64, Float64}}(Circle2D{Float64}(0.014), Translate2D{Circle2D{Float64}, Float64, Float64}(Circle2D{Float64}(0.014), 0.024, 0.0)), Translate2D{Rect2D{Float64, Float64}, Float64, Float64}(Rect2D{Float64, Float64}(0.012, 0.014), 0.012, 0.0)), Union2D{Union2D{Circle2D{Float64}, Translate2D{Circle2D{Float64}, Float64, Float64}}, Translate2D{Rect2D{Float64, Float64}, Float64, Float64}}(Union2D{Circle2D{Float64}, Translate2D{Circle2D{Float64}, Float64, Float64}}(Circle2D{Float64}(0.00875), Translate2D{Circle2D{Float64}, Float64, Float64}(Circle2D{Float64}(0.00875), 0.024, 0.0)), Translate2D{Rect2D{Float64, Float64}, Float64, Float64}(Rect2D{Float64, Float64}(0.012, 0.00875), 0.012, 0.0))), Translate2D{Rect2D{Float64, Float64}, Float64, Float64}(Rect2D{Float64, Float64}(0.0105, 0.00875), 0.0225, 0.0))
Each step is one ordinary boolean on sketches — the same difference and union the 3D solids use, applied in the sketch plane before the part has any thickness. The open-end jaw on its own:
vtk(extrude(jaw_sketch, mm(6)); n = 24)The two circles of the outer capsule become the rounded jaw tips; the slot capsule leaves the two parallel walls the nut’s flats bear against; the mouth cut makes the entry. The throat — where the nut seats — is the slot’s far circle, 17.5 mm across, 0.5 mm of clearance over the nut.
The box end and the hex opening
The other end wraps the nut completely. A plain disc is the blank; the hexagonal opening is a regular_polygon_2d, whose circumradius follows from the across-flats dimension: a hexagon’s flats are AF = R·√3 apart, so R = AF/√3. Here the opening gets the same 17.5 mm as the jaw:
box_end = translate(circle_2d(mm(15)), mm(-100), 0.0) # the disc
hex = translate(regular_polygon_2d(6, mm(jaw / sqrt(3))), mm(-100), 0.0)
vtk(extrude(difference(box_end, hex), mm(6)); n = 24)The handle and the complete spanner
The handle is a plain rectangle that laps into both ends, and everything fuses with one union; the hex opening is subtracted last:
handle = translate(rect_2d(mm(86), mm(14)), mm(-57), 0.0) # 14 mm wide
spanner = difference(union(union(jaw_sketch, handle), box_end), hex)
vtk(extrude(spanner, mm(6)); n = 24)That is the whole part — a few circles, strips, and one polygon, composed with three booleans. Nothing was sketched “by eye”: every dimension came from the ISO 4032 number, so a different nut size is a one-line change.
The inspector’s checks
The drawing office would now check the part against its dimensions. The two clearance fits are measured straight off the sketch by scanning its signed distance field: along the jaw mouth, and vertically through the box centre, the first place material ends is the half-opening:
# jaw: scan a vertical line through the open mouth
ys = range(-25e-3, 25e-3; length = 2001)
wall = [spanner(mm(24), y) < 0 for y in ys]
half = abs(uconvert(u"mm", ys[findfirst(wall) + findfirst(.!wall[findfirst(wall):end]) - 1]))
println("jaw opening: ", round(2half; digits = 1), " mm (M10 across-flats 17.0 + clearance)")jaw opening: 17.5 mm (M10 across-flats 17.0 + clearance)
# box end: scan vertically through the hex centre
ys2 = range(-16e-3, 16e-3; length = 2001)
ring = [spanner(mm(-100), y) < 0 for y in ys2]
half2 = abs(uconvert(u"mm", ys2[findfirst(ring) + findfirst(.!ring[findfirst(ring):end]) - 1]))
println("hex across-flats: ", round(2half2; digits = 1), " mm (17.5 design)")hex across-flats: 17.5 mm (17.5 design)
Both openings measure 17.5 mm — the 0.5 mm of clearance the fit requires. The overall dimensions come from the part’s bounds:
lo, hi = bounds(extrude(spanner, mm(6)))
println("overall: ", mmv(hi[1] - lo[1]), " × ", mmv(hi[2] - lo[2]), " × ", mmv(hi[3] - lo[3]), " mm (drawing: 153 × 30 × 6)")overall: 153.0 × 30.0 × 6.0 mm (drawing: 153 × 30 × 6)
153 × 30 × 6 mm — the drawing-office callouts, recovered from the geometry.
The mass
Last, weigh it. The sketch area times the thickness times the density of the tool steel:
A = ustrip(uconvert(u"mm^2", area(spanner)))
mass = density(steel) * (A * 1e-6) * 0.006 * 1e3
println("face area: ", round(A; digits = 0), " mm² (", round(100A / (153 * 30); digits = 0), "% of the bounding blank)")
println("mass: ", round(mass; digits = 0), " g")face area: 2035.0 mm² (44.0% of the bounding blank)
mass: 96.0 g
About 96 g of steel — the same order as a commercial 150 mm combination spanner, which is exactly what the fill factor shows: a spanner is a light part, 44% of its bounding blank, because the jaw mouth, the hex, and the slim handle are all cut away. That is the point of the checks: not to prove the CAD kernel can draw, but to confirm the design — clearance, proportions, weight — before it ever meets a nut.
Next steps
- Drawing a spur gear from scratch — the same sketch-level construction for a machine element: an involute tooth and a circular pattern.
- Feature operations: shell, mirror, patterns, loft — round the spanner’s edges with
fillet/chamfer, or mirror one end to the other. - Assembly mates and kinematic joints — put the spanner on an M8 bolted joint and drive the nut it fits.
- Parametric CAD: engineering parts from scratch — the flange and bracket drawings the spanner’s nuts live in.
