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