Hypotrochoid Drawing Set
A gear drawing toy: a toothed ring and three toothed wheels with pen holes. Hold the ring on a sheet of paper, put a pen through a hole in a wheel and roll the wheel around the inside of the ring to draw a closed flower of loops (a hypotrochoid); the outer teeth of the ring let a wheel roll around the outside for epitrochoids. The tooth counts choose the pattern: the number of lobes is ring teeth / gcd(ring teeth, wheel teeth), so the default 96-tooth ring with 60, 52 and 40-tooth wheels draws 8, 24 and 12 lobes, and the lobe count of each wheel is computed and reported by the model. Every part shares one tooth pitch, the internal teeth are made by subtracting a negative gear, each wheel is rolled to three positions inside the ring and checked for interference, and the pen holes (7 mm by default for a ballpoint tip, 10 mm for a thin pen barrel) sit on a sunflower spiral so each hole gives a different pattern. The print layout measures 128 x 163 x 4 mm; print all four parts flat as laid out, no supports (part = all, or one part at a time). Parameters: tooth pitch, ring inner and outer tooth counts (0 outer teeth gives a plain rim), the three wheel tooth counts, backlash, pen hole diameter, wheel and ring thickness.
# Hypotrochoid Drawing Set - a toothed ring and three toothed wheels that draw looping patterns
#
# Hold the ring on a sheet of paper, drop a wheel inside it, put a pen through
# one of the wheel's holes and roll the wheel around the inside of the ring.
# The pen traces a hypotrochoid: a flower of loops that closes on itself.
#
# The tooth counts choose the pattern. With R teeth on the ring and W on the
# wheel, the curve closes after W / gcd(R, W) turns of the wheel and has
# lobes = R / gcd(R, W)
# lobes. Default ring 96 and wheels 60, 52, 40 give 8, 24 and 12 lobes
# (gcd 12, 4, 8; a wheel with 35 teeth would give 96 lobes). The lobe count
# of each wheel is computed below and printed. Rolling a wheel around the
# optional outer teeth draws an epitrochoid with lobes = Zo / gcd(Zo, W).
#
# Teeth: every part uses the same tooth pitch (circumference / teeth), so the
# pitch radius of a part is teeth x pitch / (2 pi). The tooth is a rounded
# trapezoid: straight flanks at `flank` deg from the radial line, a
# semicircular tip, drawn once as a 2D sketch and repeated with `polar`. The
# ring's internal teeth are made by subtracting a "negative" gear whose teeth
# are the ring's tooth spaces, widened by the backlash. Each wheel is rolled
# to three positions inside the ring and shown not to interfere, and wheel 1
# is turned until its flanks touch to prove the backlash is real.
#
# Pen holes: as many as fit on a sunflower spiral, from near the center out to
# the rim, each hole a different radius so each gives a different pattern.
# Hole diameter 7 mm suits the tip of a ballpoint pen; 10 mm takes the barrel
# of a thin pen or marker.
#
# Print orientation: everything flat as laid out (part = "all"), no supports.
# Pattern math reused from the spur gear and planetary gearbox models; the
# ring is rigid enough at 4 mm to be held by hand or taped to the paper.
@param choices:["all", "ring", "wheel1", "wheel2", "wheel3"] desc:"Output: the print layout or one part"
part = "all"
@param 1.5..3 step:0.1 desc:"Tooth pitch (mm) - the same for every part" group:"Teeth"
pitch = 2.4
@param 48..160 desc:"Ring inner teeth R" group:"Teeth"
zr = 96
@param 0..240 desc:"Ring outer teeth Zo (0 = plain rim)" group:"Teeth"
zo = 144
@param 12..150 desc:"Wheel 1 teeth" group:"Teeth"
z1 = 60
@param 12..150 desc:"Wheel 2 teeth" group:"Teeth"
z2 = 52
@param 12..150 desc:"Wheel 3 teeth" group:"Teeth"
z3 = 40
@param 0.2..1 step:0.05 desc:"Total circular backlash between wheel and ring (mm) - print clearance" group:"Teeth"
backlash = 0.4
@param 3..11 step:0.5 desc:"Pen hole diameter (mm): 7 for a ballpoint tip, 10 for a pen barrel" group:"Body"
pen_hole = 7.0
@param 2..5 step:0.5 desc:"Wheel thickness (mm)" group:"Body"
wheel_t = 3.0
@param 2..6 step:0.5 desc:"Ring thickness (mm)" group:"Body"
ring_t = 4.0
# ---------------------------------------------------------------- tooth geometry
flank = 15 # flank angle from the radial line (deg)
ad = 0.30 * pitch # addendum: tooth height above the pitch circle
dd = 0.42 * pitch # dedendum: depth below it (0.12 pitch tip clearance)
foot = 0.3 * pitch # how far a tooth sketch reaches into the root disc
rim = 6 # plain rim width when zo = 0
web = 1.6 # minimum wall between pen holes and to the root circle
def rp(z) = z * pitch / (2 * pi) # pitch radius: circumference = z x pitch
def gcd(a, b) = if b == 0 then a else gcd(b, a % b)
def lobes(zring, zw) = zring / gcd(zring, zw)
assert z1 < zr and z2 < zr and z3 < zr
assert zo == 0 or zo > zr + 24 # the ring band must have room between the tooth rows
# One tooth centered on +X between root radius ri and tip radius ro, half
# width wp at the pitch radius r0, flanks straight, tip a semicircle.
def wat(wp, r0, r) = wp - (r - r0) * tan(flank)
def tooth(r0, wp, ri, ro) = sketch [
(ri - foot, -wat(wp, r0, ri)),
(ri, -wat(wp, r0, ri)),
(ro - wat(wp, r0, ro), -wat(wp, r0, ro)),
arc (ro - wat(wp, r0, ro), -wat(wp, r0, ro)) (ro, 0) (ro - wat(wp, r0, ro), wat(wp, r0, ro)),
(ri, wat(wp, r0, ri)),
(ri - foot, wat(wp, r0, ri))
]
wext = pitch / 4 - backlash / 4 # external tooth half width at the pitch circle
wneg = pitch / 4 + backlash / 4 # ring tooth space half width (the negative gear's tooth)
assert wat(wext, 0, ad) > 0.15 # the tip keeps some width
assert wat(wneg, 0, dd) > 0.15
# External gear: teeth on a root disc. Internal ring: outer boundary minus the negative gear.
def gear2d(z) = tooth(rp(z), wext, rp(z) - dd, rp(z) + ad) | polar z 0 orient:true | union (circle (rp(z) - dd))
def neg2d(z) = tooth(rp(z), wneg, rp(z) - ad, rp(z) + dd) | polar z 0 orient:true | union (circle (rp(z) - ad))
r_in = rp(zr) + dd # ring root radius (inner)
r_out = if zo > 0 then rp(zo) + ad else r_in + rim # ring outer radius (tip of the outer teeth)
outer2d = if zo > 0 then gear2d(zo) else circle r_out
ring2d = outer2d - neg2d(zr)
ring = ring2d | extrude ring_t
assert ring.solids == 1
assert ring.bbox.x ~ (2 * r_out)
assert 2 * r_out <= 200 # fits a 200 mm bed
assert zo == 0 or (rp(zo) - dd) - r_in >= 4 # solid band between the two tooth rows
# ---------------------------------------------------------------- wheels with pen holes
# Holes on a sunflower spiral r_j = r_max sqrt(j / n), angle 137.508 j: the
# nearest two centers are 1.6 r_max / sqrt(n) apart, so n is chosen to keep
# them at least one hole diameter plus `web` apart.
def r_max(z) = rp(z) - dd - pen_hole / 2 - web
def n_holes(z) = max(1, floor((1.6 * r_max(z) / (pen_hole + web)) ** 2))
def hx(z, j) = r_max(z) * sqrt(j / n_holes(z)) * cos(j * 137.508)
def hy(z, j) = r_max(z) * sqrt(j / n_holes(z)) * sin(j * 137.508)
def holes2d(z) = union [circle (pen_hole / 2) at:(hx(z, j), hy(z, j)) for j in range(1, n_holes(z) + 1)]
def holes3d(z) = holes2d(z) | extrude wheel_t
def wheel(z) = gear2d(z) - holes2d(z) | extrude wheel_t
assert r_max(z3) >= 0.5 and r_max(z2) >= 0.5 and r_max(z1) >= 0.5 # every wheel takes at least one hole
w1 = wheel(z1)
w2 = wheel(z2)
w3 = wheel(z3)
assert w1.solids == 1 and w2.solids == 1 and w3.solids == 1
assert w1.bbox.z ~ wheel_t
assert w1.bbox.x ~ (2 * (rp(z1) + ad))
# Holes never overlap each other (the union has the volume of n separate holes)
# and keep at least 1 mm of wall to the root circle.
def tube(r, h) = (cylinder (r + 10) (h * 3)) - (cylinder r (h * 4))
hole_vol = pi * (pen_hole / 2) ** 2 * wheel_t
assert (holes3d(z1)).volume ~ (n_holes(z1) * hole_vol) tol:0.1%
assert (holes3d(z2)).volume ~ (n_holes(z2) * hole_vol) tol:0.1%
assert (holes3d(z3)).volume ~ (n_holes(z3) * hole_vol) tol:0.1%
assert clear(holes3d(z1), tube(rp(z1) - dd - 1, wheel_t))
assert clear(holes3d(z2), tube(rp(z2) - dd - 1, wheel_t))
assert clear(holes3d(z3), tube(rp(z3) - dd - 1, wheel_t))
# ---------------------------------------------------------------- pitch and pattern
# The pitch is circumference / teeth on every part (identical by construction).
assert (2 * pi * rp(z1) / z1) ~ (2 * pi * rp(zr) / zr) tol:0.001
assert (2 * pi * rp(z2) / z2) ~ pitch tol:0.001
assert (2 * pi * rp(z3) / z3) ~ pitch tol:0.001
assert zo == 0 or (2 * pi * rp(zo) / zo) ~ pitch tol:0.001
print "pitch" pitch "ring pitch dia" (2 * rp(zr)) "wheel pitch dia" (2 * rp(z1)) (2 * rp(z2)) (2 * rp(z3))
print "lobes = R / gcd(R, W): wheel1" lobes(zr, z1) "wheel2" lobes(zr, z2) "wheel3" lobes(zr, z3)
print "wheel turns to close the curve: wheel1" (z1 / gcd(zr, z1)) "wheel2" (z2 / gcd(zr, z2)) "wheel3" (z3 / gcd(zr, z3))
print "pen holes per wheel" n_holes(z1) n_holes(z2) n_holes(z3)
# ---------------------------------------------------------------- rolling checks
# Wheel inside the ring: center at distance rp(R) - rp(W) at angle f. While the
# center moves by f the wheel turns by f (1 - R / W) about its own axis, so
# the tooth that started in the +X tooth space stays in mesh. Nothing may
# touch at three positions spanning one ring tooth pitch.
def cin(z) = rp(zr) - rp(z)
def inside(w, z, f) = w | rotate 0 0 (f * (1 - zr / z)) | translate (cin(z) * cos(f)) (cin(z) * sin(f)) 0
cp = 360 / zr
assert clear(ring, inside(w1, z1, 0)) and clear(ring, inside(w1, z1, cp / 3)) and clear(ring, inside(w1, z1, 2 * cp / 3))
assert clear(ring, inside(w2, z2, 0)) and clear(ring, inside(w2, z2, cp / 3)) and clear(ring, inside(w2, z2, 2 * cp / 3))
assert clear(ring, inside(w3, z3, 0)) and clear(ring, inside(w3, z3, cp / 3)) and clear(ring, inside(w3, z3, 2 * cp / 3))
# Backlash: wheel 1 may turn about +-(backlash / 2) / rp before a flank
# touches. 0.7 of that is still clear, 1.3 of it collides - the teeth engage.
dl = deg(backlash / 2 / rp(z1))
def turned(d) = w1 | rotate 0 0 d | translate cin(z1) 0 0
assert clear(ring, turned(0.7 * dl)) and clear(ring, turned(-0.7 * dl))
assert clear(ring, turned(1.3 * dl)) == false and clear(ring, turned(-1.3 * dl)) == false
# Outside the ring (epitrochoid), on the outer teeth: wheel 1 at distance
# rp(Zo) + rp(W), turned so a tooth space faces the ring's +X tooth, and
# spinning f (1 + Zo / W) while its center moves by f.
ph1 = if z1 % 2 == 0 then 180 / z1 else 0
def outside(f) = w1 | rotate 0 0 (ph1 + f * (1 + zo / z1)) | translate ((rp(zo) + rp(z1)) * cos(f)) ((rp(zo) + rp(z1)) * sin(f)) 0
cpo = 360 / max(zo, 1)
assert zo == 0 or (clear(ring, outside(0)) and clear(ring, outside(cpo / 3)) and clear(ring, outside(2 * cpo / 3)))
# ---------------------------------------------------------------- print layout
ra1 = rp(z1) + ad
ra2 = rp(z2) + ad
ra3 = rp(z3) + ad
row_y = -(r_out + 4 + max(ra1, ra2, ra3))
w1_p = w1 | translate (-(ra2 + 4 + ra1)) row_y 0
w2_p = w2 | translate 0 row_y 0
w3_p = w3 | translate (ra2 + 4 + ra3) row_y 0
assert clear(ring, w1_p) and clear(ring, w2_p) and clear(ring, w3_p)
assert clear(w1_p, w2_p) and clear(w2_p, w3_p)
layout = (ring | color "ivory") + (w1_p | color "orange") + (w2_p | color "steelblue") + (w3_p | color "yellowgreen")
assert layout.bbox.x <= 200 and layout.bbox.y <= 200
if part == "ring" then ring
else if part == "wheel1" then w1
else if part == "wheel2" then w2
else if part == "wheel3" then w3
else layout