From c36b102e87cada60a006b845a16b09d1121edb3a Mon Sep 17 00:00:00 2001 From: sliptonic Date: Tue, 9 Nov 2021 16:28:06 -0600 Subject: [PATCH 1/7] Helix Generator and Testing --- src/Mod/Path/CMakeLists.txt | 2 + src/Mod/Path/Generators/helix_generator.py | 218 ++++++++++++++++++ .../Path/PathTests/TestPathHelixGenerator.py | 37 +++ src/Mod/Path/TestPathApp.py | 14 +- 4 files changed, 264 insertions(+), 7 deletions(-) create mode 100644 src/Mod/Path/Generators/helix_generator.py create mode 100644 src/Mod/Path/PathTests/TestPathHelixGenerator.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 96156db6d1..7ba19b451e 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -148,6 +148,7 @@ SET(PathScripts_SRCS SET(Generator_SRCS Generators/drill_generator.py Generators/rotation_generator.py + Generators/helix_generator.py ) SET(PathScripts_post_SRCS @@ -231,6 +232,7 @@ SET(PathTests_SRCS PathTests/TestPathGeom.py PathTests/TestPathHelix.py PathTests/TestPathHelpers.py + PathTests/TestPathHelixGenerator.py PathTests/TestPathLog.py PathTests/TestPathOpTools.py PathTests/TestPathPost.py diff --git a/src/Mod/Path/Generators/helix_generator.py b/src/Mod/Path/Generators/helix_generator.py new file mode 100644 index 0000000000..89b3ce13bb --- /dev/null +++ b/src/Mod/Path/Generators/helix_generator.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2021 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 numpy import ceil, linspace, isclose +import Path +import PathScripts.PathLog as PathLog + +__title__ = "Helix Path Generator" +__author__ = "sliptonic (Brad Collette)" +__url__ = "https://www.freecadweb.org" +__doc__ = "Generates the helix for a single spot targetshape" +__contributors__ = "russ4262 (Russell Johnson), Lorenz Hüdepohl" + + +if True: + PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) + PathLog.trackModule(PathLog.thisModule()) +else: + PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) + + +def generate( + edge, + hole_radius, + step_down, + step_over, + tool_diameter, + inner_radius=0.0, + direction="CW", + startAt="Inside", +): + """generate(edge, hole_radius, inner_radius, step_over) ... generate helix commands. + hole_radius, inner_radius: outer and inner radius of the hole + step_over: step over radius value""" + + startPoint = edge.Vertexes[0].Point + endPoint = edge.Vertexes[1].Point + + PathLog.track( + "(helix: <{}, {}>\n hole radius {}\n inner radius {}\n step over {}\n start point {}\n end point {}\n step_down {}\n tool diameter {}\n direction {}\n startat {})".format( + startPoint.x, + startPoint.y, + hole_radius, + inner_radius, + step_over, + startPoint.z, + endPoint.z, + step_down, + tool_diameter, + direction, + startAt, + ) + ) + + if hole_radius < 0.0: + raise ValueError("hole_radius < 0") + + if not type(hole_radius) is float: + raise ValueError("hole_radius must be a float") + + if not type(inner_radius) is float: + raise ValueError("inner_radius must be a float") + + if not type(tool_diameter) is float: + raise ValueError("tool_diameter must be a float") + + if inner_radius > 0 and hole_radius - inner_radius < tool_diameter: + raise ValueError( + "hole_radius - inner_radius = {0} is < tool diameter of {1}".format( + hole_radius - inner_radius, tool_diameter + ) + ) + + if inner_radius == 0.0 and not hole_radius > tool_diameter: + raise ValueError( + "Cannot helix a hole of diameter {0} with a tool of diameter {1}".format( + 2 * hole_radius, tool_diameter + ) + ) + + elif startAt not in ["Inside", "Outside"]: + raise ValueError("Invalid value for parameter 'startAt'") + + elif direction not in ["CW", "CCW"]: + raise ValueError("Invalid value for parameter 'direction'") + + if not ( + isclose(startPoint.sub(endPoint).x, 0, rtol=1e-05, atol=1e-06) + and (isclose(startPoint.sub(endPoint).y, 0, rtol=1e-05, atol=1e-06)) + ): + raise ValueError("edge is not aligned with Z axis") + + if inner_radius > 0: + PathLog.debug("(annulus mode)\n") + hole_radius = hole_radius - tool_diameter / 2 + inner_radius = inner_radius + tool_diameter / 2 + if abs((hole_radius - inner_radius) / step_over) < 1e-5: + radii = [(hole_radius + inner_radius) / 2] + else: + nr = max(int(ceil((hole_radius - inner_radius) / step_over)), 2) + radii = linspace(hole_radius, inner_radius, nr) + + elif hole_radius <= 2 * step_over: + PathLog.debug("(single helix mode)\n") + radii = [hole_radius - tool_diameter / 2] + if radii[0] <= 0: + raise ValueError( + "Cannot helix a hole of diameter {0} with a tool of diameter {1}".format( + 2 * hole_radius, tool_diameter + ) + ) + else: + PathLog.debug("(full hole mode)\n") + hole_radius = hole_radius - tool_diameter / 2 + inner_radius = step_over / 2 + + nr = max(1 + int(ceil((hole_radius - inner_radius) / step_over)), 2) + radii = [r for r in linspace(hole_radius, inner_radius, nr) if r > 0] + if not radii: + raise ValueError( + "Cannot helix a hole of diameter {0} with a tool of diameter {1}".format( + 2 * hole_radius, tool_diameter + ) + ) + nz = max(int(ceil((startPoint.z - endPoint.z) / step_down)), 2) + zi = linspace(startPoint.z, endPoint.z, 2 * nz + 1) + + def helix_cut_r(r): + commandlist = [] + arc_cmd = "G2" if direction == "CW" else "G3" + commandlist.append( + Path.Command("G0", {"X": startPoint.x + r, "Y": startPoint.y}) + ) + commandlist.append(Path.Command("G1", {"Z": startPoint.z})) + for i in range(1, nz + 1): + commandlist.append( + Path.Command( + arc_cmd, + { + "X": startPoint.x - r, + "Y": startPoint.y, + "Z": zi[2 * i - 1], + "I": -r, + "J": 0.0, + }, + ) + ) + commandlist.append( + Path.Command( + arc_cmd, + { + "X": startPoint.x + r, + "Y": startPoint.y, + "Z": zi[2 * i], + "I": r, + "J": 0.0, + }, + ) + ) + commandlist.append( + Path.Command( + arc_cmd, + { + "X": startPoint.x - r, + "Y": startPoint.y, + "Z": endPoint.z, + "I": -r, + "J": 0.0, + }, + ) + ) + commandlist.append( + Path.Command( + arc_cmd, + { + "X": startPoint.x + r, + "Y": startPoint.y, + "Z": endPoint.z, + "I": r, + "J": 0.0, + }, + ) + ) + commandlist.append( + Path.Command( + "G0", {"X": startPoint.x, "Y": startPoint.y, "Z": startPoint.z} + ) + ) + return commandlist + + if startAt == "Inside": + radii = radii[::-1] + + commands = [] + for r in radii: + commands.extend(helix_cut_r(r)) + + return commands diff --git a/src/Mod/Path/PathTests/TestPathHelixGenerator.py b/src/Mod/Path/PathTests/TestPathHelixGenerator.py new file mode 100644 index 0000000000..d21bf0c987 --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathHelixGenerator.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2021 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 Path +import FreeCAD +import Generators.helix_generator as generator +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +import Part + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestPathHelixGenerator(PathTestUtils.PathTestBase): + def test00(self): + """Test Basic Helix Generator Return""" + self.assertTrue(True) diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index b8f7dda9a0..a1695e08be 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -28,11 +28,11 @@ from PathTests.TestPathDeburr import TestPathDeburr from PathTests.TestPathDepthParams import depthTestCases from PathTests.TestPathDressupDogbone import TestDressupDogbone from PathTests.TestPathDressupHoldingTags import TestHoldingTags -from PathTests.TestPathDrillable import TestPathDrillable from PathTests.TestPathDrillGenerator import TestPathDrillGenerator from PathTests.TestPathGeom import TestPathGeom # from PathTests.TestPathHelix import TestPathHelix from PathTests.TestPathHelpers import TestPathHelpers +from PathTests.TestPathHelixGenerator import TestPathHelixGenerator from PathTests.TestPathLog import TestPathLog from PathTests.TestPathOpTools import TestPathOpTools # from PathTests.TestPathPost import PathPostTestCases @@ -51,18 +51,17 @@ from PathTests.TestPathVcarve import TestPathVcarve from PathTests.TestPathVoronoi import TestPathVoronoi # dummy usage to get flake8 and lgtm quiet +False if depthTestCases.__name__ else True False if TestApp.__name__ else True +False if TestDressupDogbone.__name__ else True +False if TestHoldingTags.__name__ else True False if TestPathAdaptive.__name__ else True False if TestPathCore.__name__ else True False if TestPathDeburr.__name__ else True -False if depthTestCases.__name__ else True -False if TestDressupDogbone.__name__ else True -False if TestHoldingTags.__name__ else True -# False if TestPathHelix.__name__ else True False if TestPathDrillable.__name__ else True -False if TestPathDrillGenerator.__name__ else True False if TestPathGeom.__name__ else True False if TestPathHelpers.__name__ else True +# False if TestPathHelix.__name__ else True False if TestPathLog.__name__ else True False if TestPathOpTools.__name__ else True # False if TestPathPost.__name__ else True @@ -79,4 +78,5 @@ False if TestPathTooltable.__name__ else True False if TestPathUtil.__name__ else True False if TestPathVcarve.__name__ else True False if TestPathVoronoi.__name__ else True - +False if TestPathDrillGenerator.__name__ else True +False if TestPathHelixGenerator.__name__ else True From e92a0e813fa50049daed7f78d8132dbbeb276b24 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Thu, 6 Jan 2022 21:35:11 -0600 Subject: [PATCH 2/7] Path: Append unit tests for helix generator --- src/Mod/Path/Generators/helix_generator.py | 5 + .../Path/PathTests/TestPathHelixGenerator.py | 156 +++++++++++++++++- 2 files changed, 156 insertions(+), 5 deletions(-) diff --git a/src/Mod/Path/Generators/helix_generator.py b/src/Mod/Path/Generators/helix_generator.py index 89b3ce13bb..3d860da39b 100644 --- a/src/Mod/Path/Generators/helix_generator.py +++ b/src/Mod/Path/Generators/helix_generator.py @@ -56,6 +56,11 @@ def generate( startPoint = edge.Vertexes[0].Point endPoint = edge.Vertexes[1].Point + # Swap start and end points if edge line is inverted + if startPoint.z < endPoint.z: + endPoint = edge.Vertexes[0].Point + startPoint = edge.Vertexes[1].Point + PathLog.track( "(helix: <{}, {}>\n hole radius {}\n inner radius {}\n step over {}\n start point {}\n end point {}\n step_down {}\n tool diameter {}\n direction {}\n startat {})".format( startPoint.x, diff --git a/src/Mod/Path/PathTests/TestPathHelixGenerator.py b/src/Mod/Path/PathTests/TestPathHelixGenerator.py index d21bf0c987..469d101d28 100644 --- a/src/Mod/Path/PathTests/TestPathHelixGenerator.py +++ b/src/Mod/Path/PathTests/TestPathHelixGenerator.py @@ -20,18 +20,164 @@ # * * # *************************************************************************** -import Path import FreeCAD -import Generators.helix_generator as generator -import PathScripts.PathLog as PathLog -import PathTests.PathTestUtils as PathTestUtils import Part +import Path +import PathScripts.PathLog as PathLog +import Generators.helix_generator as generator +import PathTests.PathTestUtils as PathTestUtils + PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) PathLog.trackModule(PathLog.thisModule()) +def _resetArgs(): + v1 = FreeCAD.Vector(5, 5, 20) + v2 = FreeCAD.Vector(5, 5, 10) + + edg = Part.makeLine(v1, v2) + + return { + "edge": edg, + "hole_radius": 10.0, + "step_down": 1.0, + "step_over": 5.0, + "tool_diameter": 5.0, + "inner_radius": 0.0, + "direction": "CW", + "startAt": "Inside", + } + + class TestPathHelixGenerator(PathTestUtils.PathTestBase): + expectedHelixGCode = "G0 X12.500000 Y5.000000\ +G1 Z20.000000\ +G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z19.500000\ +G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z19.000000\ +G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z18.500000\ +G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z18.000000\ +G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z17.500000\ +G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z17.000000\ +G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z16.500000\ +G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z16.000000\ +G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z15.500000\ +G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z15.000000\ +G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z14.500000\ +G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z14.000000\ +G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z13.500000\ +G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z13.000000\ +G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z12.500000\ +G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z12.000000\ +G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z11.500000\ +G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z11.000000\ +G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z10.500000\ +G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z10.000000\ +G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z10.000000\ +G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z10.000000\ +G0 X5.000000 Y5.000000 Z20.000000" + def test00(self): """Test Basic Helix Generator Return""" - self.assertTrue(True) + args = _resetArgs() + result = generator.generate(**args) + self.assertTrue(type(result) is list) + self.assertTrue(type(result[0]) is Path.Command) + + gcode = "".join([r.toGCode() for r in result]) + self.assertTrue( + gcode == self.expectedHelixGCode, "Incorrect helix g-code generated" + ) + + def test01(self): + """Test Basic Helix Generator hole_radius is float > 0""" + args = _resetArgs() + args["hole_radius"] = 10 + self.assertRaises(ValueError, generator.generate, **args) + + args["hole_radius"] = -10.0 + self.assertRaises(ValueError, generator.generate, **args) + + def test02(self): + """Test Basic Helix Generator inner_radius is float""" + args = _resetArgs() + args["inner_radius"] = 2 + self.assertRaises(ValueError, generator.generate, **args) + + def test03(self): + """Test Basic Helix Generator tool_diameter is float""" + args = _resetArgs() + args["tool_diameter"] = 5 + self.assertRaises(ValueError, generator.generate, **args) + + def test04(self): + """Test Basic Helix Generator tool fit with radius difference less than tool diameter""" + args = _resetArgs() + # require tool fit 1: radius diff less than tool diam + args["hole_radius"] = 10.0 + args["inner_radius"] = 6.0 + args["tool_diameter"] = 5.0 + self.assertRaises(ValueError, generator.generate, **args) + + # require tool fit 2: hole radius less than tool diam with zero inner radius + args["hole_radius"] = 4.5 + args["inner_radius"] = 0.0 + args["tool_diameter"] = 5.0 + self.assertRaises(ValueError, generator.generate, **args) + + def test05(self): + """Test Basic Helix Generator validate the startAt enumeration value""" + args = _resetArgs() + args["startAt"] = "Other" + self.assertRaises(ValueError, generator.generate, **args) + + def test06(self): + """Test Basic Helix Generator validate the direction enumeration value""" + args = _resetArgs() + args["direction"] = "clock" + self.assertRaises(ValueError, generator.generate, **args) + + def test07(self): + """Test Basic Helix Generator verify linear edge is vertical""" + # verify linear edge is vertical: X + args = _resetArgs() + v1 = FreeCAD.Vector(5, 5, 20) + v2 = FreeCAD.Vector(5.0001, 5, 10) + edg = Part.makeLine(v1, v2) + args["edge"] = edg + self.assertRaises(ValueError, generator.generate, **args) + + # verify linear edge is vertical: Y + args = _resetArgs() + v1 = FreeCAD.Vector(5, 5.0001, 20) + v2 = FreeCAD.Vector(5, 5, 10) + edg = Part.makeLine(v1, v2) + args["edge"] = edg + self.assertRaises(ValueError, generator.generate, **args) + + def test08(self): + """Test Helix Generator with horizontal edge""" + args = _resetArgs() + v1 = FreeCAD.Vector(10, 5, 5) + v2 = FreeCAD.Vector(20, 5, 5) + edg = Part.makeLine(v1, v2) + args["edge"] = edg + self.assertRaises(ValueError, generator.generate, **args) + + def test09(self): + """Test Helix Generator with inverted vertical edge""" + args = _resetArgs() + v1 = FreeCAD.Vector(5, 5, 10) + v2 = FreeCAD.Vector(5, 5, 20) + edg = Part.makeLine(v1, v2) + args["edge"] = edg + + result = generator.generate(**args) + + self.assertTrue(type(result) is list) + self.assertTrue(type(result[0]) is Path.Command) + + gcode = "".join([r.toGCode() for r in result]) + self.assertTrue( + gcode == self.expectedHelixGCode, "Incorrect helix g-code generated" + ) From 944e74012bc01348257137441e2012b1a2490c6e Mon Sep 17 00:00:00 2001 From: sliptonic Date: Sun, 16 Jan 2022 18:42:35 -0600 Subject: [PATCH 3/7] helix generator shouldn't assume flip. shortend test case --- src/Mod/Path/Generators/helix_generator.py | 8 ++--- .../Path/PathTests/TestPathHelixGenerator.py | 33 +++---------------- 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/src/Mod/Path/Generators/helix_generator.py b/src/Mod/Path/Generators/helix_generator.py index 3d860da39b..cbbae52190 100644 --- a/src/Mod/Path/Generators/helix_generator.py +++ b/src/Mod/Path/Generators/helix_generator.py @@ -56,10 +56,6 @@ def generate( startPoint = edge.Vertexes[0].Point endPoint = edge.Vertexes[1].Point - # Swap start and end points if edge line is inverted - if startPoint.z < endPoint.z: - endPoint = edge.Vertexes[0].Point - startPoint = edge.Vertexes[1].Point PathLog.track( "(helix: <{}, {}>\n hole radius {}\n inner radius {}\n step over {}\n start point {}\n end point {}\n step_down {}\n tool diameter {}\n direction {}\n startat {})".format( @@ -115,6 +111,10 @@ def generate( ): raise ValueError("edge is not aligned with Z axis") + if startPoint.z < endPoint.z: + raise ValueError("start point is below end point") + + if inner_radius > 0: PathLog.debug("(annulus mode)\n") hole_radius = hole_radius - tool_diameter / 2 diff --git a/src/Mod/Path/PathTests/TestPathHelixGenerator.py b/src/Mod/Path/PathTests/TestPathHelixGenerator.py index 469d101d28..0f804b76a8 100644 --- a/src/Mod/Path/PathTests/TestPathHelixGenerator.py +++ b/src/Mod/Path/PathTests/TestPathHelixGenerator.py @@ -34,7 +34,7 @@ PathLog.trackModule(PathLog.thisModule()) def _resetArgs(): v1 = FreeCAD.Vector(5, 5, 20) - v2 = FreeCAD.Vector(5, 5, 10) + v2 = FreeCAD.Vector(5, 5, 18) edg = Part.makeLine(v1, v2) @@ -57,24 +57,8 @@ G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z19.500000\ G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z19.000000\ G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z18.500000\ G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z18.000000\ -G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z17.500000\ -G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z17.000000\ -G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z16.500000\ -G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z16.000000\ -G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z15.500000\ -G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z15.000000\ -G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z14.500000\ -G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z14.000000\ -G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z13.500000\ -G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z13.000000\ -G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z12.500000\ -G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z12.000000\ -G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z11.500000\ -G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z11.000000\ -G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z10.500000\ -G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z10.000000\ -G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z10.000000\ -G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z10.000000\ +G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z18.000000\ +G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z18.000000\ G0 X5.000000 Y5.000000 Z20.000000" def test00(self): @@ -167,17 +151,10 @@ G0 X5.000000 Y5.000000 Z20.000000" def test09(self): """Test Helix Generator with inverted vertical edge""" args = _resetArgs() - v1 = FreeCAD.Vector(5, 5, 10) + v1 = FreeCAD.Vector(5, 5, 18) v2 = FreeCAD.Vector(5, 5, 20) edg = Part.makeLine(v1, v2) args["edge"] = edg - result = generator.generate(**args) + self.assertRaises(ValueError, generator.generate, **args) - self.assertTrue(type(result) is list) - self.assertTrue(type(result[0]) is Path.Command) - - gcode = "".join([r.toGCode() for r in result]) - self.assertTrue( - gcode == self.expectedHelixGCode, "Incorrect helix g-code generated" - ) From 606e613e287e1f45cb1baceaa1064fe713492292 Mon Sep 17 00:00:00 2001 From: sliptonic Date: Sun, 16 Jan 2022 18:46:47 -0600 Subject: [PATCH 4/7] Added another test to drill generator and corresponding test case --- src/Mod/Path/Generators/drill_generator.py | 3 +++ src/Mod/Path/PathTests/TestPathDrillGenerator.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/src/Mod/Path/Generators/drill_generator.py b/src/Mod/Path/Generators/drill_generator.py index 84adf233a6..550b678a67 100644 --- a/src/Mod/Path/Generators/drill_generator.py +++ b/src/Mod/Path/Generators/drill_generator.py @@ -67,6 +67,9 @@ def generate(edge, dwelltime=0.0, peckdepth=0.0, repeat=1): ): raise ValueError("edge is not aligned with Z axis") + if startPoint.z < endPoint.z: + raise ValueError("start point is below end point") + cmdParams = {} cmdParams["X"] = startPoint.x cmdParams["Y"] = startPoint.y diff --git a/src/Mod/Path/PathTests/TestPathDrillGenerator.py b/src/Mod/Path/PathTests/TestPathDrillGenerator.py index 8414c5d2e6..78837b1965 100644 --- a/src/Mod/Path/PathTests/TestPathDrillGenerator.py +++ b/src/Mod/Path/PathTests/TestPathDrillGenerator.py @@ -64,7 +64,11 @@ class TestPathDrillGenerator(PathTestUtils.PathTestBase): """Test edge alignment check""" v1 = FreeCAD.Vector(0, 10, 10) v2 = FreeCAD.Vector(0, 0, 0) + e = Part.makeLine(v1, v2) + self.assertRaises(ValueError, generator.generate, e) + v1 = FreeCAD.Vector(0, 0, 0) + v2 = FreeCAD.Vector(0, 0, 10) e = Part.makeLine(v1, v2) self.assertRaises(ValueError, generator.generate, e) From a6a062a20c0bec86308af7eaab15bd09815c08f2 Mon Sep 17 00:00:00 2001 From: sliptonic Date: Mon, 17 Jan 2022 12:16:33 -0600 Subject: [PATCH 5/7] cleanup for readability. More forgiving of input. Move to center if possible before retract --- src/Mod/Path/Generators/helix_generator.py | 35 +++++++++++-------- .../Path/PathTests/TestPathHelixGenerator.py | 9 +++-- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/Mod/Path/Generators/helix_generator.py b/src/Mod/Path/Generators/helix_generator.py index cbbae52190..e3535a67a2 100644 --- a/src/Mod/Path/Generators/helix_generator.py +++ b/src/Mod/Path/Generators/helix_generator.py @@ -32,7 +32,7 @@ __doc__ = "Generates the helix for a single spot targetshape" __contributors__ = "russ4262 (Russell Johnson), Lorenz Hüdepohl" -if True: +if False: PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) PathLog.trackModule(PathLog.thisModule()) else: @@ -56,7 +56,6 @@ def generate( startPoint = edge.Vertexes[0].Point endPoint = edge.Vertexes[1].Point - PathLog.track( "(helix: <{}, {}>\n hole radius {}\n inner radius {}\n step over {}\n start point {}\n end point {}\n step_down {}\n tool diameter {}\n direction {}\n startat {})".format( startPoint.x, @@ -73,18 +72,18 @@ def generate( ) ) - if hole_radius < 0.0: - raise ValueError("hole_radius < 0") - - if not type(hole_radius) is float: + if not type(hole_radius) in [float, int]: raise ValueError("hole_radius must be a float") - if not type(inner_radius) is float: + if not type(inner_radius) in [float, int]: raise ValueError("inner_radius must be a float") - if not type(tool_diameter) is float: + if not type(tool_diameter) in [float, int]: raise ValueError("tool_diameter must be a float") + if hole_radius < 0.0: + raise ValueError("hole_radius < 0") + if inner_radius > 0 and hole_radius - inner_radius < tool_diameter: raise ValueError( "hole_radius - inner_radius = {0} is < tool diameter of {1}".format( @@ -114,7 +113,6 @@ def generate( if startPoint.z < endPoint.z: raise ValueError("start point is below end point") - if inner_radius > 0: PathLog.debug("(annulus mode)\n") hole_radius = hole_radius - tool_diameter / 2 @@ -147,8 +145,11 @@ def generate( 2 * hole_radius, tool_diameter ) ) - nz = max(int(ceil((startPoint.z - endPoint.z) / step_down)), 2) - zi = linspace(startPoint.z, endPoint.z, 2 * nz + 1) + # calculate the number of full and partial turns required + # Each full turn is two 180 degree arcs. Zsteps is equally spaced step + # down values + turncount = max(int(ceil((startPoint.z - endPoint.z) / step_down)), 2) + zsteps = linspace(startPoint.z, endPoint.z, 2 * turncount + 1) def helix_cut_r(r): commandlist = [] @@ -157,14 +158,14 @@ def generate( Path.Command("G0", {"X": startPoint.x + r, "Y": startPoint.y}) ) commandlist.append(Path.Command("G1", {"Z": startPoint.z})) - for i in range(1, nz + 1): + for i in range(1, turncount + 1): commandlist.append( Path.Command( arc_cmd, { "X": startPoint.x - r, "Y": startPoint.y, - "Z": zi[2 * i - 1], + "Z": zsteps[2 * i - 1], "I": -r, "J": 0.0, }, @@ -176,7 +177,7 @@ def generate( { "X": startPoint.x + r, "Y": startPoint.y, - "Z": zi[2 * i], + "Z": zsteps[2 * i], "I": r, "J": 0.0, }, @@ -206,6 +207,12 @@ def generate( }, ) ) + if hole_radius <= tool_diameter: + # no plug remains, safe to move to center for retract + commandlist.append( + Path.Command("G0", {"X": endPoint.x, "Y": endPoint.y, "Z": endPoint.z}) + ) + commandlist.append(Path.Command("G0", {"Z": startPoint.z})) commandlist.append( Path.Command( "G0", {"X": startPoint.x, "Y": startPoint.y, "Z": startPoint.z} diff --git a/src/Mod/Path/PathTests/TestPathHelixGenerator.py b/src/Mod/Path/PathTests/TestPathHelixGenerator.py index 0f804b76a8..27b44117b6 100644 --- a/src/Mod/Path/PathTests/TestPathHelixGenerator.py +++ b/src/Mod/Path/PathTests/TestPathHelixGenerator.py @@ -59,8 +59,10 @@ G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z18.500000\ G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z18.000000\ G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z18.000000\ G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z18.000000\ +G0 Z20.000000\ G0 X5.000000 Y5.000000 Z20.000000" + def test00(self): """Test Basic Helix Generator Return""" args = _resetArgs() @@ -69,6 +71,7 @@ G0 X5.000000 Y5.000000 Z20.000000" self.assertTrue(type(result[0]) is Path.Command) gcode = "".join([r.toGCode() for r in result]) + print(gcode) self.assertTrue( gcode == self.expectedHelixGCode, "Incorrect helix g-code generated" ) @@ -76,7 +79,7 @@ G0 X5.000000 Y5.000000 Z20.000000" def test01(self): """Test Basic Helix Generator hole_radius is float > 0""" args = _resetArgs() - args["hole_radius"] = 10 + args["hole_radius"] = '10' self.assertRaises(ValueError, generator.generate, **args) args["hole_radius"] = -10.0 @@ -85,13 +88,13 @@ G0 X5.000000 Y5.000000 Z20.000000" def test02(self): """Test Basic Helix Generator inner_radius is float""" args = _resetArgs() - args["inner_radius"] = 2 + args["inner_radius"] = '2' self.assertRaises(ValueError, generator.generate, **args) def test03(self): """Test Basic Helix Generator tool_diameter is float""" args = _resetArgs() - args["tool_diameter"] = 5 + args["tool_diameter"] = '5' self.assertRaises(ValueError, generator.generate, **args) def test04(self): From edefffae4bbc49c1dcd93c3a4b5e928211ee8fee Mon Sep 17 00:00:00 2001 From: sliptonic Date: Tue, 18 Jan 2022 17:02:33 -0600 Subject: [PATCH 6/7] basic retraction handling --- src/Mod/Path/Generators/helix_generator.py | 77 ++++++++++++------- .../Path/PathTests/TestPathHelixGenerator.py | 28 ++++++- 2 files changed, 72 insertions(+), 33 deletions(-) diff --git a/src/Mod/Path/Generators/helix_generator.py b/src/Mod/Path/Generators/helix_generator.py index e3535a67a2..2d4f2cf29e 100644 --- a/src/Mod/Path/Generators/helix_generator.py +++ b/src/Mod/Path/Generators/helix_generator.py @@ -57,7 +57,7 @@ def generate( endPoint = edge.Vertexes[1].Point PathLog.track( - "(helix: <{}, {}>\n hole radius {}\n inner radius {}\n step over {}\n start point {}\n end point {}\n step_down {}\n tool diameter {}\n direction {}\n startat {})".format( + "(helix: <{}, {}>\n hole radius {}\n inner radius {}\n step over {}\n start point {}\n end point {}\n step_down {}\n tool diameter {}\n direction {}\n startAt {})".format( startPoint.x, startPoint.y, hole_radius, @@ -72,18 +72,18 @@ def generate( ) ) - if not type(hole_radius) in [float, int]: + if type(hole_radius) not in [float, int]: raise ValueError("hole_radius must be a float") - if not type(inner_radius) in [float, int]: - raise ValueError("inner_radius must be a float") - - if not type(tool_diameter) in [float, int]: - raise ValueError("tool_diameter must be a float") - if hole_radius < 0.0: raise ValueError("hole_radius < 0") + if type(inner_radius) not in [float, int]: + raise ValueError("inner_radius must be a float") + + if type(tool_diameter) not in [float, int]: + raise ValueError("tool_diameter must be a float") + if inner_radius > 0 and hole_radius - inner_radius < tool_diameter: raise ValueError( "hole_radius - inner_radius = {0} is < tool diameter of {1}".format( @@ -91,7 +91,7 @@ def generate( ) ) - if inner_radius == 0.0 and not hole_radius > tool_diameter: + if not hole_radius * 2 > tool_diameter: raise ValueError( "Cannot helix a hole of diameter {0} with a tool of diameter {1}".format( 2 * hole_radius, tool_diameter @@ -115,13 +115,13 @@ def generate( if inner_radius > 0: PathLog.debug("(annulus mode)\n") - hole_radius = hole_radius - tool_diameter / 2 - inner_radius = inner_radius + tool_diameter / 2 - if abs((hole_radius - inner_radius) / step_over) < 1e-5: - radii = [(hole_radius + inner_radius) / 2] + outer_radius = hole_radius - tool_diameter / 2 + step_radius = inner_radius + tool_diameter / 2 + if abs((outer_radius - step_radius) / step_over) < 1e-5: + radii = [(outer_radius + step_radius) / 2] else: - nr = max(int(ceil((hole_radius - inner_radius) / step_over)), 2) - radii = linspace(hole_radius, inner_radius, nr) + nr = max(int(ceil((outer_radius - step_radius) / step_over)), 2) + radii = linspace(outer_radius, step_radius, nr) elif hole_radius <= 2 * step_over: PathLog.debug("(single helix mode)\n") @@ -132,13 +132,14 @@ def generate( 2 * hole_radius, tool_diameter ) ) + outer_radius = hole_radius else: PathLog.debug("(full hole mode)\n") - hole_radius = hole_radius - tool_diameter / 2 - inner_radius = step_over / 2 + outer_radius = hole_radius - tool_diameter / 2 + step_radius = step_over / 2 - nr = max(1 + int(ceil((hole_radius - inner_radius) / step_over)), 2) - radii = [r for r in linspace(hole_radius, inner_radius, nr) if r > 0] + nr = max(1 + int(ceil((outer_radius - step_radius) / step_over)), 2) + radii = [r for r in linspace(outer_radius, step_radius, nr) if r > 0] if not radii: raise ValueError( "Cannot helix a hole of diameter {0} with a tool of diameter {1}".format( @@ -207,18 +208,35 @@ def generate( }, ) ) - if hole_radius <= tool_diameter: - # no plug remains, safe to move to center for retract - commandlist.append( + return commandlist + + def retract(): + # try to move to a safe place to retract without leaving a dwell + # mark + retractcommands = [] + # Calculate retraction + if hole_radius <= tool_diameter: # simple case where center is clear + center_clear = True + + elif startAt == "Inside" and inner_radius == 0.0: # middle is clear + center_clear = True + else: + center_clear = False + + if center_clear: + retractcommands.append( Path.Command("G0", {"X": endPoint.x, "Y": endPoint.y, "Z": endPoint.z}) ) - commandlist.append(Path.Command("G0", {"Z": startPoint.z})) - commandlist.append( - Path.Command( - "G0", {"X": startPoint.x, "Y": startPoint.y, "Z": startPoint.z} - ) - ) - return commandlist + + # Technical Debt. + # If the operation is clearing multiple passes in annulus mode (inner + # radius > 0.0 and len(radii) > 1) then there is a derivable + # safe place which does not touch the inner or outer wall on all radii except + # the first. This is left as a future improvement. + + retractcommands.append(Path.Command("G0", {"Z": startPoint.z})) + + return retractcommands if startAt == "Inside": radii = radii[::-1] @@ -226,5 +244,6 @@ def generate( commands = [] for r in radii: commands.extend(helix_cut_r(r)) + commands.extend(retract()) return commands diff --git a/src/Mod/Path/PathTests/TestPathHelixGenerator.py b/src/Mod/Path/PathTests/TestPathHelixGenerator.py index 27b44117b6..ea2b477823 100644 --- a/src/Mod/Path/PathTests/TestPathHelixGenerator.py +++ b/src/Mod/Path/PathTests/TestPathHelixGenerator.py @@ -59,8 +59,8 @@ G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z18.500000\ G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z18.000000\ G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z18.000000\ G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z18.000000\ -G0 Z20.000000\ -G0 X5.000000 Y5.000000 Z20.000000" +G0 X5.000000 Y5.000000 Z18.000000\ +G0 Z20.000000" def test00(self): @@ -106,8 +106,8 @@ G0 X5.000000 Y5.000000 Z20.000000" args["tool_diameter"] = 5.0 self.assertRaises(ValueError, generator.generate, **args) - # require tool fit 2: hole radius less than tool diam with zero inner radius - args["hole_radius"] = 4.5 + # require tool fit 2: hole diameter not greater than tool diam with zero inner radius + args["hole_radius"] = 2.0 args["inner_radius"] = 0.0 args["tool_diameter"] = 5.0 self.assertRaises(ValueError, generator.generate, **args) @@ -161,3 +161,23 @@ G0 X5.000000 Y5.000000 Z20.000000" self.assertRaises(ValueError, generator.generate, **args) + def test10(self): + """Test Helix Retraction""" + + # if center is clear, the second to last move should be a rapid away + # from the wall + args = _resetArgs() + v1 = FreeCAD.Vector(0, 0, 20) + v2 = FreeCAD.Vector(0, 0, 18) + edg = Part.makeLine(v1, v2) + args["edge"] = edg + args["inner_radius"] = 0.0 + args["tool_diameter"] = 5.0 + result = generator.generate(**args) + self.assertTrue(result[-2].Name == "G0") + + # if center is not clear, retraction is one straight up on the last + # move. the second to last move should be a G2 + args["inner_radius"] = 2.0 + result = generator.generate(**args) + self.assertTrue(result[-2].Name == "G2") From 97b9ee8c204f5f155e0eefbf0b5e13cd03254e2d Mon Sep 17 00:00:00 2001 From: sliptonic Date: Wed, 19 Jan 2022 13:50:54 -0600 Subject: [PATCH 7/7] fix missing test import --- src/Mod/Path/TestPathApp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index a1695e08be..797f7fc410 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -28,6 +28,7 @@ from PathTests.TestPathDeburr import TestPathDeburr from PathTests.TestPathDepthParams import depthTestCases from PathTests.TestPathDressupDogbone import TestDressupDogbone from PathTests.TestPathDressupHoldingTags import TestHoldingTags +from PathTests.TestPathDrillable import TestPathDrillable from PathTests.TestPathDrillGenerator import TestPathDrillGenerator from PathTests.TestPathGeom import TestPathGeom # from PathTests.TestPathHelix import TestPathHelix