diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index be114c7e10..c6899e5e54 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -142,6 +142,9 @@ SET(PathScripts_SRCS PathScripts/PathWaterline.py PathScripts/PathWaterlineGui.py PathScripts/PostUtils.py + PathScripts/PostUtilsArguments.py + PathScripts/PostUtilsExport.py + PathScripts/PostUtilsParse.py PathScripts/__init__.py ) @@ -174,6 +177,11 @@ SET(PathScripts_post_SRCS PathScripts/post/opensbp_post.py PathScripts/post/opensbp_pre.py PathScripts/post/philips_post.py + PathScripts/post/refactored_centroid_post.py + PathScripts/post/refactored_grbl_post.py + PathScripts/post/refactored_linuxcnc_post.py + PathScripts/post/refactored_mach3_mach4_post.py + PathScripts/post/refactored_test_post.py PathScripts/post/rml_post.py PathScripts/post/rrf_post.py PathScripts/post/slic3r_pre.py @@ -221,14 +229,19 @@ SET(Tools_Shape_SRCS SET(PathTests_SRCS PathTests/__init__.py PathTests/boxtest.fcstd + PathTests/boxtest1.fcstd PathTests/Drilling_1.FCStd + PathTests/drill_test1.FCStd PathTests/PathTestUtils.py PathTests/test_adaptive.fcstd PathTests/test_centroid_00.ngc PathTests/test_filenaming.fcstd PathTests/test_geomop.fcstd PathTests/test_holes00.fcstd - PathTests/test_linuxcnc_00.ngc + PathTests/TestCentroidPost.py + PathTests/TestGrblPost.py + PathTests/TestLinuxCNCPost.py + PathTests/TestMach3Mach4Post.py PathTests/TestPathAdaptive.py PathTests/TestPathCore.py PathTests/TestPathDeburr.py @@ -259,6 +272,11 @@ SET(PathTests_SRCS PathTests/TestPathUtil.py PathTests/TestPathVcarve.py PathTests/TestPathVoronoi.py + PathTests/TestRefactoredCentroidPost.py + PathTests/TestRefactoredGrblPost.py + PathTests/TestRefactoredLinuxCNCPost.py + PathTests/TestRefactoredMach3Mach4Post.py + PathTests/TestRefactoredTestPost.py PathTests/Tools/Bit/test-path-tool-bit-bit-00.fctb PathTests/Tools/Library/test-path-tool-bit-library-00.fctl PathTests/Tools/Shape/test-path-tool-bit-shape-00.fcstd @@ -413,4 +431,3 @@ INSTALL( DESTINATION Mod/Path/Data/Threads ) - diff --git a/src/Mod/Path/PathScripts/PathPost.py b/src/Mod/Path/PathScripts/PathPost.py index 9062f31ebb..c8fa55d740 100644 --- a/src/Mod/Path/PathScripts/PathPost.py +++ b/src/Mod/Path/PathScripts/PathPost.py @@ -59,51 +59,15 @@ class _TempObject: Label = "Fixture" -def resolveFileName(job, subpartname, sequencenumber): - PathLog.track(subpartname, sequencenumber) - - validPathSubstitutions = ["D", "d", "M", "j"] - validFilenameSubstitutions = ["j", "d", "T", "t", "W", "O", "S"] - - # Look for preference default - outputpath, filename = os.path.split(PathPreferences.defaultOutputFile()) - filename, ext = os.path.splitext(filename) - - # Override with document default if it exists - if job.PostProcessorOutputFile: - matchstring = job.PostProcessorOutputFile - candidateOutputPath, candidateFilename = os.path.split(matchstring) - - if candidateOutputPath: - outputpath = candidateOutputPath - - if candidateFilename: - filename, ext = os.path.splitext(candidateFilename) - - # Strip any invalid substitutions from the ouputpath - for match in re.findall("%(.)", outputpath): - if match not in validPathSubstitutions: - outputpath = outputpath.replace(f"%{match}", "") - - # if nothing else, use current directory - if not outputpath: - outputpath = "." - - # Strip any invalid substitutions from the filename - for match in re.findall("%(.)", filename): - if match not in validFilenameSubstitutions: - filename = filename.replace(f"%{match}", "") - - # if no filename, use the active document label - if not filename: - filename = FreeCAD.ActiveDocument.Label - - # if no extension, use something sensible - if not ext: - ext = ".nc" - - # By now we should have a sanitized path, filename and extension to work with - PathLog.track(f"path: {outputpath} name: {filename} ext: {ext}") +def processFileNameSubstitutions( + job, + subpartname, + sequencenumber, + outputpath, + filename, + ext, +): + """Process any substitutions in the outputpath or filename.""" # The following section allows substitution within the path part PathLog.track(f"path before substitution: {outputpath}") @@ -116,9 +80,7 @@ def resolveFileName(job, subpartname, sequencenumber): if not D: D = "." else: - FreeCAD.Console.PrintError( - "Please save document in order to resolve output path!\n" - ) + FreeCAD.Console.PrintError("Please save document in order to resolve output path!\n") return None outputpath = outputpath.replace("%D", D) @@ -171,9 +133,7 @@ def resolveFileName(job, subpartname, sequencenumber): if "%O" in filename and job.OrderOutputBy == "Operation": filename = filename.replace("%O", subpartname) - if ( - "%S" in filename - ): # We always add a sequence number but the user can say where + if "%S" in filename: # We always add a sequence number but the user can say where filename = filename.replace("%S", str(sequencenumber)) else: filename = f"{filename}-{sequencenumber}" @@ -187,11 +147,68 @@ def resolveFileName(job, subpartname, sequencenumber): fullPath = f"{outputpath}{os.path.sep}{filename}{ext}" PathLog.track(f"full filepath: {fullPath}") + return fullPath + + +def resolveFileName(job, subpartname, sequencenumber): + PathLog.track(subpartname, sequencenumber) + + validPathSubstitutions = ["D", "d", "M", "j"] + validFilenameSubstitutions = ["j", "d", "T", "t", "W", "O", "S"] + + # Look for preference default + outputpath, filename = os.path.split(PathPreferences.defaultOutputFile()) + filename, ext = os.path.splitext(filename) + + # Override with document default if it exists + if job.PostProcessorOutputFile: + matchstring = job.PostProcessorOutputFile + candidateOutputPath, candidateFilename = os.path.split(matchstring) + + if candidateOutputPath: + outputpath = candidateOutputPath + + if candidateFilename: + filename, ext = os.path.splitext(candidateFilename) + + # Strip any invalid substitutions from the ouputpath + for match in re.findall("%(.)", outputpath): + if match not in validPathSubstitutions: + outputpath = outputpath.replace(f"%{match}", "") + + # if nothing else, use current directory + if not outputpath: + outputpath = "." + + # Strip any invalid substitutions from the filename + for match in re.findall("%(.)", filename): + if match not in validFilenameSubstitutions: + filename = filename.replace(f"%{match}", "") + + # if no filename, use the active document label + if not filename: + filename = FreeCAD.ActiveDocument.Label + + # if no extension, use something sensible + if not ext: + ext = ".nc" + + # By now we should have a sanitized path, filename and extension to work with + PathLog.track(f"path: {outputpath} name: {filename} ext: {ext}") + + fullPath = processFileNameSubstitutions( + job, + subpartname, + sequencenumber, + outputpath, + filename, + ext, + ) # This section determines whether user interaction is necessary policy = PathPreferences.defaultOutputPolicy() - openDialog = policy == "Open File Dialog" + openDialog = (policy == "Open File Dialog") # if os.path.isdir(filename) or not os.path.isdir(os.path.dirname(filename)): # # Either the entire filename resolves into a directory or the parent directory doesn't exist. # # Either way I don't know what to do - ask for help @@ -256,8 +273,7 @@ def buildPostList(job): c2 = Path.Command( "G0 Z" + str( - job.Stock.Shape.BoundBox.ZMax - + job.SetupSheet.ClearanceHeightOffset.Value + job.Stock.Shape.BoundBox.ZMax + job.SetupSheet.ClearanceHeightOffset.Value ) ) fobj.Path.addCommands(c2) @@ -292,10 +308,7 @@ def buildPostList(job): c1 = Path.Command(f) c2 = Path.Command( "G0 Z" - + str( - job.Stock.Shape.BoundBox.ZMax - + job.SetupSheet.ClearanceHeightOffset.Value - ) + + str(job.Stock.Shape.BoundBox.ZMax + job.SetupSheet.ClearanceHeightOffset.Value) ) fobj.Path = Path.Path([c1, c2]) fobj.InList.append(job) @@ -395,9 +408,7 @@ def buildPostList(job): return postlist else: PathLog.track() - finalpostlist = [ - ("allitems", [item for slist in postlist for item in slist[1]]) - ] + finalpostlist = [("allitems", [item for slist in postlist for item in slist[1]])] return finalpostlist @@ -407,9 +418,7 @@ class DlgSelectPostProcessor: firstItem = None for post in PathPreferences.allEnabledPostProcessors(): item = QtGui.QListWidgetItem(post) - item.setFlags( - QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled - ) + item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled) self.dialog.lwPostProcessor.addItem(item) if not firstItem: firstItem = item @@ -518,9 +527,7 @@ class CommandPathPost: selected = FreeCADGui.Selection.getSelectionEx() if len(selected) > 1: - FreeCAD.Console.PrintError( - "Please select a single job or other path object\n" - ) + FreeCAD.Console.PrintError("Please select a single job or other path object\n") return elif len(selected) == 1: sel = selected[0].Object diff --git a/src/Mod/Path/PathScripts/PathPostProcessor.py b/src/Mod/Path/PathScripts/PathPostProcessor.py index c692032ba1..85857c2394 100644 --- a/src/Mod/Path/PathScripts/PathPostProcessor.py +++ b/src/Mod/Path/PathScripts/PathPostProcessor.py @@ -69,23 +69,6 @@ class PostProcessor: else: instance.units = "Inch" - if hasattr(current_post, "MACHINE_NAME"): - instance.machineName = current_post.MACHINE_NAME - - if hasattr(current_post, "CORNER_MAX"): - instance.cornerMax = { - "x": current_post.CORNER_MAX["x"], - "y": current_post.CORNER_MAX["y"], - "z": current_post.CORNER_MAX["z"], - } - - if hasattr(current_post, "CORNER_MIN"): - instance.cornerMin = { - "x": current_post.CORNER_MIN["x"], - "y": current_post.CORNER_MIN["y"], - "z": current_post.CORNER_MIN["z"], - } - if hasattr(current_post, "TOOLTIP"): instance.tooltip = current_post.TOOLTIP if hasattr(current_post, "TOOLTIP_ARGS"): @@ -96,8 +79,6 @@ class PostProcessor: self.script = script self.tooltip = None self.tooltipArgs = None - self.cornerMax = None - self.cornerMin = None self.units = None self.machineName = None diff --git a/src/Mod/Path/PathScripts/PostUtils.py b/src/Mod/Path/PathScripts/PostUtils.py index c6a4aa36bd..6494dbf386 100644 --- a/src/Mod/Path/PathScripts/PostUtils.py +++ b/src/Mod/Path/PathScripts/PostUtils.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2014 Yorik van Havre * +# * Copyright (c) 2022 Larry Woestman * # * * # * This file is part of the FreeCAD CAx development system. * # * * @@ -23,23 +24,24 @@ # *************************************************************************** """ -These are a common functions and classes for creating custom post processors. +These are common functions and classes for creating custom post processors. """ from PySide import QtCore, QtGui + import FreeCAD -from PathMachineState import MachineState + import Path import Part + +from PathMachineState import MachineState from PathScripts.PathGeom import CmdMoveArc, edgeForCmd, cmdsForEdge translate = FreeCAD.Qt.translate - FreeCADGui = None if FreeCAD.GuiUp: import FreeCADGui - class GCodeHighlighter(QtGui.QSyntaxHighlighter): def __init__(self, parent=None): super(GCodeHighlighter, self).__init__(parent) @@ -147,7 +149,7 @@ def stringsplit(commandline): def fmt(num, dec, units): - """used to format axis moves, feedrate, etc for decimal places and units""" + """Use to format axis moves, feedrate, etc for decimal places and units.""" if units == "G21": # metric fnum = "%.*f" % (dec, num) else: # inch @@ -156,8 +158,7 @@ def fmt(num, dec, units): def editor(gcode): - """pops up a handy little editor to look at the code output""" - + """Pops up a handy little editor to look at the code output.""" prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Path") # default Max Highlighter Size = 512 Ko defaultMHS = 512 * 1024 @@ -176,9 +177,7 @@ def editor(gcode): FreeCAD.Console.PrintMessage( translate( "Path", - "GCode size too big ({} o), disabling syntax highlighter.".format( - gcodeSize - ), + "GCode size too big ({} o), disabling syntax highlighter.".format(gcodeSize), ) ) result = dia.exec_() @@ -190,7 +189,7 @@ def editor(gcode): def fcoms(string, commentsym): - """filter and rebuild comments with user preferred comment symbol""" + """Filter and rebuild comments with user preferred comment symbol.""" if len(commentsym) == 1: s1 = string.replace("(", commentsym) comment = s1.replace(")", "") @@ -200,8 +199,10 @@ def fcoms(string, commentsym): def splitArcs(path): - """filters a path object and replaces at G2/G3 moves with discrete G1 - returns a Path object""" + """Filter a path object and replace all G2/G3 moves with discrete G1 moves. + + Returns a Path object. + """ prefGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Path") deflection = prefGrp.GetFloat("LibAreaCurveAccuarcy", 0.01) diff --git a/src/Mod/Path/PathScripts/PostUtilsArguments.py b/src/Mod/Path/PathScripts/PostUtilsArguments.py new file mode 100644 index 0000000000..6b7b309782 --- /dev/null +++ b/src/Mod/Path/PathScripts/PostUtilsArguments.py @@ -0,0 +1,642 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2014 Yorik van Havre * +# * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2015 Dan Falck * +# * Copyright (c) 2018, 2019 Gauthier Briere * +# * Copyright (c) 2019, 2020 Schildkroet * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +""" +These are functions related to arguments and values for creating custom post processors. +""" +import argparse +import os +import shlex + + +def add_flag_type_arguments( + argument_group, default_flag, true_argument, false_argument, true_help, false_help, visible=True +): + if visible: + if default_flag: + true_help += " (default)" + else: + false_help += " (default)" + else: + true_help = false_help = argparse.SUPPRESS + argument_group.add_argument(true_argument, action="store_true", help=true_help) + argument_group.add_argument(false_argument, action="store_true", help=false_help) + + +def init_argument_defaults(argument_defaults): + """Initialize which argument to show as the default in flag-type arguments""" + argument_defaults["axis-modal"] = False + argument_defaults["bcnc"] = False + argument_defaults["comments"] = True + argument_defaults["header"] = True + argument_defaults["line-numbers"] = False + argument_defaults["metric_inches"] = True + argument_defaults["modal"] = False + argument_defaults["show-editor"] = True + argument_defaults["tlo"] = True + argument_defaults["tool_change"] = True + argument_defaults["translate_drill"] = False + + +def init_arguments_visible(arguments_visible): + """Initialize the flags for which arguments are visible in the arguments tooltip.""" + arguments_visible["bcnc"] = False + arguments_visible["axis-modal"] = True + arguments_visible["axis-precision"] = True + arguments_visible["comments"] = True + arguments_visible["feed-precision"] = True + arguments_visible["header"] = True + arguments_visible["line-numbers"] = True + arguments_visible["metric_inches"] = True + arguments_visible["modal"] = True + arguments_visible["postamble"] = True + arguments_visible["preamble"] = True + arguments_visible["precision"] = True + arguments_visible["return-to"] = False + arguments_visible["show-editor"] = True + arguments_visible["tlo"] = True + arguments_visible["tool_change"] = False + arguments_visible["translate_drill"] = False + arguments_visible["wait-for-spindle"] = False + + +def init_shared_arguments(values, argument_defaults, arguments_visible): + """Initialize the shared arguments for postprocessors.""" + parser = argparse.ArgumentParser( + prog=values["MACHINE_NAME"], usage=argparse.SUPPRESS, add_help=False + ) + shared = parser.add_argument_group("Arguments that are shared with all postprocessors") + add_flag_type_arguments( + shared, + argument_defaults["metric_inches"], + "--metric", + "--inches", + "Convert output for Metric mode (G21)", + "Convert output for US imperial mode (G20)", + arguments_visible["metric_inches"], + ) + add_flag_type_arguments( + shared, + argument_defaults["axis-modal"], + "--axis-modal", + "--no-axis-modal", + "Don't output axis values if they are the same as the previous line", + "Output axis values even if they are the same as the previous line", + arguments_visible["axis-modal"], + ) + if arguments_visible["axis-precision"]: + help_message = ( + "Number of digits of precision for axis moves, default is " + + str(values["DEFAULT_AXIS_PRECISION"]) + ) + else: + help_message = argparse.SUPPRESS + shared.add_argument( + "--axis-precision", + default=-1, + type=int, + help=help_message, + ) + add_flag_type_arguments( + shared, + argument_defaults["bcnc"], + "--bcnc", + "--no-bcnc", + "Add Job operations as bCNC block headers. Consider suppressing comments by adding --no-comments", + "Suppress bCNC block header output", + arguments_visible["bcnc"], + ) + add_flag_type_arguments( + shared, + argument_defaults["comments"], + "--comments", + "--no-comments", + "Output comments", + "Suppress comment output", + arguments_visible["comments"], + ) + if arguments_visible["feed-precision"]: + help_message = ( + "Number of digits of precision for feed rate, default is " + + str(values["DEFAULT_FEED_PRECISION"]) + ) + else: + help_message = argparse.SUPPRESS + shared.add_argument( + "--feed-precision", + default=-1, + type=int, + help=help_message, + ) + add_flag_type_arguments( + shared, + argument_defaults["header"], + "--header", + "--no-header", + "Output headers", + "Suppress header output", + arguments_visible["header"], + ) + add_flag_type_arguments( + shared, + argument_defaults["line-numbers"], + "--line-numbers", + "--no-line-numbers", + "Prefix with line numbers", + "Don't prefix with line numbers", + arguments_visible["line-numbers"], + ) + add_flag_type_arguments( + shared, + argument_defaults["modal"], + "--modal", + "--no-modal", + "Don't output the G-command name if it is the same as the previous line", + "Output the G-command name even if it is the same as the previous line", + arguments_visible["modal"], + ) + if arguments_visible["postamble"]: + help_message = ( + 'Set commands to be issued after the last command, default is "' + + values["POSTAMBLE"] + + '"' + ) + else: + help_message = argparse.SUPPRESS + shared.add_argument("--postamble", help=help_message) + if arguments_visible["preamble"]: + help_message = ( + 'Set commands to be issued before the first command, default is "' + + values["PREAMBLE"] + + '"' + ) + else: + help_message = argparse.SUPPRESS + shared.add_argument("--preamble", help=help_message) + # The --precision argument is included for backwards compatibility with some postprocessors. + # If both --axis-precision and --precision are provided, the --axis-precision value "wins". + # If both --feed-precision and --precision are provided, the --feed-precision value "wins". + if arguments_visible["precision"]: + help_message = ( + "Number of digits of precision for both feed rate and axis moves, default is " + + str(values["DEFAULT_AXIS_PRECISION"]) + + " for metric or " + + str(values["DEFAULT_INCH_AXIS_PRECISION"]) + + " for inches" + ) + else: + help_message = argparse.SUPPRESS + shared.add_argument( + "--precision", + default=-1, + type=int, + help=help_message, + ) + if arguments_visible["return-to"]: + help_message = "Move to the specified x,y,z coordinates at the end, e.g. --return-to=0,0,0 (default is do not move)" + else: + help_message = argparse.SUPPRESS + shared.add_argument("--return-to", default="", help=help_message) + add_flag_type_arguments( + shared, + argument_defaults["show-editor"], + "--show-editor", + "--no-show-editor", + "Pop up editor before writing output", + "Don't pop up editor before writing output", + arguments_visible["show-editor"], + ) + add_flag_type_arguments( + shared, + argument_defaults["tlo"], + "--tlo", + "--no-tlo", + "Output tool length offset (G43) following tool changes", + "Suppress tool length offset (G43) following tool changes", + arguments_visible["tlo"], + ) + add_flag_type_arguments( + shared, + argument_defaults["tool_change"], + "--tool_change", + "--no-tool_change", + "Insert M6 and any other tool change G-code for all tool changes", + "Convert M6 to a comment for all tool changes", + arguments_visible["tool_change"], + ) + add_flag_type_arguments( + shared, + argument_defaults["translate_drill"], + "--translate_drill", + "--no-translate_drill", + "Translate drill cycles G81, G82 & G83 into G0/G1 movements", + "Don't translate drill cycles G81, G82 & G83 into G0/G1 movements", + arguments_visible["translate_drill"], + ) + if arguments_visible["wait-for-spindle"]: + help_message = "Time to wait (in seconds) after M3, M4 (default = 0.0)" + else: + help_message = argparse.SUPPRESS + shared.add_argument("--wait-for-spindle", type=float, default=0.0, help=help_message) + return parser + + +def init_shared_values(values): + """Initialize the default values in postprocessors.""" + # + # The starting axis precision is 3 digits after the decimal point. + # + values["AXIS_PRECISION"] = 3 + # + # If this is set to "", all spaces are removed from between commands and parameters. + # + values["COMMAND_SPACE"] = " " + # + # The character that indicates a comment. While "(" is the most common, + # ";" is also used. + # + values["COMMENT_SYMBOL"] = "(" + # + # Variables storing the current position for the drill_translate routine. + # + values["CURRENT_X"] = 0 + values["CURRENT_Y"] = 0 + values["CURRENT_Z"] = 0 + # + # Default axis precision for metric is 3 digits after the decimal point. + # (see http://linuxcnc.org/docs/2.7/html/gcode/overview.html#_g_code_best_practices) + # + values["DEFAULT_AXIS_PRECISION"] = 3 + # + # The default precision for feed is also set to 3 for metric. + # + values["DEFAULT_FEED_PRECISION"] = 3 + # + # Default axis precision for inch/imperial is 4 digits after the decimal point. + # + values["DEFAULT_INCH_AXIS_PRECISION"] = 4 + # + # The default precision for feed is also set to 4 for inch/imperial. + # + values["DEFAULT_INCH_FEED_PRECISION"] = 4 + # + # If TRANSLATE_DRILL_CYCLES is True, these are the drill cycles + # that get translated to G0 and G1 commands. + # + values["DRILL_CYCLES_TO_TRANSLATE"] = ("G81", "G82", "G83") + # + # The default value of drill retractations (CURRENT_Z). + # The other possible value is G99. + # + values["DRILL_RETRACT_MODE"] = "G98" + # + # If this is set to True, then M7, M8, and M9 commands + # to enable/disable coolant will be output. + # + values["ENABLE_COOLANT"] = False + # + # If this is set to True, then commands that are placed in + # comments that look like (MC_RUN_COMMAND: blah) will be output. + # + values["ENABLE_MACHINE_SPECIFIC_COMMANDS"] = False + # + # By default the line ending characters of the output file(s) + # are written to match the system that the postprocessor runs on. + # If you need to force the line ending characters to a specific + # value, set this variable to "\n" or "\r\n" instead. + # + values["END_OF_LINE_CHARACTERS"] = os.linesep + # + # The starting precision for feed is also set to 3 digits after the decimal point. + # + values["FEED_PRECISION"] = 3 + # + # This value shows up in the post_op comment as "Finish operation:". + # At least one postprocessor changes it to "End" to produce "End operation:". + # + values["FINISH_LABEL"] = "Finish" + # + # The name of the machine the postprocessor is for + # + values["MACHINE_NAME"] = "unknown machine" + # + # The line number increment value + # + values["LINE_INCREMENT"] = 10 + # + # The line number starting value + # + values["line_number"] = 100 + # + # If this value is True, then a list of tool numbers + # with their labels are output just before the preamble. + # + values["LIST_TOOLS_IN_PREAMBLE"] = False + # + # If this value is true G-code commands are suppressed if they are + # the same as the previous line. + # + values["MODAL"] = False + # + # This defines the motion commands that might change the X, Y, and Z position. + # + values["MOTION_COMMANDS"] = [ + "G0", + "G00", + "G1", + "G01", + "G2", + "G02", + "G3", + "G03", + ] + # + # Keeps track of the motion mode currently in use. + # G90 for absolute moves, G91 for relative + # + values["MOTION_MODE"] = "G90" + # + # If True enables special processing for operations with "Adaptive" in the name + # + values["OUTPUT_ADAPTIVE"] = False + # + # If True adds bCNC operation block headers to the output G-code file. + # + values["OUTPUT_BCNC"] = False + # + # If True output comments. If False comments are suppressed. + # + values["OUTPUT_COMMENTS"] = True + # + # if False duplicate axis values are suppressed if they are the same as the previous line. + # + values["OUTPUT_DOUBLES"] = True + # + # If True output the machine name in the pre_op + # + values["OUTPUT_MACHINE_NAME"] = False + # + # If True output a header at the front of the G-code file. + # The header contains comments similar to: + # (Exported by FreeCAD) + # (Post Processor: centroid_post) + # (Cam File: box.fcstd) + # (Output Time:2020-01-01 01:02:03.123456) + # + values["OUTPUT_HEADER"] = True + # + # If True output line numbers at the front of each line. + # If False do not output line numbers. + # + values["OUTPUT_LINE_NUMBERS"] = False + # + # If True output Path labels at the beginning of each Path. + # + values["OUTPUT_PATH_LABELS"] = False + # + # If True output tool change G-code for M6 commands followed + # by any commands in the "TOOL_CHANGE" value. + # If False output the M6 command as a comment and do not output + # any commands in the "TOOL_CHANGE" value. + # + values["OUTPUT_TOOL_CHANGE"] = True + # + # This list controls the order of parameters in a line during output. + # + values["PARAMETER_ORDER"] = [ + "X", + "Y", + "Z", + "A", + "B", + "C", + "U", + "V", + "W", + "I", + "J", + "K", + "F", + "S", + "T", + "Q", + "R", + "L", + "P", + ] + # + # Any commands in this value will be output as the last commands + # in the G-code file. + # + values["POSTAMBLE"] = """""" + # + # Any commands in this value will be output after the operation(s). + # + values["POST_OPERATION"] = """""" + # + # Any commands in this value will be output after the header and + # safety block at the beginning of the G-code file. + # + values["PREAMBLE"] = """""" + # + # Any commands in this value will be output before the operation(s). + # + values["PRE_OPERATION"] = """""" + # + # Defines which G-code commands are considered "rapid" moves. + # + values["RAPID_MOVES"] = ["G0", "G00"] + # + # If True suppress any messages. + # + values["REMOVE_MESSAGES"] = True + # + # Any commands in this value are output after the operation(s) + # and post_operation commands are output but before the + # TOOLRETURN, SAFETYBLOCK, and POSTAMBLE. + # + values["RETURN_TO"] = None + # + # Any commands in this value are output after the header but before the preamble, + # then again after the TOOLRETURN but before the POSTAMBLE. + # + values["SAFETYBLOCK"] = """""" + # + # If True then the G-code editor widget is shown before writing + # the G-code to the file. + # + values["SHOW_EDITOR"] = True + # + # If True then the current machine units are output just before the PRE_OPERATION. + # + values["SHOW_MACHINE_UNITS"] = True + # + # If True then the current operation label is output just before the PRE_OPERATION. + # + values["SHOW_OPERATION_LABELS"] = True + # + # The number of decimal places to use when outputting the speed (S) parameter. + # + values["SPINDLE_DECIMALS"] = 0 + # + # The amount of time (in seconds) to wait after turning on the spindle + # using an M3 or M4 command (a floating point number). + # + values["SPINDLE_WAIT"] = 0.0 + # + # If true then then an M5 command to stop the spindle is output + # after the M6 tool change command and before the TOOL_CHANGE commands. + # + values["STOP_SPINDLE_FOR_TOOL_CHANGE"] = True + # + # These commands are ignored by commenting them out. + # Used when replacing the drill commands by G0 and G1 commands, for example. + # + values["SUPPRESS_COMMANDS"] = [] + # + # Any commands in this value are output after the M6 command + # when changing at tool (if OUTPUT_TOOL_CHANGE is True). + # + values["TOOL_CHANGE"] = """""" + # + # Any commands in this value are output after the POST_OPERATION, + # RETURN_TO, and OUTPUT_BCNC and before the SAFETYBLOCK and POSTAMBLE. + # + values["TOOLRETURN"] = """""" + # + # If true, G81, G82 & G83 drill moves are translated into G0/G1 moves. + # + values["TRANSLATE_DRILL_CYCLES"] = False + # + # These values keep track of whether we are in Metric mode (G21) + # or inches/imperial mode (G20). + # + values["UNITS"] = "G21" + values["UNIT_FORMAT"] = "mm" + values["UNIT_SPEED_FORMAT"] = "mm/min" + # + # If true a tool length command (G43) will be output following tool changes. + # + values["USE_TLO"] = True + + +def process_shared_arguments(values, parser, argstring): + """Process the arguments to the postprocessor.""" + try: + args = parser.parse_args(shlex.split(argstring)) + if args.metric: + values["UNITS"] = "G21" + if args.inches: + values["UNITS"] = "G20" + if values["UNITS"] == "G21": + values["UNIT_FORMAT"] = "mm" + values["UNIT_SPEED_FORMAT"] = "mm/min" + if values["UNITS"] == "G20": + values["UNIT_FORMAT"] = "in" + values["UNIT_SPEED_FORMAT"] = "in/min" + # The precision-related arguments need to be processed + # after the metric/inches arguments are processed. + # If both --axis-precision and --precision are given, + # the --axis-precision argument "wins". + if args.axis_precision != -1: + values["AXIS_PRECISION"] = args.axis_precision + elif args.precision != -1: + values["AXIS_PRECISION"] = args.precision + else: + if values["UNITS"] == "G21": + values["AXIS_PRECISION"] = values["DEFAULT_AXIS_PRECISION"] + if values["UNITS"] == "G20": + values["AXIS_PRECISION"] = values["DEFAULT_INCH_AXIS_PRECISION"] + # If both --feed-precision and --precision are given, + # the --feed-precision argument "wins". + if args.feed_precision != -1: + values["FEED_PRECISION"] = args.feed_precision + elif args.precision != -1: + values["FEED_PRECISION"] = args.precision + else: + if values["UNITS"] == "G21": + values["FEED_PRECISION"] = values["DEFAULT_FEED_PRECISION"] + if values["UNITS"] == "G20": + values["FEED_PRECISION"] = values["DEFAULT_INCH_FEED_PRECISION"] + if args.axis_modal: + values["OUTPUT_DOUBLES"] = False + if args.no_axis_modal: + values["OUTPUT_DOUBLES"] = True + if args.bcnc: + values["OUTPUT_BCNC"] = True + if args.no_bcnc: + values["OUTPUT_BCNC"] = False + if args.comments: + values["OUTPUT_COMMENTS"] = True + if args.no_comments: + values["OUTPUT_COMMENTS"] = False + if args.header: + values["OUTPUT_HEADER"] = True + if args.no_header: + values["OUTPUT_HEADER"] = False + if args.line_numbers: + values["OUTPUT_LINE_NUMBERS"] = True + if args.no_line_numbers: + values["OUTPUT_LINE_NUMBERS"] = False + if args.modal: + values["MODAL"] = True + if args.no_modal: + values["MODAL"] = False + if args.postamble is not None: + values["POSTAMBLE"] = args.postamble + if args.preamble is not None: + values["PREAMBLE"] = args.preamble + if args.return_to != "": + values["RETURN_TO"] = [int(v) for v in args.return_to.split(",")] + if len(values["RETURN_TO"]) != 3: + values["RETURN_TO"] = None + print("--return-to coordinates must be specified as ,,, ignoring") + if args.show_editor: + values["SHOW_EDITOR"] = True + if args.no_show_editor: + values["SHOW_EDITOR"] = False + if args.tlo: + values["USE_TLO"] = True + if args.no_tlo: + values["USE_TLO"] = False + if args.tool_change: + values["OUTPUT_TOOL_CHANGE"] = True + if args.no_tool_change: + values["OUTPUT_TOOL_CHANGE"] = False + if args.translate_drill: + values["TRANSLATE_DRILL_CYCLES"] = True + if args.no_translate_drill: + values["TRANSLATE_DRILL_CYCLES"] = False + if args.wait_for_spindle > 0.0: + values["SPINDLE_WAIT"] = args.wait_for_spindle + + except Exception: + return (False, None) + + return (True, args) diff --git a/src/Mod/Path/PathScripts/PostUtilsExport.py b/src/Mod/Path/PathScripts/PostUtilsExport.py new file mode 100644 index 0000000000..e12ac31437 --- /dev/null +++ b/src/Mod/Path/PathScripts/PostUtilsExport.py @@ -0,0 +1,282 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2014 Yorik van Havre * +# * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2015 Dan Falck * +# * Copyright (c) 2018, 2019 Gauthier Briere * +# * Copyright (c) 2019, 2020 Schildkroet * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import datetime +import os + +import FreeCAD + +from PathScripts import PathToolController +from PathScripts import PostUtils +from PathScripts import PostUtilsParse + + +# to distinguish python built-in open function from the one declared below +if open.__module__ in ["__builtin__", "io"]: + pythonopen = open + +# +# This routine processes things in the following order: +# +# OUTPUT_HEADER +# SAFETYBLOCK +# LIST_TOOLS_IN_PREAMBLE +# PREAMBLE +# OUTPUT_BCNC +# SHOW_OPERATION_LABELS +# SHOW_MACHINE_UNITS +# PRE_OPERATION +# ENABLE_COOLANT (coolant on) +# operation(s) +# POST_OPERATION +# ENABLE_COOLANT (coolant off) +# RETURN_TO +# OUTPUT_BCNC +# TOOLRETURN +# SAFETYBLOCK +# POSTAMBLE +# SHOW_EDITOR +# +# The names in all caps may be enabled/disabled/modified by setting +# the corresponding value in the postprocessor. +# + + +def export_common(values, objectslist, filename): + """Do the common parts of postprocessing the objects in objectslist to filename.""" + # + for obj in objectslist: + if not hasattr(obj, "Path"): + print( + "The object " + obj.Name + " is not a path. Please select only path and Compounds." + ) + return None + + # for obj in objectslist: + # print(obj.Name) + + print("PostProcessor: " + values["POSTPROCESSOR_FILE_NAME"] + " postprocessing...") + gcode = "" + + # write header + if values["OUTPUT_HEADER"]: + comment = PostUtilsParse.create_comment("Exported by FreeCAD", values["COMMENT_SYMBOL"]) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + comment = PostUtilsParse.create_comment( + "Post Processor: " + values["POSTPROCESSOR_FILE_NAME"], + values["COMMENT_SYMBOL"], + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + if FreeCAD.ActiveDocument: + cam_file = os.path.basename(FreeCAD.ActiveDocument.FileName) + else: + cam_file = "" + comment = PostUtilsParse.create_comment("Cam File: " + cam_file, values["COMMENT_SYMBOL"]) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + comment = PostUtilsParse.create_comment( + "Output Time: " + str(datetime.datetime.now()), values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + + # Check canned cycles for drilling + if values["TRANSLATE_DRILL_CYCLES"]: + if len(values["SUPPRESS_COMMANDS"]) == 0: + values["SUPPRESS_COMMANDS"] = ["G99", "G98", "G80"] + else: + values["SUPPRESS_COMMANDS"] += ["G99", "G98", "G80"] + + for line in values["SAFETYBLOCK"].splitlines(False): + gcode += PostUtilsParse.linenumber(values) + line + "\n" + + # Write the preamble + if values["OUTPUT_COMMENTS"]: + if values["LIST_TOOLS_IN_PREAMBLE"]: + for item in objectslist: + if hasattr(item, "Proxy") and isinstance( + item.Proxy, PathToolController.ToolController + ): + comment = PostUtilsParse.create_comment( + "T{}={}".format(item.ToolNumber, item.Name), values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + comment = PostUtilsParse.create_comment("Begin preamble", values["COMMENT_SYMBOL"]) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + for line in values["PREAMBLE"].splitlines(False): + gcode += PostUtilsParse.linenumber(values) + line + "\n" + # verify if PREAMBLE or SAFETYBLOCK have changed MOTION_MODE or UNITS + if "G90" in values["PREAMBLE"] or "G90" in values["SAFETYBLOCK"]: + values["MOTION_MODE"] = "G90" + elif "G91" in values["PREAMBLE"] or "G91" in values["SAFETYBLOCK"]: + values["MOTION_MODE"] = "G91" + else: + gcode += PostUtilsParse.linenumber(values) + values["MOTION_MODE"] + "\n" + if "G21" in values["PREAMBLE"] or "G21" in values["SAFETYBLOCK"]: + values["UNITS"] = "G21" + values["UNIT_FORMAT"] = "mm" + values["UNIT_SPEED_FORMAT"] = "mm/min" + elif "G20" in values["PREAMBLE"] or "G20" in values["SAFETYBLOCK"]: + values["UNITS"] = "G20" + values["UNIT_FORMAT"] = "in" + values["UNIT_SPEED_FORMAT"] = "in/min" + else: + gcode += PostUtilsParse.linenumber(values) + values["UNITS"] + "\n" + + for obj in objectslist: + + # Debug... + # print("\n" + "*"*70) + # dump(obj) + # print("*"*70 + "\n") + + # Skip inactive operations + if hasattr(obj, "Active"): + if not obj.Active: + continue + if hasattr(obj, "Base") and hasattr(obj.Base, "Active"): + if not obj.Base.Active: + continue + + # do the pre_op + if values["OUTPUT_BCNC"]: + comment = PostUtilsParse.create_comment( + "Block-name: " + obj.Label, values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + comment = PostUtilsParse.create_comment("Block-expand: 0", values["COMMENT_SYMBOL"]) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + comment = PostUtilsParse.create_comment("Block-enable: 1", values["COMMENT_SYMBOL"]) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + if values["OUTPUT_COMMENTS"]: + if values["SHOW_OPERATION_LABELS"]: + comment = PostUtilsParse.create_comment( + "Begin operation: %s" % obj.Label, values["COMMENT_SYMBOL"] + ) + else: + comment = PostUtilsParse.create_comment("Begin operation", values["COMMENT_SYMBOL"]) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + if values["SHOW_MACHINE_UNITS"]: + comment = PostUtilsParse.create_comment( + "Machine units: %s" % values["UNIT_SPEED_FORMAT"], values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + if values["OUTPUT_MACHINE_NAME"]: + comment = PostUtilsParse.create_comment( + "Machine: %s, %s" % (values["MACHINE_NAME"], values["UNIT_SPEED_FORMAT"]), + values["COMMENT_SYMBOL"], + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + for line in values["PRE_OPERATION"].splitlines(False): + gcode += PostUtilsParse.linenumber(values) + line + "\n" + + # get coolant mode + coolantMode = "None" + if hasattr(obj, "CoolantMode") or hasattr(obj, "Base") and hasattr(obj.Base, "CoolantMode"): + if hasattr(obj, "CoolantMode"): + coolantMode = obj.CoolantMode + else: + coolantMode = obj.Base.CoolantMode + + # turn coolant on if required + if values["ENABLE_COOLANT"]: + if values["OUTPUT_COMMENTS"]: + if not coolantMode == "None": + comment = PostUtilsParse.create_comment( + "Coolant On:" + coolantMode, values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + if coolantMode == "Flood": + gcode += PostUtilsParse.linenumber(values) + "M8" + "\n" + if coolantMode == "Mist": + gcode += PostUtilsParse.linenumber(values) + "M7" + "\n" + + # process the operation gcode + gcode += PostUtilsParse.parse(values, obj) + + # do the post_op + if values["OUTPUT_COMMENTS"]: + comment = PostUtilsParse.create_comment( + "%s operation: %s" % (values["FINISH_LABEL"], obj.Label), + values["COMMENT_SYMBOL"], + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + for line in values["POST_OPERATION"].splitlines(False): + gcode += PostUtilsParse.linenumber(values) + line + "\n" + + # turn coolant off if required + if values["ENABLE_COOLANT"]: + if not coolantMode == "None": + if values["OUTPUT_COMMENTS"]: + comment = PostUtilsParse.create_comment( + "Coolant Off:" + coolantMode, values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + gcode += PostUtilsParse.linenumber(values) + "M9" + "\n" + + if values["RETURN_TO"]: + gcode += PostUtilsParse.linenumber(values) + "G0 X%s Y%s Z%s\n" % tuple(values["RETURN_TO"]) + + # do the post_amble + if values["OUTPUT_BCNC"]: + comment = PostUtilsParse.create_comment("Block-name: post_amble", values["COMMENT_SYMBOL"]) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + comment = PostUtilsParse.create_comment("Block-expand: 0", values["COMMENT_SYMBOL"]) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + comment = PostUtilsParse.create_comment("Block-enable: 1", values["COMMENT_SYMBOL"]) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + if values["OUTPUT_COMMENTS"]: + comment = PostUtilsParse.create_comment("Begin postamble", values["COMMENT_SYMBOL"]) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + for line in values["TOOLRETURN"].splitlines(False): + gcode += PostUtilsParse.linenumber(values) + line + "\n" + for line in values["SAFETYBLOCK"].splitlines(False): + gcode += PostUtilsParse.linenumber(values) + line + "\n" + for line in values["POSTAMBLE"].splitlines(False): + gcode += PostUtilsParse.linenumber(values) + line + "\n" + + if FreeCAD.GuiUp and values["SHOW_EDITOR"]: + final = gcode + if len(gcode) > 100000: + print("Skipping editor since output is greater than 100kb") + else: + dia = PostUtils.GCodeEditorDialog() + dia.editor.setText(gcode) + result = dia.exec_() + if result: + final = dia.editor.toPlainText() + else: + final = gcode + + print("done postprocessing.") + + if not filename == "-": + gfile = pythonopen(filename, "w", newline=values["END_OF_LINE_CHARACTERS"]) + gfile.write(final) + gfile.close() + + return final diff --git a/src/Mod/Path/PathScripts/PostUtilsParse.py b/src/Mod/Path/PathScripts/PostUtilsParse.py new file mode 100644 index 0000000000..c74896bfe6 --- /dev/null +++ b/src/Mod/Path/PathScripts/PostUtilsParse.py @@ -0,0 +1,511 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2014 Yorik van Havre * +# * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2015 Dan Falck * +# * Copyright (c) 2018, 2019 Gauthier Briere * +# * Copyright (c) 2019, 2020 Schildkroet * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import re + +import FreeCAD +from FreeCAD import Units + +import Path + +from PathScripts import PostUtils + + +def create_comment(comment_string, comment_symbol): + """Create a comment from a string using the correct comment symbol.""" + if comment_symbol == "(": + comment_string = "(" + comment_string + ")" + else: + comment_string = comment_symbol + comment_string + return comment_string + + +def drill_translate(values, outstring, cmd, params): + """Translate drill cycles.""" + axis_precision_string = "." + str(values["AXIS_PRECISION"]) + "f" + feed_precision_string = "." + str(values["FEED_PRECISION"]) + "f" + + trBuff = "" + + if values["OUTPUT_COMMENTS"]: # Comment the original command + trBuff += ( + linenumber(values) + + create_comment( + values["COMMAND_SPACE"] + + format_outstring(values, outstring) + + values["COMMAND_SPACE"], + values["COMMENT_SYMBOL"], + ) + + "\n" + ) + + # cycle conversion + # currently only cycles in XY are provided (G17) + # other plains ZX (G18) and YZ (G19) are not dealt with : Z drilling only. + drill_X = Units.Quantity(params["X"], FreeCAD.Units.Length) + drill_Y = Units.Quantity(params["Y"], FreeCAD.Units.Length) + drill_Z = Units.Quantity(params["Z"], FreeCAD.Units.Length) + RETRACT_Z = Units.Quantity(params["R"], FreeCAD.Units.Length) + # R less than Z is error + if RETRACT_Z < drill_Z: + trBuff += ( + linenumber(values) + + create_comment("Drill cycle error: R less than Z", values["COMMENT_SYMBOL"]) + + "\n" + ) + return trBuff + + if values["MOTION_MODE"] == "G91": # G91 relative movements + drill_X += values["CURRENT_X"] + drill_Y += values["CURRENT_Y"] + drill_Z += values["CURRENT_Z"] + RETRACT_Z += values["CURRENT_Z"] + + if values["DRILL_RETRACT_MODE"] == "G98" and values["CURRENT_Z"] >= RETRACT_Z: + RETRACT_Z = values["CURRENT_Z"] + + # get the other parameters + drill_feedrate = Units.Quantity(params["F"], FreeCAD.Units.Velocity) + if cmd == "G83": + drill_Step = Units.Quantity(params["Q"], FreeCAD.Units.Length) + a_bit = ( + drill_Step * 0.05 + ) # NIST 3.5.16.4 G83 Cycle: "current hole bottom, backed off a bit." + elif cmd == "G82": + drill_DwellTime = params["P"] + + # wrap this block to ensure machine's values["MOTION_MODE"] is restored in case of error + try: + if values["MOTION_MODE"] == "G91": + trBuff += linenumber(values) + "G90\n" # force absolute coordinates during cycles + + strG0_RETRACT_Z = ( + "G0 Z" + + format(float(RETRACT_Z.getValueAs(values["UNIT_FORMAT"])), axis_precision_string) + + "\n" + ) + strF_Feedrate = ( + " F" + + format( + float(drill_feedrate.getValueAs(values["UNIT_SPEED_FORMAT"])), feed_precision_string + ) + + "\n" + ) + # print(strF_Feedrate) + + # preliminary movement(s) + if values["CURRENT_Z"] < RETRACT_Z: + trBuff += linenumber(values) + strG0_RETRACT_Z + trBuff += ( + linenumber(values) + + "G0 X" + + format(float(drill_X.getValueAs(values["UNIT_FORMAT"])), axis_precision_string) + + " Y" + + format(float(drill_Y.getValueAs(values["UNIT_FORMAT"])), axis_precision_string) + + "\n" + ) + if values["CURRENT_Z"] > RETRACT_Z: + # NIST GCODE 3.5.16.1 Preliminary and In-Between Motion says G0 to RETRACT_Z + # Here use G1 since retract height may be below surface ! + trBuff += ( + linenumber(values) + + "G1 Z" + + format(float(RETRACT_Z.getValueAs(values["UNIT_FORMAT"])), axis_precision_string) + + strF_Feedrate + ) + last_Stop_Z = RETRACT_Z + + # drill moves + if cmd in ("G81", "G82"): + trBuff += ( + linenumber(values) + + "G1 Z" + + format(float(drill_Z.getValueAs(values["UNIT_FORMAT"])), axis_precision_string) + + strF_Feedrate + ) + # pause where applicable + if cmd == "G82": + trBuff += linenumber(values) + "G4 P" + str(drill_DwellTime) + "\n" + trBuff += linenumber(values) + strG0_RETRACT_Z + else: # 'G83' + if params["Q"] != 0: + while 1: + if last_Stop_Z != RETRACT_Z: + clearance_depth = ( + last_Stop_Z + a_bit + ) # rapid move to just short of last drilling depth + trBuff += ( + linenumber(values) + + "G0 Z" + + format( + float(clearance_depth.getValueAs(values["UNIT_FORMAT"])), + axis_precision_string, + ) + + "\n" + ) + next_Stop_Z = last_Stop_Z - drill_Step + if next_Stop_Z > drill_Z: + trBuff += ( + linenumber(values) + + "G1 Z" + + format( + float(next_Stop_Z.getValueAs(values["UNIT_FORMAT"])), + axis_precision_string, + ) + + strF_Feedrate + ) + trBuff += linenumber(values) + strG0_RETRACT_Z + last_Stop_Z = next_Stop_Z + else: + trBuff += ( + linenumber(values) + + "G1 Z" + + format( + float(drill_Z.getValueAs(values["UNIT_FORMAT"])), + axis_precision_string, + ) + + strF_Feedrate + ) + trBuff += linenumber(values) + strG0_RETRACT_Z + break + + except Exception: + pass + + if values["MOTION_MODE"] == "G91": + trBuff += linenumber(values) + "G91\n" # Restore if changed + + return trBuff + + +def dump(obj): + """For debug...""" + for attr in dir(obj): + print("obj.%s = %s" % (attr, getattr(obj, attr))) + + +def format_outstring(values, strTable): + """Construct the line for the final output.""" + s = "" + for w in strTable: + s += w + values["COMMAND_SPACE"] + s = s.strip() + return s + + +def linenumber(values, space=None): + """Output the next line number if appropriate.""" + if values["OUTPUT_LINE_NUMBERS"]: + if space is None: + space = values["COMMAND_SPACE"] + line_num = str(values["line_number"]) + values["line_number"] += values["LINE_INCREMENT"] + return "N" + line_num + space + return "" + + +def parse(values, pathobj): + """Parse a Path.""" + out = "" + lastcommand = None + axis_precision_string = "." + str(values["AXIS_PRECISION"]) + "f" + feed_precision_string = "." + str(values["FEED_PRECISION"]) + "f" + + currLocation = {} # keep track for no doubles + firstmove = Path.Command("G0", {"X": -1, "Y": -1, "Z": -1, "F": 0.0}) + currLocation.update(firstmove.Parameters) # set First location Parameters + + if hasattr(pathobj, "Group"): # We have a compound or project. + if values["OUTPUT_COMMENTS"]: + comment = create_comment("Compound: " + pathobj.Label, values["COMMENT_SYMBOL"]) + out += linenumber(values) + comment + "\n" + for p in pathobj.Group: + out += parse(values, p) + return out + else: # parsing simple path + + # groups might contain non-path things like stock. + if not hasattr(pathobj, "Path"): + return out + + if values["OUTPUT_PATH_LABELS"] and values["OUTPUT_COMMENTS"]: + comment = create_comment("Path: " + pathobj.Label, values["COMMENT_SYMBOL"]) + out += linenumber(values) + comment + "\n" + + if values["OUTPUT_ADAPTIVE"]: + adaptiveOp = False + opHorizRapid = 0 + opVertRapid = 0 + if "Adaptive" in pathobj.Name: + adaptiveOp = True + if hasattr(pathobj, "ToolController"): + if ( + hasattr(pathobj.ToolController, "HorizRapid") + and pathobj.ToolController.HorizRapid > 0 + ): + opHorizRapid = Units.Quantity( + pathobj.ToolController.HorizRapid, FreeCAD.Units.Velocity + ) + else: + FreeCAD.Console.PrintWarning( + "Tool Controller Horizontal Rapid Values are unset" + "\n" + ) + if ( + hasattr(pathobj.ToolController, "VertRapid") + and pathobj.ToolController.VertRapid > 0 + ): + opVertRapid = Units.Quantity( + pathobj.ToolController.VertRapid, FreeCAD.Units.Velocity + ) + else: + FreeCAD.Console.PrintWarning( + "Tool Controller Vertical Rapid Values are unset" + "\n" + ) + + for c in pathobj.Path.Commands: + + # List of elements in the command, code, and params. + outstring = [] + # command M or G code or comment string + command = c.Name + if command[0] == "(": + if values["OUTPUT_COMMENTS"]: + if values["COMMENT_SYMBOL"] != "(": + command = PostUtils.fcoms(command, values["COMMENT_SYMBOL"]) + else: + continue + if values["OUTPUT_ADAPTIVE"]: + if adaptiveOp and command in values["RAPID_MOVES"]: + if opHorizRapid and opVertRapid: + command = "G1" + else: + outstring.append("(Tool Controller Rapid Values are unset)" + "\n") + + outstring.append(command) + + # if modal: suppress the command if it is the same as the last one + if values["MODAL"]: + if command == lastcommand: + outstring.pop(0) + + # Now add the remaining parameters in order + for param in values["PARAMETER_ORDER"]: + if param in c.Parameters: + if param == "F" and ( + currLocation[param] != c.Parameters[param] or values["OUTPUT_DOUBLES"] + ): + # centroid and linuxcnc don't use rapid speeds + if command not in values["RAPID_MOVES"]: + speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity) + if speed.getValueAs(values["UNIT_SPEED_FORMAT"]) > 0.0: + outstring.append( + param + + format( + float(speed.getValueAs(values["UNIT_SPEED_FORMAT"])), + feed_precision_string, + ) + ) + else: + continue + elif param in ["H", "L", "T"]: + outstring.append(param + str(int(c.Parameters[param]))) + elif param == "D": + if command in ["G41", "G42"]: + outstring.append(param + str(int(c.Parameters[param]))) + elif command in ["G96", "G97"]: + outstring.append( + param + + PostUtils.fmt( + c.Parameters[param], values["SPINDLE_DECIMALS"], "G21" + ) + ) + else: # anything else that is supported (G41.1?, G42.1?) + outstring.append(param + str(float(c.Parameters[param]))) + elif param == "P": + if command in ["G2", "G02", "G3", "G03", "G5.2", "G5.3", "G10"]: + outstring.append(param + str(int(c.Parameters[param]))) + elif command in ["G4", "G04", "G64", "G76", "G82", "G86", "G89"]: + outstring.append(param + str(float(c.Parameters[param]))) + elif command in ["G5", "G05"]: + pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + outstring.append( + param + + format( + float(pos.getValueAs(values["UNIT_FORMAT"])), + axis_precision_string, + ) + ) + else: # anything else that is supported + outstring.append(param + str(c.Parameters[param])) + elif param == "S": + outstring.append( + param + + PostUtils.fmt(c.Parameters[param], values["SPINDLE_DECIMALS"], "G21") + ) + else: + if ( + (not values["OUTPUT_DOUBLES"]) + and (param in currLocation) + and (currLocation[param] == c.Parameters[param]) + ): + continue + else: + pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + outstring.append( + param + + format( + float(pos.getValueAs(values["UNIT_FORMAT"])), + axis_precision_string, + ) + ) + + if values["OUTPUT_ADAPTIVE"]: + if adaptiveOp and command in values["RAPID_MOVES"]: + if opHorizRapid and opVertRapid: + if "Z" not in c.Parameters: + outstring.append( + "F" + + format( + float(opHorizRapid.getValueAs(values["UNIT_SPEED_FORMAT"])), + axis_precision_string, + ) + ) + else: + outstring.append( + "F" + + format( + float(opVertRapid.getValueAs(values["UNIT_SPEED_FORMAT"])), + axis_precision_string, + ) + ) + + # store the latest command + lastcommand = command + + currLocation.update(c.Parameters) + # Memorizes the current position for calculating the related movements + # and the withdrawal plan + if command in values["MOTION_COMMANDS"]: + if "X" in c.Parameters: + values["CURRENT_X"] = Units.Quantity(c.Parameters["X"], FreeCAD.Units.Length) + if "Y" in c.Parameters: + values["CURRENT_Y"] = Units.Quantity(c.Parameters["Y"], FreeCAD.Units.Length) + if "Z" in c.Parameters: + values["CURRENT_Z"] = Units.Quantity(c.Parameters["Z"], FreeCAD.Units.Length) + + if command in ("G98", "G99"): + values["DRILL_RETRACT_MODE"] = command + + if command in ("G90", "G91"): + values["MOTION_MODE"] = command + + if values["TRANSLATE_DRILL_CYCLES"]: + if command in values["DRILL_CYCLES_TO_TRANSLATE"]: + out += drill_translate(values, outstring, command, c.Parameters) + # Erase the line we just translated + outstring = [] + + if values["SPINDLE_WAIT"] > 0: + if command in ("M3", "M03", "M4", "M04"): + out += linenumber(values) + format_outstring(values, outstring) + "\n" + out += ( + linenumber(values) + + format_outstring(values, ["G4", "P%s" % values["SPINDLE_WAIT"]]) + + "\n" + ) + outstring = [] + + # Check for Tool Change: + if command in ("M6", "M06"): + if values["OUTPUT_COMMENTS"]: + comment = create_comment("Begin toolchange", values["COMMENT_SYMBOL"]) + out += linenumber(values) + comment + "\n" + if values["OUTPUT_TOOL_CHANGE"]: + if values["STOP_SPINDLE_FOR_TOOL_CHANGE"]: + # stop the spindle + out += linenumber(values) + "M5\n" + for line in values["TOOL_CHANGE"].splitlines(False): + out += linenumber(values) + line + "\n" + else: + if values["OUTPUT_COMMENTS"]: + # convert the tool change to a comment + comment = create_comment( + values["COMMAND_SPACE"] + + format_outstring(values, outstring) + + values["COMMAND_SPACE"], + values["COMMENT_SYMBOL"], + ) + out += linenumber(values) + comment + "\n" + outstring = [] + + if command == "message" and values["REMOVE_MESSAGES"]: + if values["OUTPUT_COMMENTS"] is False: + out = [] + else: + outstring.pop(0) # remove the command + + if command in values["SUPPRESS_COMMANDS"]: + if values["OUTPUT_COMMENTS"]: + # convert the command to a comment + comment = create_comment( + values["COMMAND_SPACE"] + + format_outstring(values, outstring) + + values["COMMAND_SPACE"], + values["COMMENT_SYMBOL"], + ) + out += linenumber(values) + comment + "\n" + # remove the command + outstring = [] + + # prepend a line number and append a newline + if len(outstring) >= 1: + if values["OUTPUT_LINE_NUMBERS"]: + # In this case we don't want a space after the line number + # because the space is added in the join just below. + outstring.insert(0, (linenumber(values, ""))) + + # append the line to the final output + out += values["COMMAND_SPACE"].join(outstring) + # Note: Do *not* strip `out`, since that forces the allocation + # of a contiguous string & thus quadratic complexity. + out += "\n" + + # add height offset + if command in ("M6", "M06") and values["USE_TLO"]: + out += linenumber(values) + "G43 H" + str(int(c.Parameters["T"])) + "\n" + + # Check for comments containing machine-specific commands + # to pass literally to the controller + if values["ENABLE_MACHINE_SPECIFIC_COMMANDS"]: + m = re.match(r"^\(MC_RUN_COMMAND: ([^)]+)\)$", command) + if m: + raw_command = m.group(1) + out += linenumber(values) + raw_command + "\n" + + return out diff --git a/src/Mod/Path/PathScripts/post/centroid_post.py b/src/Mod/Path/PathScripts/post/centroid_post.py index bd91b9c95d..6fe06202bb 100644 --- a/src/Mod/Path/PathScripts/post/centroid_post.py +++ b/src/Mod/Path/PathScripts/post/centroid_post.py @@ -24,6 +24,7 @@ # *************************************************************************** from __future__ import print_function +import os import FreeCAD from FreeCAD import Units import datetime @@ -81,7 +82,7 @@ COMMENT = ";" # gCode header with information about CAD-software, post-processor # and date/time if FreeCAD.ActiveDocument: - cam_file = FreeCAD.ActiveDocument.FileName + cam_file = os.path.basename(FreeCAD.ActiveDocument.FileName) else: cam_file = "" diff --git a/src/Mod/Path/PathScripts/post/grbl_post.py b/src/Mod/Path/PathScripts/post/grbl_post.py index 94d9252db5..4613683304 100755 --- a/src/Mod/Path/PathScripts/post/grbl_post.py +++ b/src/Mod/Path/PathScripts/post/grbl_post.py @@ -407,6 +407,8 @@ def export(objectslist, filename, argstring): gfile.write(final) gfile.close() + return final + def linenumber(): if not OUTPUT_LINE_NUMBERS: diff --git a/src/Mod/Path/PathScripts/post/refactored_centroid_post.py b/src/Mod/Path/PathScripts/post/refactored_centroid_post.py new file mode 100644 index 0000000000..0aaa107ab6 --- /dev/null +++ b/src/Mod/Path/PathScripts/post/refactored_centroid_post.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2015 Dan Falck * +# * Copyright (c) 2020 Schildkroet * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +from __future__ import print_function + +from PathScripts import PostUtilsArguments +from PathScripts import PostUtilsExport + +# +# The following variables need to be global variables +# to keep the PathPostProcessor.load method happy: +# +# TOOLTIP +# TOOLTIP_ARGS +# UNITS +# +# The "argument_defaults", "arguments_visible", and the "values" hashes +# need to be defined before the "init_shared_arguments" routine can be +# called to create TOOLTIP_ARGS, so they also end up having to be globals. +# TOOLTIP_ARGS can be defined, so they end up being global variables also. +# +TOOLTIP = """ +This is a postprocessor file for the Path workbench. It is used to +take a pseudo-gcode fragment outputted by a Path object, and output +real GCode suitable for a centroid 3 axis mill. This postprocessor, once placed +in the appropriate PathScripts folder, can be used directly from inside +FreeCAD, via the GUI importer or via python scripts with: + +import refactored_centroid_post +refactored_centroid_post.export(object,"/path/to/file.ncc","") +""" +# +# Default to metric mode +# +UNITS = "G21" + + +def init_values(values): + """Initialize values that are used throughout the postprocessor.""" + # + global UNITS + + PostUtilsArguments.init_shared_values(values) + # + # Set any values here that need to override the default values set + # in the init_shared_values routine. + # + # Use 4 digits for axis precision by default. + # + values["AXIS_PRECISION"] = 4 + values["DEFAULT_AXIS_PRECISION"] = 4 + values["DEFAULT_INCH_AXIS_PRECISION"] = 4 + # + # Use ";" as the comment symbol + # + values["COMMENT_SYMBOL"] = ";" + # + # Use 1 digit for feed precision by default. + # + values["FEED_PRECISION"] = 1 + values["DEFAULT_FEED_PRECISION"] = 1 + values["DEFAULT_INCH_FEED_PRECISION"] = 1 + # + # This value usually shows up in the post_op comment as "Finish operation:". + # Change it to "End" to produce "End operation:". + # + values["FINISH_LABEL"] = "End" + # + # If this value is True, then a list of tool numbers + # with their labels are output just before the preamble. + # + values["LIST_TOOLS_IN_PREAMBLE"] = True + # + # Used in the argparser code as the "name" of the postprocessor program. + # This would normally show up in the usage message in the TOOLTIP_ARGS, + # but we are suppressing the usage message, so it doesn't show up after all. + # + values["MACHINE_NAME"] = "Centroid" + # + # This list controls the order of parameters in a line during output. + # centroid doesn't want K properties on XY plane; Arcs need work. + # + values["PARAMETER_ORDER"] = [ + "X", + "Y", + "Z", + "A", + "B", + "I", + "J", + "F", + "S", + "T", + "Q", + "R", + "L", + "H", + ] + # + # Any commands in this value will be output as the last commands + # in the G-code file. + # + values["POSTAMBLE"] = """M99""" + values["POSTPROCESSOR_FILE_NAME"] = __name__ + # + # Any commands in this value will be output after the header and + # safety block at the beginning of the G-code file. + # + values["PREAMBLE"] = """G53 G00 G17""" + # + # Output any messages. + # + values["REMOVE_MESSAGES"] = False + # + # Any commands in this value are output after the header but before the preamble, + # then again after the TOOLRETURN but before the POSTAMBLE. + # + values["SAFETYBLOCK"] = """G90 G80 G40 G49""" + # + # Do not show the current machine units just before the PRE_OPERATION. + # + values["SHOW_MACHINE_UNITS"] = False + # + # Do not show the current operation label just before the PRE_OPERATION. + # + values["SHOW_OPERATION_LABELS"] = False + # + # Do not output an M5 command to stop the spindle for tool changes. + # + values["STOP_SPINDLE_FOR_TOOL_CHANGE"] = False + # + # spindle off, height offset canceled, spindle retracted + # (M25 is a centroid command to retract spindle) + # + values[ + "TOOLRETURN" + ] = """M5 +M25 +G49 H0""" + values["UNITS"] = UNITS + # + # Default to not outputting a G43 following tool changes + # + values["USE_TLO"] = False + # + # This was in the original centroid postprocessor file + # but does not appear to be used anywhere. + # + # ZAXISRETURN = """G91 G28 X0 Z0 G90""" + # + + +def init_argument_defaults(argument_defaults): + """Initialize which arguments (in a pair) are shown as the default argument.""" + PostUtilsArguments.init_argument_defaults(argument_defaults) + # + # Modify which argument to show as the default in flag-type arguments here. + # If the value is True, the first argument will be shown as the default. + # If the value is False, the second argument will be shown as the default. + # + # For example, if you want to show Metric mode as the default, use: + # argument_defaults["metric_inch"] = True + # + # If you want to show that "Don't pop up editor for writing output" is + # the default, use: + # argument_defaults["show-editor"] = False. + # + # Note: You also need to modify the corresponding entries in the "values" hash + # to actually make the default value(s) change to match. + # + + +def init_arguments_visible(arguments_visible): + """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" + PostUtilsArguments.init_arguments_visible(arguments_visible) + # + # Modify the visibility of any arguments from the defaults here. + # + arguments_visible["axis-modal"] = False + arguments_visible["precision"] = False + arguments_visible["tlo"] = False + + +def init_arguments(values, argument_defaults, arguments_visible): + """Initialize the shared argument definitions.""" + parser = PostUtilsArguments.init_shared_arguments(values, argument_defaults, arguments_visible) + # + # Add any argument definitions that are not shared with all other postprocessors here. + # + return parser + + +# +# Creating global variables and using functions to modify them +# is useful for being able to test things later. +# +values = {} +init_values(values) +argument_defaults = {} +init_argument_defaults(argument_defaults) +arguments_visible = {} +init_arguments_visible(arguments_visible) +parser = init_arguments(values, argument_defaults, arguments_visible) +# +# The TOOLTIP_ARGS value is created from the help information about the arguments. +# +TOOLTIP_ARGS = parser.format_help() + + +def export(objectslist, filename, argstring): + """Postprocess the objects in objectslist to filename.""" + # + global parser + global UNITS + global values + + # print(parser.format_help()) + + (flag, args) = PostUtilsArguments.process_shared_arguments(values, parser, argstring) + if not flag: + return None + # + # Process any additional arguments here + # + + # + # Update the global variables that might have been modified + # while processing the arguments. + # + UNITS = values["UNITS"] + + return PostUtilsExport.export_common(values, objectslist, filename) diff --git a/src/Mod/Path/PathScripts/post/refactored_grbl_post.py b/src/Mod/Path/PathScripts/post/refactored_grbl_post.py new file mode 100644 index 0000000000..61efea92ac --- /dev/null +++ b/src/Mod/Path/PathScripts/post/refactored_grbl_post.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2018, 2019 Gauthier Briere * +# * Copyright (c) 2019, 2020 Schildkroet * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +from __future__ import print_function + +from PathScripts import PostUtilsArguments +from PathScripts import PostUtilsExport + +# +# The following variables need to be global variables +# to keep the PathPostProcessor.load method happy: +# +# TOOLTIP +# TOOLTIP_ARGS +# UNITS +# +# The "argument_defaults", "arguments_visible", and the "values" hashes +# need to be defined before the "init_shared_arguments" routine can be +# called to create TOOLTIP_ARGS, so they also end up having to be globals. +# +TOOLTIP = """ +Generate g-code from a Path that is compatible with the grbl controller: + +import refactored_grbl_post +refactored_grbl_post.export(object, "/path/to/file.ncc") +""" +# +# Default to metric mode +# +UNITS = "G21" + + +def init_values(values): + """Initialize values that are used throughout the postprocessor.""" + # + global UNITS + + PostUtilsArguments.init_shared_values(values) + # + # Set any values here that need to override the default values set + # in the init_shared_values routine. + # + # + # If this is set to True, then commands that are placed in + # comments that look like (MC_RUN_COMMAND: blah) will be output. + # + values["ENABLE_MACHINE_SPECIFIC_COMMANDS"] = True + # + # Used in the argparser code as the "name" of the postprocessor program. + # This would normally show up in the usage message in the TOOLTIP_ARGS, + # but we are suppressing the usage message, so it doesn't show up after all. + # + values["MACHINE_NAME"] = "Grbl" + # + # Default to outputting Path labels at the beginning of each Path. + # + values["OUTPUT_PATH_LABELS"] = True + # + # Default to not outputting M6 tool changes (comment it) as grbl currently does not handle it + # + values["OUTPUT_TOOL_CHANGE"] = False + # + # The order of the parameters. + # Arcs may only work on the XY plane (this needs to be verified). + # + values["PARAMETER_ORDER"] = [ + "X", + "Y", + "Z", + "A", + "B", + "C", + "U", + "V", + "W", + "I", + "J", + "K", + "F", + "S", + "T", + "Q", + "R", + "L", + "P", + ] + # + # Any commands in this value will be output as the last commands + # in the G-code file. + # + values[ + "POSTAMBLE" + ] = """M5 +G17 G90 +M2""" + values["POSTPROCESSOR_FILE_NAME"] = __name__ + # + # Any commands in this value will be output after the header and + # safety block at the beginning of the G-code file. + # + values["PREAMBLE"] = """G17 G90""" + # + # Do not show the current machine units just before the PRE_OPERATION. + # + values["SHOW_MACHINE_UNITS"] = False + values["UNITS"] = UNITS + # + # Default to not outputting a G43 following tool changes + # + values["USE_TLO"] = False + + +def init_argument_defaults(argument_defaults): + """Initialize which arguments (in a pair) are shown as the default argument.""" + PostUtilsArguments.init_argument_defaults(argument_defaults) + # + # Modify which argument to show as the default in flag-type arguments here. + # If the value is True, the first argument will be shown as the default. + # If the value is False, the second argument will be shown as the default. + # + # For example, if you want to show Metric mode as the default, use: + # argument_defaults["metric_inch"] = True + # + # If you want to show that "Don't pop up editor for writing output" is + # the default, use: + # argument_defaults["show-editor"] = False. + # + # Note: You also need to modify the corresponding entries in the "values" hash + # to actually make the default value(s) change to match. + # + argument_defaults["tlo"] = False + argument_defaults["tool_change"] = False + + +def init_arguments_visible(arguments_visible): + """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" + PostUtilsArguments.init_arguments_visible(arguments_visible) + # + # Modify the visibility of any arguments from the defaults here. + # + arguments_visible["bcnc"] = True + arguments_visible["axis-modal"] = False + arguments_visible["return-to"] = True + arguments_visible["tlo"] = False + arguments_visible["tool_change"] = True + arguments_visible["translate_drill"] = True + arguments_visible["wait-for-spindle"] = True + + +def init_arguments(values, argument_defaults, arguments_visible): + """Initialize the shared argument definitions.""" + parser = PostUtilsArguments.init_shared_arguments(values, argument_defaults, arguments_visible) + # + # Add any argument definitions that are not shared with all other postprocessors here. + # + return parser + + +# +# Creating global variables and using functions to modify them +# is useful for being able to test things later. +# +values = {} +init_values(values) +argument_defaults = {} +init_argument_defaults(argument_defaults) +arguments_visible = {} +init_arguments_visible(arguments_visible) +parser = init_arguments(values, argument_defaults, arguments_visible) +# +# The TOOLTIP_ARGS value is created from the help information about the arguments. +# +TOOLTIP_ARGS = parser.format_help() + + +def export(objectslist, filename, argstring): + """Postprocess the objects in objectslist to filename.""" + # + global parser + global UNITS + global values + + # print(parser.format_help()) + + (flag, args) = PostUtilsArguments.process_shared_arguments(values, parser, argstring) + if not flag: + return None + # + # Process any additional arguments here + # + + # + # Update the global variables that might have been modified + # while processing the arguments. + # + UNITS = values["UNITS"] + + return PostUtilsExport.export_common(values, objectslist, filename) diff --git a/src/Mod/Path/PathScripts/post/refactored_linuxcnc_post.py b/src/Mod/Path/PathScripts/post/refactored_linuxcnc_post.py index a933d8447e..87bc34f6bc 100644 --- a/src/Mod/Path/PathScripts/post/refactored_linuxcnc_post.py +++ b/src/Mod/Path/PathScripts/post/refactored_linuxcnc_post.py @@ -24,278 +24,50 @@ from __future__ import print_function -import argparse -import datetime -import shlex - -import FreeCAD -from FreeCAD import Units -import Path -from PathScripts import PostUtils - -# to distinguish python built-in open function from the one declared below -if open.__module__ in ["__builtin__", "io"]: - pythonopen = open +from PathScripts import PostUtilsArguments +from PathScripts import PostUtilsExport # -# Holds various values that are used throughout the postprocessor code. +# The following variables need to be global variables +# to keep the PathPostProcessor.load method happy: # -values = {} +# TOOLTIP +# TOOLTIP_ARGS +# UNITS +# +# The "argument_defaults", "arguments_visible", and the "values" hashes +# need to be defined before the "init_shared_arguments" routine can be +# called to create TOOLTIP_ARGS, so they also end up having to be globals. +# +TOOLTIP = """This is a postprocessor file for the Path workbench. It is used to +take a pseudo-gcode fragment outputted by a Path object, and output +real GCode suitable for a linuxcnc 3 axis mill. This postprocessor, once placed +in the appropriate PathScripts folder, can be used directly from inside +FreeCAD, via the GUI importer or via python scripts with: - -def init_values(): - """Initialize many of the commonly used values.""" - values["now"] = datetime.datetime.now() - values[ - "TOOLTIP" - ] = """This is a postprocessor file for the Path workbench. It is used to - take a pseudo-gcode fragment outputted by a Path object, and output - real GCode suitable for a linuxcnc 3 axis mill. This postprocessor, once placed - in the appropriate PathScripts folder, can be used directly from inside - FreeCAD, via the GUI importer or via python scripts with: - - import linuxcnc_post - linuxcnc_post.export(object,"/path/to/file.ncc","") - """ - # - # These values set common customization preferences - # - values["OUTPUT_COMMENTS"] = True - values["OUTPUT_HEADER"] = True - values["OUTPUT_LINE_NUMBERS"] = False - values["SHOW_EDITOR"] = True - # if true commands are suppressed if the same as previous line. - values["MODAL"] = False - # if true G43 will be output following tool changes - values["USE_TLO"] = True - # if false duplicate axis values are suppressed if the same as previous line. - values["OUTPUT_DOUBLES"] = True - values["COMMAND_SPACE"] = " " - # line number starting value - values["LINENR"] = 100 - # - # These values will be reflected in the Machine configuration of the project - # - # G21 for metric, G20 for US standard - values["UNITS"] = "G21" - values["UNIT_SPEED_FORMAT"] = "mm/min" - values["UNIT_FORMAT"] = "mm" - values["MACHINE_NAME"] = "LinuxCNC" - values["CORNER_MIN"] = {"x": 0, "y": 0, "z": 0} - values["CORNER_MAX"] = {"x": 500, "y": 300, "z": 300} - values["PRECISION"] = 3 - # Preamble text will appear at the beginning of the GCODE output file. - values["PREAMBLE"] = """G17 G54 G40 G49 G80 G90""" - # Postamble text will appear following the last operation. - values[ - "POSTAMBLE" - ] = """M05 -G17 G54 G90 G80 G40 -M2 +import refactored_linuxcnc_post +refactored_linuxcnc_post.export(object,"/path/to/file.ncc","") """ - # Pre operation text will be inserted before every operation - values["PRE_OPERATION"] = """""" - # Post operation text will be inserted after every operation - values["POST_OPERATION"] = """""" - # Tool Change commands will be inserted before a tool change - values["TOOL_CHANGE"] = """""" +# +# Default to metric mode +# +UNITS = "G21" -def processArguments(values, argstring): - """Process the arguments to the postprocessor.""" - parser = argparse.ArgumentParser(prog="linuxcnc", add_help=False) - parser.add_argument("--no-header", action="store_true", help="suppress header output") - parser.add_argument("--no-comments", action="store_true", help="suppress comment output") - parser.add_argument("--line-numbers", action="store_true", help="prefix with line numbers") - parser.add_argument( - "--no-show-editor", - action="store_true", - help="don't pop up editor before writing output", - ) - parser.add_argument("--precision", default="3", help="number of digits of precision, default=3") - parser.add_argument( - "--preamble", - help='set commands to be issued before the first command, default="G17\nG90"', - ) - parser.add_argument( - "--postamble", - help='set commands to be issued after the last command, default="M05\nG17 G90\nM2"', - ) - parser.add_argument( - "--inches", action="store_true", help="Convert output for US imperial mode (G20)" - ) - parser.add_argument( - "--modal", - action="store_true", - help="Output the Same G-command Name USE NonModal Mode", - ) - parser.add_argument("--axis-modal", action="store_true", help="Output the Same Axis Value Mode") - parser.add_argument( - "--no-tlo", - action="store_true", - help="suppress tool length offset (G43) following tool changes", - ) - - values["TOOLTIP_ARGS"] = parser.format_help() - - try: - args = parser.parse_args(shlex.split(argstring)) - if args.no_header: - values["OUTPUT_HEADER"] = False - if args.no_comments: - values["OUTPUT_COMMENTS"] = False - if args.line_numbers: - values["OUTPUT_LINE_NUMBERS"] = True - if args.no_show_editor: - values["SHOW_EDITOR"] = False - values["PRECISION"] = args.precision - if args.preamble is not None: - values["PREAMBLE"] = args.preamble - if args.postamble is not None: - values["POSTAMBLE"] = args.postamble - if args.inches: - values["UNITS"] = "G20" - values["UNIT_SPEED_FORMAT"] = "in/min" - values["UNIT_FORMAT"] = "in" - values["PRECISION"] = 4 - if args.modal: - values["MODAL"] = True - if args.no_tlo: - values["USE_TLO"] = False - if args.axis_modal: - values["OUTPUT_DOUBLES"] = False - - except Exception: - return False - - return True - - -def export(objectslist, filename, argstring): - """Postprocess the objects in objectslist to filename.""" - init_values() - if not processArguments(values, argstring): - return None - - for obj in objectslist: - if not hasattr(obj, "Path"): - print( - "the object " + obj.Name + " is not a path. Please select only path and Compounds." - ) - return None - - print("postprocessing...") - gcode = "" - - # write header - if values["OUTPUT_HEADER"]: - gcode += linenumber(values) + "(Exported by FreeCAD)\n" - gcode += linenumber(values) + "(Post Processor: " + __name__ + ")\n" - gcode += linenumber(values) + "(Output Time:" + str(values["now"]) + ")\n" - - # Write the preamble - if values["OUTPUT_COMMENTS"]: - gcode += linenumber(values) + "(begin preamble)\n" - for line in values["PREAMBLE"].splitlines(False): - gcode += linenumber(values) + line + "\n" - gcode += linenumber(values) + values["UNITS"] + "\n" - - for obj in objectslist: - - # Skip inactive operations - if hasattr(obj, "Active"): - if not obj.Active: - continue - if hasattr(obj, "Base") and hasattr(obj.Base, "Active"): - if not obj.Base.Active: - continue - - # do the pre_op - if values["OUTPUT_COMMENTS"]: - gcode += linenumber(values) + "(begin operation: %s)\n" % obj.Label - gcode += linenumber(values) + "(machine units: %s)\n" % (values["UNIT_SPEED_FORMAT"]) - for line in values["PRE_OPERATION"].splitlines(True): - gcode += linenumber(values) + line - - # get coolant mode - coolantMode = "None" - if hasattr(obj, "CoolantMode") or hasattr(obj, "Base") and hasattr(obj.Base, "CoolantMode"): - if hasattr(obj, "CoolantMode"): - coolantMode = obj.CoolantMode - else: - coolantMode = obj.Base.CoolantMode - - # turn coolant on if required - if values["OUTPUT_COMMENTS"]: - if not coolantMode == "None": - gcode += linenumber(values) + "(Coolant On:" + coolantMode + ")\n" - if coolantMode == "Flood": - gcode += linenumber(values) + "M8" + "\n" - if coolantMode == "Mist": - gcode += linenumber(values) + "M7" + "\n" - - # process the operation gcode - gcode += parse(values, obj) - - # do the post_op - if values["OUTPUT_COMMENTS"]: - gcode += linenumber(values) + "(finish operation: %s)\n" % obj.Label - for line in values["POST_OPERATION"].splitlines(True): - gcode += linenumber(values) + line - - # turn coolant off if required - if not coolantMode == "None": - if values["OUTPUT_COMMENTS"]: - gcode += linenumber(values) + "(Coolant Off:" + coolantMode + ")\n" - gcode += linenumber(values) + "M9" + "\n" - - # do the post_amble - if values["OUTPUT_COMMENTS"]: - gcode += "(begin postamble)\n" - for line in values["POSTAMBLE"].splitlines(True): - gcode += linenumber(values) + line - - if FreeCAD.GuiUp and values["SHOW_EDITOR"]: - final = gcode - if len(gcode) > 100000: - print("Skipping editor since output is greater than 100kb") - else: - dia = PostUtils.GCodeEditorDialog() - dia.editor.setText(gcode) - result = dia.exec_() - if result: - final = dia.editor.toPlainText() - else: - final = gcode - - print("done postprocessing.") - - if not filename == "-": - gfile = pythonopen(filename, "w") - gfile.write(final) - gfile.close() - - return final - - -def linenumber(values): - """Output the next line number if appropriate.""" - if values["OUTPUT_LINE_NUMBERS"]: - values["LINENR"] += 10 - return "N" + str(values["LINENR"]) + " " - return "" - - -def parse(values, pathobj): - """Parse a Path.""" - out = "" - lastcommand = None - precision_string = "." + str(values["PRECISION"]) + "f" - currLocation = {} # keep track for no doubles +def init_values(values): + """Initialize values that are used throughout the postprocessor.""" + # + global UNITS + PostUtilsArguments.init_shared_values(values) + # + # Set any values here that need to override the default values set + # in the init_shared_values routine. + # + values["ENABLE_COOLANT"] = True # the order of parameters - # linuxcnc doesn't want K properties on XY plane Arcs need work. - params = [ + # linuxcnc doesn't want K properties on XY plane; Arcs need work. + values["PARAMETER_ORDER"] = [ "X", "Y", "Z", @@ -314,118 +86,104 @@ def parse(values, pathobj): "D", "P", ] - firstmove = Path.Command("G0", {"X": -1, "Y": -1, "Z": -1, "F": 0.0}) - currLocation.update(firstmove.Parameters) # set First location Parameters - - if hasattr(pathobj, "Group"): # We have a compound or project. - # if values["OUTPUT_COMMENTS"]: - # out += linenumber(values) + "(compound: " + pathobj.Label + ")\n" - for p in pathobj.Group: - out += parse(values, p) - return out - else: # parsing simple path - - # groups might contain non-path things like stock. - if not hasattr(pathobj, "Path"): - return out - - # if values["OUTPUT_COMMENTS"]: - # out += linenumber(values) + "(" + pathobj.Label + ")\n" - - for c in pathobj.Path.Commands: - - outstring = [] - command = c.Name - outstring.append(command) - - # if modal: suppress the command if it is the same as the last one - if values["MODAL"]: - if command == lastcommand: - outstring.pop(0) - - if c.Name[0] == "(" and not values["OUTPUT_COMMENTS"]: # command is a comment - continue - - # Now add the remaining parameters in order - for param in params: - if param in c.Parameters: - if param == "F" and ( - currLocation[param] != c.Parameters[param] or values["OUTPUT_DOUBLES"] - ): - if c.Name not in [ - "G0", - "G00", - ]: # linuxcnc doesn't use rapid speeds - speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity) - if speed.getValueAs(values["UNIT_SPEED_FORMAT"]) > 0.0: - outstring.append( - param - + format( - float(speed.getValueAs(values["UNIT_SPEED_FORMAT"])), - precision_string, - ) - ) - else: - continue - elif param == "T": - outstring.append(param + str(int(c.Parameters["T"]))) - elif param == "H": - outstring.append(param + str(int(c.Parameters["H"]))) - elif param == "D": - outstring.append(param + str(int(c.Parameters["D"]))) - elif param == "S": - outstring.append(param + str(int(c.Parameters["S"]))) - else: - if ( - (not values["OUTPUT_DOUBLES"]) - and (param in currLocation) - and (currLocation[param] == c.Parameters[param]) - ): - continue - else: - pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) - outstring.append( - param - + format( - float(pos.getValueAs(values["UNIT_FORMAT"])), precision_string - ) - ) - - # store the latest command - lastcommand = command - currLocation.update(c.Parameters) - - # Check for Tool Change: - if command == "M6": - # stop the spindle - out += linenumber(values) + "M5\n" - for line in values["TOOL_CHANGE"].splitlines(True): - out += linenumber(values) + line - - # add height offset - if values["USE_TLO"]: - tool_height = "\nG43 H" + str(int(c.Parameters["T"])) - outstring.append(tool_height) - - if command == "message": - if values["OUTPUT_COMMENTS"] is False: - out = [] - else: - outstring.pop(0) # remove the command - - # prepend a line number and append a newline - if len(outstring) >= 1: - if values["OUTPUT_LINE_NUMBERS"]: - outstring.insert(0, (linenumber(values))) - - # append the line to the final output - for w in outstring: - out += w + values["COMMAND_SPACE"] - # Note: Do *not* strip `out`, since that forces the allocation - # of a contiguous string & thus quadratic complexity. - out += "\n" - - return out + # + # Used in the argparser code as the "name" of the postprocessor program. + # This would normally show up in the usage message in the TOOLTIP_ARGS, + # but we are suppressing the usage message, so it doesn't show up after all. + # + values["MACHINE_NAME"] = "LinuxCNC" + # + # Any commands in this value will be output as the last commands + # in the G-code file. + # + values[ + "POSTAMBLE" + ] = """M05 +G17 G54 G90 G80 G40 +M2""" + values["POSTPROCESSOR_FILE_NAME"] = __name__ + # + # Any commands in this value will be output after the header and + # safety block at the beginning of the G-code file. + # + values["PREAMBLE"] = """G17 G54 G40 G49 G80 G90""" + values["UNITS"] = UNITS -# print(__name__ + " gcode postprocessor loaded.") +def init_argument_defaults(argument_defaults): + """Initialize which arguments (in a pair) are shown as the default argument.""" + PostUtilsArguments.init_argument_defaults(argument_defaults) + # + # Modify which argument to show as the default in flag-type arguments here. + # If the value is True, the first argument will be shown as the default. + # If the value is False, the second argument will be shown as the default. + # + # For example, if you want to show Metric mode as the default, use: + # argument_defaults["metric_inch"] = True + # + # If you want to show that "Don't pop up editor for writing output" is + # the default, use: + # argument_defaults["show-editor"] = False. + # + # Note: You also need to modify the corresponding entries in the "values" hash + # to actually make the default value(s) change to match. + # + + +def init_arguments_visible(arguments_visible): + """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" + PostUtilsArguments.init_arguments_visible(arguments_visible) + # + # Modify the visibility of any arguments from the defaults here. + # + + +def init_arguments(values, argument_defaults, arguments_visible): + """Initialize the shared argument definitions.""" + parser = PostUtilsArguments.init_shared_arguments(values, argument_defaults, arguments_visible) + # + # Add any argument definitions that are not shared with all other postprocessors here. + # + return parser + + +# +# Creating global variables and using functions to modify them +# is useful for being able to test things later. +# +values = {} +init_values(values) +argument_defaults = {} +init_argument_defaults(argument_defaults) +arguments_visible = {} +init_arguments_visible(arguments_visible) +parser = init_arguments(values, argument_defaults, arguments_visible) +# +# The TOOLTIP_ARGS value is created from the help information about the arguments. +# +TOOLTIP_ARGS = parser.format_help() + + +def export(objectslist, filename, argstring): + """Postprocess the objects in objectslist to filename.""" + # + global parser + global UNITS + global values + + # print(parser.format_help()) + + (flag, args) = PostUtilsArguments.process_shared_arguments(values, parser, argstring) + if not flag: + return None + # + # Process any additional arguments here + # + + # + # Update the global variables that might have been modified + # while processing the arguments. + # + UNITS = values["UNITS"] + + return PostUtilsExport.export_common(values, objectslist, filename) diff --git a/src/Mod/Path/PathScripts/post/refactored_mach3_mach4_post.py b/src/Mod/Path/PathScripts/post/refactored_mach3_mach4_post.py new file mode 100644 index 0000000000..17d0e40f60 --- /dev/null +++ b/src/Mod/Path/PathScripts/post/refactored_mach3_mach4_post.py @@ -0,0 +1,196 @@ +# *************************************************************************** +# * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# ***************************************************************************/ +from __future__ import print_function + +from PathScripts import PostUtilsArguments +from PathScripts import PostUtilsExport + +# +# The following variables need to be global variables +# to keep the PathPostProcessor.load method happy: +# +# TOOLTIP +# TOOLTIP_ARGS +# UNITS +# +# The "argument_defaults", "arguments_visible", and the "values" hashes +# need to be defined before the "init_shared_arguments" routine can be +# called to create TOOLTIP_ARGS, so they also end up having to be globals. +# +TOOLTIP = """ +This is a postprocessor file for the Path workbench. It is used to +take a pseudo-gcode fragment outputted by a Path object, and output +real GCode suitable for a mach3_4 3 axis mill. This postprocessor, once placed +in the appropriate PathScripts folder, can be used directly from inside +FreeCAD, via the GUI importer or via python scripts with: + +import mach3_mach4_post +mach3_mach4_post.export(object,"/path/to/file.ncc","") +""" +# +# Default to metric mode +# +UNITS = "G21" + + +def init_values(values): + """Initialize values that are used throughout the postprocessor.""" + # + global UNITS + + PostUtilsArguments.init_shared_values(values) + # + # Set any values here that need to override the default values set + # in the init_shared_values routine. + # + values["ENABLE_COOLANT"] = True + # + # Used in the argparser code as the "name" of the postprocessor program. + # This would normally show up in the usage message in the TOOLTIP_ARGS, + # but we are suppressing the usage message, so it doesn't show up after all. + # + values["MACHINE_NAME"] = "mach3_4" + # Enable special processing for operations with "Adaptive" in the name + values["OUTPUT_ADAPTIVE"] = True + # Output the machine name for mach3_mach4 instead of the machine units alone. + values["OUTPUT_MACHINE_NAME"] = True + # the order of parameters + # mach3_mach4 doesn't want K properties on XY plane; Arcs need work. + values["PARAMETER_ORDER"] = [ + "X", + "Y", + "Z", + "A", + "B", + "C", + "I", + "J", + "F", + "S", + "T", + "Q", + "R", + "L", + "H", + "D", + "P", + ] + # + # Any commands in this value will be output as the last commands + # in the G-code file. + # + values[ + "POSTAMBLE" + ] = """M05 +G17 G54 G90 G80 G40 +M2""" + values["POSTPROCESSOR_FILE_NAME"] = __name__ + # + # Any commands in this value will be output after the header and + # safety block at the beginning of the G-code file. + # + values["PREAMBLE"] = """G17 G54 G40 G49 G80 G90""" + # Output the machine name for mach3_mach4 instead of the machine units alone. + values["SHOW_MACHINE_UNITS"] = False + values["UNITS"] = UNITS + + +def init_argument_defaults(argument_defaults): + """Initialize which arguments (in a pair) are shown as the default argument.""" + PostUtilsArguments.init_argument_defaults(argument_defaults) + # + # Modify which argument to show as the default in flag-type arguments here. + # If the value is True, the first argument will be shown as the default. + # If the value is False, the second argument will be shown as the default. + # + # For example, if you want to show Metric mode as the default, use: + # argument_defaults["metric_inch"] = True + # + # If you want to show that "Don't pop up editor for writing output" is + # the default, use: + # argument_defaults["show-editor"] = False. + # + # Note: You also need to modify the corresponding entries in the "values" hash + # to actually make the default value(s) change to match. + # + + +def init_arguments_visible(arguments_visible): + """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" + PostUtilsArguments.init_arguments_visible(arguments_visible) + # + # Modify the visibility of any arguments from the defaults here. + # + arguments_visible["axis-modal"] = True + + +def init_arguments(values, argument_defaults, arguments_visible): + """Initialize the shared argument definitions.""" + parser = PostUtilsArguments.init_shared_arguments(values, argument_defaults, arguments_visible) + # + # Add any argument definitions that are not shared with all other postprocessors here. + # + return parser + + +# +# Creating global variables and using functions to modify them +# is useful for being able to test things later. +# +values = {} +init_values(values) +argument_defaults = {} +init_argument_defaults(argument_defaults) +arguments_visible = {} +init_arguments_visible(arguments_visible) +parser = init_arguments(values, argument_defaults, arguments_visible) +# +# The TOOLTIP_ARGS value is created from the help information about the arguments. +# +TOOLTIP_ARGS = parser.format_help() + + +def export(objectslist, filename, argstring): + """Postprocess the objects in objectslist to filename.""" + # + global parser + global UNITS + global values + + # print(parser.format_help()) + + (flag, args) = PostUtilsArguments.process_shared_arguments(values, parser, argstring) + if not flag: + return None + # + # Process any additional arguments here + # + + # + # Update the global variables that might have been modified + # while processing the arguments. + # + UNITS = values["UNITS"] + + return PostUtilsExport.export_common(values, objectslist, filename) diff --git a/src/Mod/Path/PathScripts/post/refactored_test_post.py b/src/Mod/Path/PathScripts/post/refactored_test_post.py new file mode 100644 index 0000000000..0c8dce879c --- /dev/null +++ b/src/Mod/Path/PathScripts/post/refactored_test_post.py @@ -0,0 +1,216 @@ +# *************************************************************************** +# * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +from __future__ import print_function + +from PathScripts import PostUtilsArguments +from PathScripts import PostUtilsExport + +# +# The following variables need to be global variables +# to keep the PathPostProcessor.load method happy: +# +# TOOLTIP +# TOOLTIP_ARGS +# UNITS +# +# The "argument_defaults", "arguments_visible", and the "values" hashes +# need to be defined before the "init_shared_arguments" routine can be +# called to create TOOLTIP_ARGS, so they also end up having to be globals. +# +TOOLTIP = """This is a postprocessor file for the Path workbench. It is used to +test the postprocessor code. It probably isn't useful for "real" gcode. + +import refactored_test_post +refactored_test_post.export(object,"/path/to/file.ncc","") +""" +# +# Default to metric mode +# +UNITS = "G21" + + +def init_values(values): + """Initialize values that are used throughout the postprocessor.""" + # + global UNITS + + PostUtilsArguments.init_shared_values(values) + # + # Set any values here that need to override the default values set + # in the init_shared_values routine. + # + # Turn off as much functionality as possible by default. + # Then the tests can turn back on the appropriate options as needed. + # + # Used in the argparser code as the "name" of the postprocessor program. + # This would normally show up in the usage message in the TOOLTIP_ARGS, + # but we are suppressing the usage message, so it doesn't show up after all. + # + values["MACHINE_NAME"] = "test" + # + # Don't output comments by default + # + values["OUTPUT_COMMENTS"] = False + # + # Don't output the header by default + # + values["OUTPUT_HEADER"] = False + # + # Convert M56 tool change commands to comments, + # which are then suppressed by default. + # + values["OUTPUT_TOOL_CHANGE"] = False + # + # Enable as many parameters as possible to be output by default + # + values["PARAMETER_ORDER"] = [ + "X", + "Y", + "Z", + "A", + "B", + "C", + "U", + "V", + "W", + "I", + "J", + "K", + "F", + "S", + "T", + "Q", + "R", + "L", + "H", + "D", + "P", + ] + values["POSTPROCESSOR_FILE_NAME"] = __name__ + # + # Do not show the editor by default since we are testing. + # + values["SHOW_EDITOR"] = False + # + # Don't show the current machine units by default + # + values["SHOW_MACHINE_UNITS"] = False + # + # Don't show the current operation label by default. + # + values["SHOW_OPERATION_LABELS"] = False + # + # Don't output an M5 command to stop the spindle after an M6 tool change by default. + # + values["STOP_SPINDLE_FOR_TOOL_CHANGE"] = False + # + # Don't output a G43 tool length command following tool changes by default. + # + values["USE_TLO"] = False + values["UNITS"] = UNITS + + +def init_argument_defaults(argument_defaults): + """Initialize which arguments (in a pair) are shown as the default argument.""" + PostUtilsArguments.init_argument_defaults(argument_defaults) + # + # Modify which argument to show as the default in flag-type arguments here. + # If the value is True, the first argument will be shown as the default. + # If the value is False, the second argument will be shown as the default. + # + # For example, if you want to show Metric mode as the default, use: + # argument_defaults["metric_inch"] = True + # + # If you want to show that "Don't pop up editor for writing output" is + # the default, use: + # argument_defaults["show-editor"] = False. + # + # Note: You also need to modify the corresponding entries in the "values" hash + # to actually make the default value(s) change to match. + # + + +def init_arguments_visible(arguments_visible): + """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" + PostUtilsArguments.init_arguments_visible(arguments_visible) + # + # Modify the visibility of any arguments from the defaults here. + # + # + # Make all arguments invisible by default. + # + for k in iter(arguments_visible): + arguments_visible[k] = False + + +def init_arguments(values, argument_defaults, arguments_visible): + """Initialize the shared argument definitions.""" + parser = PostUtilsArguments.init_shared_arguments(values, argument_defaults, arguments_visible) + # + # Add any argument definitions that are not shared with all other postprocessors here. + # + return parser + + +# +# Creating global variables and using functions to modify them +# is useful for being able to test things later. +# +values = {} +init_values(values) +argument_defaults = {} +init_argument_defaults(argument_defaults) +arguments_visible = {} +init_arguments_visible(arguments_visible) +parser = init_arguments(values, argument_defaults, arguments_visible) +# +# The TOOLTIP_ARGS value is created from the help information about the arguments. +# +TOOLTIP_ARGS = parser.format_help() + + +def export(objectslist, filename, argstring): + """Postprocess the objects in objectslist to filename.""" + # + global parser + global UNITS + global values + + # print(parser.format_help()) + + (flag, args) = PostUtilsArguments.process_shared_arguments(values, parser, argstring) + if not flag: + return None + # + # Process any additional arguments here + # + + # + # Update the global variables that might have been modified + # while processing the arguments. + # + UNITS = values["UNITS"] + + return PostUtilsExport.export_common(values, objectslist, filename) diff --git a/src/Mod/Path/PathTests/TestCentroidPost.py b/src/Mod/Path/PathTests/TestCentroidPost.py new file mode 100644 index 0000000000..6de20f4a44 --- /dev/null +++ b/src/Mod/Path/PathTests/TestCentroidPost.py @@ -0,0 +1,308 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * 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 importlib import reload + +import FreeCAD + +# import Part +import Path +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +from PathScripts.post import centroid_post as postprocessor + + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestCentroidPost(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code and objects here + that are needed for the duration of the test() methods in this class. In other words, + set up the 'global' test environment here; use the `setUp()` method to set up a 'local' + test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add code and objects here + that cleanup the test environment after the test() methods in this class have been executed. + This method does not have access to the class `self` reference. This method + is able to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and objects here + that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # header contains a time stamp that messes up unit testing. Only test length of result + args = "--no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertTrue(len(gcode.splitlines()) == 16) + + # Test without header + expected = """G90 G80 G40 G49 +;begin preamble +G53 G00 G17 +G21 +;begin operation +;end operation: testpath +;begin postamble +M5 +M25 +G49 H0 +G90 G80 G40 G49 +M99 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G90 G80 G40 G49 +G53 G00 G17 +G21 +M5 +M25 +G49 H0 +G90 G80 G40 G49 +M99 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.0000 Y20.0000 Z30.0000" + self.assertEqual(result, expected) + + args = "--no-header --axis-precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "N150 G0 X10.0000 Y20.0000 Z30.0000" + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # + # The original centroid postprocessor does not have a + # --preamble option. We end up with the default preamble. + # + args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[1] + self.assertEqual(result, "G53 G00 G17") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + # + # The original centroid postprocessor does not have a + # --postamble option. We end up with the default postamble. + # + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[-1], "M99") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[3], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + args = ("--no-header --inches --axis-precision=2 --no-show-editor") + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X0.39 Y0.79 Z1.18" + self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + # + # The original centroid postprocessor does not have a + # --modal option. We end up with the original gcode. + # + args = "--no-header --modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "G0 X10.0000 Y30.0000 Z30.0000" + self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + # + # The original centroid postprocessor does not have an + # --axis-modal option. We end up with the original gcode. + # + args = "--no-header --axis-modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "G0 X10.0000 Y30.0000 Z30.0000" + self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[5], "M6 T2") + self.assertEqual(gcode.splitlines()[6], "M3 S3000") + + # suppress TLO + # + # The original centroid postprocessor does not have an + # --no-tlo option. We end up with the original gcode. + # + args = "--no-header --no-tlo --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[6], "M3 S3000") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = ";comment" + self.assertEqual(result, expected) diff --git a/src/Mod/Path/PathTests/TestGrblPost.py b/src/Mod/Path/PathTests/TestGrblPost.py new file mode 100644 index 0000000000..f05f8b329b --- /dev/null +++ b/src/Mod/Path/PathTests/TestGrblPost.py @@ -0,0 +1,296 @@ +# -*- 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 * +# * * +# *************************************************************************** + +import FreeCAD + +# import Part +import Path +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +from importlib import reload +from PathScripts.post import grbl_post as postprocessor + + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestGrblPost(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code and objects here + that are needed for the duration of the test() methods in this class. In other words, + set up the 'global' test environment here; use the `setUp()` method to set up a 'local' + test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add code and objects here + that cleanup the test environment after the test() methods in this class have been executed. + This method does not have access to the class `self` reference. This method + is able to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and objects here + that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + args = "--no-show-editor" # header contains a time stamp that messes up unit testing. Only test length of result + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertTrue(len(gcode.splitlines()) == 13) + + # Test without header + expected = """(Begin preamble) +G17 G90 +G21 +(Begin operation: testpath) +(Path: testpath) +(Finish operation: testpath) +(Begin postamble) +M5 +G17 G90 +M2 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G17 G90 +G21 +M5 +G17 G90 +M2 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + Test imperial / inches + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + args = "--no-header --precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "N150 G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-comments --preamble='G18 G55\n' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[0] + self.assertEqual(result, "G18 G55") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[-2] + self.assertEqual(result, "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[2], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + # Technical debt. The following test fails. Precision not working + # with imperial units. + + # args = ("--no-header --inches --precision=2") + # gcode = postprocessor.export(postables, "gcode.tmp", args) + # result = gcode.splitlines()[5] + # expected = "G0 X0.39 Y0.78 Z1.18 " + # self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + # + # The grbl postprocessor does not have a --modal option. + # + # args = "--no-header --modal --no-show-editor" + # gcode = postprocessor.export(postables, "gcode.tmp", args) + # result = gcode.splitlines()[6] + # expected = "X10.000 Y30.000 Z30.000 " + # self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + # + # The grbl postprocessor does not have a --axis-modal option. + # + # args = "--no-header --axis-modal --no-show-editor" + # gcode = postprocessor.export(postables, "gcode.tmp", args) + # result = gcode.splitlines()[6] + # expected = "G0 Y30.000 " + # self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[6], "( M6 T2 )") + self.assertEqual(gcode.splitlines()[7], "M3 S3000") + + # suppress TLO + # + # The grbl postprocessor does not have a --no-tlo option. + # + # args = "--no-header --no-tlo --no-show-editor" + # gcode = postprocessor.export(postables, "gcode.tmp", args) + # self.assertEqual(gcode.splitlines()[7], "M3 S3000 ") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "(comment)" + self.assertEqual(result, expected) diff --git a/src/Mod/Path/PathTests/TestLinuxCNCPost.py b/src/Mod/Path/PathTests/TestLinuxCNCPost.py index ca70625fc9..292ad3d10c 100644 --- a/src/Mod/Path/PathTests/TestLinuxCNCPost.py +++ b/src/Mod/Path/PathTests/TestLinuxCNCPost.py @@ -89,7 +89,7 @@ class TestLinuxCNCPost(PathTestUtils.PathTestBase): postables = [self.docobj] # Test generating with header - args = "" # header contains a time stamp that messes up unit testing. Only test length of result + args = "--no-show-editor" # header contains a time stamp that messes up unit testing. Only test length of result gcode = postprocessor.export(postables, "gcode.tmp", args) self.assertTrue(len(gcode.splitlines()) == 13) @@ -109,7 +109,7 @@ M2 self.docobj.Path = Path.Path([]) postables = [self.docobj] - args = "--no-header" + args = "--no-header --no-show-editor" # args = ("--no-header --no-comments --no-show-editor --precision=2") gcode = postprocessor.export(postables, "gcode.tmp", args) self.assertEqual(gcode, expected) @@ -122,7 +122,7 @@ G17 G54 G90 G80 G40 M2 """ - args = "--no-header --no-comments" + args = "--no-header --no-comments --no-show-editor" # args = ("--no-header --no-comments --no-show-editor --precision=2") gcode = postprocessor.export(postables, "gcode.tmp", args) self.assertEqual(gcode, expected) @@ -137,13 +137,13 @@ M2 self.docobj.Path = Path.Path([c]) postables = [self.docobj] - args = "--no-header" + args = "--no-header --no-show-editor" gcode = postprocessor.export(postables, "gcode.tmp", args) result = gcode.splitlines()[5] expected = "G0 X10.000 Y20.000 Z30.000 " self.assertEqual(result, expected) - args = "--no-header --precision=2" + args = "--no-header --precision=2 --no-show-editor" gcode = postprocessor.export(postables, "gcode.tmp", args) result = gcode.splitlines()[5] expected = "G0 X10.00 Y20.00 Z30.00 " @@ -160,7 +160,7 @@ M2 self.docobj.Path = Path.Path([c]) postables = [self.docobj] - args = "--no-header --line-numbers" + args = "--no-header --line-numbers --no-show-editor" gcode = postprocessor.export(postables, "gcode.tmp", args) result = gcode.splitlines()[5] expected = "N160 G0 X10.000 Y20.000 Z30.000 " @@ -174,7 +174,7 @@ M2 self.docobj.Path = Path.Path([]) postables = [self.docobj] - args = "--no-header --no-comments --preamble='G18 G55' " + args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" gcode = postprocessor.export(postables, "gcode.tmp", args) result = gcode.splitlines()[0] self.assertEqual(result, "G18 G55") @@ -185,7 +185,7 @@ M2 """ self.docobj.Path = Path.Path([]) postables = [self.docobj] - args = "--no-header --no-comments --postamble='G0 Z50\nM2' " + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" gcode = postprocessor.export(postables, "gcode.tmp", args) result = gcode.splitlines()[-2] self.assertEqual(result, "G0 Z50") @@ -200,7 +200,7 @@ M2 self.docobj.Path = Path.Path([c]) postables = [self.docobj] - args = "--no-header --inches " + args = "--no-header --inches --no-show-editor" gcode = postprocessor.export(postables, "gcode.tmp", args) self.assertEqual(gcode.splitlines()[2], "G20") @@ -228,7 +228,7 @@ M2 self.docobj.Path = Path.Path([c, c1]) postables = [self.docobj] - args = "--no-header --modal" + args = "--no-header --modal --no-show-editor" gcode = postprocessor.export(postables, "gcode.tmp", args) result = gcode.splitlines()[6] expected = "X10.000 Y30.000 Z30.000 " @@ -245,9 +245,8 @@ M2 self.docobj.Path = Path.Path([c, c1]) postables = [self.docobj] - args = "--no-header --axis-modal" + args = "--no-header --axis-modal --no-show-editor" gcode = postprocessor.export(postables, "gcode.tmp", args) - print(gcode) result = gcode.splitlines()[6] expected = "G0 Y30.000 " self.assertEqual(result, expected) @@ -261,7 +260,7 @@ M2 self.docobj.Path = Path.Path([c, c2]) postables = [self.docobj] - args = "--no-header" + args = "--no-header --no-show-editor" gcode = postprocessor.export(postables, "gcode.tmp", args) self.assertEqual(gcode.splitlines()[5], "M5") self.assertEqual(gcode.splitlines()[6], "M6 T2 ") @@ -269,7 +268,7 @@ M2 self.assertEqual(gcode.splitlines()[8], "M3 S3000 ") # suppress TLO - args = "--no-header --no-tlo" + args = "--no-header --no-tlo --no-show-editor" gcode = postprocessor.export(postables, "gcode.tmp", args) self.assertEqual(gcode.splitlines()[7], "M3 S3000 ") @@ -283,7 +282,7 @@ M2 self.docobj.Path = Path.Path([c]) postables = [self.docobj] - args = "--no-header" + args = "--no-header --no-show-editor" gcode = postprocessor.export(postables, "gcode.tmp", args) result = gcode.splitlines()[5] expected = "(comment) " diff --git a/src/Mod/Path/PathTests/TestMach3Mach4Post.py b/src/Mod/Path/PathTests/TestMach3Mach4Post.py new file mode 100644 index 0000000000..0d0b795183 --- /dev/null +++ b/src/Mod/Path/PathTests/TestMach3Mach4Post.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * 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 importlib import reload + +import FreeCAD + +# import Part +import Path +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +from PathScripts.post import mach3_mach4_post as postprocessor + + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestMach3Mach4Post(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code and objects here + that are needed for the duration of the test() methods in this class. In other words, + set up the 'global' test environment here; use the `setUp()` method to set up a 'local' + test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add code and objects here + that cleanup the test environment after the test() methods in this class have been executed. + This method does not have access to the class `self` reference. This method + is able to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and objects here + that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # header contains a time stamp that messes up unit testing. Only test length of result + args = "--no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertTrue(len(gcode.splitlines()) == 13) + + # Test without header + expected = """(begin preamble) +G17 G54 G40 G49 G80 G90 +G21 +(begin operation: testpath) +(machine: mach3_4, mm/min) +(finish operation: testpath) +(begin postamble) +M05 +G17 G54 G90 G80 G40 +M2 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G17 G54 G40 G49 G80 G90 +G21 +M05 +G17 G54 G90 G80 G40 +M2 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + args = "--no-header --precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "N160 G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[0] + self.assertEqual(result, "G18 G55") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[-2] + self.assertEqual(result, "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[2], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + # Technical debt. The following test fails. Precision not working + # with imperial units. + + # args = ("--no-header --inches --precision=2 --no-show-editor") + # gcode = postprocessor.export(postables, "gcode.tmp", args) + # result = gcode.splitlines()[5] + # expected = "G0 X0.39 Y0.79 Z1.18" + # self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "X10.000 Y30.000 Z30.000" + self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --axis-modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "G0 Y30.000" + self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[5], "M5") + self.assertEqual(gcode.splitlines()[6], "M6 T2 ") + self.assertEqual(gcode.splitlines()[7], "G43 H2") + self.assertEqual(gcode.splitlines()[8], "M3 S3000") + + # suppress TLO + args = "--no-header --no-tlo --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[7], "M3 S3000") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "(comment)" + self.assertEqual(result, expected) diff --git a/src/Mod/Path/PathTests/TestPathPost.py b/src/Mod/Path/PathTests/TestPathPost.py index c2d1898ee0..21f9cbedf6 100644 --- a/src/Mod/Path/PathTests/TestPathPost.py +++ b/src/Mod/Path/PathTests/TestPathPost.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2016 sliptonic * +# * Copyright (c) 2022 Larry Woestman * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * @@ -20,70 +21,185 @@ # * * # *************************************************************************** -import FreeCAD -import PathScripts -import PathScripts.post -import PathScripts.PathProfileContour -import PathScripts.PathJob -import PathScripts.PathPost as PathPost -import PathScripts.PathToolController -import PathScripts.PathUtil -import PathScripts.PostUtils as PostUtils import difflib -import unittest -import Path import os -import PathScripts.PathPost as PathPost +import unittest -WriteDebugOutput = False +import FreeCAD +import Path + +from PathScripts import PathLog +from PathScripts import PathPost +from PathScripts import PathPreferences +from PathScripts import PostUtils + +from PathScripts.PathPostProcessor import PostProcessor + +# If KEEP_DEBUG_OUTPUT is False, remove the gcode file after the test succeeds. +# If KEEP_DEBUG_OUTPUT is True or the test fails leave the gcode file behind +# so it can be looked at easily. +KEEP_DEBUG_OUTPUT = False + +PathPost.LOG_MODULE = PathLog.thisModule() +PathLog.setLevel(PathLog.Level.INFO, PathPost.LOG_MODULE) -class PathPostTestCases(unittest.TestCase): +class TestPathPost(unittest.TestCase): + """Test some of the output of the postprocessors. + + So far there are three tests each for the linuxcnc + and centroid postprocessors. + """ + def setUp(self): - testfile = FreeCAD.getHomePath() + "Mod/Path/PathTests/boxtest.fcstd" - self.doc = FreeCAD.open(testfile) - self.job = FreeCAD.ActiveDocument.getObject("Job") - self.postlist = [] - currTool = None - for obj in self.job.Group: - if not isinstance(obj.Proxy, PathScripts.PathToolController.ToolController): - tc = PathScripts.PathUtil.toolControllerForOp(obj) - if tc is not None: - if tc.ToolNumber != currTool: - self.postlist.append(tc) - self.postlist.append(obj) + """Set up the postprocessor tests.""" + pass def tearDown(self): - FreeCAD.closeDocument("boxtest") + """Tear down after the postprocessor tests.""" + pass - - def testCentroid(self): - from PathScripts.post import centroid_post as postprocessor - - args = "--no-header --no-line-numbers --no-comments --no-show-editor --axis-precision=2 --feed-precision=2" - gcode = postprocessor.export(self.postlist, "gcode.tmp", args) - - referenceFile = ( - FreeCAD.getHomePath() + "Mod/Path/PathTests/test_centroid_00.ngc" + # + # You can run just this test using: + # ./FreeCAD -c -t PathTests.TestPathPost.TestPathPost.test_postprocessors + # + def test_postprocessors(self): + """Test the postprocessors.""" + # + # The tests are performed in the order they are listed: + # one test performed on all of the postprocessors + # then the next test on all of the postprocessors, etc. + # You can comment out the tuples for tests that you don't want + # to use. + # + tests_to_perform = ( + # (output_file_id, freecad_document, job_name, postprocessor_arguments, + # postprocessor_list) + # + # test with all of the defaults (metric mode, etc.) + ("default", "boxtest1", "Job", "--no-show-editor", ()), + # test in Imperial mode + ("imperial", "boxtest1", "Job", "--no-show-editor --inches", ()), + # test in metric, G55, M4, the other way around the part + ("other_way", "boxtest1", "Job001", "--no-show-editor", ()), + # test in metric, split by fixtures, G54, G55, G56 + ("split", "boxtest1", "Job002", "--no-show-editor", ()), + # test in metric mode without the header + ("no_header", "boxtest1", "Job", "--no-header --no-show-editor", ()), + # test translating G81, G82, and G83 to G00 and G01 commands + ( + "drill_translate", + "drill_test1", + "Job", + "--no-show-editor --translate_drill", + ("grbl", "refactored_grbl"), + ), ) - with open(referenceFile, "r") as fp: - refGCode = fp.read() - - # Use if this test fails in order to have a real good look at the changes - if WriteDebugOutput: - with open("testCentroid.tmp", "w") as fp: - fp.write(gcode) - - if gcode != refGCode: - msg = "".join( - difflib.ndiff(gcode.splitlines(True), refGCode.splitlines(True)) - ) - self.fail("linuxcnc output doesn't match: " + msg) + # + # The postprocessors to test. + # You can comment out any postprocessors that you don't want + # to test. + # + postprocessors_to_test = ( + "centroid", + # "fanuc", + "grbl", + "linuxcnc", + "mach3_mach4", + "refactored_centroid", + # "refactored_fanuc", + "refactored_grbl", + "refactored_linuxcnc", + "refactored_mach3_mach4", + "refactored_test", + ) + # + # Enough of the path to where the tests are stored so that + # they can be found by the python interpreter. + # + PATHTESTS_LOCATION = "Mod/Path/PathTests" + # + # The following code tries to re-use an open FreeCAD document + # as much as possible. It compares the current document with + # the document for the next test. If the names are different + # then the current document is closed and the new document is + # opened. The final document is closed at the end of the code. + # + current_document = "" + for ( + output_file_id, + freecad_document, + job_name, + postprocessor_arguments, + postprocessor_list, + ) in tests_to_perform: + if current_document != freecad_document: + if current_document != "": + FreeCAD.closeDocument(current_document) + current_document = freecad_document + current_document_path = ( + FreeCAD.getHomePath() + + PATHTESTS_LOCATION + + os.path.sep + + current_document + + ".fcstd" + ) + FreeCAD.open(current_document_path) + job = FreeCAD.ActiveDocument.getObject(job_name) + # Create the objects to be written by the postprocessor. + postlist = PathPost.buildPostList(job) + for postprocessor_id in postprocessors_to_test: + if postprocessor_list == () or postprocessor_id in postprocessor_list: + print( + "\nRunning %s test on %s postprocessor:\n" + % (output_file_id, postprocessor_id) + ) + processor = PostProcessor.load(postprocessor_id) + output_file_path = FreeCAD.getHomePath() + PATHTESTS_LOCATION + output_file_pattern = "test_%s_%s" % (postprocessor_id, output_file_id) + output_file_extension = ".ngc" + for idx, section in enumerate(postlist): + partname = section[0] + sublist = section[1] + output_filename = PathPost.processFileNameSubstitutions( + job, + partname, + idx, + output_file_path, + output_file_pattern, + output_file_extension, + ) + # print("output file: " + output_filename) + file_path, extension = os.path.splitext(output_filename) + reference_file_name = "%s%s%s" % (file_path, "_ref", extension) + # print("reference file: " + reference_file_name) + gcode = processor.export(sublist, output_filename, postprocessor_arguments) + if not gcode: + print("no gcode") + with open(reference_file_name, "r") as fp: + reference_gcode = fp.read() + if not reference_gcode: + print("no reference gcode") + # Remove the "Output Time:" line in the header from the comparison + # if it is present because it changes with every test. + gcode_lines = [i for i in gcode.splitlines(True) if "Output Time:" not in i] + reference_gcode_lines = [ + i for i in reference_gcode.splitlines(True) if "Output Time:" not in i + ] + if gcode_lines != reference_gcode_lines: + msg = "".join(difflib.ndiff(gcode_lines, reference_gcode_lines)) + self.fail( + os.path.basename(output_filename) + " output doesn't match:\n" + msg + ) + if not KEEP_DEBUG_OUTPUT: + os.remove(output_filename) + if current_document != "": + FreeCAD.closeDocument(current_document) class TestPathPostUtils(unittest.TestCase): def test010(self): - + """Test the utility functions in the PostUtils.py file.""" commands = [ Path.Command("G1 X-7.5 Y5.0 Z0.0"), Path.Command("G2 I2.5 J0.0 K0.0 X-5.0 Y7.5 Z0.0"), @@ -98,15 +214,11 @@ class TestPathPostUtils(unittest.TestCase): testpath = Path.Path(commands) self.assertTrue(len(testpath.Commands) == 9) - self.assertTrue( - len([c for c in testpath.Commands if c.Name in ["G2", "G3"]]) == 4 - ) + self.assertTrue(len([c for c in testpath.Commands if c.Name in ["G2", "G3"]]) == 4) results = PostUtils.splitArcs(testpath) # self.assertTrue(len(results.Commands) == 117) - self.assertTrue( - len([c for c in results.Commands if c.Name in ["G2", "G3"]]) == 0 - ) + self.assertTrue(len([c for c in results.Commands if c.Name in ["G2", "G3"]]) == 0) def dumpgroup(group): @@ -147,7 +259,6 @@ class TestBuildPostList(unittest.TestCase): def tearDown(self): pass - def test000(self): # check that the test file is structured correctly @@ -276,12 +387,12 @@ class TestOutputNameSubstitution(unittest.TestCase): job = doc.getObjectsByLabel("MainJob")[0] macro = FreeCAD.getUserMacroDir() - def test000(self): # Test basic name generation with empty string FreeCAD.setActiveDocument(self.doc.Label) teststring = "" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") self.job.SplitOutput = False outlist = PathPost.buildPostList(self.job) @@ -295,6 +406,7 @@ class TestOutputNameSubstitution(unittest.TestCase): # Test basic string substitution without splitting teststring = "~/Desktop/%j.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") self.job.SplitOutput = False outlist = PathPost.buildPostList(self.job) @@ -302,20 +414,24 @@ class TestOutputNameSubstitution(unittest.TestCase): subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) - self.assertEqual(filename, "~/Desktop/MainJob.nc") + self.assertEqual(os.path.normpath(filename), os.path.normpath("~/Desktop/MainJob.nc")) def test010(self): # Substitute current file path teststring = "%D/testfile.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) - self.assertEqual(filename, f"{self.testfilepath}/testfile.nc") + self.assertEqual( + os.path.normpath(filename), os.path.normpath(f"{self.testfilepath}/testfile.nc") + ) def test020(self): teststring = "%d.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) @@ -324,6 +440,7 @@ class TestOutputNameSubstitution(unittest.TestCase): def test030(self): teststring = "%M/outfile.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) @@ -333,15 +450,19 @@ class TestOutputNameSubstitution(unittest.TestCase): # unused substitution strings should be ignored teststring = "%d%T%t%W%O/testdoc.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) - self.assertEqual(filename, f"{self.testfilename}/testdoc.nc") + self.assertEqual( + os.path.normpath(filename), os.path.normpath(f"{self.testfilename}/testdoc.nc") + ) def test050(self): # explicitly using the sequence number should include it where indicated. teststring = "%S-%d.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) @@ -356,6 +477,7 @@ class TestOutputNameSubstitution(unittest.TestCase): # substitute jobname and use default sequence numbers teststring = "%j.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "MainJob-0.nc") @@ -366,6 +488,7 @@ class TestOutputNameSubstitution(unittest.TestCase): # Use Toolnumbers and default sequence numbers teststring = "%T.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) @@ -377,6 +500,7 @@ class TestOutputNameSubstitution(unittest.TestCase): # Use Tooldescriptions and default sequence numbers teststring = "%t.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) @@ -393,6 +517,7 @@ class TestOutputNameSubstitution(unittest.TestCase): teststring = "%j.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "MainJob-0.nc") @@ -402,6 +527,7 @@ class TestOutputNameSubstitution(unittest.TestCase): teststring = "%W-%j.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "G54-MainJob-0.nc") @@ -417,6 +543,7 @@ class TestOutputNameSubstitution(unittest.TestCase): teststring = "%j.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "MainJob-0.nc") @@ -426,6 +553,7 @@ class TestOutputNameSubstitution(unittest.TestCase): teststring = "%O-%j.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults(teststring, "Append Unique ID on conflict") subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "OutsideProfile-MainJob-0.nc") diff --git a/src/Mod/Path/PathTests/TestRefactoredCentroidPost.py b/src/Mod/Path/PathTests/TestRefactoredCentroidPost.py new file mode 100644 index 0000000000..26002112a5 --- /dev/null +++ b/src/Mod/Path/PathTests/TestRefactoredCentroidPost.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * 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 importlib import reload + +import FreeCAD + +# import Part +import Path +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +from PathScripts.post import refactored_centroid_post as postprocessor + + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestRefactoredCentroidPost(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code and objects here + that are needed for the duration of the test() methods in this class. In other words, + set up the 'global' test environment here; use the `setUp()` method to set up a 'local' + test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add code and objects here + that cleanup the test environment after the test() methods in this class have been executed. + This method does not have access to the class `self` reference. This method + is able to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and objects here + that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # header contains a time stamp that messes up unit testing. Only test length of result + args = "--no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertTrue(len(gcode.splitlines()) == 16) + + # Test without header + expected = """G90 G80 G40 G49 +;Begin preamble +G53 G00 G17 +G21 +;Begin operation +;End operation: testpath +;Begin postamble +M5 +M25 +G49 H0 +G90 G80 G40 G49 +M99 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G90 G80 G40 G49 +G53 G00 G17 +G21 +M5 +M25 +G49 H0 +G90 G80 G40 G49 +M99 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.0000 Y20.0000 Z30.0000" + self.assertEqual(result, expected) + + args = "--no-header --axis-precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "N150 G0 X10.0000 Y20.0000 Z30.0000" + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[1] + self.assertEqual(result, "G18 G55") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[-2] + self.assertEqual(result, "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[3], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + args = ("--no-header --inches --axis-precision=2 --no-show-editor") + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X0.39 Y0.79 Z1.18" + self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "X10.0000 Y30.0000 Z30.0000" + self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --axis-modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "G0 Y30.0000" + self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[6], "M6 T2") + self.assertEqual(gcode.splitlines()[7], "M3 S3000") + + # suppress TLO + args = "--no-header --no-tlo --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[7], "M3 S3000") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = ";comment" + self.assertEqual(result, expected) diff --git a/src/Mod/Path/PathTests/TestRefactoredGrblPost.py b/src/Mod/Path/PathTests/TestRefactoredGrblPost.py new file mode 100644 index 0000000000..470210275b --- /dev/null +++ b/src/Mod/Path/PathTests/TestRefactoredGrblPost.py @@ -0,0 +1,286 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * 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 importlib import reload + +import FreeCAD + +# import Part +import Path +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +from PathScripts.post import refactored_grbl_post as postprocessor + + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestRefactoredGrblPost(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code and objects here + that are needed for the duration of the test() methods in this class. In other words, + set up the 'global' test environment here; use the `setUp()` method to set up a 'local' + test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add code and objects here + that cleanup the test environment after the test() methods in this class have been executed. + This method does not have access to the class `self` reference. This method + is able to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and objects here + that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # header contains a time stamp that messes up unit testing. Only test length of result + args = "--no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertTrue(len(gcode.splitlines()) == 14) + + # Test without header + expected = """(Begin preamble) +G17 G90 +G21 +(Begin operation: testpath) +(Path: testpath) +(Finish operation: testpath) +(Begin postamble) +M5 +G17 G90 +M2 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G17 G90 +G21 +M5 +G17 G90 +M2 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + args = "--no-header --precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "N150 G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[0] + self.assertEqual(result, "G18 G55") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[-2] + self.assertEqual(result, "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[2], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + args = ("--no-header --inches --precision=2 --no-show-editor") + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X0.39 Y0.79 Z1.18" + self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "X10.000 Y30.000 Z30.000" + self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --axis-modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "G0 Y30.000" + self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[6], "( M6 T2 )") + self.assertEqual(gcode.splitlines()[7], "M3 S3000") + + # suppress TLO + args = "--no-header --no-tlo --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[7], "M3 S3000") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "(comment)" + self.assertEqual(result, expected) diff --git a/src/Mod/Path/PathTests/TestRefactoredLinuxCNCPost.py b/src/Mod/Path/PathTests/TestRefactoredLinuxCNCPost.py new file mode 100644 index 0000000000..4ad2afded3 --- /dev/null +++ b/src/Mod/Path/PathTests/TestRefactoredLinuxCNCPost.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * 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 importlib import reload + +import FreeCAD + +# import Part +import Path +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +from PathScripts.post import refactored_linuxcnc_post as postprocessor + + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestRefactoredLinuxCNCPost(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code and objects here + that are needed for the duration of the test() methods in this class. In other words, + set up the 'global' test environment here; use the `setUp()` method to set up a 'local' + test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add code and objects here + that cleanup the test environment after the test() methods in this class have been executed. + This method does not have access to the class `self` reference. This method + is able to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and objects here + that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # header contains a time stamp that messes up unit testing. Only test length of result + args = "--no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertTrue(len(gcode.splitlines()) == 14) + + # Test without header + expected = """(Begin preamble) +G17 G54 G40 G49 G80 G90 +G21 +(Begin operation: testpath) +(Machine units: mm/min) +(Finish operation: testpath) +(Begin postamble) +M05 +G17 G54 G90 G80 G40 +M2 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G17 G54 G40 G49 G80 G90 +G21 +M05 +G17 G54 G90 G80 G40 +M2 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + args = "--no-header --precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "N150 G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[0] + self.assertEqual(result, "G18 G55") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[-2] + self.assertEqual(result, "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[2], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + args = ("--no-header --inches --precision=2 --no-show-editor") + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X0.39 Y0.79 Z1.18" + self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "X10.000 Y30.000 Z30.000" + self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --axis-modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "G0 Y30.000" + self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[6], "M5") + self.assertEqual(gcode.splitlines()[7], "M6 T2") + self.assertEqual(gcode.splitlines()[8], "G43 H2") + self.assertEqual(gcode.splitlines()[9], "M3 S3000") + + # suppress TLO + args = "--no-header --no-tlo --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[8], "M3 S3000") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "(comment)" + self.assertEqual(result, expected) diff --git a/src/Mod/Path/PathTests/TestRefactoredMach3Mach4Post.py b/src/Mod/Path/PathTests/TestRefactoredMach3Mach4Post.py new file mode 100644 index 0000000000..b8a23c32bc --- /dev/null +++ b/src/Mod/Path/PathTests/TestRefactoredMach3Mach4Post.py @@ -0,0 +1,287 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * 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 importlib import reload + +import FreeCAD + +# import Part +import Path +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +from PathScripts.post import refactored_mach3_mach4_post as postprocessor + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestRefactoredMach3Mach4Post(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code and objects here + that are needed for the duration of the test() methods in this class. In other words, + set up the 'global' test environment here; use the `setUp()` method to set up a 'local' + test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add code and objects here + that cleanup the test environment after the test() methods in this class have been executed. + This method does not have access to the class `self` reference. This method + is able to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and objects here + that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # header contains a time stamp that messes up unit testing. Only test length of result + args = "--no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertTrue(len(gcode.splitlines()) == 14) + + # Test without header + expected = """(Begin preamble) +G17 G54 G40 G49 G80 G90 +G21 +(Begin operation: testpath) +(Machine: mach3_4, mm/min) +(Finish operation: testpath) +(Begin postamble) +M05 +G17 G54 G90 G80 G40 +M2 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G17 G54 G40 G49 G80 G90 +G21 +M05 +G17 G54 G90 G80 G40 +M2 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + args = "--no-header --precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "N150 G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[0] + self.assertEqual(result, "G18 G55") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[-2] + self.assertEqual(result, "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[2], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + args = ("--no-header --inches --precision=2 --no-show-editor") + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X0.39 Y0.79 Z1.18" + self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "X10.000 Y30.000 Z30.000" + self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --axis-modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "G0 Y30.000" + self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[6], "M5") + self.assertEqual(gcode.splitlines()[7], "M6 T2") + self.assertEqual(gcode.splitlines()[8], "G43 H2") + self.assertEqual(gcode.splitlines()[9], "M3 S3000") + + # suppress TLO + args = "--no-header --no-tlo --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[8], "M3 S3000") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "(comment)" + self.assertEqual(result, expected) diff --git a/src/Mod/Path/PathTests/TestRefactoredTestPost.py b/src/Mod/Path/PathTests/TestRefactoredTestPost.py new file mode 100644 index 0000000000..e6cccc8e7c --- /dev/null +++ b/src/Mod/Path/PathTests/TestRefactoredTestPost.py @@ -0,0 +1,1273 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * 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 importlib import reload + +import FreeCAD + +# import Part +import Path +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +from PathScripts.post import refactored_test_post as postprocessor + + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestRefactoredTestPost(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + + This method is called upon instantiation of this test class. Add code and objects here + that are needed for the duration of the test() methods in this class. In other words, + set up the 'global' test environment here; use the `setUp()` method to set up a 'local' + test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + + This method is called prior to destruction of this test class. Add code and objects here + that cleanup the test environment after the test() methods in this class have been executed. + This method does not have access to the class `self` reference. This method + is able to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + + def setUp(self): + """setUp()... + + This method is called prior to each `test()` method. Add code and objects here + that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test00000(self): + """Test Output Generation. + + Empty path. Produces only the preamble and postable. + Also tests the interactions between --comments and --header. + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with comments and header. + # The header contains a time stamp that messes up unit testing. + # Only test the length of the line that contains the time. + args = "--comments --header" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[0], "(Exported by FreeCAD)") + self.assertEqual( + gcode.splitlines()[1], "(Post Processor: PathScripts.post.refactored_test_post)" + ) + self.assertEqual(gcode.splitlines()[2], "(Cam File: )") + self.assertIn("(Output Time: ", gcode.splitlines()[3]) + self.assertTrue(len(gcode.splitlines()[3]) == 41) + self.assertEqual(gcode.splitlines()[4], "(Begin preamble)") + self.assertEqual(gcode.splitlines()[5], "G90") + self.assertEqual(gcode.splitlines()[6], "G21") + self.assertEqual(gcode.splitlines()[7], "(Begin operation)") + self.assertEqual(gcode.splitlines()[8], "(Finish operation: testpath)") + self.assertEqual(gcode.splitlines()[9], "(Begin postamble)") + + # Test with comments without header. + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +(Finish operation: testpath) +(Begin postamble) +""" + args = "--comments --no-header" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + # Test without comments with header. + args = "--no-comments --header" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[0], "(Exported by FreeCAD)") + self.assertEqual( + gcode.splitlines()[1], "(Post Processor: PathScripts.post.refactored_test_post)" + ) + self.assertEqual(gcode.splitlines()[2], "(Cam File: )") + self.assertIn("(Output Time: ", gcode.splitlines()[3]) + self.assertTrue(len(gcode.splitlines()[3]) == 41) + self.assertEqual(gcode.splitlines()[4], "G90") + self.assertEqual(gcode.splitlines()[5], "G21") + + # Test without comments or header. + expected = """G90 +G21 +""" + args = "--no-comments --no-header" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00100(self): + """Test bcnc.""" + # + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + expected = """G90 +G21 +(Block-name: testpath) +(Block-expand: 0) +(Block-enable: 1) +(Block-name: post_amble) +(Block-expand: 0) +(Block-enable: 1) +""" + args = "--bcnc" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """G90 +G21 +""" + args = "--no-bcnc" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00110(self): + """Test axis modal. + + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--axis-modal" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[3], "G0 Y30.000") + + args = "--no-axis-modal" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[3], "G0 X10.000 Y30.000 Z30.000") + + def test00120(self): + """Test axis-precision.""" + # + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--axis-precision=2" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "G0 X10.00 Y20.00 Z30.00") + + def test00130(self): + """Test comments.""" + # + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--comments" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[4], "(comment)") + + def test00140(self): + """Test feed-precision.""" + # + c = Path.Command("G1 X10 Y20 Z30 F123.123456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + # Note: The "internal" F speed is in mm/s, while the output F speed is in mm/min. + self.assertEqual(gcode.splitlines()[2], "G1 X10.000 Y20.000 Z30.000 F7387.407") + + args = "--feed-precision=2" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + # Note: The "internal" F speed is in mm/s, while the output F speed is in mm/min. + self.assertEqual(gcode.splitlines()[2], "G1 X10.000 Y20.000 Z30.000 F7387.41") + + def test00150(self): + """Test Line Numbers.""" + # + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--line-numbers" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "N120 G0 X10.000 Y20.000 Z30.000") + + def test00160(self): + """Test inches.""" + # + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--inches" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[1], "G20") + self.assertEqual(gcode.splitlines()[2], "G0 X0.3937 Y0.7874 Z1.1811") + + def test00170(self): + """Test modal. + + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--modal" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[3], "X10.000 Y30.000 Z30.000") + + args = "--no-modal" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[3], "G0 X10.000 Y30.000 Z30.000") + + def test00180(self): + """Test Post-amble.""" + # + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--postamble='G0 Z50\nM2'" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[-2], "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test00190(self): + """Test Pre-amble.""" + # + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--preamble='G18 G55'" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[0], "G18 G55") + + def test00200(self): + """Test precision.""" + # + c = Path.Command("G1 X10 Y20 Z30 F100") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--precision=2" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "G1 X10.00 Y20.00 Z30.00 F6000.00") + + args = "--inches --precision=2" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "G1 X0.39 Y0.79 Z1.18 F236.22") + + def test00210(self): + """Test return-to.""" + # + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + expected = """G90 +G21 +G0 X12 Y34 Z56 +""" + args = "--return-to='12,34,56'" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00220(self): + """Test tlo.""" + # + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--tlo" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "M6 T2") + self.assertEqual(gcode.splitlines()[3], "G43 H2") + self.assertEqual(gcode.splitlines()[4], "M3 S3000") + + # suppress TLO + args = "--no-tlo" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "M6 T2") + self.assertEqual(gcode.splitlines()[3], "M3 S3000") + + def test00230(self): + """Test tool_change.""" + # + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--tool_change" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "M6 T2") + self.assertEqual(gcode.splitlines()[3], "M3 S3000") + + args = "--comments --no-tool_change" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[5], "( M6 T2 )") + self.assertEqual(gcode.splitlines()[6], "M3 S3000") + + def test00240(self): + """Test translate_drill with G81.""" + # + c = Path.Command("G0 X1 Y2") + c1 = Path.Command("G0 Z8") + c2 = Path.Command("G90") + c3 = Path.Command("G99") + c4 = Path.Command("G81 X1 Y2 Z0 F123 R5") + c5 = Path.Command("G80") + c6 = Path.Command("G90") + + self.docobj.Path = Path.Path([c, c1, c2, c3, c4, c5, c6]) + postables = [self.docobj] + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +G99 +G81 X1.000 Y2.000 Z0.000 F7380.000 R5.000 +G80 +G90 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +G0 X1.000 Y2.000 +G1 Z5.000 F7380.000 +G1 Z0.000 F7380.000 +G0 Z5.000 +G90 +""" + args = "--translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +( G99 ) +( G81 X1.000 Y2.000 Z0.000 F7380.000 R5.000 ) +G0 X1.000 Y2.000 +G1 Z5.000 F7380.000 +G1 Z0.000 F7380.000 +G0 Z5.000 +( G80 ) +G90 +(Finish operation: testpath) +(Begin postamble) +""" + args = "--comments --translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00250(self): + """Test translate_drill with G82.""" + # + c = Path.Command("G0 X1 Y2") + c1 = Path.Command("G0 Z8") + c2 = Path.Command("G90") + c3 = Path.Command("G99") + c4 = Path.Command("G82 X1 Y2 Z0 F123 R5 P1.23456") + c5 = Path.Command("G80") + c6 = Path.Command("G90") + + self.docobj.Path = Path.Path([c, c1, c2, c3, c4, c5, c6]) + postables = [self.docobj] + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +G99 +G82 X1.000 Y2.000 Z0.000 F7380.000 R5.000 P1.23456 +G80 +G90 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +G0 X1.000 Y2.000 +G1 Z5.000 F7380.000 +G1 Z0.000 F7380.000 +G4 P1.23456 +G0 Z5.000 +G90 +""" + args = "--translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +( G99 ) +( G82 X1.000 Y2.000 Z0.000 F7380.000 R5.000 P1.23456 ) +G0 X1.000 Y2.000 +G1 Z5.000 F7380.000 +G1 Z0.000 F7380.000 +G4 P1.23456 +G0 Z5.000 +( G80 ) +G90 +(Finish operation: testpath) +(Begin postamble) +""" + args = "--comments --translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00260(self): + """Test translate_drill with G83.""" + # + c = Path.Command("G0 X1 Y2") + c1 = Path.Command("G0 Z8") + c2 = Path.Command("G90") + c3 = Path.Command("G99") + c4 = Path.Command("G83 X1 Y2 Z0 F123 Q1.5 R5") + c5 = Path.Command("G80") + c6 = Path.Command("G90") + + self.docobj.Path = Path.Path([c, c1, c2, c3, c4, c5, c6]) + postables = [self.docobj] + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +G99 +G83 X1.000 Y2.000 Z0.000 F7380.000 Q1.500 R5.000 +G80 +G90 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +G0 X1.000 Y2.000 +G1 Z5.000 F7380.000 +G1 Z3.500 F7380.000 +G0 Z5.000 +G0 Z3.575 +G1 Z2.000 F7380.000 +G0 Z5.000 +G0 Z2.075 +G1 Z0.500 F7380.000 +G0 Z5.000 +G0 Z0.575 +G1 Z0.000 F7380.000 +G0 Z5.000 +G90 +""" + args = "--translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +( G99 ) +( G83 X1.000 Y2.000 Z0.000 F7380.000 Q1.500 R5.000 ) +G0 X1.000 Y2.000 +G1 Z5.000 F7380.000 +G1 Z3.500 F7380.000 +G0 Z5.000 +G0 Z3.575 +G1 Z2.000 F7380.000 +G0 Z5.000 +G0 Z2.075 +G1 Z0.500 F7380.000 +G0 Z5.000 +G0 Z0.575 +G1 Z0.000 F7380.000 +G0 Z5.000 +( G80 ) +G90 +(Finish operation: testpath) +(Begin postamble) +""" + args = "--comments --translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00270(self): + """Test translate_drill with G81 and G91.""" + # + c = Path.Command("G0 X1 Y2") + c1 = Path.Command("G0 Z8") + c2 = Path.Command("G91") + c3 = Path.Command("G99") + c4 = Path.Command("G81 X1 Y2 Z0 F123 R5") + c5 = Path.Command("G80") + c6 = Path.Command("G90") + + self.docobj.Path = Path.Path([c, c1, c2, c3, c4, c5, c6]) + postables = [self.docobj] + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +G99 +G81 X1.000 Y2.000 Z0.000 F7380.000 R5.000 +G80 +G90 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +G90 +G0 Z13.000 +G0 X2.000 Y4.000 +G1 Z8.000 F7380.000 +G0 Z13.000 +G91 +G90 +""" + args = "--translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +( G99 ) +( G81 X1.000 Y2.000 Z0.000 F7380.000 R5.000 ) +G90 +G0 Z13.000 +G0 X2.000 Y4.000 +G1 Z8.000 F7380.000 +G0 Z13.000 +G91 +( G80 ) +G90 +(Finish operation: testpath) +(Begin postamble) +""" + args = "--comments --translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00280(self): + """Test translate_drill with G82 and G91.""" + # + c = Path.Command("G0 X1 Y2") + c1 = Path.Command("G0 Z8") + c2 = Path.Command("G91") + c3 = Path.Command("G99") + c4 = Path.Command("G82 X1 Y2 Z0 F123 R5 P1.23456") + c5 = Path.Command("G80") + c6 = Path.Command("G90") + + self.docobj.Path = Path.Path([c, c1, c2, c3, c4, c5, c6]) + postables = [self.docobj] + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +G99 +G82 X1.000 Y2.000 Z0.000 F7380.000 R5.000 P1.23456 +G80 +G90 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +G90 +G0 Z13.000 +G0 X2.000 Y4.000 +G1 Z8.000 F7380.000 +G4 P1.23456 +G0 Z13.000 +G91 +G90 +""" + args = "--translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +( G99 ) +( G82 X1.000 Y2.000 Z0.000 F7380.000 R5.000 P1.23456 ) +G90 +G0 Z13.000 +G0 X2.000 Y4.000 +G1 Z8.000 F7380.000 +G4 P1.23456 +G0 Z13.000 +G91 +( G80 ) +G90 +(Finish operation: testpath) +(Begin postamble) +""" + args = "--comments --translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00290(self): + """Test translate_drill with G83 and G91.""" + # + c = Path.Command("G0 X1 Y2") + c1 = Path.Command("G0 Z8") + c2 = Path.Command("G91") + c3 = Path.Command("G99") + c4 = Path.Command("G83 X1 Y2 Z0 F123 Q1.5 R5") + c5 = Path.Command("G80") + c6 = Path.Command("G90") + + self.docobj.Path = Path.Path([c, c1, c2, c3, c4, c5, c6]) + postables = [self.docobj] + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +G99 +G83 X1.000 Y2.000 Z0.000 F7380.000 Q1.500 R5.000 +G80 +G90 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +G90 +G0 Z13.000 +G0 X2.000 Y4.000 +G1 Z11.500 F7380.000 +G0 Z13.000 +G0 Z11.575 +G1 Z10.000 F7380.000 +G0 Z13.000 +G0 Z10.075 +G1 Z8.500 F7380.000 +G0 Z13.000 +G0 Z8.575 +G1 Z8.000 F7380.000 +G0 Z13.000 +G91 +G90 +""" + args = "--translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +( G99 ) +( G83 X1.000 Y2.000 Z0.000 F7380.000 Q1.500 R5.000 ) +G90 +G0 Z13.000 +G0 X2.000 Y4.000 +G1 Z11.500 F7380.000 +G0 Z13.000 +G0 Z11.575 +G1 Z10.000 F7380.000 +G0 Z13.000 +G0 Z10.075 +G1 Z8.500 F7380.000 +G0 Z13.000 +G0 Z8.575 +G1 Z8.000 F7380.000 +G0 Z13.000 +G91 +( G80 ) +G90 +(Finish operation: testpath) +(Begin postamble) +""" + args = "--comments --translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00300(self): + """Test wait-for-spindle.""" + # + c = Path.Command("M3 S3000") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "M3 S3000") + + args = "--wait-for-spindle=1.23456" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "M3 S3000") + self.assertEqual(gcode.splitlines()[3], "G4 P1.23456") + + c = Path.Command("M4 S3000") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + # This also tests that the default for --wait-for-spindle + # goes back to 0.0 (no wait) + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "M4 S3000") + + args = "--wait-for-spindle=1.23456" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "M4 S3000") + self.assertEqual(gcode.splitlines()[3], "G4 P1.23456") + + def test01000(self): + """Test G0 command Generation.""" + # + c = Path.Command("G0 X10 Y20 Z30 A40 B50 C60 U70 V80 W90") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G0 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01010(self): + """Test G1 command Generation.""" + # + c = Path.Command("G1 X10 Y20 Z30 A40 B50 C60 U70 V80 W90 F1.23456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + # Test argument order + c = Path.Command("G1 F1.23456 Z30 V80 C60 W90 X10 B50 U70 Y20 A40") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01020(self): + """Test G2 command Generation.""" + # + c = Path.Command("G2 X10 Y20 Z30 I40 J50 P60 F1.23456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G2 X10.000 Y20.000 Z30.000 I40.000 J50.000 F74.074 P60 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + # + c = Path.Command("G2 X10 Y20 Z30 R40 P60 F1.23456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G2 X10.000 Y20.000 Z30.000 F74.074 R40.000 P60 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01030(self): + """Test G3 command Generation.""" + # + c = Path.Command("G3 X10 Y20 Z30 I40 J50 P60 F1.23456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G3 X10.000 Y20.000 Z30.000 I40.000 J50.000 F74.074 P60 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + # + c = Path.Command("G3 X10 Y20 Z30 R40 P60 F1.23456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G3 X10.000 Y20.000 Z30.000 F74.074 R40.000 P60 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01040(self): + """Test G4 command Generation.""" + # Should some sort of "precision" be applied to the P parameter? + # The code as currently written does not do so intentionally. + # The P parameter indicates "time to wait" where a 0.001 would + # be a millisecond wait, so more than 3 or 4 digits of precision + # might be useful. + c = Path.Command("G4 P1.23456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G4 P1.23456 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01170(self): + """Test G17 command Generation.""" + # + c = Path.Command("G17") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G17 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01171(self): + """Test G17.1 command Generation.""" + # + c = Path.Command("G17.1") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G17.1 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01180(self): + """Test G18 command Generation.""" + # + c = Path.Command("G18") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G18 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01181(self): + """Test G18.1 command Generation.""" + # + c = Path.Command("G18.1") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G18.1 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01190(self): + """Test G19 command Generation.""" + # + c = Path.Command("G19") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G19 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01191(self): + """Test G19.1 command Generation.""" + # + c = Path.Command("G19.1") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G19.1 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01200(self): + """Test G20 command Generation.""" + # + c = Path.Command("G20") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G20 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01210(self): + """Test G21 command Generation.""" + # + c = Path.Command("G21") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G21 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01400(self): + """Test G40 command Generation.""" + # + c = Path.Command("G40") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G40 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01410(self): + """Test G41 command Generation.""" + # + c = Path.Command("G41 D1.23456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G41 D1 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + c = Path.Command("G41 D0") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G41 D0 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01411(self): + """Test G41.1 command Generation.""" + # + c = Path.Command("G41.1 D1.23456 L3") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G41.1 L3 D1.23456 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01420(self): + """Test G42 command Generation.""" + # + c = Path.Command("G42 D1.23456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G42 D1 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + c = Path.Command("G42 D0") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G42 D0 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01421(self): + """Test G42.1 command Generation.""" + # + c = Path.Command("G42.1 D1.23456 L3") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G42.1 L3 D1.23456 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) diff --git a/src/Mod/Path/PathTests/boxtest1.fcstd b/src/Mod/Path/PathTests/boxtest1.fcstd new file mode 100644 index 0000000000..210307fd72 Binary files /dev/null and b/src/Mod/Path/PathTests/boxtest1.fcstd differ diff --git a/src/Mod/Path/PathTests/drill_test1.FCStd b/src/Mod/Path/PathTests/drill_test1.FCStd new file mode 100644 index 0000000000..b79b620edb Binary files /dev/null and b/src/Mod/Path/PathTests/drill_test1.FCStd differ diff --git a/src/Mod/Path/PathTests/test_refactored_linuxcnc_imperial_ref.ngc b/src/Mod/Path/PathTests/test_refactored_linuxcnc_imperial_ref.ngc deleted file mode 100644 index 7b87914c10..0000000000 --- a/src/Mod/Path/PathTests/test_refactored_linuxcnc_imperial_ref.ngc +++ /dev/null @@ -1,40 +0,0 @@ -(begin preamble) -G17 G54 G40 G49 G80 G90 -G20 -(begin operation: G54) -(machine units: in/min) -G54 -G0 Z0.3543 -(finish operation: G54) -(begin operation: 5mm Endmill001) -(machine units: in/min) -(5mm Endmill001) -M5 -M6 T1 -G43 H1 -M3 S123 -(finish operation: 5mm Endmill001) -(begin operation: Profile) -(machine units: in/min) -(Profile) -(Compensated Tool Path. Diameter: 5.0) -G0 Z0.3543 -G0 X0.0000 Y0.0000 -G0 Y-0.0984 -G0 Y-0.0984 Z0.2756 -G1 X0.0000 Y-0.0984 Z-0.0394 F13.3937 -G2 X-0.0984 Y0.0000 Z-0.0394 I0.0000 J0.0984 F5.5276 -G1 X-0.0984 Y0.1181 Z-0.0394 F5.5276 -G2 X0.0000 Y0.2165 Z-0.0394 I0.0984 J0.0000 F5.5276 -G1 X0.1181 Y0.2165 Z-0.0394 F5.5276 -G2 X0.2165 Y0.1181 Z-0.0394 I0.0000 J-0.0984 F5.5276 -G1 X0.2165 Y0.0000 Z-0.0394 F5.5276 -G2 X0.1181 Y-0.0984 Z-0.0394 I-0.0984 J0.0000 F5.5276 -G1 X0.0000 Y-0.0984 Z-0.0394 F5.5276 -G0 Z0.3543 -G0 Z0.3543 -(finish operation: Profile) -(begin postamble) -M05 -G17 G54 G90 G80 G40 -M2 diff --git a/src/Mod/Path/PathTests/test_refactored_linuxcnc_metric_ref.ngc b/src/Mod/Path/PathTests/test_refactored_linuxcnc_metric_ref.ngc deleted file mode 100644 index e016c791e1..0000000000 --- a/src/Mod/Path/PathTests/test_refactored_linuxcnc_metric_ref.ngc +++ /dev/null @@ -1,40 +0,0 @@ -(begin preamble) -G17 G54 G40 G49 G80 G90 -G21 -(begin operation: G54) -(machine units: mm/min) -G54 -G0 Z9.000 -(finish operation: G54) -(begin operation: 5mm Endmill001) -(machine units: mm/min) -(5mm Endmill001) -M5 -M6 T1 -G43 H1 -M3 S123 -(finish operation: 5mm Endmill001) -(begin operation: Profile) -(machine units: mm/min) -(Profile) -(Compensated Tool Path. Diameter: 5.0) -G0 Z9.000 -G0 X0.000 Y0.000 -G0 Y-2.500 -G0 Y-2.500 Z7.000 -G1 X0.000 Y-2.500 Z-1.000 F340.200 -G2 X-2.500 Y0.000 Z-1.000 I0.000 J2.500 F140.400 -G1 X-2.500 Y3.000 Z-1.000 F140.400 -G2 X0.000 Y5.500 Z-1.000 I2.500 J0.000 F140.400 -G1 X3.000 Y5.500 Z-1.000 F140.400 -G2 X5.500 Y3.000 Z-1.000 I0.000 J-2.500 F140.400 -G1 X5.500 Y0.000 Z-1.000 F140.400 -G2 X3.000 Y-2.500 Z-1.000 I-2.500 J0.000 F140.400 -G1 X0.000 Y-2.500 Z-1.000 F140.400 -G0 Z9.000 -G0 Z9.000 -(finish operation: Profile) -(begin postamble) -M05 -G17 G54 G90 G80 G40 -M2 diff --git a/src/Mod/Path/PathTests/test_refactored_linuxcnc_other_way_ref.ngc b/src/Mod/Path/PathTests/test_refactored_linuxcnc_other_way_ref.ngc deleted file mode 100644 index 85d1480d23..0000000000 --- a/src/Mod/Path/PathTests/test_refactored_linuxcnc_other_way_ref.ngc +++ /dev/null @@ -1,40 +0,0 @@ -(begin preamble) -G17 G54 G40 G49 G80 G90 -G21 -(begin operation: G55) -(machine units: mm/min) -G55 -G0 Z9.000 -(finish operation: G55) -(begin operation: 5mm Endmill003) -(machine units: mm/min) -(5mm Endmill003) -M5 -M6 T2 -G43 H2 -M4 S123 -(finish operation: 5mm Endmill003) -(begin operation: Profile001) -(machine units: mm/min) -(Profile001) -(Compensated Tool Path. Diameter: 5.0) -G0 Z9.000 -G0 X0.000 Y0.000 -G0 Y-2.500 -G0 Y-2.500 Z7.000 -G1 X0.000 Y-2.500 Z-1.000 F340.200 -G1 X3.000 Y-2.500 Z-1.000 F140.400 -G3 X5.500 Y0.000 Z-1.000 I0.000 J2.500 F140.400 -G1 X5.500 Y3.000 Z-1.000 F140.400 -G3 X3.000 Y5.500 Z-1.000 I-2.500 J0.000 F140.400 -G1 X0.000 Y5.500 Z-1.000 F140.400 -G3 X-2.500 Y3.000 Z-1.000 I0.000 J-2.500 F140.400 -G1 X-2.500 Y0.000 Z-1.000 F140.400 -G3 X0.000 Y-2.500 Z-1.000 I2.500 J0.000 F140.400 -G0 Z9.000 -G0 Z9.000 -(finish operation: Profile001) -(begin postamble) -M05 -G17 G54 G90 G80 G40 -M2 diff --git a/src/Mod/Path/PathTests/test_refactored_linuxcnc_split_1_ref.ngc b/src/Mod/Path/PathTests/test_refactored_linuxcnc_split_1_ref.ngc deleted file mode 100644 index b05f7fbcb8..0000000000 --- a/src/Mod/Path/PathTests/test_refactored_linuxcnc_split_1_ref.ngc +++ /dev/null @@ -1,40 +0,0 @@ -(begin preamble) -G17 G54 G40 G49 G80 G90 -G21 -(begin operation: G54) -(machine units: mm/min) -G54 -G0 Z9.000 -(finish operation: G54) -(begin operation: 5mm Endmill006) -(machine units: mm/min) -(5mm Endmill006) -M5 -M6 T2 -G43 H2 -M3 S123 -(finish operation: 5mm Endmill006) -(begin operation: Profile002) -(machine units: mm/min) -(Profile002) -(Compensated Tool Path. Diameter: 5.0) -G0 Z9.000 -G0 X0.000 Y0.000 -G0 X-2.500 Y0.000 -G0 X-2.500 Y0.000 Z7.000 -G1 X-2.500 Y0.000 Z-1.000 F340.200 -G1 X-2.500 Y3.000 Z-1.000 F140.400 -G2 X0.000 Y5.500 Z-1.000 I2.500 J0.000 F140.400 -G1 X3.000 Y5.500 Z-1.000 F140.400 -G2 X5.500 Y3.000 Z-1.000 I0.000 J-2.500 F140.400 -G1 X5.500 Y0.000 Z-1.000 F140.400 -G2 X3.000 Y-2.500 Z-1.000 I-2.500 J0.000 F140.400 -G1 X0.000 Y-2.500 Z-1.000 F140.400 -G2 X-2.500 Y0.000 Z-1.000 I0.000 J2.500 F140.400 -G0 Z9.000 -G0 Z9.000 -(finish operation: Profile002) -(begin postamble) -M05 -G17 G54 G90 G80 G40 -M2 diff --git a/src/Mod/Path/PathTests/test_refactored_linuxcnc_split_2_ref.ngc b/src/Mod/Path/PathTests/test_refactored_linuxcnc_split_2_ref.ngc deleted file mode 100644 index f442e77f99..0000000000 --- a/src/Mod/Path/PathTests/test_refactored_linuxcnc_split_2_ref.ngc +++ /dev/null @@ -1,40 +0,0 @@ -(begin preamble) -G17 G54 G40 G49 G80 G90 -G21 -(begin operation: G55) -(machine units: mm/min) -G55 -G0 Z9.000 -(finish operation: G55) -(begin operation: 5mm Endmill006) -(machine units: mm/min) -(5mm Endmill006) -M5 -M6 T2 -G43 H2 -M3 S123 -(finish operation: 5mm Endmill006) -(begin operation: Profile002) -(machine units: mm/min) -(Profile002) -(Compensated Tool Path. Diameter: 5.0) -G0 Z9.000 -G0 X0.000 Y0.000 -G0 X-2.500 Y0.000 -G0 X-2.500 Y0.000 Z7.000 -G1 X-2.500 Y0.000 Z-1.000 F340.200 -G1 X-2.500 Y3.000 Z-1.000 F140.400 -G2 X0.000 Y5.500 Z-1.000 I2.500 J0.000 F140.400 -G1 X3.000 Y5.500 Z-1.000 F140.400 -G2 X5.500 Y3.000 Z-1.000 I0.000 J-2.500 F140.400 -G1 X5.500 Y0.000 Z-1.000 F140.400 -G2 X3.000 Y-2.500 Z-1.000 I-2.500 J0.000 F140.400 -G1 X0.000 Y-2.500 Z-1.000 F140.400 -G2 X-2.500 Y0.000 Z-1.000 I0.000 J2.500 F140.400 -G0 Z9.000 -G0 Z9.000 -(finish operation: Profile002) -(begin postamble) -M05 -G17 G54 G90 G80 G40 -M2 diff --git a/src/Mod/Path/PathTests/test_refactored_linuxcnc_split_3_ref.ngc b/src/Mod/Path/PathTests/test_refactored_linuxcnc_split_3_ref.ngc deleted file mode 100644 index 373ec42358..0000000000 --- a/src/Mod/Path/PathTests/test_refactored_linuxcnc_split_3_ref.ngc +++ /dev/null @@ -1,40 +0,0 @@ -(begin preamble) -G17 G54 G40 G49 G80 G90 -G21 -(begin operation: G56) -(machine units: mm/min) -G56 -G0 Z9.000 -(finish operation: G56) -(begin operation: 5mm Endmill006) -(machine units: mm/min) -(5mm Endmill006) -M5 -M6 T2 -G43 H2 -M3 S123 -(finish operation: 5mm Endmill006) -(begin operation: Profile002) -(machine units: mm/min) -(Profile002) -(Compensated Tool Path. Diameter: 5.0) -G0 Z9.000 -G0 X0.000 Y0.000 -G0 X-2.500 Y0.000 -G0 X-2.500 Y0.000 Z7.000 -G1 X-2.500 Y0.000 Z-1.000 F340.200 -G1 X-2.500 Y3.000 Z-1.000 F140.400 -G2 X0.000 Y5.500 Z-1.000 I2.500 J0.000 F140.400 -G1 X3.000 Y5.500 Z-1.000 F140.400 -G2 X5.500 Y3.000 Z-1.000 I0.000 J-2.500 F140.400 -G1 X5.500 Y0.000 Z-1.000 F140.400 -G2 X3.000 Y-2.500 Z-1.000 I-2.500 J0.000 F140.400 -G1 X0.000 Y-2.500 Z-1.000 F140.400 -G2 X-2.500 Y0.000 Z-1.000 I0.000 J2.500 F140.400 -G0 Z9.000 -G0 Z9.000 -(finish operation: Profile002) -(begin postamble) -M05 -G17 G54 G90 G80 G40 -M2 diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index 6f58743165..3d0a0606f9 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -38,9 +38,7 @@ from PathTests.TestPathHelixGenerator import TestPathHelixGenerator from PathTests.TestPathLog import TestPathLog from PathTests.TestPathOpTools import TestPathOpTools -# from PathTests.TestPathPost import PathPostTestCases -from PathTests.TestLinuxCNCPost import TestLinuxCNCPost -from PathTests.TestPathPost import OutputOrderingTestCases +# from PathTests.TestPathPost import TestPathPost from PathTests.TestPathPost import TestPathPostUtils from PathTests.TestPathPost import TestBuildPostList from PathTests.TestPathPost import TestOutputNameSubstitution @@ -61,11 +59,23 @@ from PathTests.TestPathUtil import TestPathUtil from PathTests.TestPathVcarve import TestPathVcarve from PathTests.TestPathVoronoi import TestPathVoronoi +from PathTests.TestCentroidPost import TestCentroidPost +from PathTests.TestGrblPost import TestGrblPost +from PathTests.TestLinuxCNCPost import TestLinuxCNCPost +from PathTests.TestMach3Mach4Post import TestMach3Mach4Post +from PathTests.TestRefactoredCentroidPost import TestRefactoredCentroidPost +from PathTests.TestRefactoredGrblPost import TestRefactoredGrblPost +from PathTests.TestRefactoredLinuxCNCPost import TestRefactoredLinuxCNCPost +from PathTests.TestRefactoredMach3Mach4Post import TestRefactoredMach3Mach4Post +from PathTests.TestRefactoredTestPost import TestRefactoredTestPost + # dummy usage to get flake8 and lgtm quiet False if depthTestCases.__name__ else True False if TestApp.__name__ else True +False if TestBuildPostList.__name__ else True False if TestDressupDogbone.__name__ else True False if TestHoldingTags.__name__ else True +False if TestOutputNameSubstitution.__name__ else True False if TestPathAdaptive.__name__ else True False if TestPathCore.__name__ else True False if TestPathDeburr.__name__ else True @@ -75,11 +85,7 @@ 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 TestPathPostImport.__name__ else True # False if TestPathPost.__name__ else True -False if TestBuildPostList.__name__ else True -False if TestOutputNameSubstitution.__name__ else True -False if TestLinuxCNCPost.__name__ else True False if TestPathPostUtils.__name__ else True False if TestPathPreferences.__name__ else True False if TestPathPropertyBag.__name__ else True @@ -98,3 +104,13 @@ False if TestPathVcarve.__name__ else True False if TestPathVoronoi.__name__ else True False if TestPathDrillGenerator.__name__ else True False if TestPathHelixGenerator.__name__ else True + +False if TestCentroidPost.__name__ else True +False if TestGrblPost.__name__ else True +False if TestLinuxCNCPost.__name__ else True +False if TestMach3Mach4Post.__name__ else True +False if TestRefactoredCentroidPost.__name__ else True +False if TestRefactoredGrblPost.__name__ else True +False if TestRefactoredLinuxCNCPost.__name__ else True +False if TestRefactoredMach3Mach4Post.__name__ else True +False if TestRefactoredTestPost.__name__ else True