diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 0f3eba076f..91331038e8 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -146,8 +146,9 @@ SET(PathScripts_SRCS SET(Generator_SRCS Generators/drill_generator.py - Generators/rotation_generator.py Generators/helix_generator.py + Generators/rotation_generator.py + Generators/threadmilling_generator.py Generators/toolchange_generator.py ) @@ -242,6 +243,7 @@ SET(PathTests_SRCS PathTests/TestPathStock.py PathTests/TestPathToolChangeGenerator.py PathTests/TestPathThreadMilling.py + PathTests/TestPathThreadMillingGenerator.py PathTests/TestPathTool.py PathTests/TestPathToolBit.py PathTests/TestPathToolController.py diff --git a/src/Mod/Path/Generators/threadmilling_generator.py b/src/Mod/Path/Generators/threadmilling_generator.py new file mode 100644 index 0000000000..3694acaac0 --- /dev/null +++ b/src/Mod/Path/Generators/threadmilling_generator.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +from __future__ import print_function + +import FreeCAD +import Path +import PathScripts.PathGeom as PathGeom +import PathScripts.PathLog as PathLog +import math +from PySide.QtCore import QT_TRANSLATE_NOOP + +__title__ = "Path Thread Milling generator" +__author__ = "sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Path thread milling operation." + +if False: + PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) + PathLog.trackModule(PathLog.thisModule()) +else: + PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) + +translate = FreeCAD.Qt.translate + + +def _comment(path, msg): + """Nice for debugging to insert markers into generated g-code""" + if False: + path.append(Path.Command("(------- {} -------)".format(msg))) + + +class _Thread(object): + """Helper class for dealing with different thread types""" + + def __init__(self, cmd, zStart, zFinal, pitch, internal): + self.cmd = cmd + if zStart < zFinal: + self.pitch = pitch + else: + self.pitch = -pitch + self.hPitch = self.pitch / 2 + self.zStart = zStart + self.zFinal = zFinal + self.internal = internal + + def overshoots(self, z): + """overshoots(z) ... returns true if adding another half helix goes beyond the thread bounds""" + if self.pitch < 0: + return z + self.hPitch < self.zFinal + return z + self.hPitch > self.zFinal + + def adjustX(self, x, dx): + """adjustX(x, dx) ... move x by dx, the direction depends on the thread settings""" + if self.isG3() == (self.pitch > 0): + return x + dx + return x - dx + + def adjustY(self, y, dy): + """adjustY(y, dy) ... move y by dy, the direction depends on the thread settings""" + if self.isG3(): + return y - dy + return y - dy + + def isG3(self): + """isG3() ... returns True if this is a G3 command""" + return self.cmd in ["G3", "G03", "g3", "g03"] + + def isUp(self): + """isUp() ... returns True if the thread goes from the bottom up""" + return self.pitch > 0 + + def g4Opposite(self): + return "G2" if self.isG3() else "G3" + + def g4LeadInOut(self): + if self.internal: + return self.cmd + return self.g4Opposite() + + def g4Start2Elevator(self): + return self.g4Opposite() + + +def generate(center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, start): + """generate(center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, start) ... returns the g-code to mill the given internal thread""" + thread = _Thread(cmd, zStart, zFinal, pitch, radius > elevator) + + yMin = center.y - radius + yMax = center.y + radius + + path = [] + # 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. + if leadInOut: + _comment(path, "lead-in") + if start is None: + path.append(Path.Command("G0", {"X": center.x, "Y": center.y + elevator})) + path.append(Path.Command("G0", {"Z": thread.zStart})) + else: + path.append( + Path.Command( + thread.g4Start2Elevator(), + { + "X": center.x, + "Y": center.y + elevator, + "Z": thread.zStart, + "I": (center.x - start.x) / 2, + "J": (center.y + elevator - start.y) / 2, + "K": (thread.zStart - start.z) / 2, + }, + ) + ) + path.append( + Path.Command( + thread.g4LeadInOut(), + {"Y": yMax, "J": (yMax - (center.y + elevator)) / 2}, + ) + ) + _comment(path, "lead-in") + else: + if start is None: + path.append(Path.Command("G0", {"X": center.x, "Y": center.y + elevator})) + path.append(Path.Command("G0", {"Z": thread.zStart})) + else: + path.append( + Path.Command( + "G0", {"X": center.x, "Y": center.y + elevator, "Z": thread.zStart} + ) + ) + path.append(Path.Command("G1", {"Y": yMax})) + + z = thread.zStart + r = -radius + i = 0 + while not PathGeom.isRoughly(z, thread.zFinal): + 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 = z + thread.hPitch + + if PathGeom.isRoughly(z, thread.zFinal): + x = center.x + y = yMin if (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(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) * (radius - elevator) + dy = math.sin(a) * (radius - elevator) + PathLog.debug("") + PathLog.debug("a={}: dx={:.2f}, dy={:.2f}".format(a / math.pi * 180, dx, dy)) + + elevatorX = x - dx + elevatorY = y - dy + PathLog.debug( + "({:.2f}, {:.2f}) -> ({:.2f}, {:.2f})".format(x, y, elevatorX, elevatorY) + ) + + if leadInOut: + _comment(path, "lead-out") + path.append( + Path.Command( + thread.g4LeadInOut(), + {"X": elevatorX, "Y": elevatorY, "I": -dx / 2, "J": -dy / 2}, + ) + ) + _comment(path, "lead-out") + else: + path.append(Path.Command("G1", {"X": elevatorX, "Y": elevatorY})) + + return (path, FreeCAD.Vector(elevatorX, elevatorY, thread.zFinal)) diff --git a/src/Mod/Path/PathScripts/PathThreadMilling.py b/src/Mod/Path/PathScripts/PathThreadMilling.py index e742122d32..7ce1ead63e 100644 --- a/src/Mod/Path/PathScripts/PathThreadMilling.py +++ b/src/Mod/Path/PathScripts/PathThreadMilling.py @@ -28,6 +28,7 @@ import PathScripts.PathCircularHoleBase as PathCircularHoleBase import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp +import Generators.threadmilling_generator as threadmilling import math from PySide.QtCore import QT_TRANSLATE_NOOP @@ -117,172 +118,6 @@ def elevatorRadius(obj, center, internal, tool): return dy -def comment(path, msg): - if False: - path.append(Path.Command("(------- {} -------)".format(msg))) - - -class _Thread(object): - """Helper class for dealing with different thread types""" - - def __init__(self, cmd, zStart, zFinal, pitch, internal): - self.cmd = cmd - if zStart < zFinal: - self.pitch = pitch - else: - self.pitch = -pitch - self.hPitch = self.pitch / 2 - self.zStart = zStart - self.zFinal = zFinal - self.internal = internal - - def overshoots(self, z): - """overshoots(z) ... returns true if adding another half helix goes beyond the thread bounds""" - if self.pitch < 0: - return z + self.hPitch < self.zFinal - return z + self.hPitch > self.zFinal - - def adjustX(self, x, dx): - """adjustX(x, dx) ... move x by dx, the direction depends on the thread settings""" - if self.isG3() == (self.pitch > 0): - return x + dx - return x - dx - - def adjustY(self, y, dy): - """adjustY(y, dy) ... move y by dy, the direction depends on the thread settings""" - if self.isG3(): - return y - dy - return y - dy - - def isG3(self): - """isG3() ... returns True if this is a G3 command""" - return self.cmd in ["G3", "G03", "g3", "g03"] - - def isUp(self): - """isUp() ... returns True if the thread goes from the bottom up""" - return self.pitch > 0 - - def g4Opposite(self): - return "G2" if self.isG3() else "G3" - - def g4LeadInOut(self): - if self.internal: - return self.cmd - return self.g4Opposite() - - def g4Start2Elevator(self): - return self.g4Opposite() - - -def threadCommands( - center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, start -): - """threadCommands(center, cmd, zStart, zFinal, pitch, radius) ... returns the g-code to mill the given internal thread""" - thread = _Thread(cmd, zStart, zFinal, pitch, radius > elevator) - - yMin = center.y - radius - yMax = center.y + radius - - path = [] - # 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. - if leadInOut: - comment(path, "lead-in") - if start is None: - path.append(Path.Command("G0", {"X": center.x, "Y": center.y + elevator})) - path.append(Path.Command("G0", {"Z": thread.zStart})) - else: - path.append( - Path.Command( - thread.g4Start2Elevator(), - { - "X": center.x, - "Y": center.y + elevator, - "Z": thread.zStart, - "I": (center.x - start.x) / 2, - "J": (center.y + elevator - start.y) / 2, - "K": (thread.zStart - start.z) / 2, - }, - ) - ) - path.append( - Path.Command( - thread.g4LeadInOut(), - {"Y": yMax, "J": (yMax - (center.y + elevator)) / 2}, - ) - ) - comment(path, "lead-in") - else: - if start is None: - path.append(Path.Command("G0", {"X": center.x, "Y": center.y + elevator})) - path.append(Path.Command("G0", {"Z": thread.zStart})) - else: - path.append( - Path.Command( - "G0", {"X": center.x, "Y": center.y + elevator, "Z": thread.zStart} - ) - ) - path.append(Path.Command("G1", {"Y": yMax})) - - z = thread.zStart - r = -radius - i = 0 - while not PathGeom.isRoughly(z, thread.zFinal): - 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 = z + thread.hPitch - - if PathGeom.isRoughly(z, thread.zFinal): - x = center.x - y = yMin if (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(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) * (radius - elevator) - dy = math.sin(a) * (radius - elevator) - PathLog.debug("") - PathLog.debug("a={}: dx={:.2f}, dy={:.2f}".format(a / math.pi * 180, dx, dy)) - - elevatorX = x - dx - elevatorY = y - dy - PathLog.debug( - "({:.2f}, {:.2f}) -> ({:.2f}, {:.2f})".format(x, y, elevatorX, elevatorY) - ) - - if leadInOut: - comment(path, "lead-out") - path.append( - Path.Command( - thread.g4LeadInOut(), - {"X": elevatorX, "Y": elevatorY, "I": -dx / 2, "J": -dy / 2}, - ) - ) - comment(path, "lead-out") - else: - path.append(Path.Command("G1", {"X": elevatorX, "Y": elevatorY})) - - return (path, FreeCAD.Vector(elevatorX, elevatorY, thread.zFinal)) - - class ObjectThreadMilling(PathCircularHoleBase.ObjectOp): """Proxy object for thread milling operation.""" @@ -594,7 +429,7 @@ class ObjectThreadMilling(PathCircularHoleBase.ObjectOp): # shortcuts when moving to the elevator position self.commandlist.append(move2clearance) start = None - commands, start = threadCommands( + commands, start = threadmilling.generate( loc, gcode, zStart, diff --git a/src/Mod/Path/PathTests/TestPathThreadMilling.py b/src/Mod/Path/PathTests/TestPathThreadMilling.py index bc29512301..fcfb49f1c1 100644 --- a/src/Mod/Path/PathTests/TestPathThreadMilling.py +++ b/src/Mod/Path/PathTests/TestPathThreadMilling.py @@ -92,213 +92,3 @@ class TestPathThreadMilling(PathTestBase): PathThreadMilling.threadPasses(5, radii, False, 10, 9, 0, 0), [9.552786, 9.367544, 9.225403, 9.105573, 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, start = PathThreadMilling.threadCommands( - center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, None - ) - - 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) - self.assertCoincide(start, FreeCAD.Vector(0, 2, zFinal)) - - 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, start = PathThreadMilling.threadCommands( - center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, None - ) - - 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) - self.assertCoincide(start, FreeCAD.Vector(0, 2, zFinal)) - - 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, start = PathThreadMilling.threadCommands( - center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, None - ) - - 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) - self.assertCoincide(start, FreeCAD.Vector(0, -2, zFinal)) - - 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, start = PathThreadMilling.threadCommands( - center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, None - ) - - 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) - self.assertCoincide(start, FreeCAD.Vector(-2, 0, zFinal)) - - 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, start = PathThreadMilling.threadCommands( - center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, None - ) - - 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) - self.assertCoincide(start, FreeCAD.Vector(2, 0, zFinal)) - - def test50(self): - """Verify lead in/out commands for a single thread""" - - center = FreeCAD.Vector() - cmd = "G2" - zStart = 0 - zFinal = 1 - pitch = 1 - radius = 3 - leadInOut = True - elevator = 2 - - path, start = PathThreadMilling.threadCommands( - center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, None - ) - - gcode = [ - "G0 X0.000000 Y2.000000", - "G0 Z0.000000", - #'(------- lead-in -------)', - "G2 J0.500000 Y3.000000", - #'(------- lead-in -------)', - "G2 J-3.000000 Y-3.000000 Z0.500000", - "G2 J3.000000 Y3.000000 Z1.000000", - #'(------- lead-out -------)', - "G2 I0.000000 J-0.500000 X0.000000 Y2.000000", - #'(------- lead-out -------)', - ] - self.assertEqual([p.toGCode() for p in path], gcode) - self.assertCoincide(start, FreeCAD.Vector(0, 2, zFinal)) - - def test51(self): - """Verify lead in/out commands for one and a half threads""" - - center = FreeCAD.Vector() - cmd = "G2" - zStart = 0 - zFinal = 1.5 - pitch = 1 - radius = 3 - leadInOut = True - elevator = 2 - - path, start = PathThreadMilling.threadCommands( - center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, None - ) - - gcode = [ - "G0 X0.000000 Y2.000000", - "G0 Z0.000000", - #'(------- lead-in -------)', - "G2 J0.500000 Y3.000000", - #'(------- lead-in -------)', - "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", - #'(------- lead-out -------)', - "G2 I0.000000 J0.500000 X0.000000 Y-2.000000", - #'(------- lead-out -------)', - ] - self.assertEqual([p.toGCode() for p in path], gcode) - self.assertCoincide(start, FreeCAD.Vector(0, -2, zFinal)) - diff --git a/src/Mod/Path/PathTests/TestPathThreadMillingGenerator.py b/src/Mod/Path/PathTests/TestPathThreadMillingGenerator.py new file mode 100644 index 0000000000..c73e5ea592 --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathThreadMillingGenerator.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD +import PathScripts.PathGeom as PathGeom +import Generators.threadmilling_generator as threadmilling +import math + +from PathTests.PathTestUtils import PathTestBase + + +def radii(internal, major, minor, toolDia, toolCrest): + """test radii function for simple testing""" + return (minor, major) + + +class TestPathThreadMillingGenerator(PathTestBase): + """Test thread milling generator.""" + + def test00(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, start = threadmilling.generate( + center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, None + ) + + 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) + self.assertCoincide(start, FreeCAD.Vector(0, 2, zFinal)) + + def test01(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, start = threadmilling.generate( + center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, None + ) + + 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) + self.assertCoincide(start, FreeCAD.Vector(0, 2, zFinal)) + + def test02(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, start = threadmilling.generate( + center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, None + ) + + 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) + self.assertCoincide(start, FreeCAD.Vector(0, -2, zFinal)) + + def test03(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, start = threadmilling.generate( + center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, None + ) + + 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) + self.assertCoincide(start, FreeCAD.Vector(-2, 0, zFinal)) + + def test04(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, start = threadmilling.generate( + center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, None + ) + + 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) + self.assertCoincide(start, FreeCAD.Vector(2, 0, zFinal)) + + def test10(self): + """Verify lead in/out commands for a single thread""" + + center = FreeCAD.Vector() + cmd = "G2" + zStart = 0 + zFinal = 1 + pitch = 1 + radius = 3 + leadInOut = True + elevator = 2 + + path, start = threadmilling.generate( + center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, None + ) + + gcode = [ + "G0 X0.000000 Y2.000000", + "G0 Z0.000000", + #'(------- lead-in -------)', + "G2 J0.500000 Y3.000000", + #'(------- lead-in -------)', + "G2 J-3.000000 Y-3.000000 Z0.500000", + "G2 J3.000000 Y3.000000 Z1.000000", + #'(------- lead-out -------)', + "G2 I0.000000 J-0.500000 X0.000000 Y2.000000", + #'(------- lead-out -------)', + ] + self.assertEqual([p.toGCode() for p in path], gcode) + self.assertCoincide(start, FreeCAD.Vector(0, 2, zFinal)) + + def test11(self): + """Verify lead in/out commands for one and a half threads""" + + center = FreeCAD.Vector() + cmd = "G2" + zStart = 0 + zFinal = 1.5 + pitch = 1 + radius = 3 + leadInOut = True + elevator = 2 + + path, start = threadmilling.generate( + center, cmd, zStart, zFinal, pitch, radius, leadInOut, elevator, None + ) + + gcode = [ + "G0 X0.000000 Y2.000000", + "G0 Z0.000000", + #'(------- lead-in -------)', + "G2 J0.500000 Y3.000000", + #'(------- lead-in -------)', + "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", + #'(------- lead-out -------)', + "G2 I0.000000 J0.500000 X0.000000 Y-2.000000", + #'(------- lead-out -------)', + ] + self.assertEqual([p.toGCode() for p in path], gcode) + self.assertCoincide(start, FreeCAD.Vector(0, -2, zFinal)) diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index 9465b2eeb6..28a58e0fb9 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -46,6 +46,7 @@ from PathTests.TestPathRotationGenerator import TestPathRotationGenerator from PathTests.TestPathSetupSheet import TestPathSetupSheet from PathTests.TestPathStock import TestPathStock from PathTests.TestPathThreadMilling import TestPathThreadMilling +from PathTests.TestPathThreadMillingGenerator import TestPathThreadMillingGenerator from PathTests.TestPathTool import TestPathTool from PathTests.TestPathToolBit import TestPathToolBit from PathTests.TestPathToolChangeGenerator import TestPathToolChangeGenerator @@ -77,6 +78,7 @@ False if TestPathRotationGenerator.__name__ else True False if TestPathSetupSheet.__name__ else True False if TestPathStock.__name__ else True False if TestPathThreadMilling.__name__ else True +False if TestPathThreadMillingGenerator.__name__ else True False if TestPathTool.__name__ else True False if TestPathToolBit.__name__ else True False if TestPathToolChangeGenerator.__name__ else True