"""Spur gear cut from the ISO 53 basic rack: module m, z teeth, pressure angle 20 degrees, addendum 1.0 m, dedendum 1.25 m, no profile shift, a plain disc with a round bore. Axis along Z, centred on the origin. detail = "full": involute flanks, and below them the root curve the rack's tip corner cuts (a trochoid), which undercuts small tooth counts as a real cutter does; flanks are smooth splines on computed involute points, the root curve a polyline. detail = "simple": a disc at the tip diameter, for big assemblies.""" import math from steve import Model module = 1 # ISO 54 module teeth = 20 # Number of teeth bore = 8 # mm Bore width = 8 # mm Face width detail = "full" # full: real teeth; simple: a disc at the tip diameter PRESSURE_ANGLE = 20 # degrees: ISO 53:1998 standard basic rack, profile A ADDENDUM = 1.0 # x module: ISO 53 DEDENDUM = 1.25 # x module: ISO 53 FLANK_POINTS = 5 # involute points on each flank MIN_WALL = 1.0 # x module: least ring left between the bore and the root circle, or the gear reads as a thin hoop a = math.radians(PRESSURE_ANGLE) rp = module * teeth / 2 rb = rp * math.cos(a) ra = rp + ADDENDUM * module h = DEDENDUM * module rf = rp - h if rf - bore / 2 < MIN_WALL * module: raise ValueError(f"Bore {bore} leaves less than one module of wall under the teeth: the root diameter is {2 * rf:.2f} mm.") def involute(angle): return math.tan(angle) - angle def flank_angle(r): """Half the tooth's angular width on the involute at radius r (r >= rb).""" return math.pi / (2 * teeth) + involute(a) - involute(math.acos(rb / r)) # the rack's tip corner, at c from the tooth's axis on the rack's tip line, traces the root curve as the rack rolls on the pitch circle c = math.pi * module / 4 + h * math.tan(a) start = c / rp root_curve = [] for i in range(40): beta = start * (1 + i / 39 * 1.5) x = (c - beta * rp) * math.cos(beta) + rf * math.sin(beta) y = -(c - beta * rp) * math.sin(beta) + rf * math.cos(beta) root_curve.append((math.hypot(x, y), math.atan2(x, y))) if h <= rp * math.sin(a) ** 2: # no undercut: the involute starts where the rack's flank ends join = max(rf, math.sqrt(rb ** 2 + (rp * math.sin(a) - h / math.sin(a)) ** 2)) else: # undercut: the root curve crosses the involute join = next((r for r, angle in root_curve if r >= rb and angle >= flank_angle(r)), max(rb, rf)) kept = [p for p in root_curve if p[0] < join - 1e-6] step = max(1, len(kept) // 4) root_part = kept[::step] + [(join, flank_angle(join) if join >= rb else kept[-1][1])] if kept else [] if root_part and join >= rb: root_part[-1] = (join, flank_angle(join)) flank_part = [(join + (ra - join) * i / (FLANK_POINTS - 1), 0) for i in range(FLANK_POINTS)] flank_part = [(r, flank_angle(r)) for r, _ in flank_part] if not root_part: root_part = [(rf, flank_angle(max(rf, rb)))] def at(r, angle): return (r * math.sin(angle), r * math.cos(angle)) model = Model() if detail == "simple": with model.sketch("gear_sketch", on="XY", name="Disc") as sk: sk.circle("tip", (0, 0), ra) sk.circle("bore_d", (0, 0), bore / 2) else: with model.sketch("gear_sketch", on="XY", name="Tooth profile") as sk: for n in range(teeth): centre = 2 * math.pi * n / teeth rise = [at(r, centre - angle) for r, angle in root_part] fall = [at(r, centre + angle) for r, angle in reversed(root_part)] if len(rise) > 1: sk.polyline(f"root_a_{n}", rise, closed=False) sk.polyline(f"root_b_{n}", fall, closed=False) sk.spline(f"flank_a_{n}", [at(r, centre - angle) for r, angle in flank_part]) sk.polyline(f"tip_{n}", [at(flank_part[-1][0], centre - flank_part[-1][1]), at(flank_part[-1][0], centre + flank_part[-1][1])], closed=False) sk.spline(f"flank_b_{n}", [at(r, centre + angle) for r, angle in reversed(flank_part)]) following = centre + 2 * math.pi / teeth sk.polyline(f"land_{n}", [fall[-1], at(root_part[0][0], following - root_part[0][1])], closed=False) sk.circle("bore_d", (0, 0), bore / 2) model.extrude("gear", "gear_sketch", distance=width, both=True, name="Gear") model.material("steel-generic", color="#b4b8bd") result = model.result()