diff --git a/src/Mod/PartDesign/InvoluteGearFeature.py b/src/Mod/PartDesign/InvoluteGearFeature.py index b6c06f6940..952c57a5c7 100644 --- a/src/Mod/PartDesign/InvoluteGearFeature.py +++ b/src/Mod/PartDesign/InvoluteGearFeature.py @@ -122,13 +122,17 @@ class _InvoluteGear: doc=QtCore.QT_TRANSLATE_NOOP("App::Property", "The radius of the fillet at the root of the tooth, normalized by the module."), default=lambda: 0.375 if is_restore else 0.38) + ensure_property("App::PropertyFloat","ProfileShiftCoefficient", + doc=QtCore.QT_TRANSLATE_NOOP("App::Property", + "The distance by which the reference profile is shifted outwards, normalized by the module."), + default=0.0) def execute(self,obj): w = fcgear.FCWireBuilder() generator_func = involute.CreateExternalGear if obj.ExternalGear else involute.CreateInternalGear generator_func(w, obj.Modules.Value, obj.NumberOfTeeth, obj.PressureAngle.Value, split=obj.HighPrecision, addCoeff=obj.AddendumCoefficient, dedCoeff=obj.DedendumCoefficient, - filletCoeff=obj.RootFilletCoefficient) + filletCoeff=obj.RootFilletCoefficient, shiftCoeff=obj.ProfileShiftCoefficient) gearw = Part.Wire([o.toShape() for o in w.wire]) obj.Shape = gearw obj.positionBySupport() @@ -199,6 +203,7 @@ class _InvoluteGearTaskPanel: self.form.doubleSpinBox_Addendum.valueChanged.connect(assignValue("AddendumCoefficient")) self.form.doubleSpinBox_Dedendum.valueChanged.connect(assignValue("DedendumCoefficient")) self.form.doubleSpinBox_RootFillet.valueChanged.connect(assignValue("RootFilletCoefficient")) + self.form.doubleSpinBox_ProfileShift.valueChanged.connect(assignValue("ProfileShiftCoefficient")) self.update() @@ -222,6 +227,7 @@ class _InvoluteGearTaskPanel: assign("AddendumCoefficient", self.form.doubleSpinBox_Addendum, self.form.label_Addendum) assign("DedendumCoefficient", self.form.doubleSpinBox_Dedendum, self.form.label_Dedendum) assign("RootFilletCoefficient", self.form.doubleSpinBox_RootFillet, self.form.label_RootFillet) + assign("ProfileShiftCoefficient", self.form.doubleSpinBox_ProfileShift, self.form.label_ProfileShift) def changeEvent(self, event): if event == QtCore.QEvent.LanguageChange: @@ -237,6 +243,7 @@ class _InvoluteGearTaskPanel: self.obj.AddendumCoefficient = self.form.doubleSpinBox_Addendum.value() self.obj.DedendumCoefficient = self.form.doubleSpinBox_Dedendum.value() self.obj.RootFilletCoefficient = self.form.doubleSpinBox_RootFillet.value() + self.obj.ProfileShiftCoefficient = self.form.doubleSpinBox_ProfileShift.value() def transferFrom(self): "Transfer from the object to the dialog" @@ -248,6 +255,7 @@ class _InvoluteGearTaskPanel: self.form.doubleSpinBox_Addendum.setValue(self.obj.AddendumCoefficient) self.form.doubleSpinBox_Dedendum.setValue(self.obj.DedendumCoefficient) self.form.doubleSpinBox_RootFillet.setValue(self.obj.RootFilletCoefficient) + self.form.doubleSpinBox_ProfileShift.setValue(self.obj.ProfileShiftCoefficient) def getStandardButtons(self): return int(QtGui.QDialogButtonBox.Ok) | int(QtGui.QDialogButtonBox.Cancel)| int(QtGui.QDialogButtonBox.Apply) diff --git a/src/Mod/PartDesign/InvoluteGearFeature.ui b/src/Mod/PartDesign/InvoluteGearFeature.ui index 2e9dea93e8..91749efd95 100644 --- a/src/Mod/PartDesign/InvoluteGearFeature.ui +++ b/src/Mod/PartDesign/InvoluteGearFeature.ui @@ -6,8 +6,8 @@ 0 0 - 248 - 270 + 253 + 301 @@ -233,6 +233,29 @@ + + + + Profile Shift Coefficient + + + + + + + -3.000000000000000 + + + 3.000000000000000 + + + 0.100000000000000 + + + 0.000000000000000 + + + diff --git a/src/Mod/PartDesign/PartDesignTests/TestInvoluteGear.py b/src/Mod/PartDesign/PartDesignTests/TestInvoluteGear.py index 39c216c700..d93d6bfdea 100644 --- a/src/Mod/PartDesign/PartDesignTests/TestInvoluteGear.py +++ b/src/Mod/PartDesign/PartDesignTests/TestInvoluteGear.py @@ -21,8 +21,10 @@ import unittest import pathlib +from math import pi, tan, cos, acos import FreeCAD +Quantity = FreeCAD.Units.Quantity # FIXME from FreeCAD.Units import Quantity doesn't work from FreeCAD import Vector from Part import makeCircle, Precision import InvoluteGearFeature @@ -150,6 +152,66 @@ class TestInvoluteGear(unittest.TestCase): self.assertNoIntersection(hub.Shape, makeCircle(tip_diameter/2 - delta), "Teeth extent below tip circle") self.assertNoIntersection(hub.Shape, makeCircle(root_diameter/2 + delta), "Teeth extend beyond root circle") + def testShiftedExternalGearProfile(self): + gear = InvoluteGearFeature.makeInvoluteGear('InvoluteGear') + gear.NumberOfTeeth = 9 # odd number to have a tooth space on the negative X-axis + gear.ProfileShiftCoefficient = 0.6 + self.assertSuccessfulRecompute(gear) + self.assertClosedWire(gear.Shape) + # first, verify the radial dimensions + xm = gear.ProfileShiftCoefficient * gear.Modules + Rref = gear.NumberOfTeeth * gear.Modules / 2 + Rtip = Rref + gear.AddendumCoefficient * gear.Modules + xm + Rroot = Rref - gear.DedendumCoefficient * gear.Modules + xm + delta = Quantity("20 um") # 20 micron is as good as it gets + self.assertIntersection(gear.Shape, makeCircle(Rref), "Expecting intersection at reference circle") + self.assertNoIntersection(gear.Shape, makeCircle(Rtip + delta), "Teeth extent beyond tip circle") + self.assertNoIntersection(gear.Shape, makeCircle(Rroot - delta), "Teeth extend below root circle") + # to verify the angular dimensions, we use an "over pin measurement" + Dpin, Rc = external_pin_diameter_and_distance( + z=gear.NumberOfTeeth, + m=gear.Modules.getValueAs('mm'), + a=gear.PressureAngle.getValueAs('rad'), + x=gear.ProfileShiftCoefficient) + Rpin = Quantity(f"{Dpin/2} mm") + delta = Quantity("1 um") # our angular precision is much greater then the radial one + self.assertIntersection(gear.Shape, makeCircle(Rpin + delta, Vector(-Rc)), + msg="Expecting intersection with enlarged pin") + self.assertNoIntersection(gear.Shape, makeCircle(Rpin - delta, Vector(-Rc)), + msg="Expecting no intersection with reduced pin") + + def testShiftedInternalGearProfile(self): + gear = InvoluteGearFeature.makeInvoluteGear('InvoluteGear') + gear.NumberOfTeeth = 11 # odd number to have a tooth space on the negative X-axis + gear.ExternalGear = False # to ensure "clean" flanks we need to tweak some more props + gear.ProfileShiftCoefficient = 0.4 + gear.AddendumCoefficient = 0.6 + gear.DedendumCoefficient = 0.8 + self.assertSuccessfulRecompute(gear) + self.assertClosedWire(gear.Shape) + # first, verify the radial dimensions + xm = gear.ProfileShiftCoefficient * gear.Modules + Rref = gear.NumberOfTeeth * gear.Modules / 2 + # For internal, too, positive shift is outwards. So this is *not* inverted. + Rtip = Rref - gear.AddendumCoefficient * gear.Modules + xm + Rroot = Rref + gear.DedendumCoefficient * gear.Modules + xm + delta = Quantity("20 um") # 20 micron is as good as it gets + self.assertIntersection(gear.Shape, makeCircle(Rref), "Expecting intersection at reference circle") + self.assertNoIntersection(gear.Shape, makeCircle(Rtip - delta), "Teeth extent below tip circle") + self.assertNoIntersection(gear.Shape, makeCircle(Rroot + delta), "Teeth extend beyond root circle") + # to verify the angular dimensions, we use an "over pin measurement" + Dpin, Rc = internal_pin_diameter_and_distance( + z=gear.NumberOfTeeth, + m=gear.Modules.getValueAs('mm'), + a=gear.PressureAngle.getValueAs('rad'), + x=gear.ProfileShiftCoefficient) + Rpin = Quantity(f"{Dpin/2} mm") + delta = Quantity("1 um") # our angular precision is much greater then the radial one + self.assertIntersection(gear.Shape, makeCircle(Rpin + delta, Vector(-Rc)), + msg="Expecting intersection with enlarged pin") + self.assertNoIntersection(gear.Shape, makeCircle(Rpin - delta, Vector(-Rc)), + msg="Expecting no intersection with reduced pin") + def testZeroFilletExternalGearProfile_BaseAboveRoot(self): gear = InvoluteGearFeature.makeInvoluteGear('InvoluteGear') # below 42 teeth, with default dedendum 1.25, we have some non-involute flanks @@ -261,3 +323,81 @@ class TestInvoluteGear(unittest.TestCase): def assertSolid(self, shape, msg=None): self.assertEqual(shape.ShapeType, 'Solid', msg=msg) + + +def inv(a): + """the involute function""" + return tan(a) - a + + +def external_pin_diameter_and_distance(z, m, a, x): + """Calculates the ideal pin diameter for over pins measurement and its distance + for extrnal spur gears. + + z is the number of teeth + m is the module, in millimeter + a is the pressure angle, in radians + x is the profile shift coefficient + + returns the tuple of ideal pin diameter and its center distance from the gear's center + """ + # Equations taken from http://qtcgears.com/tools/catalogs/PDF_Q420/Tech.pdf + # Table 10-13 (1-4) and Table 10-14 (4a) + + # 1. Half Tooth Space Angle at Base Circle + nu = pi / (2 * z) - inv(a) - 2 * x * tan(a) / z + + # 2. The Pressure Angle at the Point Pin is Tangent to Tooth Surface + ap = acos(z * m * cos(a) / (z * m + 2 * x * m)) + + # 3. The Pressure Angle at Pin Center + phi = tan(ap) + nu + + # 4. Ideal Pin Diameter + dp = z * m * cos(a) * (inv(phi) + nu) + + # 4a. Over Pins Measurement, even number of teeth + # As we return the distance from the gear's center, we need dm to pass thought this center + # and that's only the case for a dm for an even number of teeth. However, this center distance + # is also valid for an odd number of teeth, as we don't measure pin-to-pin but pin-to-center. + dm = z * m * cos(a) / cos(phi) + dp + + # Eq. 10-12 on page T46 + rc = (dm - dp) / 2 + return (dp, rc) + + +def internal_pin_diameter_and_distance(z, m, a, x): + """Calculates the ideal pin diameter for over pins measurement and its distance + for intrnal spur gears. + + z is the number of teeth + m is the module, in millimeter + a is the pressure angle, in radians + x is the profile shift coefficient + + returns the tuple of ideal pin diameter and its center distance from the gear's center + """ + # Equations taken from http://qtcgears.com/tools/catalogs/PDF_Q420/Tech.pdf + # Table 10-17 (1-4) and Table 10-18 (4a) + + # 1. Half Tooth Space Angle at Base Circle + nu = pi / (2 * z) + inv(a) + 2 * x * tan(a) / z + + # 2. The Pressure Angle at the Point Pin is Tangent to Tooth Surface + ap = acos(z * m * cos(a) / (z * m + 2 * x * m)) + + # 3. The Pressure Angle at Pin Center + phi = tan(ap) - nu + + # 4. Ideal Pin Diameter + dp = z * m * cos(a) * (nu - inv(phi)) + + # 4a. Over Pins Measurement, even number of teeth + # As we return the distance from the gear's center, we need dm to pass thought this center + # and that's only the case for a dm for an even number of teeth. However, this center distance + # is also valid for an odd number of teeth, as we don't measure pin-to-pin but pin-to-center. + dm = z * m * cos(a) / cos(phi) - dp + + rc = (dm + dp) / 2 + return (dp, rc) diff --git a/src/Mod/PartDesign/fcgear/involute.py b/src/Mod/PartDesign/fcgear/involute.py index d298fc27ec..99ca16b0e5 100644 --- a/src/Mod/PartDesign/fcgear/involute.py +++ b/src/Mod/PartDesign/fcgear/involute.py @@ -23,14 +23,15 @@ # 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, radians +from math import cos, sin, tan, pi, acos, asin, atan, sqrt, radians from math import comb as binom def CreateExternalGear(w, m, Z, phi, split=True, addCoeff=1.0, dedCoeff=1.25, - filletCoeff=0.375): + filletCoeff=0.375, + shiftCoeff=0.0): """ Create an external gear @@ -43,6 +44,7 @@ def CreateExternalGear(w, m, Z, phi, filletCoeff is the root fillet radius, normalized by the module. The default of 0.375 matches the hard-coded value (1.5 * 0.25) of the implementation up to v0.20. The ISO Rack specified 0.38, though. + shiftCoeff is the profile shift coefficient (profile shift normalized by module) 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 @@ -56,13 +58,15 @@ def CreateExternalGear(w, m, Z, phi, split_involute=split, outer_height_coefficient=addCoeff, inner_height_coefficient=dedCoeff, - inner_fillet_coefficient=filletCoeff) + inner_fillet_coefficient=filletCoeff, + profile_shift_coefficient=shiftCoeff) def CreateInternalGear(w, m, Z, phi, split=True, addCoeff=0.6, dedCoeff=1.25, - filletCoeff=0.375): + filletCoeff=0.375, + shiftCoeff=0.0): """ Create an internal gear @@ -81,6 +85,7 @@ def CreateInternalGear(w, m, Z, phi, filletCoeff is the root fillet radius, normalized by the module. The default of 0.375 matches the hard-coded value (1.5 * 0.25) of the implementation up to v0.20. The ISO Rack specified 0.38, though. + shiftCoeff is the profile shift coefficient (profile shift normalized by module) 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 @@ -95,7 +100,8 @@ def CreateInternalGear(w, m, Z, phi, rotation=pi/Z, # rotate by half a tooth to align the "inner" tooth with the X-axis outer_height_coefficient=dedCoeff, inner_height_coefficient=addCoeff, - outer_fillet_coefficient=filletCoeff) + outer_fillet_coefficient=filletCoeff, + profile_shift_coefficient=shiftCoeff) def _create_involute_profile( @@ -108,7 +114,8 @@ def _create_involute_profile( outer_height_coefficient=1.0, inner_height_coefficient=1.0, outer_fillet_coefficient=0.0, - inner_fillet_coefficient=0.0): + inner_fillet_coefficient=0.0, + profile_shift_coefficient=0.0): """ Create an involute gear profile in the given wire builder @@ -125,8 +132,9 @@ def _create_involute_profile( The "_coefficient" suffix denotes values normalized by the module. """ - outer_height = outer_height_coefficient * module # external: addendum, internal: dedendum - inner_height = inner_height_coefficient * module # external: dedendum, internal: addednum + profile_shift = profile_shift_coefficient * module + outer_height = outer_height_coefficient * module + profile_shift # ext: addendum, int: dedednum + inner_height = inner_height_coefficient * module - profile_shift # ext: dedendum, int: addednum # ****** calculate radii # All distances from the center of the profile start with "R". @@ -248,8 +256,13 @@ def _create_involute_profile( else: inv = BezCoeffs(Rb, Rfo, 4, fs, fe) + # ****** calculate angular tooth thickness at reference circle + enlargement_by_shift = profile_shift * tan(pressure_angle) / Rref + tooth_thickness_half_angle = angular_pitch / 4 + enlargement_by_shift + psi = tooth_thickness_half_angle # for the formulae below, a symbol is more readable + # rotate all points to make the tooth symetric to the X axis - inv = [rotate(pt, -base_to_ref - angular_pitch / 4) for pt in inv] + inv = [rotate(pt, -base_to_ref - psi) for pt in inv] # create the back profile of tooth (mirror image on X axis) invR = [mirror(pt) for pt in inv] @@ -257,12 +270,12 @@ def _create_involute_profile( # ****** calculate section junction points. # Those are the points where the named element ends (start is the end of the previous element). # Suffix _back is back of this tooth, suffix _next is front of next tooth. - inner_fillet = toCartesian(Rfi, -angular_pitch / 4 - start_to_ref) # top of fillet + inner_fillet = toCartesian(Rfi, -psi - start_to_ref) # top of fillet inner_fillet_back = mirror(inner_fillet) # flip to make same point on back of tooth - inner_circle_back = toCartesian(Ri, angular_pitch / 4 + start_to_ref + inner_fillet_angle) - inner_circle_next = toCartesian(Ri, 3 * angular_pitch / 4 - start_to_ref - inner_fillet_angle) + inner_circle_back = toCartesian(Ri, psi + start_to_ref + inner_fillet_angle) + inner_circle_next = toCartesian(Ri, angular_pitch - psi - start_to_ref - inner_fillet_angle) inner_fillet_next = rotate(inner_fillet, angular_pitch) # top of fillet, front of next tooth - outer_fillet = toCartesian(Ro, -angular_pitch / 4 + ref_to_stop + outer_fillet_angle) + outer_fillet = toCartesian(Ro, -psi + ref_to_stop + outer_fillet_angle) outer_circle = mirror(outer_fillet) # ****** build the gear profile using the provided wire builder