Merge pull request #7007 from LarryWoestman/post_refactor

Path:  (WIP) Add some refactored postprocessors along with some tests.
This commit is contained in:
sliptonic
2022-07-12 09:29:12 -05:00
committed by GitHub
29 changed files with 6604 additions and 254 deletions

View File

@@ -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
)

View File

@@ -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):

View File

@@ -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

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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)

View File

@@ -0,0 +1,655 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> *
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2015 Dan Falck <ddfalck@gmail.com> *
# * Copyright (c) 2018, 2019 Gauthier Briere *
# * Copyright (c) 2019, 2020 Schildkroet *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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 <x>,<y>,<z>, 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)

View File

@@ -0,0 +1,313 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> *
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2015 Dan Falck <ddfalck@gmail.com> *
# * Copyright (c) 2018, 2019 Gauthier Briere *
# * Copyright (c) 2019, 2020 Schildkroet *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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 = "<None>"
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

View File

@@ -0,0 +1,579 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> *
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2015 Dan Falck <ddfalck@gmail.com> *
# * Copyright (c) 2018, 2019 Gauthier Briere *
# * Copyright (c) 2019, 2020 Schildkroet *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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

View File

@@ -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 = "<None>"

View File

@@ -407,6 +407,8 @@ def export(objectslist, filename, argstring):
gfile.write(final)
gfile.close()
return final
def linenumber():
if not OUTPUT_LINE_NUMBERS:

View File

@@ -0,0 +1,255 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2015 Dan Falck <ddfalck@gmail.com> *
# * Copyright (c) 2020 Schildkroet *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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)

View File

@@ -0,0 +1,222 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2018, 2019 Gauthier Briere *
# * Copyright (c) 2019, 2020 Schildkroet *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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)

View File

@@ -0,0 +1,189 @@
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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)

View File

@@ -0,0 +1,196 @@
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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)

View File

@@ -0,0 +1,216 @@
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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)

View File

@@ -0,0 +1,308 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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)

View File

@@ -0,0 +1,297 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * *
# * 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)

View File

@@ -0,0 +1,290 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * *
# * 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)

View File

@@ -0,0 +1,291 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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)

View File

@@ -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

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2016 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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")

View File

@@ -0,0 +1,290 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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)

View File

@@ -0,0 +1,286 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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)

View File

@@ -0,0 +1,288 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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)

View File

@@ -0,0 +1,287 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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)

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@@ -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

View File

@@ -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