Merge pull request #8934 from jbaehr/partdesign-ingolutegear-profileshift
PD: Implement Profile Shift for InvoluteGear
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>248</width>
|
||||
<height>270</height>
|
||||
<width>253</width>
|
||||
<height>301</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -233,6 +233,29 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="label_ProfileShift">
|
||||
<property name="text">
|
||||
<string>Profile Shift Coefficient</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QDoubleSpinBox" name="doubleSpinBox_ProfileShift">
|
||||
<property name="minimum">
|
||||
<double>-3.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>3.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>0.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user