From 24578466bc9da652e44d96262aaa77576da59b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20B=C3=A4hr?= Date: Tue, 14 Feb 2023 20:27:17 +0100 Subject: [PATCH] PD: some cleanup of InvoluteGear This commit removes obsolete files, gets rid of py2 habits like `xrange` and fixes some comments and blank lines. --- src/Mod/PartDesign/CMakeLists.txt | 1 - src/Mod/PartDesign/fcgear/README | 4 - src/Mod/PartDesign/fcgear/fcgeardialog.py | 71 ------------ src/Mod/PartDesign/fcgear/involute.py | 125 ++++++++++------------ 4 files changed, 55 insertions(+), 146 deletions(-) delete mode 100644 src/Mod/PartDesign/fcgear/fcgeardialog.py diff --git a/src/Mod/PartDesign/CMakeLists.txt b/src/Mod/PartDesign/CMakeLists.txt index a73b83499e..70be47ecd2 100644 --- a/src/Mod/PartDesign/CMakeLists.txt +++ b/src/Mod/PartDesign/CMakeLists.txt @@ -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 ) diff --git a/src/Mod/PartDesign/fcgear/README b/src/Mod/PartDesign/fcgear/README index c276c090ce..785530d31e 100644 --- a/src/Mod/PartDesign/fcgear/README +++ b/src/Mod/PartDesign/fcgear/README @@ -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 . Distributed under the LGPL licence. diff --git a/src/Mod/PartDesign/fcgear/fcgeardialog.py b/src/Mod/PartDesign/fcgear/fcgeardialog.py deleted file mode 100644 index cfa80c4143..0000000000 --- a/src/Mod/PartDesign/fcgear/fcgeardialog.py +++ /dev/null @@ -1,71 +0,0 @@ -# (c) 2014 David Douard -# -# 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_() diff --git a/src/Mod/PartDesign/fcgear/involute.py b/src/Mod/PartDesign/fcgear/involute.py index 97eafc5af2..d650766cc2 100644 --- a/src/Mod/PartDesign/fcgear/involute.py +++ b/src/Mod/PartDesign/fcgear/involute.py @@ -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))