Source code for sprocket

"""

Parametric Sprockets

name: sprocket.py
by:   Gumyr
date: February 13th 2026

desc:

    This python/build123d code is a parameterized chain sprocket generator.
    Given a chain pitch, a number of teeth and other optional parameters, a
    sprocket centered on the origin is generated.

license:

    Copyright 2026 Gumyr

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.

"""

from math import cos, pi, radians, sqrt
from build123d import *
from build123d.geometry import TOLERANCE


[docs] class Sprocket(BasePartObject): """ Create a new sprocket object as defined by the given parameters. The input parameter defaults are appropriate for a standard bicycle chain. Args: num_teeth (int): number of teeth on the perimeter of the sprocket chain_pitch (float): distance between the centers of two adjacent rollers. Defaults to 1/2 inch. roller_diameter (float): size of the cylindrical rollers within the chain. Defaults to 5/16 inch. clearance (float): size of the gap between the chain's rollers and the sprocket's teeth. Defaults to 0. thickness (float): thickness of the sprocket. Defaults to 0.084 inch. bolt_circle_diameter (float): diameter of the mounting bolt hole pattern. Defaults to 0. num_mount_bolts (int): number of bolt holes (default 0) - if 0, no bolt holes are added to the sprocket mount_bolt_diameter (float): size of the bolt holes use to mount the sprocket. Defaults to 0. bore_diameter (float): size of the central hole in the sprocket (default 0) - if 0, no bore hole is added to the sprocket rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0) align (Align | tuple[Align, Align, Align] | None, optional): align MIN, CENTER, or MAX of object. Defaults to Align.CENTER mode (Mode, optional): combine mode. Defaults to Mode.ADD **NOTE**: Default parameters are for standard single sprocket bicycle chains. Attributes: pitch_radius (float): radius of the circle formed by the center of the chain rollers outer_radius (float): size of the sprocket from center to tip of the teeth pitch_circumference (float): circumference of the sprocket at the pitch radius plan (Face): 2D plan of the base sprocket without any cutouts Example: .. doctest:: >>> s = Sprocket(num_teeth=32) >>> print(s.pitch_radius) 64.78458745735234 >>> s.rotate((0,0,0),(0,0,1),10) """ @property def pitch_radius(self): """The radius of the circle formed by the center of the chain rollers""" return Sprocket.sprocket_pitch_radius(self.num_teeth, self.chain_pitch) @property def outer_radius(self): """The size of the sprocket from center to tip of the teeth""" if self._flat_teeth: o_radius = self.pitch_radius + self.roller_diameter / 4 else: o_radius = sqrt(self.pitch_radius**2 - (self.chain_pitch / 2) ** 2) + sqrt( (self.chain_pitch - self.roller_diameter / 2) ** 2 - (self.chain_pitch / 2) ** 2 ) return o_radius @property def pitch_circumference(self): """The circumference of the sprocket at the pitch radius""" return Sprocket.sprocket_circumference(self.num_teeth, self.chain_pitch) def __init__( self, num_teeth: int, chain_pitch: float = (1 / 2) * IN, roller_diameter: float = (5 / 16) * IN, clearance: float = 0.0, thickness: float = 0.084 * IN, bolt_circle_diameter: float = 0.0, num_mount_bolts: int = 0, mount_bolt_diameter: float = 0.0, bore_diameter: float = 0.0, rotation: RotationLike = (0, 0, 0), align: None | Align | tuple[Align, Align, Align] = Align.CENTER, mode: Mode = Mode.ADD, ): """Validate inputs and create the chain assembly object""" self.num_teeth = num_teeth self.chain_pitch = chain_pitch self.roller_diameter = roller_diameter self.clearance = clearance self.thickness = thickness self.bolt_circle_diameter = bolt_circle_diameter self.num_mount_bolts = num_mount_bolts self.mount_bolt_diameter = mount_bolt_diameter self.bore_diameter = bore_diameter # Validate inputs """Ensure that the roller would fit in the chain""" if self.roller_diameter >= self.chain_pitch: raise ValueError( f"roller_diameter {self.roller_diameter} is too large for chain_pitch {self.chain_pitch}" ) if not isinstance(num_teeth, int) or num_teeth <= 2: raise ValueError( f"num_teeth must be an integer greater than 2 not {num_teeth}" ) # Create the sprocket () sprocket = self._make_sprocket() # Remove an extraneous Compound wrapper if isinstance(sprocket, Compound): sprocket = sprocket.unwrap() super().__init__(sprocket, rotation, align, mode) def _make_sprocket(self) -> Compound: """Create a new sprocket object as defined by the class attributes""" tooth_tip = Sprocket._make_tooth_outline( self.num_teeth, self.chain_pitch, self.roller_diameter, self.clearance ) tooth_face = Pos(Z=-self.thickness / 2) * Face( Wire( tooth_tip.edges() + [Line((0, 0), tooth_tip @ 0), Line((0, 0), tooth_tip @ 1)] ) ) tooth = extrude(tooth_face, self.thickness, (0, 0, 1)) # Chamfer the outside edges if the sprocket has "flat" teeth self._flat_teeth = len(tooth_tip.edges()) == 5 if self._flat_teeth: tip_face = ( tooth.faces().filter_by(GeomType.CYLINDER).sort_by_distance((0, 0))[-1] ) to_chamfer = tip_face.edges().filter_by(GeomType.CIRCLE) tooth = chamfer( to_chamfer, self.thickness * 0.25, self.thickness * 0.5, reference=tip_face, ) sprocket = Solid() + PolarLocations(0, self.num_teeth) * tooth sprocket.orientation += (0, 0, 90) # # Create bolt holes if ( self.bolt_circle_diameter != 0 and self.num_mount_bolts != 0 and self.mount_bolt_diameter != 0 ): sprocket -= PolarLocations( self.bolt_circle_diameter / 2, self.num_mount_bolts ) * Cylinder(self.mount_bolt_diameter / 2, self.thickness) # # Create a central bore if self.bore_diameter != 0: sprocket -= Cylinder(self.bore_diameter / 2, self.thickness) return sprocket @property def plan(self) -> Face: """2D plan of the base sprocket without any cutouts""" tooth_tip = Sprocket._make_tooth_outline( self.num_teeth, self.chain_pitch, self.roller_diameter, self.clearance ) tooth_face = Face( Wire( tooth_tip.edges() + [Line((0, 0), tooth_tip @ 0), Line((0, 0), tooth_tip @ 1)] ) ) if tooth_face.normal_at().Z == -1: tooth_face = -tooth_face sprocket_plan = Face() + PolarLocations(0, self.num_teeth) * tooth_face return sprocket_plan
[docs] @staticmethod def sprocket_pitch_radius(num_teeth: int, chain_pitch: float) -> float: """ Calculate and return the pitch radius of a sprocket with the given number of teeth and chain pitch Parameters ---------- num_teeth : int the number of teeth on the perimeter of the sprocket chain_pitch : float the distance between two adjacent pins in a single link (default 1/2 inch) """ return sqrt(chain_pitch * chain_pitch / (2 * (1 - cos(2 * pi / num_teeth))))
[docs] @staticmethod def sprocket_circumference(num_teeth: int, chain_pitch: float) -> float: """ Calculate and return the pitch circumference of a sprocket with the given number of teeth and chain pitch Parameters ---------- num_teeth : int the number of teeth on the perimeter of the sprocket chain_pitch : float the distance between two adjacent pins in a single link (default 1/2 inch) """ return ( 2 * pi * sqrt(chain_pitch * chain_pitch / (2 * (1 - cos(2 * pi / num_teeth)))) )
@staticmethod def _make_tooth_outline( num_teeth: int, chain_pitch: float, roller_diameter: float, clearance: float = 0.0, ) -> Wire: """ Create a Wire in the shape of a single tooth of the sprocket defined by the input parameters There are two different shapes that the tooth could take: 1) "Spiky" teeth: given sufficiently large rollers, there is no circular top 2) "Flat" teeth: given smaller rollers, a circular "flat" section bridges the space between roller slots """ roller_rad = roller_diameter / 2 + clearance tooth_a_degrees = 360 / num_teeth pitch_rad = sqrt(chain_pitch**2 / (2 * (1 - cos(radians(tooth_a_degrees))))) outer_rad = pitch_rad + roller_rad / 2 outer_circle = CenterArc((0, 0), outer_rad, 0, 360) roller_circle = CenterArc( Vector(pitch_rad, 0).rotate(Axis.Z, tooth_a_degrees / 2), roller_rad, 0, 360 ) link_circle = CenterArc( Vector(pitch_rad, 0).rotate(Axis.Z, -tooth_a_degrees / 2), chain_pitch - roller_rad, 0, 360, ) roller_line_pnt = Line(roller_circle.arc_center, link_circle.arc_center) @ ( roller_rad / chain_pitch ) outer_pnt = link_circle.find_intersection_points(outer_circle).sort_by(Axis.Y)[ -1 ] roller_start_pnt = ( PolarLine((0, 0), pitch_rad - roller_rad, tooth_a_degrees / 2) @ 1 ) arc1 = roller_circle.trim(roller_start_pnt, roller_line_pnt) if outer_pnt.Y > 0: # "Flat" topped sprockets arc2 = link_circle.trim(roller_line_pnt, outer_pnt) arc3 = RadiusArc(outer_pnt, (outer_pnt.X, -outer_pnt.Y), outer_rad) arc4 = arc2.mirror(Plane.XZ) arc5 = arc1.mirror(Plane.XZ) tooth_perimeter = Wire([arc1, arc2, arc3, arc4, arc5]) else: link_axis_pnt = link_circle.intersect(Axis.X).sort_by(Axis.X)[-1] arc2 = link_circle.trim(roller_line_pnt, link_axis_pnt) arc3 = arc2.mirror(Plane.XZ) arc4 = arc1.mirror(Plane.XZ) tooth_perimeter = Wire([arc1, arc2, arc3, arc4]) return tooth_perimeter