From d5e341b044d86787c8e9788eba7535711d73cd53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20B=C3=A4hr?= Date: Tue, 14 Feb 2023 22:48:43 +0100 Subject: [PATCH] PD: Fix exception from InvoluteGear with zero fillets While gears without a root fillet may have limited real-live use cases, during number-input it happens regulary to have a zero fillet radius as intermediate state. With this commit such a situation is now handled gracefully. --- .../PartDesignTests/TestInvoluteGear.py | 23 ++++++++ src/Mod/PartDesign/fcgear/involute.py | 55 +++++++++++++------ 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/src/Mod/PartDesign/PartDesignTests/TestInvoluteGear.py b/src/Mod/PartDesign/PartDesignTests/TestInvoluteGear.py index a732c5e449..a0ca90954b 100644 --- a/src/Mod/PartDesign/PartDesignTests/TestInvoluteGear.py +++ b/src/Mod/PartDesign/PartDesignTests/TestInvoluteGear.py @@ -130,6 +130,29 @@ 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 testZeroFilletExternalGearProfile_BaseAboveRoot(self): + gear = InvoluteGearFeature.makeInvoluteGear('InvoluteGear') + # below 42 teeth, with default dedendum 1.25, we have some non-involute flanks + gear.NumberOfTeeth = 41 + gear.RootFilletCoefficient = 0 + self.assertSuccessfulRecompute(gear) + self.assertClosedWire(gear.Shape) + + def testZeroFilletExternalGearProfile_BaseBelowRoot(self): + gear = InvoluteGearFeature.makeInvoluteGear('InvoluteGear') + # above 41 teeth, with default dedendum 1.25, the root is within the involute flank + gear.NumberOfTeeth = 42 + gear.RootFilletCoefficient = 0 + self.assertSuccessfulRecompute(gear) + self.assertClosedWire(gear.Shape) + + def testZeroFilletInternalGearProfile(self): + gear = InvoluteGearFeature.makeInvoluteGear('InvoluteGear') + gear.ExternalGear = False + gear.RootFilletCoefficient = 0 + self.assertSuccessfulRecompute(gear) + self.assertClosedWire(gear.Shape) + def testUsagePadGearProfile(self): profile = InvoluteGearFeature.makeInvoluteGear('GearProfile') body = self.Doc.addObject('PartDesign::Body','GearBody') diff --git a/src/Mod/PartDesign/fcgear/involute.py b/src/Mod/PartDesign/fcgear/involute.py index 4bbafde3d5..97eafc5af2 100644 --- a/src/Mod/PartDesign/fcgear/involute.py +++ b/src/Mod/PartDesign/fcgear/involute.py @@ -63,7 +63,7 @@ def CreateExternalGear(w, m, Z, phi, Rf = Rc # radius at top of fillet, assuming fillet below involute filletWithinInvolute = Rf > Rb # above the base circle we have the involute, # below we have a straight line towards the center. - if (filletWithinInvolute): + if (filletWithinInvolute and fRad > 0): # In this case we need tangency of the involute and the fillet circle. # It has to be somewhere between max(Rb,Rroot) and Rc. # So we need the radius r from the origin depending on the tangent angle, for both: @@ -149,7 +149,16 @@ def CreateExternalGear(w, m, Z, phi, t_inc = 2.0 * pi / float(Z) thetas = [(x * t_inc) for x in range(Z)] - w.move(fillet) # start at top of fillet + # Make sure we begin *exactly* where our last curve ends. + # In theory start == rotate(end, angle_of_last_tooth), but in practice we have limited + # precision. Especially if we don't have a fillet, we end at rootNext, not filletNext. + # And even though these two should also be equal, they are calculated differently, + # which is enough for the resulting wire not being closed any more. + # So to be on the save side, we begin at rotate(end, angle_of_last_tooth), not start. + if fRad > 0: + w.move(rotate(filletNext, thetas[-1])) # start at top of front profile + else: + w.move(rotate(rootNext, thetas[-1])) # start at top of front profile for theta in thetas: w.theta = theta @@ -171,10 +180,12 @@ def CreateExternalGear(w, m, Z, phi, 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 + if fRad > 0: + w.arc(rootR, fRad, 0) # back fillet w.arc(rootNext, Rroot, 1) # root circle arc - w.arc(filletNext, fRad, 0) + if fRad > 0: + w.arc(filletNext, fRad, 0) w.close() return w @@ -239,14 +250,16 @@ def CreateInternalGear(w, m, Z, phi, # interpretation of it: The tangent angle of the involute experiences the same rotation as the # involute itself. So its is just a simple offset: Our q(r) becomes qi(r) - qc(i) - phi_corr, # where phi_corr is the amount we (virtually) turn our involute around the origin. - phi_corr = genInvolutePolar(Rb, Rroot) + atan(fRad / Rroot) - q = lambda r: (sqrt(r**2 - Rb**2) / Rb - - asin((r**2 - fRad**2 - Rc**2) / (2 * fRad * Rc)) - - phi_corr) - q_prime = lambda r: (r / (sqrt(-Rb**2 + r**2) * Rb) - - r / (fRad * Rc * sqrt(1 - 1/4 * (r**2 - fRad**2 - Rc**2)**2 / (fRad**2 * Rc**2)))) - - Rf = findRootNewton(q, q_prime, x_min=max(Rb, Rc), x_max=Rroot) + if (fRad > 0): + phi_corr = genInvolutePolar(Rb, Rroot) + atan(fRad / Rroot) + q = lambda r: (sqrt(r**2 - Rb**2) / Rb + - asin((r**2 - fRad**2 - Rc**2) / (2 * fRad * Rc)) + - phi_corr) + q_prime = lambda r: (r / (sqrt(-Rb**2 + r**2) * Rb) + - r / (fRad * Rc * sqrt(1 - 1/4 * (r**2 - fRad**2 - Rc**2)**2 / (fRad**2 * Rc**2)))) + Rf = findRootNewton(q, q_prime, x_min=max(Rb, Rc), x_max=Rroot) + else: + Rf = Rroot # no fillet # ****** calculate angles (all in radians) @@ -298,8 +311,16 @@ def CreateInternalGear(w, m, Z, phi, t_inc = 2.0 * pi / float(Z) thetas = [(x * t_inc) for x in range(Z)] - w.move(fillet) # start at top of front profile - + # Make sure we begin *exactly* where our last curve ends. + # In theory start == rotate(end, angle_of_last_tooth), but in practice we have limited + # precision. Especially if we don't have a fillet, we end at rootNext, not filletNext. + # And even though these two should also be equal, they are calculated differently, + # which is enough for the resulting wire not being closed any more. + # So to be on the save side, we begin at rotate(end, angle_of_last_tooth), not start. + if fRad > 0: + w.move(rotate(filletNext, thetas[-1])) # start at top of front profile + else: + w.move(rotate(rootNext, thetas[-1])) # start at top of front profile for theta in thetas: w.theta = theta @@ -324,10 +345,12 @@ def CreateInternalGear(w, m, Z, phi, w.curve(*invR[1:]) if (rootNext[1] > rootR[1]): # is there a section of root circle between fillets? - w.arc(rootR, fRad, 1) # back fillet + if fRad > 0: + w.arc(rootR, fRad, 1) # back fillet w.arc(rootNext, Rroot, 1) # root circle arc - w.arc(filletNext, fRad, 1) + if fRad > 0: + w.arc(filletNext, fRad, 1) w.close()