From e05231aa7f562259258e80bff43a21f2bda3f632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20B=C3=A4hr?= Date: Fri, 17 Mar 2023 00:05:05 +0100 Subject: [PATCH] PD: Implement Profile Shift for InvoluteGear This commit adds the ability to shift the involute profile inside or outside. Profile shift is implemented as coefficient, i.e. normalized by the module, so that it the whole profile scales with the module without chaning shape. To verify the profile, the tests implement an "over pins measurement" using formulae found in literature. Backward compatibility with FreeCAD-v0.20 is garanteed by already existing tests, not touched by this commit. This addresses issue #5618. --- src/Mod/PartDesign/InvoluteGearFeature.py | 10 +- src/Mod/PartDesign/InvoluteGearFeature.ui | 27 +++- .../PartDesignTests/TestInvoluteGear.py | 140 ++++++++++++++++++ src/Mod/PartDesign/fcgear/involute.py | 39 +++-- 4 files changed, 200 insertions(+), 16 deletions(-) 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