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