PD: some cleanup of InvoluteGear

This commit removes obsolete files, gets rid of py2 habits like `xrange`
and fixes some comments and blank lines.
This commit is contained in:
Jonas Bähr
2023-02-14 20:27:17 +01:00
committed by Uwe
parent 44914ab85f
commit 24578466bc
4 changed files with 55 additions and 146 deletions

View File

@@ -63,7 +63,6 @@ set(PartDesign_TestFixtures
set(PartDesign_GearScripts
fcgear/__init__.py
fcgear/fcgear.py
fcgear/fcgeardialog.py
fcgear/involute.py
fcgear/svggear.py
)

View File

@@ -22,9 +22,5 @@ Dockrey on
https://github.com/attoparsec/inkscape-extensions.git
The simplest way to use it is to copy the example macro file
gear.FCMacro to ~/.FreeCAD/ (make sure the fcgear directory is in the
FreeCAD's Python path).
Copyright 2014 David Douard <david.douard@gmail.com>.
Distributed under the LGPL licence.

View File

@@ -1,71 +0,0 @@
# (c) 2014 David Douard <david.douard@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License (LGPL)
# as published by the Free Software Foundation; either version 2 of
# the License, or (at your option) any later version.
# for detail see the LICENCE text file.
#
# FCGear 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 Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with FCGear; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
from PyQt4 import QtGui as qt
import fcgear
import FreeCAD, FreeCADGui
class GearCreationFrame(qt.QFrame):
def __init__(self, parent=None):
super(GearCreationFrame, self).__init__(parent)
self.Z = qt.QSpinBox(value=26)
self.m = qt.QDoubleSpinBox(value=2.5)
self.angle = qt.QDoubleSpinBox(value=20)
self.split = qt.QComboBox()
self.split.addItems(['2x3', '1x4'])
l = qt.QFormLayout(self)
l.setFieldGrowthPolicy(l.ExpandingFieldsGrow)
l.addRow('Number of teeth:', self.Z)
l.addRow('Module (mm):', self.m)
l.addRow('Pressure angle:', self.angle)
l.addRow('Number of curves:', self.split)
class GearDialog(qt.QDialog):
def __init__(self, parent=None):
super(GearDialog, self).__init__(parent)
self.gc = GearCreationFrame()
btns = qt.QDialogButtonBox.Ok | qt.QDialogButtonBox.Cancel
buttonBox = qt.QDialogButtonBox(btns,
accepted=self.accept,
rejected=self.reject)
l = qt.QVBoxLayout(self)
l.addWidget(self.gc)
l.addWidget(buttonBox)
self.setWindowTitle('Gear creation dialog')
def accept(self):
if FreeCAD.ActiveDocument is None:
FreeCAD.newDocument("Gear")
gear = fcgear.makeGear(self.gc.m.value(),
self.gc.Z.value(),
self.gc.angle.value(),
not self.gc.split.currentIndex())
# Use gear to silence static analyzer complaints about unused variables (TODO: Waiting on PEP640 or similar)
False if gear.__name__ else True
FreeCADGui.SendMsgToActiveView("ViewFit")
return super(GearDialog, self).accept()
if __name__ == '__main__':
a = qt.QApplication([])
w = GearDialog()
w.show()
a.exec_()

View File

@@ -23,9 +23,8 @@
# License along with FCGear; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
from math import cos, sin, pi, acos, asin, atan, sqrt
xrange = range
from math import cos, sin, pi, acos, asin, atan, sqrt, radians
from math import comb as binom
def CreateExternalGear(w, m, Z, phi,
@@ -38,7 +37,7 @@ def CreateExternalGear(w, m, Z, phi,
w is wirebuilder object (in which the gear will be constructed)
m is the gear's module (pitch diameter divided by the number of teeth)
Z is the number of teeth
phi is the gear's pressure angle
phi is the gear's pressure angle, in degrees
addCoeff is the addendum coefficient (addendum normalized by module)
dedCoeff is the dedendum coefficient (dedendum normalized by module)
filletCoeff is the root fillet radius, normalized by the module.
@@ -51,16 +50,17 @@ def CreateExternalGear(w, m, Z, phi,
"""
# ****** external gear specifications
addendum = addCoeff * m # distance from pitch circle to tip circle
dedendum = dedCoeff * m # pitch circle to root, sets clearance
dedendum = dedCoeff * m # distance from pitch circle to root circle
# Calculate radii
Rpitch = Z * m / 2 # pitch circle radius
Rb = Rpitch*cos(phi * pi / 180) # base circle radius
Rpitch = Z * m / 2 # pitch circle radius
Rb = Rpitch*cos(radians(phi)) # base circle radius
Ra = Rpitch + addendum # tip (addendum) circle radius
Rroot = Rpitch - dedendum # root circle radius
fRad = filletCoeff * m # fillet radius, max 1.5*clearance
fRad = filletCoeff * m # fillet radius
Rc = Rroot + fRad # radius at the center of the fillet circle
Rf = Rc # radius at top of fillet, assuming fillet below involute
filletWithinInvolute = Rf > Rb # above the base circle we have the involute,
# below we have a straight line towards the center.
if (filletWithinInvolute and fRad > 0):
@@ -99,12 +99,10 @@ def CreateExternalGear(w, m, Z, phi,
- asin((-r**2 + fRad**2 + Rc**2) / (2 * fRad * Rc)))
q_prime = lambda r: (r / (sqrt(-Rb**2 + r**2) * Rb)
+ r / (fRad * Rc * sqrt(1 - 1/4 * (r**2 - fRad**2 - Rc**2)**2 / (fRad**2 * Rc**2))))
Rf = findRootNewton(q, q_prime, x_min=max(Rb, Rroot), x_max=Rc)
# ****** calculate angles (all in radians)
pitchAngle = 2 * pi / Z # angle subtended by whole tooth (rads)
pitchAngle = 2 * pi / Z # angle subtended by whole tooth
baseToPitchAngle = genInvolutePolar(Rb, Rpitch)
pitchToFilletAngle = baseToPitchAngle # profile starts at base circle
if (filletWithinInvolute): # start profile at top of fillet
@@ -130,24 +128,21 @@ def CreateExternalGear(w, m, Z, phi,
else:
inv = BezCoeffs(Rb, Ra, 4, fs, fe)
# create the back profile of tooth (mirror image)
invR = []
for i, pt in enumerate(inv):
# rotate all points to put pitch point at y = 0
ptx, pty = inv[i] = rotate(pt, -baseToPitchAngle - pitchAngle / 4)
# generate the back of tooth profile nodes, mirror coords in X axis
invR.append((ptx, -pty))
# rotate all points to make the tooth symetric to the X axis
inv = [rotate(pt, -baseToPitchAngle - pitchAngle / 4) for pt in inv]
# create the back profile of tooth (mirror image on X axis)
invR = [mirror(pt) for pt in inv]
# ****** calculate section junction points R=back of tooth, Next=front of next tooth)
fillet = toCartesian(Rf, -pitchAngle / 4 - pitchToFilletAngle) # top of fillet
filletR = [fillet[0], -fillet[1]] # flip to make same point on back of tooth
filletR = mirror(fillet) # flip to make same point on back of tooth
rootR = toCartesian(Rroot, pitchAngle / 4 + pitchToFilletAngle + filletAngle)
rootNext = toCartesian(Rroot, 3 * pitchAngle / 4 - pitchToFilletAngle - filletAngle)
filletNext = rotate(fillet, pitchAngle) # top of fillet, front of next tooth
# Build the shapes using FreeCAD.Part
t_inc = 2.0 * pi / float(Z)
thetas = [(x * t_inc) for x in range(Z)]
# Build the shapes using the provided WireBuilder
thetas = [x * pitchAngle for x in range(Z)]
# Make sure we begin *exactly* where our last curve ends.
# In theory start == rotate(end, angle_of_last_tooth), but in practice we have limited
@@ -190,6 +185,7 @@ def CreateExternalGear(w, m, Z, phi,
w.close()
return w
def CreateInternalGear(w, m, Z, phi,
split=True,
addCoeff=0.6, dedCoeff=1.25,
@@ -200,7 +196,7 @@ def CreateInternalGear(w, m, Z, phi,
w is wirebuilder object (in which the gear will be constructed)
m is the gear's module (pitch diameter divided by the number of teeth)
Z is the number of teeth
phi is the gear's pressure angle
phi is the gear's pressure angle, in degrees
addCoeff is the addendum coefficient (addendum normalized by module)
The default of 0.6 comes from the "Handbook of Gear Design" by Gitin M. Maitra,
with the goal to push the addendum circle beyond the base circle to avoid non-involute
@@ -219,15 +215,15 @@ def CreateInternalGear(w, m, Z, phi,
"""
# ****** external gear specifications
addendum = addCoeff * m # distance from pitch circle to tip circle
dedendum = dedCoeff * m # pitch circle to root, sets clearance
dedendum = dedCoeff * m # distance from pitch circle to root circle
# Calculate radii
Rpitch = Z * m / 2 # pitch circle radius
Rb = Rpitch*cos(phi * pi / 180) # base circle radius
Rb = Rpitch*cos(radians(phi)) # base circle radius
Ra = Rpitch - addendum # tip (addendum) circle radius
Rroot = Rpitch + dedendum # root circle radius
fRad = filletCoeff * m # fillet radius, max 1.5*clearance
Rc = Rroot - fRad # radius at the center of the fillet circle
fRad = filletCoeff * m # fillet radius
Rc = Rroot - fRad # radius at the center of the fillet circle
tipWithinInvolute = Ra > Rb # above the base circle we have the involute,
# below we have a straight line towards the center.
@@ -261,14 +257,14 @@ def CreateInternalGear(w, m, Z, phi,
else:
Rf = Rroot # no fillet
# ****** calculate angles (all in radians)
pitchAngle = 2 * pi / Z # angle subtended by whole tooth (rads)
pitchAngle = 2 * pi / Z # angle subtended by whole tooth
baseToPitchAngle = genInvolutePolar(Rb, Rpitch)
tipToPitchAngle = baseToPitchAngle
if (tipWithinInvolute): # start profile at tip, not base
tipToPitchAngle -= genInvolutePolar(Rb, Ra)
pitchToFilletAngle = genInvolutePolar(Rb, Rf) - baseToPitchAngle;
filletWidth = sqrt(fRad**2 - (Rf - Rc)**2)
filletAngle = atan(filletWidth / Rf)
@@ -289,27 +285,22 @@ def CreateInternalGear(w, m, Z, phi,
else:
invR = BezCoeffs(Rb, Rf, 4, fs, fe)
# create the back profile of tooth (mirror image)
inv = []
for i, pt in enumerate(invR):
# rotate involute to put center of tooth at y = 0
ptx, pty = invR[i] = rotate(pt, pitchAngle / 4 - baseToPitchAngle)
# generate the back of tooth profile nodes, flip Y coords
inv.append((ptx, -pty))
# rotate all points to make the tooth symetric to the X axis
invR = [rotate(pt, -baseToPitchAngle + pitchAngle / 4) for pt in invR]
# create the front profile of tooth (mirror image on X axis)
inv = [mirror(pt) for pt in invR]
# ****** calculate section junction points R=back of tooth, Next=front of next tooth)
#fillet = inv[6] # top of fillet, front of tooth #toCartesian(Rf, -pitchAngle / 4 - pitchToFilletAngle) # top of fillet
fillet = [ptx,-pty]
tip = toCartesian(Ra, -pitchAngle/4+tipToPitchAngle) # tip, front of tooth
tipR = [ tip[0], -tip[1] ]
#filletR = [fillet[0], -fillet[1]] # flip to make same point on back of tooth
fillet = inv[-1] # end of fillet, front of tooth; right where the involute starts
tip = toCartesian(Ra, -pitchAngle / 4 + tipToPitchAngle) # tip, front of tooth
tipR = mirror(tip)
rootR = toCartesian(Rroot, pitchAngle / 4 + pitchToFilletAngle + filletAngle)
rootNext = toCartesian(Rroot, 3 * pitchAngle / 4 - pitchToFilletAngle - filletAngle)
filletNext = rotate(fillet, pitchAngle) # top of fillet, front of next tooth
filletNext = rotate(fillet, pitchAngle) # end of fillet, front of next tooth
# Build the shapes using FreeCAD.Part
t_inc = 2.0 * pi / float(Z)
thetas = [(x * t_inc) for x in range(Z)]
# Build the shapes using the provided WireBuilder
thetas = [x * pitchAngle for x in range(Z)]
# Make sure we begin *exactly* where our last curve ends.
# In theory start == rotate(end, angle_of_last_tooth), but in practice we have limited
@@ -331,7 +322,7 @@ def CreateInternalGear(w, m, Z, phi,
w.curve(*inv[-2::-1])
if (not tipWithinInvolute):
w.line(tip) # line from tip down to base circle
w.line(tip) # line to tip down from base circle
w.arc(tipR, Ra, 1) # arc across addendum circle
@@ -352,30 +343,34 @@ def CreateInternalGear(w, m, Z, phi,
if fRad > 0:
w.arc(filletNext, fRad, 1)
w.close()
return w
def genInvolutePolar(Rb, R):
"""returns the involute angle as function of radius R.
"""return the involute angle as function of radius R.
Rb = base circle radius
"""
return (sqrt(R*R - Rb*Rb) / Rb) - acos(Rb / R)
def rotate(pt, rads):
"rotate pt by rads radians about origin"
"""rotate pt by rads radians about origin"""
sinA = sin(rads)
cosA = cos(rads)
return (pt[0] * cosA - pt[1] * sinA,
pt[0] * sinA + pt[1] * cosA)
def mirror(pt):
"""mirror pt on the X axis, i.e. flip its Y"""
return (pt[0], -pt[1])
def toCartesian(radius, angle):
"convert polar coords to cartesian"
return [radius * cos(angle), radius * sin(angle)]
"""convert polar coords to cartesian"""
return (radius * cos(angle), radius * sin(angle))
def findRootNewton(f, f_prime, x_min, x_max):
"""Appy Newton's Method to find the root of f within x_min and x_max
@@ -398,10 +393,11 @@ def findRootNewton(f, f_prime, x_min, x_max):
raise RuntimeError(f"No convergence after {i+1} iterations.")
def chebyExpnCoeffs(j, func):
N = 50 # a suitably large number N>>p
c = 0
for k in xrange(1, N + 1):
for k in range(1, N + 1):
c += func(cos(pi * (k - 0.5) / N)) * cos(pi * j * (k - 0.5) / N)
return 2 *c / N
@@ -422,36 +418,25 @@ def chebyPolyCoeffs(p, func):
# [ 0, 5, 0,-20, 0, 16], # T5(x) = 0 5x 0 -20xxx 0 +16xxxxx
# ... ]
for k in xrange(1, p):
for j in xrange(len(T[k]) - 1):
for k in range(1, p):
for j in range(len(T[k]) - 1):
T[k + 1][j + 1] = 2 * T[k][j]
for j in xrange(len(T[k - 1])):
for j in range(len(T[k - 1])):
T[k + 1][j] -= T[k - 1][j]
# convert the chebyshev function series into a simple polynomial
# and collect like terms, out T polynomial coefficients
for k in xrange(p + 1):
for k in range(p + 1):
fnCoeff.append(chebyExpnCoeffs(k, func))
for k in xrange(p + 1):
for pwr in xrange(p + 1):
for k in range(p + 1):
for pwr in range(p + 1):
coeffs[pwr] += fnCoeff[k] * T[k][pwr]
coeffs[0] -= fnCoeff[0] / 2 # fix the 0th coeff
return coeffs
def binom(n, k):
coeff = 1
for i in xrange(n - k + 1, n + 1):
coeff *= i
for i in xrange(1, k + 1):
coeff /= i
return coeff
def bezCoeff(i, p, polyCoeffs):
'''generate the polynomial coeffs in one go'''
return sum(binom(i, j) * polyCoeffs[j] / binom(p, j) for j in range(i+1))
@@ -495,7 +480,7 @@ def BezCoeffs(baseRadius, limitRadius, order, fstart, fstop):
bzCoeffs = []
polyCoeffsX = chebyPolyCoeffs(p, involuteXbez)
polyCoeffsY = chebyPolyCoeffs(p, involuteYbez)
for i in xrange(p + 1):
for i in range(p + 1):
bx = bezCoeff(i, p, polyCoeffsX)
by = bezCoeff(i, p, polyCoeffsY)
bzCoeffs.append((bx, by))