Parametric Spur Gear Set
A pair of spur gears that mesh correctly, with an optional base plate and two pins so the pair can be mounted and turned by hand. Each flank is a true involute of the base circle computed point by point and joined with a spline, and the root is trimmed by simulating a standard rack cutter rolling past the tooth, which gives the proper undercut on small pinions. With the defaults (module 2, 12 and 24 teeth, 20 degree pressure angle) the pitch diameters are 24 and 48 mm, the outer diameters 28 and 52 mm, the center distance 36 mm, and the whole print layout measures 76 x 72 x 14 mm. The gears are laid out in mesh but separated by a 0.3 mm backlash, and the plate with its 4.5 mm pins sits beside them; print everything flat as laid out, no supports needed. You can change the module, both tooth counts, pressure angle, face width, backlash, bore diameters with a round, D-flat or keyway bore, and the pin running clearance, or turn the base plate off.
# Spur Gear Set - involute gear pair from module and tooth count
#
# Two spur gears laid out in mesh at the exact center distance
# a = m * (z1 + z2) / 2, plus an optional base plate with two pins so the
# pair can be mounted and turned by hand.
#
# Tooth profile: each flank is a true involute of the base circle,
# computed point by point (polar angle inv(alpha) = tan(alpha) - alpha) and
# joined with a spline. Below the involute the tooth is trimmed by
# simulating a standard rack cutter (addendum 1.25 m) rolling past it, which
# gives the undercut on small pinions and a trochoid root fillet.
# Addendum = m, dedendum = 1.25 m. Each tooth is thinned by backlash / 2 at
# the pitch circle, so the pair has a total circular backlash of `backlash`.
#
# Print orientation: everything flat on the bed as laid out (gear faces and
# plate bottom on Z = 0, pins pointing up). No supports needed.
# Use: push each gear onto its pin on the plate (or onto your own shafts at
# center distance a) and turn. Increase `backlash` if the printed pair binds.
@param 0.5..5 step:0.25 desc:"Module m (mm) - tooth size, pitch diameter = m x z" group:"Gear"
m = 2.0
@param 8..60 desc:"Teeth on gear 1 (pinion)" group:"Gear"
z1 = 12
@param 8..120 desc:"Teeth on gear 2" group:"Gear"
z2 = 24
@param 14.5..25 step:0.5 desc:"Pressure angle (deg)" group:"Gear"
pa = 20
@param 2..40 desc:"Face width / gear thickness (mm)" group:"Gear"
face = 8
@param 0.05..1 step:0.05 desc:"Total circular backlash at the pitch circle (mm) - print clearance between teeth" group:"Gear"
backlash = 0.3
@param 1..30 step:0.5 desc:"Bore diameter of gear 1 (mm)" group:"Bore"
bore1 = 5.0
@param 1..60 step:0.5 desc:"Bore diameter of gear 2 (mm)" group:"Bore"
bore2 = 5.0
@param choices:["round", "dflat", "keyway"] desc:"Bore shape" group:"Bore"
bore_style = "round"
@param 0.2..3 step:0.1 desc:"D-flat depth from the bore circle (mm)" group:"Bore"
flat_depth = 0.5
@param 0.5..10 step:0.5 desc:"Keyway width (mm)" group:"Bore"
key_w = 2.0
@param 0.3..5 step:0.1 desc:"Keyway depth beyond the bore circle (mm)" group:"Bore"
key_d = 1.0
@param desc:"Add a base plate with two pins at the center distance" group:"Base plate"
base_plate = true
@param 2..8 step:0.5 desc:"Base plate thickness (mm)" group:"Base plate"
plate_t = 4.0
@param 0.1..0.6 step:0.05 desc:"Running clearance between pin and bore, per side (mm)" group:"Base plate"
pin_clear = 0.25
# ---------------------------------------------------------------- geometry
a = m * (z1 + z2) / 2 # center distance
n = 12 # involute points per flank
nr = 30 # rack cutter positions per side
def rp(z) = m * z / 2 # pitch radius
def rb(z) = rp(z) * cos(pa) # base radius
def ra(z) = rp(z) + m # tip radius (addendum m)
def rf(z) = rp(z) - 1.25 * m # root radius (dedendum 1.25 m)
def r0(z) = max(rb(z), rf(z)) # involute starts here
# Involute function (deg) at radius r, and half tooth angle at radius r.
# The tooth is thinned by backlash / 2 (backlash / 4 per flank) at the pitch circle.
def inv(z, r) = deg(tan(acos(rb(z) / r))) - acos(rb(z) / r)
def half(z, r) = 90 / z + inv(z, rp(z)) - inv(z, r) - deg(backlash / 4 / rp(z))
def fr(z, k) = r0(z) + (ra(z) - r0(z)) * k / n # flank radius, k = 0..n
def px(r, t) = r * cos(t)
def py(r, t) = r * sin(t)
# One tooth centered on +X: radial foot inside the root disc, lower flank up
# (spline through involute points), tip arc, upper flank down.
def tooth(z) = sketch [
(px(rf(z) - 0.3 * m, -half(z, r0(z))), py(rf(z) - 0.3 * m, -half(z, r0(z)))),
(px(r0(z), -half(z, r0(z))), py(r0(z), -half(z, r0(z)))),
spline [(px(fr(z, k), -half(z, fr(z, k))), py(fr(z, k), -half(z, fr(z, k)))) for k in range(1, n + 1)],
arc (px(ra(z), -half(z, ra(z))), py(ra(z), -half(z, ra(z)))) (ra(z), 0) (px(ra(z), half(z, ra(z))), py(ra(z), half(z, ra(z)))),
spline [(px(fr(z, k), half(z, fr(z, k))), py(fr(z, k), half(z, fr(z, k)))) for k in range(n - 1, -1, -1)],
(px(rf(z) - 0.3 * m, half(z, r0(z))), py(rf(z) - 0.3 * m, half(z, r0(z)))),
(px(rf(z) - 0.3 * m, -half(z, r0(z))), py(rf(z) - 0.3 * m, -half(z, r0(z))))
]
# Rack cutter tip (the part deeper than the involute contact limit
# rp sin^2(pa)), rolled to position f (rad) next to the gap at side s (+1/-1).
# Rack coords (u radial, v tangential) are rotated back into the gear frame.
def d0(z) = rp(z) * sin(pa) ** 2 + 0.02
def hw(d) = (pi * m / 2 + backlash / 2) / 2 - d * tan(pa)
def vc(z, s) = s * pi * m / 2
def gx(u, v, f) = u * cos(deg(f)) + v * sin(deg(f))
def gy(u, v, f) = -u * sin(deg(f)) + v * cos(deg(f))
def rk(z, s, f, u, v) = (gx(u, vc(z, s) + v + rp(z) * f, f), gy(u, vc(z, s) + v + rp(z) * f, f))
def cutter(z, s, f) = polyline [
rk(z, s, f, rp(z) - d0(z), -hw(d0(z))),
rk(z, s, f, rp(z) - 1.25 * m, -hw(1.25 * m)),
rk(z, s, f, rp(z) - 1.25 * m, hw(1.25 * m)),
rk(z, s, f, rp(z) - d0(z), hw(d0(z)))
]
def fpos(z, i) = 2.5 * m / rp(z) * (i - nr) / nr
def rack(z) = (union [cutter(z, -1, fpos(z, i)) for i in range(2 * nr + 1)]) + (union [cutter(z, 1, fpos(z, i)) for i in range(2 * nr + 1)])
def cut_tooth(z) = if d0(z) < 1.25 * m - 0.05 then (tooth(z) - rack(z)) else tooth(z)
def bore_shape(d) = if bore_style == "dflat" then ((circle (d / 2)) - (rect (d + 2) d at:(0, d - flat_depth))) else (if bore_style == "keyway" then ((circle (d / 2)) + (rect key_w (d / 2 + key_d) at:(0, (d / 2 + key_d) / 2))) else circle (d / 2))
def gear(z, d) = cut_tooth(z) | polar z 0 orient:true | union (circle rf(z)) | diff bore_shape(d) | extrude face
# Gear 1 at the origin with a tooth on +X; gear 2 at (a, 0) turned so a gap
# faces gear 1 (tooth into gap, centered in the backlash).
phase2 = 180 + 180 / z2
g1 = gear(z1, bore1)
g2c = gear(z2, bore2)
def g1at(t) = g1 | rotate 0 0 t
def g2at(t) = g2c | rotate 0 0 (phase2 - t * z1 / z2) | translate a 0 0
g2 = g2at(0)
# ---------------------------------------------------------------- checks
assert g1.solids == 1
assert g2.solids == 1
assert g1.bbox.z ~ face
# Outer diameter m (z + 2): the tips touch a circle 0.05 mm inside it and
# stay clear of one 0.01 mm outside it.
def ring_out(r) = (cylinder (r + 5) (face * 3)) - (cylinder r (face * 4))
assert clear(g1, ring_out(ra(z1) + 0.01)) and clear(ring_out(ra(z1) - 0.05), g1) == false
assert clear(g2c, ring_out(ra(z2) + 0.01)) and clear(ring_out(ra(z2) - 0.05), g2c) == false
assert bore1 / 2 < rf(z1) - 1.5 and bore2 / 2 < rf(z2) - 1.5
print "pitch dia" m * z1 m * z2 "outer dia" m * (z1 + 2) m * (z2 + 2) "center distance" a
# In mesh but not touching, at four phases across one tooth pitch
p1 = 360 / z1
assert clear(g1, g2)
assert clear(g1at(p1 / 4), g2at(p1 / 4))
assert clear(g1at(p1 / 2), g2at(p1 / 2))
assert clear(g1at(3 * p1 / 4), g2at(3 * p1 / 4))
# Backlash: gear 2 may turn about +-(backlash / 2) / rp2 before the flanks
# touch. 1.15 of that collides; 0.75 of it is still clear (on undercut
# pinions with few teeth the mating tip meets the undercut edge at about 0.85,
# on normal tooth counts contact is at 0.95-1.05 of the nominal value).
dl = deg(backlash / 2 / rp(z2))
g2p075 = g2c | rotate 0 0 (phase2 + 0.75 * dl) | translate a 0 0
g2m075 = g2c | rotate 0 0 (phase2 - 0.75 * dl) | translate a 0 0
g2p115 = g2c | rotate 0 0 (phase2 + 1.15 * dl) | translate a 0 0
g2m115 = g2c | rotate 0 0 (phase2 - 1.15 * dl) | translate a 0 0
assert clear(g1, g2p075) and clear(g1, g2m075)
assert clear(g1, g2p115) == false and clear(g1, g2m115) == false
# ---------------------------------------------------------------- base plate
spacer = 0.6 # thin ring the gear rides on
pr1 = (if bore_style == "dflat" then bore1 / 2 - flat_depth else bore1 / 2) - pin_clear
pr2 = (if bore_style == "dflat" then bore2 / 2 - flat_depth else bore2 / 2) - pin_clear
pw = max(bore1, bore2) / 2 + 5 # half width of the plate
pin_h = plate_t + spacer + face + 1
def pin(r, x) = (cylinder r pin_h | floor | edges >Z | chamfer (min(0.5, r / 3))) | translate x 0 0
def ring(r, x) = cylinder (r + 1.5) (plate_t + spacer) | floor | translate x 0 0
plate_asm = union [
rect a (2 * pw) at:(a / 2, 0) | extrude plate_t,
circle pw at:(0, 0) | extrude plate_t,
circle pw at:(a, 0) | extrude plate_t,
ring(pr1, 0), ring(pr2, a), pin(pr1, 0), pin(pr2, a)
]
lift = plate_t + spacer
assert pr1 > 0.5 and pr2 > 0.5
assert clear(plate_asm, g1 | translate 0 0 lift)
assert clear(plate_asm, g2 | translate 0 0 lift)
# Print layout: plate beside the gears
plate = plate_asm | translate 0 (-(ra(z2) + pw + 5)) 0
assert clear(plate, g1) and clear(plate, g2)
g1 | color "orange"
g2 | color "steelblue"
if base_plate then (plate | color "gray") else g1