From 587f6b220b61a25154b7739e051b36c12683fb55 Mon Sep 17 00:00:00 2001 From: jriegel Date: Sun, 16 Feb 2014 16:44:07 +0100 Subject: [PATCH] add fcgear http://forum.freecadweb.org/viewtopic.php?f=9&t=5703 --- src/Mod/PartDesign/CMakeLists.txt | 12 ++ src/Mod/PartDesign/fcgear/README | 30 +++ src/Mod/PartDesign/fcgear/__init__.py | 0 src/Mod/PartDesign/fcgear/fcgear.py | 108 ++++++++++ src/Mod/PartDesign/fcgear/fcgeardialog.py | 67 ++++++ src/Mod/PartDesign/fcgear/involute.py | 251 ++++++++++++++++++++++ src/Mod/PartDesign/fcgear/svggear.py | 71 ++++++ 7 files changed, 539 insertions(+) create mode 100644 src/Mod/PartDesign/fcgear/README create mode 100644 src/Mod/PartDesign/fcgear/__init__.py create mode 100644 src/Mod/PartDesign/fcgear/fcgear.py create mode 100644 src/Mod/PartDesign/fcgear/fcgeardialog.py create mode 100644 src/Mod/PartDesign/fcgear/involute.py create mode 100644 src/Mod/PartDesign/fcgear/svggear.py diff --git a/src/Mod/PartDesign/CMakeLists.txt b/src/Mod/PartDesign/CMakeLists.txt index 956811364..9ad8c1d81 100644 --- a/src/Mod/PartDesign/CMakeLists.txt +++ b/src/Mod/PartDesign/CMakeLists.txt @@ -27,6 +27,18 @@ INSTALL( Scripts/Spring.py DESTINATION Mod/PartDesign/Scripts + ) + +INSTALL( + FILES + fcgear/__init__.py + fcgear/fcgear.py + fcgear/fcgeardialog.py + fcgear/involute.py + fcgear/svggear.py + DESTINATION + Mod/PartDesign/fcgear + ) SET(WizardShaft_SRCS diff --git a/src/Mod/PartDesign/fcgear/README b/src/Mod/PartDesign/fcgear/README new file mode 100644 index 000000000..c276c090c --- /dev/null +++ b/src/Mod/PartDesign/fcgear/README @@ -0,0 +1,30 @@ +================================================ + FCGear: an Involute Gear Generator for FreeCAD +================================================ + +This is a simple gear generation tool usable in FreeCAD. The tooth +profiles are approximations of the ideal involutes by Bezier curves, +according the paper: + + Approximation of Involute Curves for CAD-System Processing + Higuchi et al. approximation to an involute. + ref: YNU Digital Eng Lab Memorandum 05-1 + http://maekawalab-ynu.com/papers.html + +This code is based on the JavaScript implementation of the published +method provided by A.R. Collins in his gearUtils.js tool: + + Based on gearUtils-03.js by Dr A.R.Collins + Latest version: + +Also took inspirations from the Inkscape extension provided by Matthew +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/__init__.py b/src/Mod/PartDesign/fcgear/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/Mod/PartDesign/fcgear/fcgear.py b/src/Mod/PartDesign/fcgear/fcgear.py new file mode 100644 index 000000000..02805169f --- /dev/null +++ b/src/Mod/PartDesign/fcgear/fcgear.py @@ -0,0 +1,108 @@ +# (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 math import cos, sin, pi, acos, asin, atan, sqrt + +import FreeCAD, FreeCADGui, Part +from FreeCAD import Base, Console +import involute +reload(involute) +rotate = involute.rotate + + +def makeGear(m, Z, angle, split=True): + if FreeCAD.ActiveDocument is None: + FreeCAD.newDocument("Gear") + doc = FreeCAD.ActiveDocument + w = FCWireBuilder() + involute.CreateExternalGear(w, m, Z, angle, split) + gearw = Part.Wire([o.toShape() for o in w.wire]) + gear = doc.addObject("Part::Feature", "Gear") + gear.Shape = gearw + return gear + + +class FCWireBuilder(object): + """A helper class to prepare a Part.Wire object""" + def __init__(self): + self.pos = None + self.theta = 0.0 + self.wire = [] + + def move(self, p): + """set current position""" + self.pos = Base.Vector(*p) + + def line(self, p): + """Add a segment between self.pos and p""" + p = rotate(p, self.theta) + end = Base.Vector(*p) + self.wire.append(Part.Line(self.pos, end)) + self.pos = end + + def arc(self, p, r, sweep): + """"Add an arc from self.pos to p which radius is r + sweep (0 or 1) determine the orientation of the arc + """ + p = rotate(p, self.theta) + end = Base.Vector(*p) + mid = Base.Vector(*(midpoints(p, self.pos, r)[sweep])) + self.wire.append(Part.Arc(self.pos, mid, end)) + self.pos = end + + def curve(self, *points): + """Add a Bezier curve from self.pos to points[-1] + every other points are the control points of the Bezier curve (which + will thus be of degree len(points) ) + """ + points = [Base.Vector(*rotate(p, self.theta)) for p in points] + bz = Part.BezierCurve() + bz.setPoles([self.pos] + points) + self.wire.append(bz) + self.pos = points[-1] + + def close(self): + pass + +def midpoints(p1, p2, r): + """A very ugly function that returns the midpoint of a p1 and p2 + on the circle which radius is r and which pass throught p1 and + p2 + + Return the 2 possible solutions + """ + vx, vy = p2[0]-p1[0], p2[1]-p1[1] + b = (vx**2 + vy**2)**.5 + v = (vx/b, vy/b) + cosA = b**2 / (2*b*r) + A = acos(cosA) + + vx, vy = rotate(v, A) + c1 = (p1[0]+r*vx, p1[1]+r*vy) + m1x, m1y = ((p1[0]+p2[0])/2 - c1[0], (p1[1]+p2[1])/2 - c1[1]) + dm1 = (m1x**2+m1y**2)**.5 + m1x, m1y = (c1[0] + r*m1x/dm1, c1[1] + r*m1y/dm1) + m1 = (m1x, m1y) + + vx, vy = rotate(v, -A) + c2 = (p1[0]+r*vx, p1[1]+r*vy) + m2x, m2y = ((p1[0]+p2[0])/2 - c2[0], (p1[1]+p2[1])/2 - c2[1]) + dm2 = (m2x**2+m2y**2)**.5 + m2x, m2y = (c2[0] + r*m2x/dm2, c2[1] + r*m2y/dm2) + m2 = (m2x, m2y) + + return m1, m2 diff --git a/src/Mod/PartDesign/fcgear/fcgeardialog.py b/src/Mod/PartDesign/fcgear/fcgeardialog.py new file mode 100644 index 000000000..435ec50f9 --- /dev/null +++ b/src/Mod/PartDesign/fcgear/fcgeardialog.py @@ -0,0 +1,67 @@ +# (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('Modules (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 cration 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()) + 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 new file mode 100644 index 000000000..27f070bd0 --- /dev/null +++ b/src/Mod/PartDesign/fcgear/involute.py @@ -0,0 +1,251 @@ +# (c) 2014 David Douard +# Based on https://github.com/attoparsec/inkscape-extensions.git +# Based on gearUtils-03.js by Dr A.R.Collins +# http://www.arc.id.au/gearDrawing.html +# +# Calculation of Bezier coefficients for +# Higuchi et al. approximation to an involute. +# ref: YNU Digital Eng Lab Memorandum 05-1 +# +# 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 math import cos, sin, pi, acos, asin, atan, sqrt + +def CreateExternalGear(w, m, Z, phi, split=True): + """ + Create an external gear + + w is wirebuilder object (in which the gear will be constructed) + + if split is True, each profile of a teeth will consist in 2 Bezier + curves of degree 3, otherwise it will be made of one Bezier curve + of degree 4 + """ + # ****** external gear specifications + addendum = m # distance from pitch circle to tip circle + dedendum = 1.25 * m # pitch circle to root, sets clearance + clearance = dedendum - addendum + + # Calculate radii + Rpitch = Z * m / 2 # pitch circle radius + Rb = Rpitch*cos(phi * pi / 180) # base circle radius + Ra = Rpitch + addendum # tip (addendum) circle radius + Rroot = Rpitch - dedendum # root circle radius + fRad = 1.5 * clearance # fillet radius, max 1.5*clearance + Rf = sqrt((Rroot + fRad)**2 - fRad**2) # radius at top of fillet + if (Rb < Rf): + Rf = Rroot + clearance + + # ****** calculate angles (all in radians) + pitchAngle = 2 * pi / Z # angle subtended by whole tooth (rads) + baseToPitchAngle = genInvolutePolar(Rb, Rpitch) + pitchToFilletAngle = baseToPitchAngle # profile starts at base circle + if (Rf > Rb): # start profile at top of fillet (if its greater) + pitchToFilletAngle -= genInvolutePolar(Rb, Rf) + + filletAngle = atan(fRad / (fRad + Rroot)) # radians + + # ****** generate Higuchi involute approximation + fe = 1 # fraction of profile length at end of approx + fs = 0.01 # fraction of length offset from base to avoid singularity + if (Rf > Rb): + fs = (Rf**2 - Rb**2) / (Ra**2 - Rb**2) # offset start to top of fillet + + if split: + # approximate in 2 sections, split 25% along the involute + fm = fs + (fe - fs) / 4 # fraction of length at junction (25% along profile) + dedInv = BezCoeffs(m, Z, phi, 3, fs, fm) + addInv = BezCoeffs(m, Z, phi, 3, fm, fe) + + # join the 2 sets of coeffs (skip duplicate mid point) + inv = dedInv + addInv[1:] + else: + inv = BezCoeffs(m, Z, phi, 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)) + + # ****** 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 + 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)] + + w.move(fillet) # start at top of fillet + + for theta in thetas: + w.theta = theta + if (Rf < Rb): + w.line(inv[0]) # line from fillet up to base circle + + if split: + w.curve(inv[1], inv[2], inv[3]) + w.curve(inv[4], inv[5], inv[6]) + w.arc(invR[6], Ra, 1) # arc across addendum circle + w.curve(invR[5], invR[4], invR[3]) + w.curve(invR[2], invR[1], invR[0]) + else: + w.curve(*inv[1:]) + w.arc(invR[-1], Ra, 1) # arc across addendum circle + w.curve(*invR[-2::-1]) + + if (Rf < Rb): + w.line(filletR) # line down to topof fillet + + if (rootNext[1] > rootR[1]): # is there a section of root circle between fillets? + w.arc(rootR, fRad, 0) # back fillet + w.arc(rootNext, Rroot, 1) # root circle arc + + w.arc(filletNext, fRad, 0) + + w.close() + return w + + + +def genInvolutePolar(Rb, R): + """returns 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" + sinA = sin(rads) + cosA = cos(rads) + return (pt[0] * cosA - pt[1] * sinA, + pt[0] * sinA + pt[1] * cosA) + + + +def toCartesian(radius, angle): + "convert polar coords to cartesian" + return [radius * cos(angle), radius * sin(angle)] + + +def chebyExpnCoeffs(j, func): + N = 50 # a suitably large number N>>p + c = 0 + for k in xrange(1, N + 1): + c += func(cos(pi * (k - 0.5) / N)) * cos(pi * j * (k - 0.5) / N) + return 2 *c / N + + +def chebyPolyCoeffs(p, func): + coeffs = [0]*(p+1) + fnCoeff = [] + T = [coeffs[:] for i in range(p+1)] + T[0][0] = 1 + T[1][1] = 1 + # now generate the Chebyshev polynomial coefficient using + # formula T(k+1) = 2xT(k) - T(k-1) which yields + # T = [ [ 1, 0, 0, 0, 0, 0], # T0(x) = +1 + # [ 0, 1, 0, 0, 0, 0], # T1(x) = 0 +x + # [-1, 0, 2, 0, 0, 0], # T2(x) = -1 0 +2xx + # [ 0, -3, 0, 4, 0, 0], # T3(x) = 0 -3x 0 +4xxx + # [ 1, 0, -8, 0, 8, 0], # T4(x) = +1 0 -8xx 0 +8xxxx + # [ 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): + T[k + 1][j + 1] = 2 * T[k][j] + for j in xrange(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): + fnCoeff.append(chebyExpnCoeffs(k, func)) + + for k in xrange(p + 1): + for pwr in xrange(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)) + + + # Parameters: + # module - sets the size of teeth (see gear design texts) + # numTeeth - number of teeth on the gear + # pressure angle - angle in degrees, usually 14.5 or 20 + # order - the order of the Bezier curve to be fitted [3, 4, 5, ..] + # fstart - fraction of distance along tooth profile to start + # fstop - fraction of distance along profile to stop +def BezCoeffs(module, numTeeth, pressureAngle, order, fstart, fstop): + Rpitch = module * numTeeth / 2 # pitch circle radius + phi = pressureAngle # pressure angle + Rb = Rpitch * cos(phi * pi / 180) # base circle radius + Ra = Rpitch + module # addendum radius (outer radius) + ta = sqrt(Ra * Ra - Rb * Rb) / Rb # involute angle at addendum + te = sqrt(fstop) * ta # involute angle, theta, at end of approx + ts = sqrt(fstart) * ta # involute angle, theta, at start of approx + p = order # order of Bezier approximation + + def involuteXbez(t): + "Equation of involute using the Bezier parameter t as variable" + # map t (0 <= t <= 1) onto x (where -1 <= x <= 1) + x = t * 2 - 1 + # map theta (where ts <= theta <= te) from x (-1 <=x <= 1) + theta = x * (te - ts) / 2 + (ts + te) / 2 + return Rb * (cos(theta) + theta * sin(theta)) + + def involuteYbez(t): + "Equation of involute using the Bezier parameter t as variable" + # map t (0 <= t <= 1) onto x (where -1 <= x <= 1) + x = t * 2 - 1 + # map theta (where ts <= theta <= te) from x (-1 <=x <= 1) + theta = x * (te - ts) / 2 + (ts + te) / 2 + return Rb * (sin(theta) - theta * cos(theta)) + + # calc Bezier coeffs + bzCoeffs = [] + polyCoeffsX = chebyPolyCoeffs(p, involuteXbez) + polyCoeffsY = chebyPolyCoeffs(p, involuteYbez) + for i in xrange(p + 1): + bx = bezCoeff(i, p, polyCoeffsX) + by = bezCoeff(i, p, polyCoeffsY) + bzCoeffs.append((bx, by)) + return bzCoeffs + diff --git a/src/Mod/PartDesign/fcgear/svggear.py b/src/Mod/PartDesign/fcgear/svggear.py new file mode 100644 index 000000000..14e5d66ff --- /dev/null +++ b/src/Mod/PartDesign/fcgear/svggear.py @@ -0,0 +1,71 @@ +# (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 + +import itertools +from math import cos, sin +from involute import CreateExternalGear, rotate + +def makeGear(m, Z, angle): + w = SVGWireBuilder() + CreateExternalGear(w, m, Z, angle) + return '\n'.join(w.svg) + +class SVGWireBuilder(object): + def __init__(self): + self.theta = 0.0 + self.pos = None + self.svg = [] + + def move(self, p): + p = rotate(p, self.theta) + self.svg.append('M %s,%s' % (p[0], p[1])) + self.pos = p + + def line(self, p): + p = rotate(p, self.theta) + self.svg.append('L %s,%s' % (p[0], p[1])) + self.pos = p + + def arc(self, p, r, sweep): + p = rotate(p, self.theta) + self.svg.append('A %s,%s 0,0,%s %s,%s' % (r, r, str(sweep), p[0], p[1])) + self.pos = p + + def curve(self, *points): + """Add a Bezier curve from self.pos to points[-1] + every other points are the control points of the Bezier curve (which + will thus be of degree len(points) ) + """ + assert len(points) == 3 + points = [rotate(p, self.theta) for p in points] + self.svg.append('C %s,%s %s,%s %s,%s' % tuple(itertools.chain(*points))) + self.pos = points[-1] + + def close(self): + self.svg.append('Z') + +if __name__ == '__main__': + from optparse import OptionParser + p = OptionParser() + p.add_option('-a', '--angle', help='pressure angle', + dest='angle', default=20) + opts, args = p.parse_args() + if len(args) != 2: + p.error() + m, Z = [float(v) for v in args] + print makeGear(m, int(Z), float(opts.angle)) +