├── requirements.txt ├── thumbnail.png ├── backends ├── text.py └── dxf.py ├── LICENSE.md ├── README.md └── gear.py /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | shapely 3 | -------------------------------------------------------------------------------- /thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmakoide/gear-profile-generator/HEAD/thumbnail.png -------------------------------------------------------------------------------- /backends/text.py: -------------------------------------------------------------------------------- 1 | def write(out, geom): 2 | ring_list = [geom.exterior] + list(geom.interiors) 3 | for ring in ring_list: 4 | for x, y in ring.coords: 5 | out.write('%f %f\n' % (x, y)) 6 | 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018 Alexandre Devert 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /backends/dxf.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | 4 | 5 | def write(out, geom): 6 | # Generate the header 7 | out.write(' 999\n') 8 | out.write('DXF created from gear.py (https://github.com/marmakoide/gear-profile-generator)\n') 9 | out.write(' 0\n') 10 | out.write('SECTION\n') 11 | out.write(' 2\n') 12 | out.write('HEADER\n') 13 | out.write(' 9\n') 14 | out.write('$ACADVER\n') 15 | out.write(' 1\n') 16 | out.write('AC1009\n') 17 | out.write(' 0\n') 18 | out.write('ENDSEC\n') 19 | 20 | out.write(' 0\n') 21 | out.write('SECTION\n') 22 | out.write(' 2\n') 23 | out.write('TABLES\n') 24 | out.write(' 0\n') 25 | out.write('TABLE\n') 26 | out.write(' 2\n') 27 | out.write('LAYER\n') 28 | out.write(' 0\n') 29 | out.write('LAYER\n') 30 | out.write('2\n') 31 | out.write('1\n') 32 | out.write('0\n') 33 | out.write('ENDTAB\n') 34 | out.write(' 0\n') 35 | out.write('ENDSEC\n') 36 | out.write(' 0\n') 37 | out.write('SECTION\n') 38 | out.write(' 2\n') 39 | out.write('BLOCKS\n') 40 | out.write(' 0\n') 41 | out.write('ENDSEC\n') 42 | 43 | # For each ring 44 | out.write(' 0\n') 45 | out.write('SECTION\n') 46 | out.write(' 2\n') 47 | out.write('ENTITIES\n') 48 | 49 | for ring in itertools.chain.from_iterable([[geom.exterior], geom.interiors]): 50 | for vertex_pair in zip(ring.coords[:-1], ring.coords[1:]): 51 | out.write(' 0\n') 52 | out.write('LINE\n') 53 | out.write(' 8\n') 54 | out.write('1\n') 55 | out.write(' 62\n') 56 | out.write('1\n') 57 | for i, (x, y) in enumerate(vertex_pair): 58 | out.write(' %d\n' % (10 + i)) 59 | out.write('%f\n' % x) 60 | out.write(' %d\n' % (20 + i)) 61 | out.write('%f\n' % y) 62 | 63 | out.write(' 0\n') 64 | out.write('ENDSEC\n') 65 | 66 | # Generate the footer 67 | out.write(' 0\n') 68 | out.write('EOF\n') 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gear-profile-generator 2 | 3 | This script generates spur gear profiles (or outlines). It gives a correct output 4 | even for low numbers of teeth. 5 | 6 | ![thumbnail](https://raw.githubusercontent.com/marmakoide/gear-profile-generator/master/thumbnail.png) 7 | 8 | The gears generated with this script have been 3d printed and worked fine. 9 | However, I am no mechanical engineer, I'm a guy who code late in the evening 10 | while having a beer. If you use this script to design a suborbital passenger 11 | rocket, and that rocket blows up due to a faulty gear design, I take no 12 | responsability. 13 | 14 | The implementation is based on a method explained on the excellent Michal 15 | Zalewski's [Guerrila Guide of CNC Machining](http://lcamtuf.coredump.cx/gcnc), 16 | [chapter 6](http://lcamtuf.coredump.cx/gcnc/ch6). 17 | 18 | ## Getting Started 19 | 20 | ### Prerequisites 21 | 22 | You will need 23 | 24 | * Python 2.7 or above 25 | * [Numpy](http://www.numpy.org) 26 | * [Shapely](https://github.com/Toblerity/Shapely) 27 | 28 | 29 | ### Usage 30 | 31 | The script generates a file from the gear parameters specified from the command-line 32 | 33 | ``` 34 | python gear.py --teeth-count=17 --tooth-width=0.25 --pressure-angle=20 --backlash=0.1 -tdxf -oout.dxf 35 | ``` 36 | 37 | This command will generate a spur gear with 38 | 39 | * 17 teeth 40 | * a pressure angle of 20 degrees 41 | * a backlash of 0.1 42 | 43 | and it will be saved as a DXF file named *out.dxf*. 44 | 45 | If you generate an inner gear, to design a planetary gearbox for instance, use 46 | a negative backlash. The text output is just the raw coordinates of the multiline 47 | defining the gear's shape. 48 | 49 | ## Authors 50 | 51 | * **Alexandre Devert** - *Initial work* - [marmakoide](https://github.com/marmakoide) 52 | 53 | ## License 54 | 55 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 56 | 57 | -------------------------------------------------------------------------------- /gear.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import numpy 3 | import argparse 4 | import itertools 5 | 6 | import backends.dxf 7 | import backends.text 8 | 9 | from shapely.ops import cascaded_union 10 | from shapely.geometry import Point, MultiPoint, Polygon, box 11 | from shapely.affinity import rotate, scale, translate 12 | 13 | 14 | 15 | def rot_matrix(x): 16 | c, s = numpy.cos(x), numpy.sin(x) 17 | return numpy.array([[c, -s], [s, c]]) 18 | 19 | 20 | 21 | def rotation(X, angle, center = None): 22 | if center is None: 23 | return numpy.dot(X, rot_matrix(angle)) 24 | else: 25 | return numpy.dot(X - center, rot_matrix(angle)) + center 26 | 27 | 28 | 29 | def deg2rad(x): 30 | return (numpy.pi / 180) * x 31 | 32 | 33 | 34 | def generate(teeth_count = 8, 35 | tooth_width = 1., 36 | pressure_angle = deg2rad(20.), 37 | backlash = 0., 38 | frame_count = 16): 39 | tooth_width -= backlash 40 | pitch_circumference = tooth_width * 2 * teeth_count 41 | pitch_radius = pitch_circumference / (2 * numpy.pi) 42 | addendum = tooth_width * (2 / numpy.pi) 43 | dedendum = addendum 44 | outer_radius = pitch_radius + addendum 45 | print pitch_radius - dedendum 46 | # Tooth profile 47 | profile = numpy.array([ 48 | [-(.5 * tooth_width + addendum * numpy.tan(pressure_angle)), addendum], 49 | [-(.5 * tooth_width - dedendum * numpy.tan(pressure_angle)), -dedendum], 50 | [ (.5 * tooth_width - dedendum * numpy.tan(pressure_angle)), -dedendum], 51 | [ (.5 * tooth_width + addendum * numpy.tan(pressure_angle)) , addendum] 52 | ]) 53 | 54 | outer_circle = Point(0., 0.).buffer(outer_radius) 55 | 56 | poly_list = [] 57 | prev_X = None 58 | l = 2 * tooth_width / pitch_radius 59 | for theta in numpy.linspace(0, l, frame_count): 60 | X = rotation(profile + numpy.array((-theta * pitch_radius, pitch_radius)), theta) 61 | if prev_X is not None: 62 | poly_list.append(MultiPoint([x for x in X] + [x for x in prev_X]).convex_hull) 63 | prev_X = X 64 | 65 | def circle_sector(angle, r): 66 | box_a = rotate(box(0., -2 * r, 2 * r, 2 * r), -angle / 2, Point(0., 0.)) 67 | box_b = rotate(box(-2 * r, -2 * r, 0, 2 * r), angle / 2, Point(0., 0.)) 68 | return Point(0., 0.).buffer(r).difference(box_a.union(box_b)) 69 | 70 | # Generate a tooth profile 71 | tooth_poly = cascaded_union(poly_list) 72 | tooth_poly = tooth_poly.union(scale(tooth_poly, -1, 1, 1, Point(0., 0.))) 73 | 74 | # Generate the full gear 75 | gear_poly = Point(0., 0.).buffer(outer_radius) 76 | for i in range(0, teeth_count): 77 | gear_poly = rotate(gear_poly.difference(tooth_poly), (2 * numpy.pi) / teeth_count, Point(0., 0.), use_radians = True) 78 | 79 | # Job done 80 | return gear_poly, pitch_radius 81 | 82 | 83 | 84 | def main(): 85 | # Command line parsing 86 | parser = argparse.ArgumentParser(description = 'Generate 2d spur gears profiles') 87 | parser.add_argument('-c', '--teeth-count', type = int, default = 17, help = 'Teeth count') 88 | parser.add_argument('-w', '--tooth-width', type = float, default = 10., help = 'Tooth width') 89 | parser.add_argument('-p', '--pressure-angle', type = float, default = 20., help = 'Pressure angle in degrees') 90 | parser.add_argument('-n', '--frame-count', type = int, default = 16, help = 'Number of frames used to build the involute') 91 | parser.add_argument('-b', '--backlash', type = float, default = 0.2, help = 'Backlash') 92 | parser.add_argument('-t', '--output-type', choices = ['dxf', 'text'], default = 'dxf', help = 'Output type') 93 | parser.add_argument('-o', '--output-path', default = 'out', help = 'Output file') 94 | args = parser.parse_args() 95 | 96 | # Input parameters safety checks 97 | if args.teeth_count <= 0: 98 | sys.stderr.write('Invalid teeth count\n') 99 | sys.exit(1) 100 | 101 | # Generate the shape 102 | poly, pitch_radius = generate(args.teeth_count, 103 | args.tooth_width, 104 | deg2rad(args.pressure_angle), 105 | args.backlash, 106 | args.frame_count) 107 | 108 | # Write the shape to the output 109 | print 'pitch radius =', pitch_radius 110 | 111 | with open(args.output_path, 'w') as f: 112 | if args.output_type == 'dxf': 113 | backends.dxf.write(f, poly) 114 | elif args.output_type == 'text': 115 | backends.text.write(f, poly) 116 | 117 | 118 | 119 | if __name__ == '__main__': 120 | main() 121 | --------------------------------------------------------------------------------