diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 42b9dd98f..8608684b8 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -11,6 +11,7 @@ INSTALL( InitGui.py PathCommands.py TestPathApp.py + DESTINATION Mod/Path ) @@ -71,10 +72,10 @@ SET(PathScripts_SRCS PathScripts/dynapath_post.py PathScripts/example_post.py PathScripts/example_pre.py - PathScripts/generic_post.py PathScripts/linuxcnc_post.py PathScripts/opensbp_post.py PathScripts/opensbp_pre.py + PathScripts/phillips_post.py PathScripts/rml_post.py PathScripts/slic3r_pre.py ) diff --git a/src/Mod/Path/PathCommands.py b/src/Mod/Path/PathCommands.py index d837c98be..95618b0b1 100644 --- a/src/Mod/Path/PathCommands.py +++ b/src/Mod/Path/PathCommands.py @@ -42,7 +42,8 @@ class _CommandSelectLoop: def GetResources(self): return {'Pixmap' : 'Path-SelectLoop', 'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_SelectLoop","Finish Selecting Loop"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_SelectLoop","Complete loop selection from two edges")} + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_SelectLoop","Complete loop selection from two edges"), + 'CmdType': "ForEdit"} def IsActive(self): if bool(FreeCADGui.Selection.getSelection()) is False: @@ -77,29 +78,3 @@ class _CommandSelectLoop: if FreeCAD.GuiUp: FreeCADGui.addCommand('Path_SelectLoop',_CommandSelectLoop()) - -def findShape(shape,subname=None,subtype=None): - '''To find a higher oder shape containing the subshape with subname. - E.g. to find the wire containing 'Edge1' in shape, - findShape(shape,'Edge1','Wires') - ''' - if not subname: - return shape - ret = shape.getElement(subname) - if not subtype or not ret or ret.isNull(): - return ret; - if subname.startswith('Face'): - tp = 'Faces' - elif subname.startswith('Edge'): - tp = 'Edges' - elif subname.startswith('Vertex'): - tp = 'Vertex' - else: - return ret - for obj in getattr(shape,subtype): - for sobj in getattr(obj,tp): - if sobj.isEqual(ret): - return obj - return ret - - diff --git a/src/Mod/Path/PathScripts/PathPost.py b/src/Mod/Path/PathScripts/PathPost.py index 38b2c3f68..4505368c5 100644 --- a/src/Mod/Path/PathScripts/PathPost.py +++ b/src/Mod/Path/PathScripts/PathPost.py @@ -80,7 +80,6 @@ class DlgSelectPostProcessor: class CommandPathPost: def resolveFileName(self, job): - #print("resolveFileName(%s)" % job.Label) path = PathPreferences.defaultOutputFile() if job.OutputFile: path = job.OutputFile @@ -135,7 +134,6 @@ class CommandPathPost: else: filename = None - #print("resolveFileName(%s, %s) -> '%s'" % (path, policy, filename)) return filename def resolvePostProcessor(self, job): diff --git a/src/Mod/Path/PathScripts/centroid_post.py b/src/Mod/Path/PathScripts/centroid_post.py index ce42dc60b..2977f9bc8 100644 --- a/src/Mod/Path/PathScripts/centroid_post.py +++ b/src/Mod/Path/PathScripts/centroid_post.py @@ -27,7 +27,6 @@ TOOLTIP=''' example post for Centroid CNC mill''' import FreeCAD import datetime now = datetime.datetime.now() -import Path, PathScripts from PathScripts import PostUtils diff --git a/src/Mod/Path/PathScripts/dumper_post.py b/src/Mod/Path/PathScripts/dumper_post.py index f5c7f3ef9..1268d3018 100644 --- a/src/Mod/Path/PathScripts/dumper_post.py +++ b/src/Mod/Path/PathScripts/dumper_post.py @@ -69,6 +69,7 @@ def export(objectslist, filename,argstring): final = output print("done postprocessing.") + return final def parse(pathobj): diff --git a/src/Mod/Path/PathScripts/dynapath_post.py b/src/Mod/Path/PathScripts/dynapath_post.py index 10f6ad29c..c2a919e66 100644 --- a/src/Mod/Path/PathScripts/dynapath_post.py +++ b/src/Mod/Path/PathScripts/dynapath_post.py @@ -180,7 +180,7 @@ def export(objectslist,filename,argstring): print("done postprocessing.") gfile = pythonopen(filename,"wb") - gfile.write(gcode) + gfile.write(final) gfile.close() diff --git a/src/Mod/Path/PathScripts/example_pre.py b/src/Mod/Path/PathScripts/example_pre.py index 0a90cf3af..fd06fb32e 100644 --- a/src/Mod/Path/PathScripts/example_pre.py +++ b/src/Mod/Path/PathScripts/example_pre.py @@ -1,25 +1,25 @@ -#*************************************************************************** -#* (c) Yorik van Havre (yorik@uncreated.net) 2014 * -#* * -#* 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 * -#* * -#***************************************************************************/ +# *************************************************************************** +# * (c) Yorik van Havre (yorik@uncreated.net) 2014 * +# * * +# * 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 * +# * * +# ***************************************************************************/ ''' @@ -31,7 +31,8 @@ Read the Path Workbench documentation to know how to create Path objects from GCode. ''' -import os, Path +import os +import Path import FreeCAD # to distinguish python built-in open function from the one declared below @@ -43,21 +44,20 @@ def open(filename): "called when freecad opens a file." docname = os.path.splitext(os.path.basename(filename))[0] doc = FreeCAD.newDocument(docname) - insert(filename,doc.Name) + insert(filename, doc.Name) -def insert(filename,docname): +def insert(filename, docname): "called when freecad imports a file" gfile = pythonopen(filename) gcode = gfile.read() gfile.close() gcode = parse(gcode) doc = FreeCAD.getDocument(docname) - obj = doc.addObject("Path::Feature","Path") + obj = doc.addObject("Path::Feature", "Path") path = Path.Path(gcode) obj.Path = path - def parse(inputstring): "parse(inputstring): returns a parsed output string" print("preprocessing...") @@ -98,4 +98,3 @@ def parse(inputstring): print(__name__ + " gcode preprocessor loaded.") - diff --git a/src/Mod/Path/PathScripts/generic_post.py b/src/Mod/Path/PathScripts/generic_post.py deleted file mode 100644 index 92786c491..000000000 --- a/src/Mod/Path/PathScripts/generic_post.py +++ /dev/null @@ -1,424 +0,0 @@ -# -*- coding: utf-8 -*- - -#*************************************************************************** -#* * -#* Copyright (c) 2016 Christoph Blaue * -#* * -#* 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 __future__ import print_function -TOOLTIP='''Post processor for Maho M 600E mill - -Machines with Philips or Heidenhain control should be very easy to adapt. - -The post processor is configurable by changing the values of constants. -No programming experience required. This can make a generated g-code -program more readable and since older machines have very -limited memory it seems sensible to reduce the number of commands and -parameters, like e.g. suppress the units in the header and at every hop. -''' - -# reload in python console: -# import generic_post -# reload(generic_post) - -''' example post for Maho M 600E mill''' -import FreeCAD -import time -import Path, PathScripts -from PathScripts import PostUtils -import math - -#*************************************************************************** -# user editable stuff here - -MACHINE_NAME = 'Maho 600E' -CORNER_MIN = {'x':-51.877, 'y':0, 'z':0 } #use metric for internal units -CORNER_MAX = {'x':591.5, 'y':391.498, 'z':391.5 } #use metric for internal units - -UNITS = 'G21' # use metric units - # possible values: - # 'G20' for inches, - # 'G21' for metric units. - # a mapping to different GCodes is handled in the GCODE MAP below - -UNITS_INCLUDED = False # do not include the units in the GCode program - # possible values: - # True if units should be included - # False if units should not be included - # usually the units to be used are defined in the machine constants and almost never change, - # so this can be set to False. - -COMMENT = '' - # possible values: - # ';' centroid or sinumerik comment symbol, - # '' leave blank for bracketed comments style "(comment comment comment)" - # '...' any other symbol to start comments - # currently this can be only one symbol, if it should be a sequence of characters - # in PostUtils.py the line - # if len(commentsym)==1: - # should be changed to - # if len(commentsym)>1: - -SHOW_EDITOR = True - # possible values: - # True before the file is written it is shown to the user for inspection - # False the file is written directly - -LINENUMBERS = True - # possible values: - # True if linenumbers of the form N1 N2 ... should be included - # False linennumbers are suppressed - # if linenumbers are used, header and footer get numbered as well - -STARTLINENR = 1 # first linenumber used - # possible values: - # any integer value >= 0 - # to have the possibility to change some initial values directly at the CNC machine - # without renumbering the rest it is possible to start the numbering of the file with some value > 0 - -LINENUMBER_INCREMENT = 1 - # possible values: - # any integer value > 0 - # similar to STARTLINENR it is possible to leave gaps in the linenumbering of subsequent lines - -MODAL = True - # possible values: - # True repeated GCodes in subsequent lines are suppressed, like in the following snippet - # G1 X10 Y20 - # X10 Y30 - # False repeated GCodes in subsequent lines are repeated in the GCode file - # G1 X10 Y20 - # G1 X10 Y30 - -MODALPARAMS = ['X','Y','Z','S','F'] # suppress these parameters if they haven't changed - # possible values: - # any list of GCode parameters - # if a parameter doesn't change from one line to the next ( or even further) it is suppressed. - # Example: - # G1 X10 Y20 - # G1 Y30 - # If in addition MODAL is set to True, the generated GCode changes to - # G1 X10 Y20 - # Y30 - -SWAP_G2_G3 = True # some machines have the sign of the X-axis swapped, so they behave like milling from the bottom - # possible values: - # True if left and right turns are to be swapped - # False don't swap - # this might be special with some maho machines or even with mine and might be changed in the machine constants as well - -SWAP_Y_Z = True # machines with an angle milling head do not switch axes, so we do it here - # possible values: - # True if Y and Z values have to be swapped - # False do not swap - # For vertical milling machines the Z-axis is horizontal (of course). - # If they have an angle milling head, they mill vertical, alas the Z-axis stays horizontal. - # With this parameter we can swap the output values of Y and Z. - # For commands G2 and G3 this means that J and K are swapped as well - -ABSOLUTE_CIRCLE_CENTER = True - # possible values: - # True use absolute values for the circle center in commands G2, G3 - # False values for I, J, K are given relative to the last point - -USE_RADIUS_IF_POSSIBLE = True - # possible values: - # True if in commands G2 and G3 the usage of radius R is preferred - # False if in commands G2 and G3 we use always I and J - # When milling arcs there are two reasons to use the radius instead of the center: - # 1. the GCode program might be easier to understand - # 2. Some machines seem to have a different scheme for calculating / rounding the values of the center - # Thus it is possible that the machine complains, that the endpoint of the arc does not lie on the arc. - # Using the radius instead avoids this problem. - # The post processor takes care of the fact, that only angles <= 180 degrees can be used with R - # for larger angles the center is used independent of the setting of this constant - -RADIUS_COMMENT = True - # possible values: - # True for better understanding the radius of an arc is included as a comment - # False no additional comment is included - # In case the comments are included they are always included with the bracketing syntax like '(R20.456)' - # and never with the comment symbol, because the radius might appear in the middle of a line. - -GCODE_MAP = {'M1':'M0', 'M6':'M66', 'G20':'G70', 'G21':'G71'} # cb: this could be used to swap G2/G3 - # possible values: - # Comma separated list of values of the form 'sourceGCode':'targetGCode' - # - # Although the basic movement commands G0, G1, G2 seem to be used uniformly in different GCode dialects, - # this is not the case for all commands. - # E.g the Maho dialect uses G70 and G71 for the units inches vs. metric. - # The map {'M1':'M0', 'G20':'G70', 'G21':'G71'} maps the optional stop command M1 to M0, - # because some Maho machines do not have the optional button on its panel - # in addition it maps inches G20 to G70 and metric G21 to G71 - -AXIS_DECIMALS = 3 - # possible values: - # integer >= 0 - -FEED_DECIMALS = 2 - # possible values: - # integer >= 0 - -SPINDLE_DECIMALS = 0 - # possible values: - # integer >= 0 - -# The header is divided into two parts, one is dynamic, the other is a static GCode header. -# If the current selection and the current time should be included in the header, -# it has to be generated at execution time, and thus it cannot be held in constant values. -# The last linefeed should be ommitted, it is inserted automatically -# linenumbers are inserted automatically if LINENUMBERS is True -# if you don't want to use this header you have to provide a minimal function -# def mkHeader(selection): -# return '' - - - -def mkHeader(selection): - # this is within a function, because otherwise filename and time don't change when changing the FreeCAD project -# now = datetime.datetime.now() - now = time.strftime("%Y-%m-%d %H:%M") - originfile = FreeCAD.ActiveDocument.FileName - headerNoNumber = "%PM\n" # this line gets no linenumber - if hasattr(selection[0],"Description"): - description = selection[0].Description - else: - description = "" - headerNoNumber += "N9XXX (" + description + ", " + now + ")\n" # this line gets no linenumber, it is already a specially numbered - header = "" -# header += "(Output Time:" + str(now) + ")\n" - header += "(" + originfile + ")\n" - header += "(Exported by FreeCAD)\n" - header += "(Post Processor: " + __name__ +")\n" - header += "(Target machine: " + MACHINE_NAME + ")" - return headerNoNumber + linenumberify(header) - -GCODE_HEADER = "" # do not terminate with a newline, it is inserted by linenumberify -#GCODE_HEADER = "G40 G90" # do not terminate with a newline, it is inserted by linenumberify - #possible values: - # any sequence of GCode, multiple lines are welcome - # this constant header follows the text generated by the function mkheader - # linenumbers are inserted automatically if LINENUMBERS is True - -GCODE_FOOTER = "M30" # do not terminate with a newline, it is inserted by linenumberify - #possible values: - # any sequence of GCode, multiple lines are welcome - # the footer is used to clean things up, reset modal commands and stop the machine - # linenumbers are inserted automatically if LINENUMBERS is True - -# don't edit with the stuff below the next line unless you know what you're doing :) -#*************************************************************************** - -linenr = 0 # variable has to be global because it is used by linenumberify and export - -if open.__module__ == '__builtin__': - pythonopen = open - -def angleUnder180(command,lastX,lastY,x,y,i,j): - # radius R can be used iff angle is < 180. - # This is the case - # if the previous point is left of the current and the center is below (or on) the connection line - # or if the previous point is right of the current and the center is above (or on) the connection line - middleOfLineY = (lastY + y)/2 - centerY = lastY + j - if ((command == 'G2' and ( (lastX == x and ((lastY=0) or (lastY > y and i <= 0))) or (lastX < x and centerY <= middleOfLineY) or (lastX > x and centerY >= middleOfLineY))) - or (command == 'G3' and ((lastX == x and ((lastY y and i >= 0))) or (lastX < x and centerY >= middleOfLineY) or (lastX > x and centerY <= middleOfLineY)))): - return True - else: - return False - -def mapGCode(command): - if command in GCODE_MAP: - mappedCommand = GCODE_MAP[command] - else: - mappedCommand = command - if SWAP_G2_G3: - if command == 'G2': - mappedCommand = 'G3' - elif command == 'G3': - mappedCommand = 'G2' - return mappedCommand - -def linenumberify(GCodeString): - # add a linenumber at every beginning of line - global linenr - if not LINENUMBERS: - result = GCodeString + "\n" - else: - result = ''; - strList = GCodeString.split("\n") - for s in strList: - if s: - # only non empty lines get numbered. the special lines "%PM" and prognumber "N9XXX" are skipped - - result += "N" + str(linenr) + " " + s + "\n" - linenr += LINENUMBER_INCREMENT - else: - result += s + "\n" - return result - -def export(selection,filename,argstring): - global linenr - linenr = STARTLINENR - lastX = 0 - lastY = 0 - lastZ = 0 - params = ['X','Y','Z','A','B','I','J','F','H','S','T','Q','R','L'] #Using XY plane most of the time so skipping K - modalParamsDict = dict() - for mp in MODALPARAMS: - modalParamsDict[mp] = None - for obj in selection: - if not hasattr(obj,"Path"): - print("the object " + obj.Name + " is not a path. Please select only path and Compounds.") - return - myMachine = None - for pathobj in selection: - if hasattr(pathobj,"MachineName"): - myMachine = pathobj.MachineName - if hasattr(pathobj, "MachineUnits"): - if pathobj.MachineUnits == "Metric": - UNITS = "G21" - else: - UNITS = "G20" - if myMachine is None: - print("No machine found in this selection") - - gcode ='' - gcode+= mkHeader(selection) - gcode+= linenumberify(GCODE_HEADER) - if UNITS_INCLUDED: - gcode += linenumberify(mapGCode(UNITS)) - - lastcommand = None - - gobjects = [] - for g in selection[0].Group: - if g.Name <>'Machine': #filtering out gcode home position from Machine object - gobjects.append(g) - - for obj in gobjects: - if hasattr(obj,'Comment'): - gcode += linenumberify('(' + obj.Comment + ')') - for c in obj.Path.Commands: - outstring = [] - command = c.Name - - if (command != UNITS or UNITS_INCLUDED): - if command[0]=='(': - command = PostUtils.fcoms(command, COMMENT) - mappedCommand = mapGCode(command) # the mapping is done for output only! For internal things we still use the old value. - - if not MODAL or command != lastcommand: - outstring.append(mappedCommand) -# if MODAL == True: ) -# #\better: append iff MODAL == False ) -# if command == lastcommand: ) -# outstring.pop(0!#\ ) - if c.Parameters >= 1: - for param in params: - if param in c.Parameters: - if (param in MODALPARAMS) and (modalParamsDict[str(param)] == c.Parameters[str(param)]): - # do nothing or append white space - outstring.append(' ') - elif param == 'F': - outstring.append(param + PostUtils.fmt(c.Parameters['F'], FEED_DECIMALS,UNITS)) - elif param == 'H': - outstring.append(param + str(int(c.Parameters['H']))) - elif param == 'S': - outstring.append(param + PostUtils.fmt(c.Parameters['S'], SPINDLE_DECIMALS,'G21')) #rpm is unitless-therefore I had to 'fake it out' by using metric units which don't get converted from entered value - elif param == 'T': - outstring.append(param + str(int(c.Parameters['T']))) - elif param == 'I' and (command == 'G2' or command == 'G3'): - # this is the special case for circular paths, where relative coordinates have to be changed to absolute - i = c.Parameters['I'] - # calculate the radius r - j = c.Parameters['J'] - r = math.sqrt(i**2 + j**2) - if USE_RADIUS_IF_POSSIBLE and angleUnder180(command,lastX,lastY,c.Parameters['X'],c.Parameters['Y'],i,j): - outstring.append('R' + PostUtils.fmt(r,AXIS_DECIMALS,UNITS)) - else: - if RADIUS_COMMENT: - outstring.append('(R' + PostUtils.fmt(r,AXIS_DECIMALS,UNITS) + ')') - if ABSOLUTE_CIRCLE_CENTER: - i += lastX - outstring.append(param + PostUtils.fmt(i,AXIS_DECIMALS,UNITS)) - elif param == 'J' and (command == 'G2' or command == 'G3'): - # this is the special case for circular paths, where incremental center has to be changed to absolute center - i = c.Parameters['I'] - j = c.Parameters['J'] - if USE_RADIUS_IF_POSSIBLE and angleUnder180(command,lastX,lastY,c.Parameters['X'],c.Parameters['Y'],i,j): - # R is handled with the I parameter, here: do nothing at all, keep the structure as with I command - pass - else: - if ABSOLUTE_CIRCLE_CENTER: - j += lastY - if SWAP_Y_Z: - # we have to swap j and k as well - outstring.append('K' + PostUtils.fmt(j,AXIS_DECIMALS,UNITS)) - else: - outstring.append(param + PostUtils.fmt(j,AXIS_DECIMALS,UNITS)) - elif param == 'K' and (command == 'G2' or command == 'G3'): - # this is the special case for circular paths, where incremental center has to be changed to absolute center - outstring.append('(' + param + PostUtils.fmt(c.Parameters[param],AXIS_DECIMALS,UNITS) + ')') - z = c.Parameters['Z'] - k = c.Parameters['K'] - if USE_RADIUS_IF_POSSIBLE and angleUnder180(command,lastX,lastY,c.Parameters['X'],c.Parameters['Y'],i,j): - # R is handled with the I parameter, here: do nothing at all, keep the structure as with I command - pass - else: - if ABSOLUTE_CIRCLE_CENTER: - k += lastZ - if SWAP_Y_Z: - # we have to swap j and k as well - outstring.append('J' + PostUtils.fmt(j,AXIS_DECIMALS,UNITS)) - else: - outstring.append(param + PostUtils.fmt(j,AXIS_DECIMALS,UNITS)) - elif param == 'Y' and SWAP_Y_Z: - outstring.append('Z' + PostUtils.fmt(c.Parameters[param],AXIS_DECIMALS,UNITS)) - elif param == 'Z' and SWAP_Y_Z: - outstring.append('Y' + PostUtils.fmt(c.Parameters[param],AXIS_DECIMALS,UNITS)) - else: - outstring.append(param + PostUtils.fmt(c.Parameters[param],AXIS_DECIMALS,UNITS)) - - if param in MODALPARAMS: - modalParamsDict[str(param)] = c.Parameters[param] - # save the last X, Y, Z values - if 'X' in c.Parameters: - lastX = c.Parameters['X'] - if 'Y' in c.Parameters: - lastY = c.Parameters['Y'] - if 'Z' in c.Parameters: - lastZ = c.Parameters['Z'] - outstr = str(outstring) - outstr =outstr.replace(']','') - outstr =outstr.replace('[','') - outstr =outstr.replace("'",'') - outstr =outstr.replace(",",'') - if LINENUMBERS: - gcode += "N" + str(linenr) + " " - linenr += LINENUMBER_INCREMENT - gcode+= outstr + '\n' - lastcommand = c.Name - gcode+= linenumberify(GCODE_FOOTER) - if SHOW_EDITOR: - PostUtils.editor(gcode) - gfile = pythonopen(filename,"wb") - gfile.write(gcode) - gfile.close() diff --git a/src/Mod/Path/PathScripts/opensbp_post.py b/src/Mod/Path/PathScripts/opensbp_post.py index fbef1fc0d..8b657ed8c 100644 --- a/src/Mod/Path/PathScripts/opensbp_post.py +++ b/src/Mod/Path/PathScripts/opensbp_post.py @@ -141,7 +141,7 @@ def export(objectslist, filename, argstring): # Write the output gfile = pythonopen(filename, "wb") - gfile.write(gcode) + gfile.write(final) gfile.close() @@ -325,5 +325,3 @@ def linenumber(): print(__name__ + " gcode postprocessor loaded.") - -# eof diff --git a/src/Mod/Path/PathScripts/opensbp_pre.py b/src/Mod/Path/PathScripts/opensbp_pre.py index 00c3b1721..50e34243d 100644 --- a/src/Mod/Path/PathScripts/opensbp_pre.py +++ b/src/Mod/Path/PathScripts/opensbp_pre.py @@ -93,7 +93,6 @@ def parse(inputstring): movecommand = ['G1', 'G0', 'G02', 'G03'] for l in lines: - # print l # remove any leftover trailing and preceding spaces l = l.strip() if not l: diff --git a/src/Mod/Path/PathScripts/phillips_post.py b/src/Mod/Path/PathScripts/phillips_post.py new file mode 100644 index 000000000..8b1223b83 --- /dev/null +++ b/src/Mod/Path/PathScripts/phillips_post.py @@ -0,0 +1,469 @@ +# -*- coding: utf-8 -*- + +#*************************************************************************** +#* * +#* Copyright (c) 2016 Christoph Blaue * +#* * +#* 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 * +#* * +#*************************************************************************** + +TOOLTIP = '''Post processor for Maho M 600E mill + +Machines with Philips or Heidenhain control should be very easy to adapt. + +The post processor is configurable by changing the values of constants. +No programming experience required. This can make a generated g-code +program more readable and since older machines have very +limited memory it seems sensible to reduce the number of commands and +parameters, like e.g. suppress the units in the header and at every hop. +''' + +# reload in python console: +# import generic_post +# reload(generic_post) + +''' example post for Maho M 600E mill''' +import FreeCAD +import time +from PathScripts import PostUtils +import math + +#*************************************************************************** +# user editable stuff here + +MACHINE_NAME = 'Maho 600E' +CORNER_MIN = {'x': -51.877, 'y': 0, 'z': 0} # use metric for internal units +# use metric for internal units +CORNER_MAX = {'x': 591.5, 'y': 391.498, 'z': 391.5} + +UNITS = 'G21' # use metric units +# possible values: +# 'G20' for inches, +# 'G21' for metric units. +# a mapping to different GCodes is handled in the GCODE MAP below + +UNITS_INCLUDED = False # do not include the units in the GCode program +# possible values: +# True if units should be included +# False if units should not be included +# usually the units to be used are defined in the machine constants and almost never change, +# so this can be set to False. + +COMMENT = '' +# possible values: +# ';' centroid or sinumerik comment symbol, +# '' leave blank for bracketed comments style "(comment comment comment)" +# '...' any other symbol to start comments +# currently this can be only one symbol, if it should be a sequence of characters +# in PostUtils.py the line +# if len(commentsym)==1: +# should be changed to +# if len(commentsym)>1: + +SHOW_EDITOR = True +# possible values: +# True before the file is written it is shown to the user for inspection +# False the file is written directly + +LINENUMBERS = True +# possible values: +# True if linenumbers of the form N1 N2 ... should be included +# False linennumbers are suppressed +# if linenumbers are used, header and footer get numbered as well + +STARTLINENR = 1 # first linenumber used +# possible values: +# any integer value >= 0 +# to have the possibility to change some initial values directly at the CNC machine +# without renumbering the rest it is possible to start the numbering of +# the file with some value > 0 + +LINENUMBER_INCREMENT = 1 +# possible values: +# any integer value > 0 +# similar to STARTLINENR it is possible to leave gaps in the linenumbering +# of subsequent lines + +MODAL = True +# possible values: +# True repeated GCodes in subsequent lines are suppressed, like in the following snippet +# G1 X10 Y20 +# X10 Y30 +# False repeated GCodes in subsequent lines are repeated in the GCode file +# G1 X10 Y20 +# G1 X10 Y30 + +# suppress these parameters if they haven't changed +MODALPARAMS = ['X', 'Y', 'Z', 'S', 'F'] +# possible values: +# any list of GCode parameters +# if a parameter doesn't change from one line to the next ( or even further) it is suppressed. +# Example: +# G1 X10 Y20 +# G1 Y30 +# If in addition MODAL is set to True, the generated GCode changes to +# G1 X10 Y20 +# Y30 + +SWAP_G2_G3 = True # some machines have the sign of the X-axis swapped, so they behave like milling from the bottom +# possible values: +# True if left and right turns are to be swapped +# False don't swap +# this might be special with some maho machines or even with mine and +# might be changed in the machine constants as well + +SWAP_Y_Z = True # machines with an angle milling head do not switch axes, so we do it here +# possible values: +# True if Y and Z values have to be swapped +# False do not swap +# For vertical milling machines the Z-axis is horizontal (of course). +# If they have an angle milling head, they mill vertical, alas the Z-axis stays horizontal. +# With this parameter we can swap the output values of Y and Z. +# For commands G2 and G3 this means that J and K are swapped as well + +ABSOLUTE_CIRCLE_CENTER = True +# possible values: +# True use absolute values for the circle center in commands G2, G3 +# False values for I, J, K are given relative to the last point + +USE_RADIUS_IF_POSSIBLE = True +# possible values: +# True if in commands G2 and G3 the usage of radius R is preferred +# False if in commands G2 and G3 we use always I and J +# When milling arcs there are two reasons to use the radius instead of the center: +# 1. the GCode program might be easier to understand +# 2. Some machines seem to have a different scheme for calculating / rounding the values of the center +# Thus it is possible that the machine complains, that the endpoint of the arc does not lie on the arc. +# Using the radius instead avoids this problem. +# The post processor takes care of the fact, that only angles <= 180 degrees can be used with R +# for larger angles the center is used independent of the setting of this +# constant + +RADIUS_COMMENT = True +# possible values: +# True for better understanding the radius of an arc is included as a comment +# False no additional comment is included +# In case the comments are included they are always included with the bracketing syntax like '(R20.456)' +# and never with the comment symbol, because the radius might appear in +# the middle of a line. + +GCODE_MAP = {'M1': 'M0', 'M6': 'M66', 'G20': 'G70', + 'G21': 'G71'} # cb: this could be used to swap G2/G3 +# possible values: +# Comma separated list of values of the form 'sourceGCode':'targetGCode' +# +# Although the basic movement commands G0, G1, G2 seem to be used uniformly in different GCode dialects, +# this is not the case for all commands. +# E.g the Maho dialect uses G70 and G71 for the units inches vs. metric. +# The map {'M1':'M0', 'G20':'G70', 'G21':'G71'} maps the optional stop command M1 to M0, +# because some Maho machines do not have the optional button on its panel +# in addition it maps inches G20 to G70 and metric G21 to G71 + +AXIS_DECIMALS = 3 +# possible values: +# integer >= 0 + +FEED_DECIMALS = 2 +# possible values: +# integer >= 0 + +SPINDLE_DECIMALS = 0 +# possible values: +# integer >= 0 + +# The header is divided into two parts, one is dynamic, the other is a static GCode header. +# If the current selection and the current time should be included in the header, +# it has to be generated at execution time, and thus it cannot be held in constant values. +# The last linefeed should be ommitted, it is inserted automatically +# linenumbers are inserted automatically if LINENUMBERS is True +# if you don't want to use this header you have to provide a minimal function +# def mkHeader(selection): +# return '' + + +def mkHeader(selection): + # this is within a function, because otherwise filename and time don't change when changing the FreeCAD project + # now = datetime.datetime.now() + now = time.strftime("%Y-%m-%d %H:%M") + originfile = FreeCAD.ActiveDocument.FileName + headerNoNumber = "%PM\n" # this line gets no linenumber + if hasattr(selection[0], "Description"): + description = selection[0].Description + else: + description = "" + # this line gets no linenumber, it is already a specially numbered + headerNoNumber += "N9XXX (" + description + ", " + now + ")\n" + header = "" +# header += "(Output Time:" + str(now) + ")\n" + header += "(" + originfile + ")\n" + header += "(Exported by FreeCAD)\n" + header += "(Post Processor: " + __name__ + ")\n" + header += "(Target machine: " + MACHINE_NAME + ")" + return headerNoNumber + linenumberify(header) + +GCODE_HEADER = "" # do not terminate with a newline, it is inserted by linenumberify +# GCODE_HEADER = "G40 G90" # do not terminate with a newline, it is inserted by linenumberify +# possible values: +# any sequence of GCode, multiple lines are welcome +# this constant header follows the text generated by the function mkheader +# linenumbers are inserted automatically if LINENUMBERS is True + +# do not terminate with a newline, it is inserted by linenumberify +GCODE_FOOTER = "M30" +# possible values: +# any sequence of GCode, multiple lines are welcome +# the footer is used to clean things up, reset modal commands and stop the machine +# linenumbers are inserted automatically if LINENUMBERS is True + +# don't edit with the stuff below the next line unless you know what you're doing :) +#*************************************************************************** + +linenr = 0 # variable has to be global because it is used by linenumberify and export + +if open.__module__ == '__builtin__': + pythonopen = open + + +def angleUnder180(command, lastX, lastY, x, y, i, j): + # radius R can be used iff angle is < 180. + # This is the case + # if the previous point is left of the current and the center is below (or on) the connection line + # or if the previous point is right of the current and the center is above + # (or on) the connection line + middleOfLineY = (lastY + y) / 2 + centerY = lastY + j + if ((command == 'G2' and ((lastX == x and ((lastY < y and i >= 0) or (lastY > y and i <= 0))) or (lastX < x and centerY <= middleOfLineY) or (lastX > x and centerY >= middleOfLineY))) + or (command == 'G3' and ((lastX == x and ((lastY < y and i <= 0) or (lastY > y and i >= 0))) or (lastX < x and centerY >= middleOfLineY) or (lastX > x and centerY <= middleOfLineY)))): + return True + else: + return False + + +def mapGCode(command): + if command in GCODE_MAP: + mappedCommand = GCODE_MAP[command] + else: + mappedCommand = command + if SWAP_G2_G3: + if command == 'G2': + mappedCommand = 'G3' + elif command == 'G3': + mappedCommand = 'G2' + return mappedCommand + + +def linenumberify(GCodeString): + # add a linenumber at every beginning of line + global linenr + if not LINENUMBERS: + result = GCodeString + "\n" + else: + result = '' + strList = GCodeString.split("\n") + for s in strList: + if s: + # only non empty lines get numbered. the special lines "%PM" + # and prognumber "N9XXX" are skipped + + result += "N" + str(linenr) + " " + s + "\n" + linenr += LINENUMBER_INCREMENT + else: + result += s + "\n" + return result + + +def export(selection, filename, argstring): + global linenr + linenr = STARTLINENR + lastX = 0 + lastY = 0 + lastZ = 0 + params = ['X', 'Y', 'Z', 'A', 'B', 'I', 'J', 'F', 'H', 'S', 'T', + 'Q', 'R', 'L'] # Using XY plane most of the time so skipping K + modalParamsDict = dict() + for mp in MODALPARAMS: + modalParamsDict[mp] = None + for obj in selection: + if not hasattr(obj, "Path"): + print("the object " + obj.Name + " is not a path. Please select only path and Compounds.") + return + myMachine = None + for pathobj in selection: + if hasattr(pathobj, "MachineName"): + myMachine = pathobj.MachineName + if hasattr(pathobj, "MachineUnits"): + if pathobj.MachineUnits == "Metric": + UNITS = "G21" + else: + UNITS = "G20" + if myMachine is None: + print("No machine found in this selection") + + gcode = '' + gcode += mkHeader(selection) + gcode += linenumberify(GCODE_HEADER) + if UNITS_INCLUDED: + gcode += linenumberify(mapGCode(UNITS)) + + lastcommand = None + + gobjects = [] + for g in selection[0].Group: + if g.Name <> 'Machine': # filtering out gcode home position from Machine object + gobjects.append(g) + + for obj in gobjects: + if hasattr(obj, 'Comment'): + gcode += linenumberify('(' + obj.Comment + ')') + for c in obj.Path.Commands: + outstring = [] + command = c.Name + + if (command != UNITS or UNITS_INCLUDED): + if command[0] == '(': + command = PostUtils.fcoms(command, COMMENT) + # the mapping is done for output only! For internal things we + # still use the old value. + mappedCommand = mapGCode(command) + + if not MODAL or command != lastcommand: + outstring.append(mappedCommand) +# if MODAL == True: ) +# #\better: append iff MODAL == False ) +# if command == lastcommand: ) +# outstring.pop(0!#\ ) + if c.Parameters >= 1: + for param in params: + if param in c.Parameters: + if (param in MODALPARAMS) and (modalParamsDict[str(param)] == c.Parameters[str(param)]): + # do nothing or append white space + outstring.append(' ') + elif param == 'F': + outstring.append( + param + PostUtils.fmt(c.Parameters['F'], FEED_DECIMALS, UNITS)) + elif param == 'H': + outstring.append( + param + str(int(c.Parameters['H']))) + elif param == 'S': + # rpm is unitless-therefore I had to 'fake it + # out' by using metric units which don't get + # converted from entered value + outstring.append( + param + PostUtils.fmt(c.Parameters['S'], SPINDLE_DECIMALS, 'G21')) + elif param == 'T': + outstring.append( + param + str(int(c.Parameters['T']))) + elif param == 'I' and (command == 'G2' or command == 'G3'): + # this is the special case for circular paths, + # where relative coordinates have to be changed + # to absolute + i = c.Parameters['I'] + # calculate the radius r + j = c.Parameters['J'] + r = math.sqrt(i**2 + j**2) + if USE_RADIUS_IF_POSSIBLE and angleUnder180(command, lastX, lastY, c.Parameters['X'], c.Parameters['Y'], i, j): + outstring.append( + 'R' + PostUtils.fmt(r, AXIS_DECIMALS, UNITS)) + else: + if RADIUS_COMMENT: + outstring.append( + '(R' + PostUtils.fmt(r, AXIS_DECIMALS, UNITS) + ')') + if ABSOLUTE_CIRCLE_CENTER: + i += lastX + outstring.append( + param + PostUtils.fmt(i, AXIS_DECIMALS, UNITS)) + elif param == 'J' and (command == 'G2' or command == 'G3'): + # this is the special case for circular paths, + # where incremental center has to be changed to + # absolute center + i = c.Parameters['I'] + j = c.Parameters['J'] + if USE_RADIUS_IF_POSSIBLE and angleUnder180(command, lastX, lastY, c.Parameters['X'], c.Parameters['Y'], i, j): + # R is handled with the I parameter, here: + # do nothing at all, keep the structure as + # with I command + pass + else: + if ABSOLUTE_CIRCLE_CENTER: + j += lastY + if SWAP_Y_Z: + # we have to swap j and k as well + outstring.append( + 'K' + PostUtils.fmt(j, AXIS_DECIMALS, UNITS)) + else: + outstring.append( + param + PostUtils.fmt(j, AXIS_DECIMALS, UNITS)) + elif param == 'K' and (command == 'G2' or command == 'G3'): + # this is the special case for circular paths, + # where incremental center has to be changed to + # absolute center + outstring.append( + '(' + param + PostUtils.fmt(c.Parameters[param], AXIS_DECIMALS, UNITS) + ')') + z = c.Parameters['Z'] + k = c.Parameters['K'] + if USE_RADIUS_IF_POSSIBLE and angleUnder180(command, lastX, lastY, c.Parameters['X'], c.Parameters['Y'], i, j): + # R is handled with the I parameter, here: + # do nothing at all, keep the structure as + # with I command + pass + else: + if ABSOLUTE_CIRCLE_CENTER: + k += lastZ + if SWAP_Y_Z: + # we have to swap j and k as well + outstring.append( + 'J' + PostUtils.fmt(j, AXIS_DECIMALS, UNITS)) + else: + outstring.append( + param + PostUtils.fmt(j, AXIS_DECIMALS, UNITS)) + elif param == 'Y' and SWAP_Y_Z: + outstring.append( + 'Z' + PostUtils.fmt(c.Parameters[param], AXIS_DECIMALS, UNITS)) + elif param == 'Z' and SWAP_Y_Z: + outstring.append( + 'Y' + PostUtils.fmt(c.Parameters[param], AXIS_DECIMALS, UNITS)) + else: + outstring.append( + param + PostUtils.fmt(c.Parameters[param], AXIS_DECIMALS, UNITS)) + + if param in MODALPARAMS: + modalParamsDict[str(param)] = c.Parameters[ + param] + # save the last X, Y, Z values + if 'X' in c.Parameters: + lastX = c.Parameters['X'] + if 'Y' in c.Parameters: + lastY = c.Parameters['Y'] + if 'Z' in c.Parameters: + lastZ = c.Parameters['Z'] + outstr = str(outstring) + outstr = outstr.replace(']', '') + outstr = outstr.replace('[', '') + outstr = outstr.replace("'", '') + outstr = outstr.replace(",", '') + if LINENUMBERS: + gcode += "N" + str(linenr) + " " + linenr += LINENUMBER_INCREMENT + gcode += outstr + '\n' + lastcommand = c.Name + gcode += linenumberify(GCODE_FOOTER) + if SHOW_EDITOR: + PostUtils.editor(gcode) + gfile = pythonopen(filename, "wb") + gfile.write(gcode) + gfile.close() diff --git a/src/Mod/Path/PathScripts/rml_post.py b/src/Mod/Path/PathScripts/rml_post.py index edd9835d5..470c86a93 100644 --- a/src/Mod/Path/PathScripts/rml_post.py +++ b/src/Mod/Path/PathScripts/rml_post.py @@ -72,7 +72,7 @@ def setjog(): return "" def addheader(): - return [ "PA;PA;" ] # absolute positioning + return [ "PA;PA;" ] # absolute positioning def addfooter(): return [] @@ -103,7 +103,7 @@ def feed(x=None, y=None, z=None, state={}): pass # XXX: is this used? return c -def jog(x=None, y=None, z=None, state={}): +def jog(x=None, y=None, z=None, state={}): c = [] if x is not None and y is not None: x, y = float(x), float(y) @@ -137,7 +137,6 @@ def xyarc(args, state): # TODO: consider direction #print('p = Part.ArcOfCircle(Part.Circle(FreeCAD.Vector(%f, %f), FreeCAD.Vector(0, 0, 1), %f), %f, %f)' % (center.x, center.y, radius, p0, p1)) for p in points: - #print('p', p.x, p.y) c += feed(p.x, p.y, state['Z'], state) return c @@ -216,18 +215,18 @@ def convertgcode(cmd, args, state): def parse(inputstring): "parse(inputstring): returns a parsed output string" - + state = { 'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'XYspeed': -1.0, 'Zspeed': -1.0 } output = [] # header output += addheader() output += motoron() - + output += speed(2.0, 1.0, state) # defaults # TODO: respect clearance height - + # treat the input line by line lines = inputstring.split("\n") for line in lines: diff --git a/src/Mod/Path/PathScripts/slic3r_pre.py b/src/Mod/Path/PathScripts/slic3r_pre.py index 004fb5aa4..82c77f77f 100644 --- a/src/Mod/Path/PathScripts/slic3r_pre.py +++ b/src/Mod/Path/PathScripts/slic3r_pre.py @@ -1,32 +1,32 @@ -#*************************************************************************** -#* (c) Yorik van Havre (yorik@uncreated.net) 2015 * -#* * -#* 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 * -#* * -#***************************************************************************/ - +# *************************************************************************** +# * (c) Yorik van Havre (yorik@uncreated.net) 2015 * +# * * +# * 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 * +# * * +# ***************************************************************************/ ''' This is an preprocessor to read gcode files produced from slic3r. ''' -import os, Path +import os +import Path import FreeCAD # to distinguish python built-in open function from the one declared below @@ -38,30 +38,30 @@ def open(filename): "called when freecad opens a file." docname = os.path.splitext(os.path.basename(filename))[0] doc = FreeCAD.newDocument(docname) - insert(filename,doc.Name) + insert(filename, doc.Name) -def insert(filename,docname): +def insert(filename, docname): "called when freecad imports a file" gfile = pythonopen(filename) gcode = gfile.read() gfile.close() gcode = parse(gcode) doc = FreeCAD.getDocument(docname) - obj = doc.addObject("Path::Feature","Path") + obj = doc.addObject("Path::Feature", "Path") path = Path.Path(gcode) obj.Path = path - + def parse(inputstring): "parse(inputstring): returns a parsed output string" print("preprocessing...") - + # split the input by line lines = inputstring.split("\n") output = "" lastcommand = None - + for l in lines: # remove any leftover trailing and preceding spaces l = l.strip() @@ -70,12 +70,12 @@ def parse(inputstring): continue if l[0].upper() in ["N"]: # remove line numbers - l = l.split(" ",1)[1] + l = l.split(" ", 1)[1] if ";" in l: # replace ; comments with () - l = l.replace(";","(") - l = l+")" - if l[0].upper() in ["G","M","("]: + l = l.replace(";", "(") + l = l + ")" + if l[0].upper() in ["G", "M", "("]: # found a G or M command: we store it output += l + "\n" last = l[0].upper() @@ -88,7 +88,7 @@ def parse(inputstring): elif lastcommand: # no G or M command: we repeat the last one output += lastcommand + " " + l + "\n" - + print("done preprocessing.") return output