From de6ab3fc798f55fa59aa8a83b99d60604c6abc4f Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 21 Feb 2022 22:18:18 -0800 Subject: [PATCH] Added thread generation unit tests and fixed finishing the thread --- src/Mod/Path/PathScripts/PathThreadMilling.py | 121 ++++++---------- .../Path/PathTests/TestPathThreadMilling.py | 132 ++++++++++++++++++ 2 files changed, 178 insertions(+), 75 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathThreadMilling.py b/src/Mod/Path/PathScripts/PathThreadMilling.py index ebd2a1f7a2..61e7c4a5c6 100644 --- a/src/Mod/Path/PathScripts/PathThreadMilling.py +++ b/src/Mod/Path/PathScripts/PathThreadMilling.py @@ -81,6 +81,24 @@ def threadPasses(count, radii, internal, majorDia, minorDia, toolDia, toolCrest) return [major - dr * (i + 1) for i in range(count)] +def elevatorRadius(obj, center, internal, tool): + '''elevatorLocation(obj, center, internal, tool) ... return suitable location for the tool elevator''' + + if internal: + dy = float(obj.MinorDiameter - tool.Diameter) / 2 - 1 + if dy < 0: + if (obj.MinorDiameter < tool.Diameter): + PathLog.error("The selected tool is too big (d={}) for milling a thread with minor diameter D={}".format(tool.Diameter, obj.MinorDiameter)) + dy = 0 + else: + dy = float(obj.MajorDiameter + tool.Diameter) / 2 + 1 + + return dy + +def comment(path, msg): + if True: + path.append(Path.Command("(------- {} -------)".format(msg))) + class _ThreadInternal(object): """Helper class for dealing with different thread types""" @@ -121,29 +139,30 @@ class _ThreadInternal(object): return self.pitch > 0 -def threadCommandsInternal(loc, cmd, zStart, zFinal, pitch, radius, leadInOut): - """threadCommandsInternal(loc, cmd, zStart, zFinal, pitch, radius) ... returns the g-code to mill the given internal thread""" +def threadCommands(center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator): + """threadCommands(center, cmd, zStart, zFinal, pitch, radius) ... returns the g-code to mill the given internal thread""" thread = _ThreadInternal(cmd, zStart, zFinal, pitch) - yMin = loc.y - radius - yMax = loc.y + radius + yMin = center.y - radius + yMax = center.y + radius path = [] - # at this point the tool is at a safe height (depending on the previous thread), so we can move + # at this point the tool is at a safe heiht (depending on the previous thread), so we can move # into position first, and then drop to the start height. If there is any material in the way this # op hasn't been setup properly. - path.append(Path.Command("G0", {"X": loc.x, "Y": loc.y})) + path.append(Path.Command("G0", {"X": center.x, "Y": center.y + elevator})) path.append(Path.Command("G0", {"Z": thread.zStart})) if leadInOut: - path.append(Path.Command(thread.cmd, {"Y": yMax, "J": (yMax - loc.y) / 2})) + comment(path, 'lead-in') + path.append(Path.Command(thread.cmd, {"Y": yMax, "J": (yMax - (center.y + elevator)) / 2})) + comment(path, 'lead-in') else: path.append(Path.Command("G1", {"Y": yMax})) z = thread.zStart r = -radius i = 0 - while True: - z = thread.zStart + i * thread.hPitch + while not PathGeom.isRoughly(z, thread.zFinal): if thread.overshoots(z): break if 0 == (i & 0x01): @@ -153,93 +172,44 @@ def threadCommandsInternal(loc, cmd, zStart, zFinal, pitch, radius, leadInOut): path.append(Path.Command(thread.cmd, {"Y": y, "Z": z + thread.hPitch, "J": r})) r = -r i = i + 1 + z = z + thread.hPitch - z = thread.zStart + i * thread.hPitch if PathGeom.isRoughly(z, thread.zFinal): - x = loc.x + x = center.x + y = yMin if 0 == (i & 0x01) else yMax else: n = math.fabs(thread.zFinal - thread.zStart) / thread.hPitch k = n - int(n) dy = math.cos(k * math.pi) dx = math.sin(k * math.pi) - y = thread.adjustY(loc.y, r * dy) - x = thread.adjustX(loc.x, r * dx) + y = thread.adjustY(center.y, r * dy) + x = thread.adjustX(center.x, r * dx) + comment(path, 'finish-thread') path.append( Path.Command(thread.cmd, {"X": x, "Y": y, "Z": thread.zFinal, "J": r}) ) + comment(path, 'finish-thread') + + a = math.atan2(y - center.y, x - center.x) + dx = math.cos(a) * elevator + dy = math.sin(a) * elevator if leadInOut: + comment(path, 'lead-out') path.append( Path.Command( thread.cmd, - {"X": loc.x, "Y": loc.y, "I": (loc.x - x) / 2, "J": (loc.y - y) / 2}, + {"X": center.x + dx, "Y": center.y + dy, "I": dx / 2, "J": dy / 2}, ) ) - else: - path.append(Path.Command("G1", {"X": loc.x, "Y": loc.y})) - return path + comment(path, 'lead-out') - -def threadCommandsExternal(loc, cmd, zStart, zFinal, pitch, radius, leadInOut): - """threadCommandsExternal(loc, cmd, zStart, zFinal, pitch, radius) ... returns the g-code to mill the given internal thread""" - thread = _ThreadInternal(cmd, zStart, zFinal, pitch) - - yMin = loc.y - radius - yMax = loc.y + radius - - path = [] - # at this point the tool is at a safe height (depending on the previous thread), so we can move - # into position first, and then drop to the start height. If there is any material in the way this - # op hasn't been setup properly. - path.append(Path.Command("G0", {"X": loc.x, "Y": loc.y})) - path.append(Path.Command("G0", {"Z": thread.zStart})) - if leadInOut: - path.append(Path.Command(thread.cmd, {"Y": yMax, "J": (yMax - loc.y) / 2})) - else: - path.append(Path.Command("G1", {"Y": yMax})) - - z = thread.zStart - r = -radius - i = 0 - while True: - z = thread.zStart + i * thread.hPitch - if thread.overshoots(z): - break - if 0 == (i & 0x01): - y = yMin - else: - y = yMax - path.append(Path.Command(thread.cmd, {"Y": y, "Z": z + thread.hPitch, "J": r})) - r = -r - i = i + 1 - - z = thread.zStart + i * thread.hPitch - if PathGeom.isRoughly(z, thread.zFinal): - x = loc.x - else: - n = math.fabs(thread.zFinal - thread.zStart) / thread.hPitch - k = n - int(n) - dy = math.cos(k * math.pi) - dx = math.sin(k * math.pi) - y = thread.adjustY(loc.y, r * dy) - x = thread.adjustX(loc.x, r * dx) - path.append( - Path.Command(thread.cmd, {"X": x, "Y": y, "Z": thread.zFinal, "J": r}) - ) - - if leadInOut: - path.append( - Path.Command( - thread.cmd, - {"X": loc.x, "Y": loc.y, "I": (loc.x - x) / 2, "J": (loc.y - y) / 2}, - ) - ) - else: - path.append(Path.Command("G1", {"X": loc.x, "Y": loc.y})) + path.append(Path.Command("G1", {"X": center.x + dx, "Y": center.y - dy})) return path + class ObjectThreadMilling(PathCircularHoleBase.ObjectOp): """Proxy object for thread milling operation.""" @@ -485,6 +455,7 @@ class ObjectThreadMilling(PathCircularHoleBase.ObjectOp): def executeThreadMill(self, obj, loc, gcode, zStart, zFinal, pitch): PathLog.track(obj.Label, loc, gcode, zStart, zFinal, pitch) + elevator = elevatorRadius(obj, loc, self._isThreadInternal(obj), self.tool) self.commandlist.append( Path.Command("G0", {"Z": obj.ClearanceHeight.Value, "F": self.vertRapid}) @@ -499,7 +470,7 @@ class ObjectThreadMilling(PathCircularHoleBase.ObjectOp): float(self.tool.Diameter), float(self.tool.Crest), ): - commands = threadCommandsInternal(loc, gcode, zStart, zFinal, pitch, radius, obj.LeadInOut) + commands = threadCommands(loc, gcode, zStart, zFinal, pitch, radius, obj.LeadInOut, elevator) for cmd in commands: p = cmd.Parameters diff --git a/src/Mod/Path/PathTests/TestPathThreadMilling.py b/src/Mod/Path/PathTests/TestPathThreadMilling.py index 0a296145bb..db15f2a628 100644 --- a/src/Mod/Path/PathTests/TestPathThreadMilling.py +++ b/src/Mod/Path/PathTests/TestPathThreadMilling.py @@ -20,6 +20,7 @@ # * * # *************************************************************************** +import FreeCAD import PathScripts.PathGeom as PathGeom import PathScripts.PathThreadMilling as PathThreadMilling import math @@ -74,3 +75,134 @@ class TestPathThreadMilling(PathTestBase): self.assertList(PathThreadMilling.threadPasses(2, radii, False, 10, 9, 0, 0), [9.5, 9]) self.assertList(PathThreadMilling.threadPasses(5, radii, False, 10, 9, 0, 0), [9.8, 9.6, 9.4, 9.2, 9]) + def test40(self): + '''Verify thread commands for a single thread''' + + center = FreeCAD.Vector() + cmd = 'G2' + zStart = 0 + zFinal = 1 + pitch = 1 + radius = 3 + leadInOut = False + elevator = 2 + + path = PathThreadMilling.threadCommands(center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator) + + gcode = [ + 'G0 X0.000000 Y2.000000', + 'G0 Z0.000000', + 'G1 Y3.000000', + 'G2 J-3.000000 Y-3.000000 Z0.500000', + 'G2 J3.000000 Y3.000000 Z1.000000', + 'G1 X0.000000 Y2.000000', + ] + self.assertEqual([p.toGCode() for p in path], gcode) + + def test41(self): + '''Verify thread commands for a thwo threads''' + + center = FreeCAD.Vector() + cmd = 'G2' + zStart = 0 + zFinal = 2 + pitch = 1 + radius = 3 + leadInOut = False + elevator = 2 + + path = PathThreadMilling.threadCommands(center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator) + + gcode = [ + 'G0 X0.000000 Y2.000000', + 'G0 Z0.000000', + 'G1 Y3.000000', + 'G2 J-3.000000 Y-3.000000 Z0.500000', + 'G2 J3.000000 Y3.000000 Z1.000000', + 'G2 J-3.000000 Y-3.000000 Z1.500000', + 'G2 J3.000000 Y3.000000 Z2.000000', + 'G1 X0.000000 Y2.000000', + ] + self.assertEqual([p.toGCode() for p in path], gcode) + + def test42(self): + '''Verify thread commands for a one and a half threads''' + + center = FreeCAD.Vector() + cmd = 'G2' + zStart = 0 + zFinal = 1.5 + pitch = 1 + radius = 3 + leadInOut = False + elevator = 2 + + path = PathThreadMilling.threadCommands(center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator) + + gcode = [ + 'G0 X0.000000 Y2.000000', + 'G0 Z0.000000', + 'G1 Y3.000000', + 'G2 J-3.000000 Y-3.000000 Z0.500000', + 'G2 J3.000000 Y3.000000 Z1.000000', + 'G2 J-3.000000 Y-3.000000 Z1.500000', + 'G1 X0.000000 Y-2.000000', + ] + self.assertEqual([p.toGCode() for p in path], gcode) + + def test43(self): + '''Verify thread commands for a one and 3 quarter threads''' + + center = FreeCAD.Vector() + cmd = 'G2' + zStart = 0 + zFinal = 1.75 + pitch = 1 + radius = 3 + leadInOut = False + elevator = 2 + + path = PathThreadMilling.threadCommands(center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator) + + gcode = [ + 'G0 X0.000000 Y2.000000', + 'G0 Z0.000000', + 'G1 Y3.000000', + 'G2 J-3.000000 Y-3.000000 Z0.500000', + 'G2 J3.000000 Y3.000000 Z1.000000', + 'G2 J-3.000000 Y-3.000000 Z1.500000', + '(------- finish-thread -------)', + 'G2 J3.000000 X-3.000000 Y0.000000 Z1.750000', + '(------- finish-thread -------)', + 'G1 X-2.000000 Y0.000000', + ] + self.assertEqual([p.toGCode() for p in path], gcode) + + def test44(self): + '''Verify thread commands for a one and 3 quarter threads - CCW''' + + center = FreeCAD.Vector() + cmd = 'G3' + zStart = 0 + zFinal = 1.75 + pitch = 1 + radius = 3 + leadInOut = False + elevator = 2 + + path = PathThreadMilling.threadCommands(center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator) + + gcode = [ + 'G0 X0.000000 Y2.000000', + 'G0 Z0.000000', + 'G1 Y3.000000', + 'G3 J-3.000000 Y-3.000000 Z0.500000', + 'G3 J3.000000 Y3.000000 Z1.000000', + 'G3 J-3.000000 Y-3.000000 Z1.500000', + '(------- finish-thread -------)', + 'G3 J3.000000 X3.000000 Y0.000000 Z1.750000', + '(------- finish-thread -------)', + 'G1 X2.000000 Y0.000000', + ] + self.assertEqual([p.toGCode() for p in path], gcode) +