Rubber Band Gun
A flat toy pistol for office rubber bands with a ratchet-wheel release: several bands are hooked between the notch at the muzzle and the hook teeth of a wheel at the back, and every pull of the trigger turns the wheel by exactly one tooth and fires one band. The trigger is an anchor escapement: one pallet holds a tooth at rest, pulling lifts it out while a second pallet drops into the path of the same tooth, and letting go hands the wheel back to the first pallet, so no return spring is needed and the wheel can never run free. The body is two mirror-image shells with spacer ribs that form a channel for the wheel and trigger; four M3 x 16 screws with nuts (3.4 mm holes) serve as the wheel axle, the trigger pivot and two clamps. The print layout is 150 x 190 x 5.7 mm and the assembled gun is 150 x 95 x 11.4 mm; all parts print flat with no supports in PLA or PETG, and the band path (89 mm from the hook to the live tooth, about 1.45x stretch for a #16 band) is reported by the model. You can change the overall length, the number of teeth (4-8, 6 is the default and releases most crisply), the trigger travel and the running clearance. This is a toy: never aim it at faces.
# Rubber Band Gun - flat pistol with a ratchet-wheel release
#
# A toy pistol for office rubber bands. Several bands are hooked between the
# notch at the muzzle and the teeth of a wheel at the back; every pull of the
# trigger lets the wheel turn by exactly one tooth and fires one band.
#
# Parts (print layout, all flat on Z = 0, no supports):
# - left and right body shells (mirror images): a 3 mm side plate with 2.7 mm
# spacer ribs around the edge; together they form a 5.4 mm channel in which
# the wheel and the trigger turn, and the bands run above the barrel
# - ratchet wheel with n hook teeth (band side undercut, pallet side radial)
# - trigger: an anchor escapement. Pallet A holds a tooth at rest; pulling
# lifts A out and lowers pallet B into the path of the same tooth, so the
# wheel (driven by the bands) turns `adv1` deg and stops on B; letting go
# lifts B and drops A, and the wheel runs the last `pitch - adv1` deg onto A.
# Both pallet faces push the trigger back toward its rest stop, so no
# return spring is needed. The pallet motion is checked here with clear()
# at rest, at 30% and 60% of the pull and at full pull, and at four wheel
# angles during the free run between the pallets.
# Hardware: four M3 x 16 screws with nuts (holes 3 + 2 * fit = 3.4 mm): wheel
# axle, trigger pivot, one at the muzzle and one in the grip clamp the shells.
# Bands: the band path is printed; a standard #16 band (63.5 mm flat length,
# 127 mm around) is stretched about 1.45x at the default length, 2.5x at 220 mm.
#
# Axes: X = barrel (+X toward the muzzle), Y up, wheel center at the origin.
# Toy: do not aim at faces.
@param choices:["assembled", "print", "shell_left", "shell_right", "wheel", "trigger"] desc:"Output: assembled view, print layout, or one part"
part = "print"
@param 130..220 step:5 desc:"Overall length, heel to muzzle (mm)"
length = 150
@param 4..8 desc:"Teeth on the wheel = shots between reloads"
teeth = 6
@param 15..19 desc:"Trigger travel (deg)"
pull = 17
@param 0.15..0.4 step:0.05 desc:"Running clearance: axle holes and the wheel/trigger channel, per side (mm)"
fit = 0.2
# --- Fixed dimensions ---------------------------------------------------------
t = 5 # wheel and trigger thickness
plate = 3 # shell side plate
rib_h = t / 2 + fit # spacer rib height on each shell
chan = t + 2 * fit # channel between the plates
total = 2 * plate + chan # assembled thickness
bore = 3 + 2 * fit # M3 axle hole
n = teeth
pitch = 360 / n
rt = max(15, 2.5 * n) # wheel tip radius: grows with the tooth count so the pockets keep their size
rr = rt - 2.5 # wheel root radius
gam = 30 # pocket angle: root of a tooth sits this far behind its tip
adv1 = min(45, 0.7 * pitch) # wheel rotation during the pull; the rest on release
# --- Wheel geometry -----------------------------------------------------------
def px(r, a) = r * cos(a)
def py(r, a) = r * sin(a)
land = degrees(1.2 / rt) # flat tip land (1.2 mm) so a pallet keeps hold during recoil
u = atan2(rr * sin(gam - land), rt - rr * cos(gam - land)) # undercut of the band flank from radial (deg)
th_live = 97 - u # live tooth (next to fire): its band flank leans 7 deg back from vertical
th0 = th_live - pitch # the tooth held by pallet A at rest
gap_a = degrees(0.3 / rt) # 0.3 mm gap between a resting tooth and a pallet face
th_a = th0 - gap_a # pallet A face (rest)
th_b = th_a - adv1 # pallet B face (pulled)
th_t = th_a - adv1 / 2 # trigger pivot on the bisector of the pallets, 9 mm from each face
rho_t = rt * cos(adv1 / 2) + sqrt(81 - (rt * sin(adv1 / 2)) ** 2)
tx = px(rho_t, th_t)
ty = py(rho_t, th_t)
rel_ang = th_live + u - adv1 # band flank angle of the fired tooth after the pull (release when < ~60)
# tooth k: radial front face (the pallets push here), flat tip land, undercut
# band flank down to the pocket root gam deg behind the tip
teeth2d = [polyline [(px(rr, th0 + k * pitch), py(rr, th0 + k * pitch)),
(px(rt, th0 + k * pitch), py(rt, th0 + k * pitch)),
(px(rt, th0 + k * pitch + land), py(rt, th0 + k * pitch + land)),
(px(rr, th0 + k * pitch + gam), py(rr, th0 + k * pitch + gam))] for k in range(n)]
wheel2d = circle rr | union teeth2d
# --- Trigger geometry (rest pose; the B group is built pulled, then rotated back) ---
# Each pallet is a small hook. Its locking face crosses the tip circle at the
# face angle and is tilted `tf` deg from radial toward the direction the hook
# moves, so the tooth pressing on it slides along the face with little recoil
# (about 1.7 deg) and the pressure still turns the trigger toward rest (draw).
# Local pallet coordinates: r = radial from the tip circle, s = along the arc (ccw).
tf0 = atan2(rho_t * cos(adv1 / 2) - rt, rho_t * sin(adv1 / 2)) # direction a face point moves, from radial (deg)
tf = tf0 - 12 # 12 deg of draw: the tooth's push turns the trigger toward rest
lift = 9 * radians(pull) * cos(tf0) # radial travel of a pallet face over the full pull (mm)
dep_a = rt - rr - 0.75 * lift # hook A corner above the pocket floor: A lets go at 75% of the pull
dep_b = rt - rr - 0.55 * lift # hook B corner: B enters the tooth path at 45% of the pull
def lx(a, r, s) = px(rt + r, a) - s * sin(a)
def ly(a, r, s) = py(rt + r, a) + s * cos(a)
def hook(a, dep, m) = polyline [(lx(a, -(rt - rr - dep), m * (rt - rr - dep) * tan(tf)), ly(a, -(rt - rr - dep), m * (rt - rr - dep) * tan(tf))),
(lx(a, 2.5, -m * 2.5 * tan(tf)), ly(a, 2.5, -m * 2.5 * tan(tf))),
(lx(a, 2.5, -3.5), ly(a, 2.5, -3.5)),
(lx(a, 0.3, m * (rt - rr - dep) * tan(tf) - 2.2), ly(a, 0.3, m * (rt - rr - dep) * tan(tf) - 2.2))]
# A: the tooth's tip corner lands on the face, so the face crosses the tip circle
# at th_a. B: the face leans the other way and its inner corner meets the flat
# front of the tooth first, so that corner is placed at th_b (face shifted by cb).
cb = degrees((rt - rr - dep_b) * tan(tf) / rt)
pal_a = hook(th_a, dep_a, -1)
pal_b = hook(th_b - cb, dep_b, 1)
def arm(a) = wire [(tx, ty), (lx(a, 2.3, -1.4), ly(a, 2.3, -1.4))] | offset 2.2 cap:"square"
arm_a = arm(th_a)
arm_b = arm(th_b - cb)
grp_b = union [arm_b, pal_b] | rotate pull origin:(tx, ty)
boss = circle 5 at:(tx, ty)
lean = 20 # finger lever leans forward (deg from vertical)
y_blk = min(-26, ty - 8) # bottom of the lower block (the trigger slot is here)
y_grip = -80
pad_y = y_blk - 18 # finger pad bottom
ll = (ty - pad_y - 3.5) / cos(lean)
ex = tx + ll * sin(lean)
ey = ty - ll * cos(lean)
lever = wire [(tx, ty), (ex, ey)] | offset 3.5
trig_fr = union [boss, arm_a, lever] # frame: must never enter the tooth circle
trig2d = union [trig_fr, pal_a, grp_b]
# Trigger slot in the bottom edge: front wall is the rest stop (0.3 from the lever)
x_stop = tx + (ty - y_blk) * tan(lean) + 3.5 / cos(lean) + 0.3
rdx = (ty - y_blk - 2.8) * tan(lean) - 3.5 / cos(lean) # lever rear edge at the rib top, rest
rdy = -(ty - y_blk - 2.8)
x_rear = tx + rdx * cos(pull) + rdy * sin(pull) - 1.0 # same point at full pull, minus 1 mm
# --- Body outline (side view) -------------------------------------------------
x_back = -34 # back of the body above the grip
dg = (y_blk - y_grip) * tan(15) # grip rake: the heel is dg behind x_back
x_muz = x_back - dg + length
y_top = 6 # barrel top: the bands run above it
y_bar = -14 # barrel bottom
def r2(x0, y0, x1, y1) = rect (x1 - x0) (y1 - y0) at:((x0 + x1) / 2, (y0 + y1) / 2)
barrel = r2(x_back, y_bar, x_muz, y_top)
block = r2(x_back, y_blk, 42, y_bar + 0.2)
grip = polyline [(x_back, y_blk + 0.2), (x_back + 26, y_blk + 0.2), (x_back + 26 - dg, y_grip), (x_back - dg, y_grip)]
bump = polyline [(x_muz - 9, y_top - 0.2), (x_muz - 10, y_top + 5), (x_muz, y_top + 5), (x_muz, y_top - 0.2)]
outline = union [barrel, circle (rr - 3), block, grip, bump]
mx = x_muz - 16 # muzzle clamp screw
my = -4
gx = x_back + 13 - dg + 8 * tan(15) # grip clamp screw
gy = y_grip + 8
# Spacer ribs: 2.8 mm bands along the perimeter, except around the wheel (the
# bands need the open channel there and the teeth sweep it) and at the trigger
# slot; plus the solid hook and two screw bosses
rw = 2.8
rib_top = r2(rt + 3, y_top - rw, x_muz, y_top)
rib_bar = r2(42 - 0.2, y_bar, x_muz, y_bar + rw)
rib_muz = r2(x_muz - rw, y_bar, x_muz, y_top)
rib_frt = r2(42 - rw, y_blk, 42, y_bar + 0.2)
rib_bt1 = r2(x_stop, y_blk, 42, y_blk + rw)
rib_bt2 = r2(x_back + 26 - 0.2, y_blk, x_rear, y_blk + rw)
rib_gf = polyline [(x_back + 26, y_blk + rw), (x_back + 26 - dg, y_grip), (x_back + 26 - dg - 2.9, y_grip), (x_back + 26 - 2.9, y_blk + rw)]
rib_gb = polyline [(x_back, y_blk + rw), (x_back + 2.9, y_blk + rw), (x_back + 2.9 - dg, y_grip), (x_back - dg, y_grip)]
rib_gbt = r2(x_back - dg, y_grip, x_back + 26 - dg, y_grip + rw)
rib_bk = r2(x_back, y_blk, x_back + rw, y_top)
ribs2d = union [rib_top, rib_bar, rib_muz, rib_frt, rib_bt1, rib_bt2, rib_gf, rib_gb, rib_gbt, rib_bk,
bump, circle 4.5 at:(mx, my), circle 4.5 at:(gx, gy)]
# --- 3D parts -----------------------------------------------------------------
def bore_at(x, y) = cylinder (bore / 2) 30 | translate x y 0
bores = union [bore_at(0, 0), bore_at(tx, ty), bore_at(mx, my), bore_at(gx, gy)]
half_l = (outline | extrude plate) + (ribs2d | extrude (rib_h + 0.1) | translate 0 0 (plate - 0.1)) - bores
z0 = plate + fit
wheel = (wheel2d | extrude t) - bore_at(0, 0) | translate 0 0 z0
trigger = (trig2d | extrude t) - bore_at(tx, ty) | translate 0 0 z0
ribs3d = ribs2d | extrude chan | translate 0 0 plate # both shells' ribs, as seen by the moving parts
def wh(a) = wheel | rotate 0 0 a
def tr(a) = trigger | rotate 0 0 (-a) origin:(tx, ty, 0) # a > 0 pulls the trigger
# --- Checks: parts ------------------------------------------------------------
assert half_l.solids == 1
assert wheel.solids == 1
assert trigger.solids == 1
assert half_l.valid and wheel.valid and trigger.valid
assert wheel.bbox.x <= 2 * rt + 0.01 and wheel.bbox.x >= 2 * rr
assert rr * (pitch - gam) * pi / 180 >= 2.5 # band pocket floor is wide enough for a band
assert dep_a >= 0.5 and dep_b >= 0.5 and lift >= 1.2 # hooks stay above the pocket floor and lift clear
assert rel_ang <= 66 # fired tooth leans forward enough to let the band slide off
# --- Checks: escapement kinematics --------------------------------------------
disc = cylinder (rt + 0.2) t | translate 0 0 (z0 + t / 2)
frame3d = trig_fr | extrude t | translate 0 0 z0
pal_b3d = grp_b | extrude t | translate 0 0 z0
pal_a3d = pal_a | extrude t | translate 0 0 z0 | rotate 0 0 (-pull) origin:(tx, ty, 0)
assert clear(frame3d, disc) # the trigger frame stays outside the teeth
assert clear(frame3d | rotate 0 0 (-pull) origin:(tx, ty, 0), disc)
assert clear(pal_b3d, disc) # pallet B is retracted at rest
assert clear(pal_a3d, disc) # pallet A is lifted clear at full pull
assert clear(tr(0), wh(0)) # rest: tooth land sits 0.3 mm from pallet A
assert clear(tr(0), wh(-1.5)) == false # ... and cannot turn forward
assert clear(tr(0.3 * pull), wh(3.5)) and clear(tr(0.6 * pull), wh(3.5)) # recoil room while A slides along the land
assert clear(tr(0.3 * pull), wh(-1.5)) == false # A still holds at 30% of the pull
assert clear(tr(0.6 * pull), wh(-1.5)) == false # ... and at 60%
assert clear(tr(0.6 * pull), wh(-adv1 - 6)) == false # ... when B is already in the tooth path (handover)
# full pull: A has let go (checked against the disc above); the wheel runs freely into B
assert clear(tr(pull), wh(-0.3 * adv1)) and clear(tr(pull), wh(-adv1 / 2))
assert clear(tr(pull), wh(-3 * adv1 / 4)) and clear(tr(pull), wh(-adv1))
assert clear(tr(pull), wh(-adv1 - 2.5)) == false # ... and stops on pallet B after adv1 deg
assert clear(tr(0), wh(-adv1)) and clear(tr(0), wh(-adv1 - (pitch - adv1) / 2)) # release: B lets go, wheel runs on
assert clear(tr(0), wh(-pitch)) # ... onto pallet A: one tooth per cycle
# trigger vs body channel, wheel vs body channel
assert clear(tr(0), ribs3d) and clear(tr(pull / 2), ribs3d) and clear(tr(pull), ribs3d)
assert clear(tr(-2), ribs3d) == false # the slot's front wall is the rest stop
assert clear(wh(0), ribs3d) and clear(wh(-pitch / 2), ribs3d)
assert clear(wheel, trigger | translate 0 0 0) # assembled parts do not interfere
# --- Band path -------------------------------------------------------------------
hx = x_muz - 9.5
hy = y_top + 2.5
lpx = px(rr, th_live + gam)
lpy = py(rr, th_live + gam)
path = sqrt((hx - lpx) ** 2 + (hy - lpy) ** 2)
loop = 2 * path + 6 # stretched band circumference (both strands + wrap)
ratio = loop / 127 # vs a #16 band (127 mm around)
capacity = floor((250 - 100) / pitch) + 1
assert ratio >= 1.1 and ratio <= 3.0
assert hx >= rt + 30
print "wheel: teeth" n "pitch (deg)" pitch "turn per pull" adv1 "per release" (pitch - adv1)
print "live tooth at (deg)" th_live "undercut" u "band flank after firing (deg)" rel_ang
print "trigger pivot" tx ty "travel (deg)" pull "pallet lift (mm)" lift "face tilt (deg)" tf "draw 12"
print "band path hook to live tooth (mm)" path "stretched loop (mm)" loop "#16 stretch ratio" ratio "bands at once" capacity
print "screws: M3 x 16 x4, holes" bore "channel" chan "assembled thickness" total
# --- Layouts ---------------------------------------------------------------------
half_r = half_l | mirror "Z" | translate 0 0 total
assembled = union [half_l, half_r, wheel, trigger]
assert assembled.bbox.x ~ length
assert assembled.bbox.z ~ total
assert assembled.solids == 3 # shells fuse at the rib faces; wheel and trigger are free
half_rp = half_l | mirror "Y" | translate 0 (2 * (y_top + 5) + 8) 0 # second shell flipped, 8 mm above the first
wheel_p = wheel | floor | translate (x_back + 32 + rt) (-40 - rt) 0 # wheel and trigger under the barrel, in front of the grip
trig_p = trigger | floor | translate (x_muz - 30 - tx) (-42 - ty) 0 # trigger pivot lands at (muzzle - 30, -42)
layout = union [half_l, half_rp, wheel_p, trig_p]
assert layout.solids == 4
print "print layout" layout.bbox
if part == "assembled" then assembled
else if part == "print" then layout
else if part == "shell_left" then half_l
else if part == "shell_right" then half_rp
else if part == "wheel" then (wheel | floor)
else (trigger | floor)