Created
May 20, 2015 23:16
-
-
Save koppi/2e45f344106474961909 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env python | |
# -*- coding: utf-8 -*- | |
''' | |
Copyright (C) 2007 Aaron Spike (aaron @ ekips.org) | |
Copyright (C) 2007 Tavmjong Bah (tavmjong @ free.fr) | |
Copyright (C) http://cnc-club.ru/forum/viewtopic.php?f=33&t=434&p=2594#p2500 | |
Copyright (C) 2014 Jürgen Weigert ([email protected]) | |
Copyright (C) 2015 Jakob Flierl ([email protected]) | |
This program is free software; you can redistribute it and/or modify | |
it under the terms of the GNU General Public License as published by | |
the Free Software Foundation; either version 2 of the License, or | |
(at your option) any later version. | |
This program is distributed in the hope that it will be useful, | |
but WITHOUT ANY WARRANTY; without even the implied warranty of | |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
GNU General Public License for more details. | |
You should have received a copy of the GNU General Public License | |
along with this program; if not, write to the Free Software | |
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |
2014-03-20 [email protected] 0.2 Option --accuracy=0 for automatic added. | |
2014-03-21 sent upstream: | |
https://bugs.launchpad.net/inkscape/+bug/1295641 | |
2014-03-21 [email protected] 0.3 Fixed center of rotation for gears with odd | |
umber of teeth. | |
2014-04-04 juewei 0.7 Revamped calc_unit_factor(). | |
2014-04-05 juewei 0.7a Correctly positioned rack gear. | |
The geometry above the meshing line is wrong. | |
2014-04-06 juewei 0.7b Undercut detection added. Reference: | |
http://nptel.ac.in/courses/IIT-MADRAS/Machine_Design_II/pdf/2_2.pdf | |
Manually merged | |
https://github.com/jnweiger/inkscape-gears-dev/pull/15 | |
2014-04-07 juewei 0.7c | |
Manually merged | |
https://github.com/jnweiger/inkscape-gears-dev/pull/17 | |
2014-04-09 juewei 0.8 Fixed | |
https://github.com/jnweiger/inkscape-gears-dev/issues/19 | |
Ring gears are ready for production now. | |
Thanks neon22 for driving this. | |
Profile shift implemented (Advanced Tab), fixing | |
https://github.com/jnweiger/inkscape-gears-dev/issues/9 | |
2015-05-18 koppi Generate DXF instead of SVG, remove Inkscape dependency | |
From http://en.wikipedia.org/wiki/Involute_gear : | |
The involute gear profile, originally designed by Leonhard Euler,[1] is the | |
most commonly used system for gearing today, with cycloidal gearing still | |
used for some specialties such as clocks. In an involute gear, the profiles | |
of the teeth are involutes of a circle. (The involute of a circle is the | |
spiraling curve traced by the end of an imaginary taut string unwinding | |
itself from that stationary circle called the base circle.) | |
''' | |
from __future__ import print_function | |
import sys | |
from optparse import OptionParser | |
from math import pi, cos, sin, tan, radians, degrees, ceil, asin, acos, sqrt | |
import ezdxf | |
two_pi = 2 * pi | |
__version__ = '0.8a' | |
def linspace(a,b,n): | |
""" return list of linear interp of a to b in n steps | |
- if a and b are ints - you'll get an int result. | |
- n must be an integer | |
""" | |
return [a+x*(b-a)/(n-1) for x in range(0,n)] | |
def involute_intersect_angle(Rb, R): | |
" " | |
Rb, R = float(Rb), float(R) | |
return (sqrt(R**2 - Rb**2) / (Rb)) - (acos(Rb / R)) | |
def point_on_circle(radius, angle): | |
" return xy coord of the point at distance radius from origin at angle " | |
x = radius * cos(angle) | |
y = radius * sin(angle) | |
return (x, y) | |
def points_to_bbox(p): | |
""" from a list of points (x,y pairs) | |
- return the lower-left xy and upper-right xy | |
""" | |
llx = urx = p[0][0] | |
lly = ury = p[0][1] | |
for x in p[1:]: | |
if x[0] < llx: llx = x[0] | |
elif x[0] > urx: urx = x[0] | |
if x[1] < lly: lly = x[1] | |
elif x[1] > ury: ury = x[1] | |
return (llx, lly, urx, ury) | |
def points_to_bbox_center(p): | |
""" from a list of points (x,y pairs) | |
- find midpoint of bounding box around all points | |
- return (x,y) | |
""" | |
bbox = points_to_bbox(p) | |
return ((bbox[0]+bbox[2])/2.0, (bbox[1]+bbox[3])/2.0) | |
### Undercut support functions | |
def undercut_min_teeth(pitch_angle, k=1.0): | |
""" computes the minimum tooth count for a | |
spur gear so that no undercut with the given pitch_angle (in deg) | |
and an addendum = k * metric_module, where 0 < k < 1 | |
Note: | |
The return value should be rounded upwards for perfect safety. E.g. | |
min_teeth = int(math.ceil(undercut_min_teeth(20.0))) # 18, not 17 | |
""" | |
x = sin(radians(pitch_angle)) | |
return 2*k /(x*x) | |
def undercut_max_k(teeth, pitch_angle=20.0): | |
""" computes the maximum k value for a given teeth count and pitch_angle | |
so that no undercut occurs. | |
""" | |
x = sin(radians(pitch_angle)) | |
return 0.5 * teeth * x * x | |
def undercut_min_angle(teeth, k=1.0): | |
""" computes the minimum pitch angle, to that the given teeth count (and | |
profile shift) cause no undercut. | |
""" | |
return degrees(asin(min(0.856, sqrt(2.0*k/teeth)))) # max 59.9 deg | |
def have_undercut(teeth, pitch_angle=20.0, k=1.0): | |
""" returns true if the specified number of teeth would | |
cause an undercut. | |
""" | |
return (teeth < undercut_min_teeth(pitch_angle, k)) | |
## gather all basic gear calculations in one place | |
def gear_calculations(num_teeth, circular_pitch, pressure_angle, | |
clearance=0, ring_gear=False, profile_shift=0.): | |
""" Put base calcs for spur/ring gears in one place. | |
- negative profile shifting helps against undercut. | |
""" | |
diametral_pitch = pi / circular_pitch | |
pitch_diameter = num_teeth / diametral_pitch | |
pitch_radius = pitch_diameter / 2.0 | |
addendum = 1 / diametral_pitch | |
#dedendum = 1.157 / diametral_pitch # auto calc clearance | |
dedendum = addendum | |
dedendum *= 1+profile_shift | |
addendum *= 1-profile_shift | |
if ring_gear: | |
addendum = addendum + clearance # our method | |
else: | |
dedendum = dedendum + clearance # our method | |
# | |
# | |
base_radius = pitch_diameter * cos(radians(pressure_angle)) / 2.0 | |
outer_radius = pitch_radius + addendum | |
root_radius = pitch_radius - dedendum | |
# Tooth thickness: Tooth width along pitch circle. | |
tooth_thickness = ( pi * pitch_diameter ) / ( 2.0 * num_teeth ) | |
# we don't use these | |
working_depth = 2 / diametral_pitch | |
whole_depth = 2.157 / diametral_pitch | |
#outside_diameter = (num_teeth + 2) / diametral_pitch | |
# | |
return (pitch_radius, base_radius, | |
addendum, dedendum, outer_radius, root_radius, | |
tooth_thickness | |
) | |
def generate_rack_points(tooth_count, pitch, addendum, pressure_angle, | |
base_height, tab_length, clearance=0, | |
draw_guides=False): | |
""" Return path (suitable for svg) of the Rack gear. | |
- rack gear uses straight sides | |
- involute on a circle of infinite radius is a linear ramp | |
- the meshing circle touches at y = 0, | |
- the highest elevation of the teeth is at y = +addendum | |
- the lowest elevation of the teeth is at y = -addendum-clearance | |
- the base_height extends downwards from the lowest elevation. | |
- we generate this middle tooth exactly centered on the y=0 line. | |
(an extra tooth on the right hand side, if num of teeth is even) | |
""" | |
spacing = 0.5 * pitch # rolling one pitch distance on the | |
# spur gear pitch_diameter. | |
# | |
# roughly center rack in drawing, exact position is so that it meshes | |
# nicely with the spur gear. | |
# | |
# -0.5*spacing has a gap in the center. | |
# +0.5*spacing has a tooth in the center. | |
fudge = +0.5 * spacing | |
tas = tan(radians(pressure_angle)) * addendum | |
tasc = tan(radians(pressure_angle)) * (addendum+clearance) | |
base_top = addendum+clearance | |
base_bot = addendum+clearance+base_height | |
x_lhs = - pitch * int(0.5*tooth_count-.5) - spacing - tab_length - tasc + fudge | |
#inkex.debug("angle=%s spacing=%s"%(pressure_angle, spacing)) | |
# Start with base tab on LHS | |
points = [] # make list of points | |
points.append((x_lhs, base_bot)) | |
points.append((x_lhs, base_top)) | |
x = x_lhs + tab_length+tasc | |
# An involute on a circle of infinite radius is a simple linear ramp. | |
# We need to add curve at bottom and use clearance. | |
for i in range(tooth_count): | |
# move along path, generating the next 'tooth' | |
# pitch line is at y=0. the left edge hits the pitch line at x | |
points.append((x-tasc, base_top)) | |
points.append((x+tas, -addendum)) | |
points.append((x+spacing-tas, -addendum)) | |
points.append((x+spacing+tasc, base_top)) | |
x += pitch | |
x -= spacing # remove last adjustment | |
# add base on RHS | |
x_rhs = x+tasc+tab_length | |
points.append((x_rhs, base_top)) | |
points.append((x_rhs, base_bot)) | |
# We don't close the path here. Caller does it. | |
# points.append((x_lhs, base_bot)) | |
# Draw line representing the pitch circle of infinite diameter | |
guide_path = None | |
if draw_guides: | |
p = [] | |
p.append( (x_lhs + 0.5 * tab_length, 0) ) | |
p.append( (x_rhs - 0.5 * tab_length, 0) ) | |
guide_path = points_to_svgd(p) | |
# return points ready for use in an SVG 'path' | |
return (points, guide_path) | |
def generate_spur_points(teeth, base_radius, pitch_radius, outer_radius, | |
root_radius, accuracy_involute, accuracy_circular): | |
""" given a set of core gear params | |
- generate the svg path for the gear | |
""" | |
half_thick_angle = two_pi / (4.0 * teeth ) #?? = pi / (2.0 * teeth) | |
pitch_to_base_angle = involute_intersect_angle(base_radius, pitch_radius) | |
pitch_to_outer_angle = involute_intersect_angle(base_radius, outer_radius) - pitch_to_base_angle | |
start_involute_radius = max(base_radius, root_radius) | |
radii = linspace(start_involute_radius, outer_radius, accuracy_involute) | |
angles = [involute_intersect_angle(base_radius, r) for r in radii] | |
centers = [(x * two_pi / float( teeth) ) for x in range( teeth ) ] | |
points = [] | |
for c in centers: | |
# Angles | |
pitch1 = c - half_thick_angle | |
base1 = pitch1 - pitch_to_base_angle | |
offsetangles1 = [ base1 + x for x in angles] | |
points1 = [ point_on_circle( radii[i], offsetangles1[i]) for i in range(0,len(radii)) ] | |
pitch2 = c + half_thick_angle | |
base2 = pitch2 + pitch_to_base_angle | |
offsetangles2 = [ base2 - x for x in angles] | |
points2 = [ point_on_circle( radii[i], offsetangles2[i]) for i in range(0,len(radii)) ] | |
points_on_outer_radius = [ point_on_circle(outer_radius, x) for x in linspace(offsetangles1[-1], offsetangles2[-1], accuracy_circular) ] | |
if root_radius > base_radius: | |
pitch_to_root_angle = pitch_to_base_angle - involute_intersect_angle(base_radius, root_radius ) | |
root1 = pitch1 - pitch_to_root_angle | |
root2 = pitch2 + pitch_to_root_angle | |
points_on_root = [point_on_circle (root_radius, x) for x in linspace(root2, root1+(two_pi/float(teeth)), accuracy_circular) ] | |
p_tmp = points1 + points_on_outer_radius[1:-1] + points2[::-1] + points_on_root[1:-1] # [::-1] reverses list; [1:-1] removes first and last element | |
else: | |
points_on_root = [point_on_circle (root_radius, x) for x in linspace(base2, base1+(two_pi/float(teeth)), accuracy_circular) ] | |
p_tmp = points1 + points_on_outer_radius[1:-1] + points2[::-1] + points_on_root # [::-1] reverses list | |
points.extend( p_tmp ) | |
return (points) | |
def r2d(radians): | |
return 180 * radians / pi | |
def generate_spokes_path(parent, | |
root_radius, spoke_width, spoke_count, | |
mount_radius, mount_hole, | |
unit_factor, unit_label): | |
""" given a set of constraints | |
- generate the svg path for the gear spokes | |
- lies between mount_radius (inner hole) and root_radius (bottom of the teeth) | |
- spoke width also defines the spacing at the root_radius | |
- mount_radius is adjusted so that spokes fit if there is room | |
- if no room (collision) then spokes not drawn | |
""" | |
# Spokes | |
collision = False # assume we draw spokes | |
messages = [] # messages to send back about changes. | |
path = '' | |
r_outer = root_radius - spoke_width | |
# checks for collision with spokes | |
# check for mount hole collision with inner spokes | |
if mount_radius <= mount_hole/2: | |
adj_factor = (r_outer - mount_hole/2) / 5 | |
if adj_factor < 0.1: | |
# not enough reasonable room | |
collision = True | |
messages.append("adj_factor < 0.1: %f" % (adj_factor)) | |
else: | |
mount_radius = mount_hole/2 + adj_factor # small fix | |
messages.append("mount support too small. Auto increased to %2.2f%s." % (mount_radius/unit_factor*2, unit_label)) | |
# then check to see if cross-over on spoke width | |
if spoke_width * spoke_count +0.5 >= two_pi * mount_radius: | |
adj_factor = 1.2 # wrong value. its probably one of | |
# the points distances calculated below | |
mount_radius += adj_factor | |
messages.append("too many spokes. Increased mount support by %2.3f%s" % (adj_factor/unit_factor, unit_label)) | |
# check for collision with outer rim | |
if r_outer <= mount_radius: | |
# not enough room to draw spokes so cancel | |
collision = True | |
if collision: # don't draw spokes if no room. | |
messages.append("not enough room for spokes. Decrease --spoke-width.") | |
parent.warn_spokes = True | |
else: # draw spokes | |
for i in range(spoke_count): | |
points = [] | |
start_a, end_a = i * two_pi / spoke_count, (i+1) * two_pi / spoke_count | |
# inner circle around mount | |
asin_factor = spoke_width/mount_radius/2 | |
# check if need to clamp radius | |
asin_factor = max(-1.0, min(1.0, asin_factor)) # no longer needed - resized above | |
a = asin(asin_factor) | |
points += [ point_on_circle(mount_radius, start_a + a), point_on_circle(mount_radius, end_a - a)] | |
# is inner circle too small | |
asin_factor = spoke_width/r_outer/2 | |
# check if need to clamp radius | |
asin_factor = max(-1.0, min(1.0, asin_factor)) # no longer needed - resized above | |
a = asin(asin_factor) | |
points += [point_on_circle(r_outer, end_a - a), point_on_circle(r_outer, start_a + a) ] | |
path += ( | |
"M %f,%f" % points[0] + | |
"A %f,%f %s %s %s %f,%f" % tuple((mount_radius, mount_radius, 0, 0 if spoke_count!=1 else 1, 1 ) + points[1]) + | |
"L %f,%f" % points[2] + | |
"A %f,%f %s %s %s %f,%f" % tuple((r_outer, r_outer, 0, 0 if spoke_count!=1 else 1, 0 ) + points[3]) + | |
"Z" | |
) | |
# parent.d.append(sdxf.Arc(layer=self.parent.layer_spokes, center=(0,0), radius=mount_radius, startAngle=r2d(start_a), endAngle=r2d(end_a))) # FIXME | |
parent.msp.add_line(points[0], points[1], dxfattribs={'layer': parent.layer_spokes}) | |
parent.msp.add_line(points[0], points[1], dxfattribs={'layer': parent.layer_spokes}) | |
parent.msp.add_line(points[1], points[2], dxfattribs={'layer': parent.layer_spokes}) | |
parent.msp.add_arc((0,0), r_outer, r2d(start_a + a), r2d(end_a - a), dxfattribs={'layer': parent.layer_spokes}) | |
# parent.d.append(sdxf.Line(layer=parent..layer_spokes, points=[points[2],points[3]])) | |
parent.msp.add_line(points[3], points[0], dxfattribs={'layer': parent.layer_spokes}) | |
return (path, messages) | |
class Gears: | |
def __init__(self): | |
self.tty = "" | |
self.notes = [] | |
self.warnings = [] # list of extra messages to be shown in annotations | |
self.warn_undercut = False | |
self.warn_spokes = False | |
self.op = OptionParser() | |
self.op.add_option("-o", "--output", | |
action="store", type="string", | |
dest="output", default="gear.dxf", | |
help="DXF output filename") | |
self.op.add_option("-t", "--teeth", | |
action="store", type="int", | |
dest="teeth", default=24, | |
help="Number of teeth") | |
self.op.add_option("-s", "--system", | |
action="store", type="string", | |
dest="system", default='CP', | |
help="Select system: 'CP' (Cyclic Pitch (default)), 'DP' (Diametral Pitch), 'MM' (Metric Module)") | |
self.op.add_option("-d", "--dimension", | |
action="store", type="float", | |
dest="dimension", default=1.0, | |
help="Tooth size, depending on system (which defaults to CP)") | |
self.op.add_option("-a", "--angle", | |
action="store", type="float", | |
dest="angle", default=20.0, | |
help="Pressure Angle (common values: 14.5, 20, 25 degrees)") | |
self.op.add_option("-p", "--profile-shift", | |
action="store", type="float", | |
dest="profile_shift", default=20.0, | |
help="Profile shift [in percent of the module]. Negative values help against undercut") | |
self.op.add_option("-u", "--units", | |
action="store", type="string", | |
dest="units", default='mm', | |
help="Units this dialog is using") | |
self.op.add_option("-A", "--accuracy", | |
action="store", type="int", | |
dest="accuracy", default=0, | |
help="Accuracy of involute: automatic: 5..20 (default), best: 20(default), medium 10, low: 5; good acuracy is important with a low tooth count") | |
# Clearance: Radial distance between top of tooth on one gear to bottom of gap on another. | |
self.op.add_option("", "--clearance", | |
action="store", type="float", | |
dest="clearance", default=0.0, | |
help="Clearance between bottom of gap of this gear and top of tooth of another") | |
self.op.add_option("", "--annotation", | |
action="store_true", | |
dest="annotation", default=False, | |
help="Draw annotation text") | |
self.op.add_option("-i", "--internal-ring", | |
action="store_true", | |
dest="internal_ring", default=False, | |
help="Ring (or Internal) gear style (default: normal spur gear)") | |
self.op.add_option("", "--mount-hole", | |
action="store", type="float", | |
dest="mount_hole", default=5, | |
help="Mount hole diameter") | |
self.op.add_option("", "--mount-diameter", | |
action="store", type="float", | |
dest="mount_diameter", default=15, | |
help="Mount support diameter") | |
self.op.add_option("", "--spoke-count", | |
action="store", type="int", | |
dest="spoke_count", default=3, | |
help="Spokes count") | |
self.op.add_option("", "--spoke-width", | |
action="store", type="float", | |
dest="spoke_width", default=5, | |
help="Spoke width") | |
self.op.add_option("", "--holes-rounding", | |
action="store", type="float", | |
dest="holes_rounding", default=5, | |
help="Holes rounding") | |
self.op.add_option("-x", "--centercross", | |
action="store_true", | |
dest="centercross", default=False, | |
help="Draw cross in center") | |
self.op.add_option("-c", "--pitchcircle", | |
action="store_true", | |
dest="pitchcircle", default=False, | |
help="Draw pitch circle (for mating)") | |
self.op.add_option("-r", "--draw-rack", | |
action="store_true", | |
dest="drawrack", default=False, | |
help="Draw rack gear instead of spur gear") | |
self.op.add_option("", "--rack-teeth-length", | |
action="store", type="int", | |
dest="teeth_length", default=12, | |
help="Length (in teeth) of rack") | |
self.op.add_option("", "--rack-base-height", | |
action="store", type="float", | |
dest="base_height", default=8, | |
help="Height of base of rack") | |
self.op.add_option("", "--rack-base-tab", | |
action="store", type="float", | |
dest="base_tab", default=14, | |
help="Length of tabs on ends of rack") | |
self.op.add_option("", "--dxf-layer-mount-hole", | |
action="store", type="string", | |
dest="layer_mount_hole", default="0", | |
help="DXF layer mount hole") | |
self.op.add_option("", "--dxf-layer-gear_outer", | |
action="store", type="string", | |
dest="layer_gear_outer", default="0", | |
help="DXF layer gear_outer") | |
self.op.add_option("", "--dxf-layer-rack", | |
action="store", type="string", | |
dest="layer_rack", default="0", | |
help="DXF layer rack") | |
self.op.add_option("", "--dxf-layer-pitch", | |
action="store", type="string", | |
dest="layer_pitch", default="0", | |
help="DXF layer pitch") | |
self.op.add_option("", "--dxf-layer-cross", | |
action="store", type="string", | |
dest="layer_cross", default="0", | |
help="DXF layer cross") | |
self.op.add_option("", "--dxf-layer-spokes", | |
action="store", type="string", | |
dest="layer_spokes", default="0", | |
help="DXF layer spokes") | |
self.op.add_option("", "--dxf-layer-annotation", | |
action="store", type="string", | |
dest="layer_annotation", default="annotation", | |
help="DXF layer annotation") | |
(self.options, args) = self.op.parse_args() | |
self.layer_mount_hole = self.options.layer_mount_hole | |
self.layer_gear_outer = self.options.layer_gear_outer | |
self.layer_rack = self.options.layer_rack | |
self.layer_spokes = self.options.layer_spokes | |
self.layer_pitch = self.options.layer_pitch | |
self.layer_cross = self.options.layer_cross | |
self.layer_annotation = self.options.layer_annotation | |
self.d = ezdxf.new(dxfversion='AC1024') | |
# self.d.layers = [] | |
self.msp = self.d.modelspace() | |
l = [] | |
l.append(self.layer_mount_hole) | |
l.append(self.layer_gear_outer) | |
l.append(self.layer_rack) | |
l.append(self.layer_spokes) | |
l.append(self.layer_pitch) | |
l.append(self.layer_cross) | |
l.append(self.layer_annotation) | |
# remove duplicates | |
l = list(set(l)) | |
# remove "0" | |
l.remove("0") | |
c = 1 | |
for i in sorted(l): | |
#print("adding layer %s color: %i" %(i, c)) | |
self.d.layers.create(i, dxfattribs={'color': c}) | |
c+=1 | |
def calc_unit_factor(self): | |
""" return the scale factor for all dimension conversions. | |
- The document units are always irrelevant as | |
everything in inkscape is expected to be in 90dpi pixel units | |
""" | |
# namedView = self.document.getroot().find(inkex.addNS('namedview', 'sodipodi')) | |
# doc_units = inkex.uutounit(1.0, namedView.get(inkex.addNS('document-units', 'inkscape'))) | |
dialog_units = self.uutounit(1.0, self.options.units) | |
unit_factor = 1.0 / dialog_units | |
return unit_factor | |
def calc_circular_pitch(self): | |
""" We use math based on circular pitch. | |
Expressed in inkscape units which is 90dpi 'pixel' units. | |
""" | |
dimension = self.options.dimension | |
#self.tty += "unit_factor=%s, doc_units=%s, dialog_units=%s (%s), system=%s" % (unit_factor, doc_units, dialog_units, self.options.units, self.options.system) | |
if self.options.system == 'CP': # circular pitch | |
circular_pitch = dimension | |
elif self.options.system == 'DP': # diametral pitch | |
circular_pitch = pi / dimension | |
elif self.options.system == 'MM': # module (metric) | |
circular_pitch = dimension * pi / 25.4 | |
else: | |
inkex.debug("unknown system '%s', try CP, DP, MM" % self.options.system) | |
# circular_pitch defines the size in inches. | |
# We divide the internal inch factor (px = 90dpi), to remove the inch | |
# unit. | |
# The internal inkscape unit is always px, | |
# it is independent of the doc_units! | |
return circular_pitch / self.uutounit(1.0, 'in') | |
# def uutounit(self, a, b): | |
#return 0.0104166666667 | |
# return 0.1 | |
def uutounit(self, val, unit): | |
return val / (self.__uuconv[unit] / self.__uuconv['mm']) | |
__uuconv = {'in':96.0, 'pt':1.33333333333, 'px':1.0, 'mm':3.77952755913, 'cm':37.7952755913, | |
'm':3779.52755913, 'km':3779527.55913, 'pc':16.0, 'yd':3456.0 , 'ft':1152.0} | |
def effect(self): | |
""" Calculate Gear factors from inputs. | |
- Make list of radii, angles, and centers for each tooth and | |
iterate through them | |
- Turn on other visual features e.g. cross, rack, annotations, etc | |
""" | |
# calculate unit factor for units defined in dialog. | |
unit_factor = self.calc_unit_factor() | |
# User defined options | |
teeth = self.options.teeth | |
# Angle of tangent to tooth at circular pitch wrt radial line. | |
angle = self.options.angle | |
# Clearance: Radial distance between top of tooth on one gear to | |
# bottom of gap on another. | |
clearance = self.options.clearance * unit_factor | |
mount_hole = self.options.mount_hole * unit_factor | |
# for spokes | |
mount_radius = self.options.mount_diameter * 0.5 * unit_factor | |
spoke_count = self.options.spoke_count | |
spoke_width = self.options.spoke_width * unit_factor | |
holes_rounding = self.options.holes_rounding * unit_factor # unused | |
# visible guide lines | |
centercross = self.options.centercross # draw center or not (boolean) | |
pitchcircle = self.options.pitchcircle # draw pitch circle or not (boolean) | |
# Accuracy of teeth curves | |
accuracy_involute = 20 # Number of points of the involute curve | |
accuracy_circular = 9 # Number of points on circular parts | |
if self.options.accuracy is not None: | |
if self.options.accuracy == 0: | |
# automatic | |
if teeth < 10: accuracy_involute = 20 | |
elif teeth < 30: accuracy_involute = 12 | |
else: accuracy_involute = 6 | |
else: | |
accuracy_involute = self.options.accuracy | |
accuracy_circular = max(3, int(accuracy_involute/2) - 1) # never less than three | |
# self.tty += "accuracy_circular=%s accuracy_involute=%s" % (accuracy_circular, accuracy_involute) | |
# Pitch (circular pitch): Length of the arc from one tooth to the next) | |
# Pitch diameter: Diameter of pitch circle. | |
pitch = self.calc_circular_pitch() | |
# Replace section below with this call to get the combined gear_calculations() above | |
(pitch_radius, base_radius, addendum, dedendum, | |
outer_radius, root_radius, tooth) = gear_calculations(teeth, pitch, angle, clearance, self.options.internal_ring, self.options.profile_shift*0.01) | |
# Detect Undercut of teeth | |
## undercut = int(ceil(undercut_min_teeth( angle ))) | |
## needs_undercut = teeth < undercut #? no longer needed ? | |
if have_undercut(teeth, angle, 1.0): | |
min_teeth = int(ceil(undercut_min_teeth(angle, 1.0))) | |
min_angle = undercut_min_angle(teeth, 1.0) + .1 | |
max_k = undercut_max_k(teeth, angle) | |
msg = "undercut: try --teeth=%d or --angle=%.1f or more, or --profile-shift=%d" % (min_teeth, min_angle, int(100.*max_k)-100.) | |
# alas annotation cannot handle the degree symbol. Also it ignore newlines. | |
# so split and make a list | |
self.warnings.extend(msg.split("\n")) | |
self.tty += msg | |
self.undercut = True | |
# All base calcs done. Start building gear | |
points = generate_spur_points(teeth, base_radius, pitch_radius, outer_radius, root_radius, accuracy_involute, accuracy_circular) | |
p = self.msp.add_lwpolyline(points, dxfattribs={'layer': self.layer_gear_outer}) | |
p.closed = True | |
bbox_center = points_to_bbox_center( points ) | |
# Spokes (add to current path) | |
if not self.options.internal_ring and spoke_count > 0: # only draw internals if spur gear | |
spokes_path, msg = generate_spokes_path(self, root_radius, spoke_width, spoke_count, mount_radius, mount_hole, | |
unit_factor, self.options.units) | |
self.warnings.extend(msg) | |
# Draw mount hole | |
# A : rx,ry x-axis-rotation, large-arch-flag, sweepflag x,y | |
r = mount_hole / 2 | |
else: | |
# its a ring gear | |
# which only has an outer ring where width = spoke width | |
r = outer_radius + spoke_width | |
self.msp.add_arc((0,0), r, 0, 360, dxfattribs={'layer': self.layer_mount_hole}) | |
# Add center | |
if centercross: | |
cs = pitch / 3 # centercross length | |
self.msp.add_line((-cs,0), (cs,0), dxfattribs={'layer': self.layer_cross}) | |
self.msp.add_line((0,-cs), (0,cs), dxfattribs={'layer': self.layer_cross}) | |
# Add pitch circle (for mating) | |
if pitchcircle: | |
self.msp.add_arc((0, 0), pitch_radius, 0, 360, dxfattribs={'layer': self.layer_pitch}) | |
# Add Rack (below) | |
if self.options.drawrack: | |
base_height = self.options.base_height * unit_factor | |
tab_width = self.options.base_tab * unit_factor | |
tooth_count = self.options.teeth_length | |
(points, guide_path) = generate_rack_points(tooth_count, pitch, addendum, angle, | |
base_height, tab_width, clearance, pitchcircle) | |
# position below Gear, so that it meshes nicely | |
xoff = 0 ## if teeth % 4 == 2. | |
xoff = -0.5*pitch ## if teeth % 4 == 0. | |
xoff = -0.75*pitch ## if teeth % 4 == 3. | |
xoff = -0.25*pitch ## if teeth % 4 == 1. | |
xoff = (-0.5, -0.25, 0, -0.75)[teeth % 4] * pitch | |
pnew = [] | |
for p in points: | |
pnew.append((p[0]+xoff, p[1]+pitch_radius)) | |
p = self.msp.add_lwpolyline(pnew, dxfattribs={'layer': self.layer_rack}) | |
p.closed = True | |
outer_dia = outer_radius * 2 | |
self.notes.extend(['teeth: %10d' % (teeth), | |
'spoke count: %6d' % (spoke_count), | |
'mount hole diam: %10.3f%s' % (mount_hole, self.options.units), | |
'cp: %10.4f%s' % (pitch / unit_factor, self.options.units), | |
'dp: %10.3f' % (pi / pitch * unit_factor), | |
'module: %10.4f' % (pitch / pi * 25.4), | |
'pressure angle: %9.2f' % (angle), | |
'pitch diameter: %10.3f%s' % (pitch_radius * 2 / unit_factor, self.options.units), | |
'outer diameter: %10.3f%s' % (outer_dia / unit_factor, self.options.units), | |
'base diameter: %10.3f%s' % (base_radius * 2 / unit_factor, self.options.units), | |
'mount diameter: %10.3f%s' % (mount_radius * 2, self.options.units), | |
'spoke width: %10.3f%s' % (spoke_width, self.options.units), | |
'addendum: %11.4f%s' % (addendum / unit_factor, self.options.units), | |
'dedendum: %11.4f%s' % (dedendum / unit_factor, self.options.units) | |
]) | |
if (self.warn_undercut): | |
self.notes.extend(['warn_undercut: true']) | |
if (self.warn_spokes): | |
self.notes.extend(['warn_spokes: true']) | |
# Add Annotations (above) | |
if self.options.annotation: | |
if self.options.internal_ring: | |
outer_dia += 2 * spoke_width | |
# text height relative to gear size. | |
# ranges from 10 to 22 over outer radius size 60 to 360 | |
text_height = max(10, min(10+(outer_dia-60)/24, 22)) | |
# position above | |
y = - outer_radius - (len(self.notes)+1) * text_height * 1.2 | |
for note in reversed(self.notes): | |
self.msp.add_text(note, dxfattribs={'height': text_height, | |
'layer': self.layer_annotation}).set_pos((0,y), align='LEFT') | |
y += text_height * 1.2 | |
def stderr(*objs): | |
print(*objs, file=sys.stderr) | |
if __name__ == '__main__': | |
e = Gears() | |
e.effect() | |
e.d.saveas(e.options.output) | |
if (len(e.notes) > 0): | |
stderr("## specs") | |
for note in e.notes: | |
stderr(" * " + note) | |
if (len(e.warnings) > 0): | |
stderr("## warnings") | |
for warning in e.warnings: | |
stderr(" * " + warning) | |
if (len(e.warnings) == 0): | |
print("ok") | |
sys.exit(0) | |
else: | |
print("exiting with warnings") | |
exit(1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment