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..f185678151 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}") @@ -187,13 +151,71 @@ 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" # 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 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 # openDialog = True @@ -235,7 +257,7 @@ def resolveFileName(job, subpartname, sequencenumber): def buildPostList(job): """Takes the job and determines the specific objects and order to postprocess Returns a list of objects which can be passed to - exportObjectsWith() for final posting""" + exportObjectsWith() for final posting.""" wcslist = job.Fixtures orderby = job.OrderOutputBy @@ -243,8 +265,8 @@ def buildPostList(job): if orderby == "Fixture": PathLog.debug("Ordering by Fixture") - # Order by fixture means all operations and tool changes will be completed in one - # fixture before moving to the next. + # Order by fixture means all operations and tool changes will be + # completed in one fixture before moving to the next. currTool = None for index, f in enumerate(wcslist): 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..96e9d6c831 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,18 +24,20 @@ # *************************************************************************** """ -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 @@ -147,7 +150,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 +159,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 @@ -190,7 +192,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 +202,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..df0895ccda --- /dev/null +++ b/src/Mod/Path/PathScripts/PostUtilsArguments.py @@ -0,0 +1,655 @@ +# -*- 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..9c9263f8af --- /dev/null +++ b/src/Mod/Path/PathScripts/PostUtilsExport.py @@ -0,0 +1,313 @@ +# -*- 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..ea2ed95663 --- /dev/null +++ b/src/Mod/Path/PathScripts/PostUtilsParse.py @@ -0,0 +1,579 @@ +# -*- 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 new file mode 100644 index 0000000000..87bc34f6bc --- /dev/null +++ b/src/Mod/Path/PathScripts/post/refactored_linuxcnc_post.py @@ -0,0 +1,189 @@ +# *************************************************************************** +# * 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 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 refactored_linuxcnc_post +refactored_linuxcnc_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 + # the order of parameters + # linuxcnc 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", + ] + # + # 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 + + +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..c77f133c88 --- /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..24ff17a9af --- /dev/null +++ b/src/Mod/Path/PathTests/TestGrblPost.py @@ -0,0 +1,297 @@ +# -*- 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 + # 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 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 new file mode 100644 index 0000000000..003865ba00 --- /dev/null +++ b/src/Mod/Path/PathTests/TestLinuxCNCPost.py @@ -0,0 +1,290 @@ +# -*- 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 linuxcnc_post as postprocessor + + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestLinuxCNCPost(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 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 + 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 = "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") + # 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] + + 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/TestMach3Mach4Post.py b/src/Mod/Path/PathTests/TestMach3Mach4Post.py new file mode 100644 index 0000000000..3902734e06 --- /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/TestPathHelixGenerator.py b/src/Mod/Path/PathTests/TestPathHelixGenerator.py index b6588901b7..c8d6504ca5 100644 --- a/src/Mod/Path/PathTests/TestPathHelixGenerator.py +++ b/src/Mod/Path/PathTests/TestPathHelixGenerator.py @@ -80,7 +80,6 @@ G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z18.000000\ G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z18.000000\ G0 X5.000000 Y5.000000 Z18.000000G0 Z20.000000" - def test00(self): """Test Basic Helix Generator Return""" args = _resetArgs() @@ -118,7 +117,8 @@ G0 X5.000000 Y5.000000 Z18.000000G0 Z20.000000" args["tool_diameter"] = 5.0 self.assertRaises(ValueError, generator.generate, **args) - # require tool fit 2: hole diameter not greater than tool diam with zero inner radius + # require tool fit 2: hole diameter not greater than tool diam + # with zero inner radius args["hole_radius"] = 2.0 args["inner_radius"] = 0.0 args["tool_diameter"] = 5.0 diff --git a/src/Mod/Path/PathTests/TestPathPost.py b/src/Mod/Path/PathTests/TestPathPost.py index c6bb4ebad6..3d980fc778 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,117 +21,199 @@ # * * # *************************************************************************** -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 testLinuxCNC(self): - from PathScripts.post import linuxcnc_post as postprocessor - - args = ( - "--no-header --no-line-numbers --no-comments --no-show-editor --precision=2" + # + # 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"), + ), ) - gcode = postprocessor.export(self.postlist, "gcode.tmp", args) - - referenceFile = ( - FreeCAD.getHomePath() + "Mod/Path/PathTests/test_linuxcnc_00.ngc" + # + # 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", ) - 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("testLinuxCNC.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) - - def testLinuxCNCImperial(self): - from PathScripts.post import linuxcnc_post as postprocessor - - args = "--no-header --no-line-numbers --no-comments --no-show-editor --precision=2 --inches" - gcode = postprocessor.export(self.postlist, "gcode.tmp", args) - - referenceFile = ( - FreeCAD.getHomePath() + "Mod/Path/PathTests/test_linuxcnc_10.ngc" - ) - 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("testLinuxCNCImplerial.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) - - 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" - ) - 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) + # + # 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"), @@ -194,7 +277,6 @@ class TestBuildPostList(unittest.TestCase): def tearDown(self): pass - def test000(self): # check that the test file is structured correctly @@ -323,12 +405,14 @@ 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) @@ -342,6 +426,9 @@ 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) @@ -349,20 +436,31 @@ 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) @@ -371,6 +469,9 @@ 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) @@ -380,15 +481,24 @@ 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) @@ -403,6 +513,9 @@ 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") @@ -413,6 +526,9 @@ 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) @@ -424,6 +540,9 @@ 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) @@ -440,6 +559,9 @@ 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") @@ -449,6 +571,9 @@ 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") @@ -464,6 +589,9 @@ 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") @@ -473,6 +601,9 @@ 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..4c49c03167 --- /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..9affd355ca --- /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..267dc10f84 --- /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..dab611262d --- /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..20be8a9f19 --- /dev/null +++ b/src/Mod/Path/PathTests/TestRefactoredTestPost.py @@ -0,0 +1,1278 @@ +# -*- 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_linuxcnc_00.ngc b/src/Mod/Path/PathTests/test_linuxcnc_00.ngc deleted file mode 100644 index 6256f88731..0000000000 --- a/src/Mod/Path/PathTests/test_linuxcnc_00.ngc +++ /dev/null @@ -1,68 +0,0 @@ -G17 G90 -G21 -(Default_Tool) -M6 T2 -M3 S0.00 -(Contour) -(Uncompensated Tool Path) -G0 Z15.00 -G90 -G17 -G0 Z15.00 -G0 X10.00 Y10.00 -G0 Z10.00 -G1 X10.00 Y10.00 Z9.00 -G1 X10.00 Y0.00 Z9.00 -G1 X0.00 Y0.00 Z9.00 -G1 X0.00 Y10.00 Z9.00 -G1 X10.00 Y10.00 Z9.00 -G1 X10.00 Y10.00 Z8.00 -G1 X10.00 Y0.00 Z8.00 -G1 X0.00 Y0.00 Z8.00 -G1 X0.00 Y10.00 Z8.00 -G1 X10.00 Y10.00 Z8.00 -G1 X10.00 Y10.00 Z7.00 -G1 X10.00 Y0.00 Z7.00 -G1 X0.00 Y0.00 Z7.00 -G1 X0.00 Y10.00 Z7.00 -G1 X10.00 Y10.00 Z7.00 -G1 X10.00 Y10.00 Z6.00 -G1 X10.00 Y0.00 Z6.00 -G1 X0.00 Y0.00 Z6.00 -G1 X0.00 Y10.00 Z6.00 -G1 X10.00 Y10.00 Z6.00 -G1 X10.00 Y10.00 Z5.00 -G1 X10.00 Y0.00 Z5.00 -G1 X0.00 Y0.00 Z5.00 -G1 X0.00 Y10.00 Z5.00 -G1 X10.00 Y10.00 Z5.00 -G1 X10.00 Y10.00 Z4.00 -G1 X10.00 Y0.00 Z4.00 -G1 X0.00 Y0.00 Z4.00 -G1 X0.00 Y10.00 Z4.00 -G1 X10.00 Y10.00 Z4.00 -G1 X10.00 Y10.00 Z3.00 -G1 X10.00 Y0.00 Z3.00 -G1 X0.00 Y0.00 Z3.00 -G1 X0.00 Y10.00 Z3.00 -G1 X10.00 Y10.00 Z3.00 -G1 X10.00 Y10.00 Z2.00 -G1 X10.00 Y0.00 Z2.00 -G1 X0.00 Y0.00 Z2.00 -G1 X0.00 Y10.00 Z2.00 -G1 X10.00 Y10.00 Z2.00 -G1 X10.00 Y10.00 Z1.00 -G1 X10.00 Y0.00 Z1.00 -G1 X0.00 Y0.00 Z1.00 -G1 X0.00 Y10.00 Z1.00 -G1 X10.00 Y10.00 Z1.00 -G1 X10.00 Y10.00 Z0.00 -G1 X10.00 Y0.00 Z0.00 -G1 X0.00 Y0.00 Z0.00 -G1 X0.00 Y10.00 Z0.00 -G1 X10.00 Y10.00 Z0.00 -G0 Z15.00 -M05 -G00 X-1.0 Y1.0 -G17 G90 -M2 diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index e4d397a126..3d0a0606f9 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -38,6 +38,7 @@ from PathTests.TestPathHelixGenerator import TestPathHelixGenerator from PathTests.TestPathLog import TestPathLog from PathTests.TestPathOpTools import TestPathOpTools +# from PathTests.TestPathPost import TestPathPost from PathTests.TestPathPost import TestPathPostUtils from PathTests.TestPathPost import TestBuildPostList from PathTests.TestPathPost import TestOutputNameSubstitution @@ -58,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 @@ -72,10 +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 TestPathPostUtils.__name__ else True False if TestPathPreferences.__name__ else True False if TestPathPropertyBag.__name__ else True @@ -94,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