Pārlūkot izejas kodu

- added a Laser preprocessor named 'Z_laser' which will change the Z to the Travel Z on each ToolChange event allowing therefore control of the dot size
- by default now a new blank Geometry object created by FlatCAM is of type multigeo
- made sure that optimizations of lines when importing SVG or DXF as lines will not encounter polygons but only LinesStrings or LinearRings, otherwise having crashes
- fixed the import SVG and import DXF, when importing as Geometry to be imported as multigeo tool
- fixed the import SVG and import DXF, the source files will be saved as loaded into the source_file attribute of the resulting object (be it Geometry or Gerber)

Marius Stanciu 5 gadi atpakaļ
vecāks
revīzija
b8fb64a143
7 mainītis faili ar 309 papildinājumiem un 85 dzēšanām
  1. 5 0
      CHANGELOG.md
  2. 71 71
      appEditors/AppGeoEditor.py
  3. 54 5
      appEditors/appGCodeEditor.py
  4. 29 3
      appObjects/AppObject.py
  5. 15 4
      app_Main.py
  6. 24 2
      camlib.py
  7. 111 0
      preprocessors/Z_laser.py

+ 5 - 0
CHANGELOG.md

@@ -11,6 +11,11 @@ CHANGELOG for FlatCAM beta
 
 
 - working on a proper GCode Editor
 - working on a proper GCode Editor
 - wip in the GCode Editor
 - wip in the GCode Editor
+- added a Laser preprocessor named 'Z_laser' which will change the Z to the Travel Z on each ToolChange event allowing therefore control of the dot size
+- by default now a new blank Geometry object created by FlatCAM is of type multigeo
+- made sure that optimizations of lines when importing SVG or DXF as lines will not encounter polygons but only LinesStrings or LinearRings, otherwise having crashes
+- fixed the import SVG and import DXF, when importing as Geometry to be imported as multigeo tool
+- fixed the import SVG and import DXF, the source files will be saved as loaded into the source_file attribute of the resulting object (be it Geometry or Gerber)
 
 
 21.07.2020
 21.07.2020
 
 

+ 71 - 71
appEditors/AppGeoEditor.py

@@ -3979,77 +3979,6 @@ class AppGeoEditor(QtCore.QObject):
         # self.storage = AppGeoEditor.make_storage()
         # self.storage = AppGeoEditor.make_storage()
         self.replot()
         self.replot()
 
 
-    def edit_fcgeometry(self, fcgeometry, multigeo_tool=None):
-        """
-        Imports the geometry from the given FlatCAM Geometry object
-        into the editor.
-
-        :param fcgeometry:      GeometryObject
-        :param multigeo_tool:   A tool for the case of the edited geometry being of type 'multigeo'
-        :return:                None
-        """
-        assert isinstance(fcgeometry, Geometry), "Expected a Geometry, got %s" % type(fcgeometry)
-
-        self.deactivate()
-        self.activate()
-
-        self.set_ui()
-
-        # Hide original geometry
-        self.fcgeometry = fcgeometry
-        fcgeometry.visible = False
-
-        # Set selection tolerance
-        DrawToolShape.tolerance = fcgeometry.drawing_tolerance * 10
-
-        self.select_tool("select")
-
-        if self.app.defaults['geometry_spindledir'] == 'CW':
-            if self.app.defaults['geometry_editor_milling_type'] == 'cl':
-                milling_type = 1    # CCW motion = climb milling (spindle is rotating CW)
-            else:
-                milling_type = -1   # CW motion = conventional milling (spindle is rotating CW)
-        else:
-            if self.app.defaults['geometry_editor_milling_type'] == 'cl':
-                milling_type = -1    # CCW motion = climb milling (spindle is rotating CCW)
-            else:
-                milling_type = 1   # CW motion = conventional milling (spindle is rotating CCW)
-
-        # Link shapes into editor.
-        if multigeo_tool:
-            self.multigeo_tool = multigeo_tool
-            geo_to_edit = self.flatten(geometry=fcgeometry.tools[self.multigeo_tool]['solid_geometry'],
-                                       orient_val=milling_type)
-            self.app.inform.emit(
-                '[WARNING_NOTCL] %s: %s %s: %s' % (
-                    _("Editing MultiGeo Geometry, tool"),
-                    str(self.multigeo_tool),
-                    _("with diameter"),
-                    str(fcgeometry.tools[self.multigeo_tool]['tooldia'])
-                )
-            )
-        else:
-            geo_to_edit = self.flatten(geometry=fcgeometry.solid_geometry, orient_val=milling_type)
-
-        for shape in geo_to_edit:
-            if shape is not None:
-                if type(shape) == Polygon:
-                    self.add_shape(DrawToolShape(shape.exterior))
-                    for inter in shape.interiors:
-                        self.add_shape(DrawToolShape(inter))
-                else:
-                    self.add_shape(DrawToolShape(shape))
-
-        self.replot()
-
-        # updated units
-        self.units = self.app.defaults['units'].upper()
-        self.decimals = self.app.decimals
-
-        # start with GRID toolbar activated
-        if self.app.ui.grid_snap_btn.isChecked() is False:
-            self.app.ui.grid_snap_btn.trigger()
-
     def on_buffer_tool(self):
     def on_buffer_tool(self):
         buff_tool = BufferSelectionTool(self.app, self)
         buff_tool = BufferSelectionTool(self.app, self)
         buff_tool.run()
         buff_tool.run()
@@ -4700,6 +4629,77 @@ class AppGeoEditor(QtCore.QObject):
 
 
         return snap_x, snap_y
         return snap_x, snap_y
 
 
+    def edit_fcgeometry(self, fcgeometry, multigeo_tool=None):
+        """
+        Imports the geometry from the given FlatCAM Geometry object
+        into the editor.
+
+        :param fcgeometry:      GeometryObject
+        :param multigeo_tool:   A tool for the case of the edited geometry being of type 'multigeo'
+        :return:                None
+        """
+        assert isinstance(fcgeometry, Geometry), "Expected a Geometry, got %s" % type(fcgeometry)
+
+        self.deactivate()
+        self.activate()
+
+        self.set_ui()
+
+        # Hide original geometry
+        self.fcgeometry = fcgeometry
+        fcgeometry.visible = False
+
+        # Set selection tolerance
+        DrawToolShape.tolerance = fcgeometry.drawing_tolerance * 10
+
+        self.select_tool("select")
+
+        if self.app.defaults['geometry_spindledir'] == 'CW':
+            if self.app.defaults['geometry_editor_milling_type'] == 'cl':
+                milling_type = 1    # CCW motion = climb milling (spindle is rotating CW)
+            else:
+                milling_type = -1   # CW motion = conventional milling (spindle is rotating CW)
+        else:
+            if self.app.defaults['geometry_editor_milling_type'] == 'cl':
+                milling_type = -1    # CCW motion = climb milling (spindle is rotating CCW)
+            else:
+                milling_type = 1   # CW motion = conventional milling (spindle is rotating CCW)
+
+        # Link shapes into editor.
+        if multigeo_tool:
+            self.multigeo_tool = multigeo_tool
+            geo_to_edit = self.flatten(geometry=fcgeometry.tools[self.multigeo_tool]['solid_geometry'],
+                                       orient_val=milling_type)
+            self.app.inform.emit(
+                '[WARNING_NOTCL] %s: %s %s: %s' % (
+                    _("Editing MultiGeo Geometry, tool"),
+                    str(self.multigeo_tool),
+                    _("with diameter"),
+                    str(fcgeometry.tools[self.multigeo_tool]['tooldia'])
+                )
+            )
+        else:
+            geo_to_edit = self.flatten(geometry=fcgeometry.solid_geometry, orient_val=milling_type)
+
+        for shape in geo_to_edit:
+            if shape is not None:
+                if type(shape) == Polygon:
+                    self.add_shape(DrawToolShape(shape.exterior))
+                    for inter in shape.interiors:
+                        self.add_shape(DrawToolShape(inter))
+                else:
+                    self.add_shape(DrawToolShape(shape))
+
+        self.replot()
+
+        # updated units
+        self.units = self.app.defaults['units'].upper()
+        self.decimals = self.app.decimals
+
+        # start with GRID toolbar activated
+        if self.app.ui.grid_snap_btn.isChecked() is False:
+            self.app.ui.grid_snap_btn.trigger()
+
     def update_fcgeometry(self, fcgeometry):
     def update_fcgeometry(self, fcgeometry):
         """
         """
         Transfers the geometry tool shape buffer to the selected geometry
         Transfers the geometry tool shape buffer to the selected geometry

+ 54 - 5
appEditors/appGCodeEditor.py

@@ -36,14 +36,15 @@ class AppGCodeEditor(QtCore.QObject):
 
 
         self.ui = AppGCodeEditorUI(app=self.app)
         self.ui = AppGCodeEditorUI(app=self.app)
 
 
-        # #################################################################################
-        # ################### SIGNALS #####################################################
-        # #################################################################################
-
         self.gcode_obj = None
         self.gcode_obj = None
         self.code_edited = ''
         self.code_edited = ''
 
 
     def set_ui(self):
     def set_ui(self):
+        """
+
+        :return:
+        :rtype:
+        """
         # #############################################################################################################
         # #############################################################################################################
         # ############# ADD a new TAB in the PLot Tab Area
         # ############# ADD a new TAB in the PLot Tab Area
         # #############################################################################################################
         # #############################################################################################################
@@ -73,9 +74,18 @@ class AppGCodeEditor(QtCore.QObject):
         self.ui.append_text.set_value(self.app.defaults["cncjob_append"])
         self.ui.append_text.set_value(self.app.defaults["cncjob_append"])
         self.ui.prepend_text.set_value(self.app.defaults["cncjob_prepend"])
         self.ui.prepend_text.set_value(self.app.defaults["cncjob_prepend"])
 
 
-        self.ui.exit_editor_button.buttonSave.clicked.connect(self.update_fcgcode)
+        # #################################################################################
+        # ################### SIGNALS #####################################################
+        # #################################################################################
+        self.ui.update_gcode_button.clicked.connect(self.insert_gcode)
+        self.ui.exit_editor_button.clicked.connect(self.update_fcgcode)
 
 
     def build_ui(self):
     def build_ui(self):
+        """
+
+        :return:
+        :rtype:
+        """
         # Remove anything else in the GUI Selected Tab
         # Remove anything else in the GUI Selected Tab
         self.app.ui.selected_scroll_area.takeWidget()
         self.app.ui.selected_scroll_area.takeWidget()
         # Put ourselves in the GUI Selected Tab
         # Put ourselves in the GUI Selected Tab
@@ -84,12 +94,27 @@ class AppGCodeEditor(QtCore.QObject):
         self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
         self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
 
 
     def ui_connect(self):
     def ui_connect(self):
+        """
+
+        :return:
+        :rtype:
+        """
         pass
         pass
 
 
     def ui_disconnect(self):
     def ui_disconnect(self):
+        """
+
+        :return:
+        :rtype:
+        """
         pass
         pass
 
 
     def handleTextChanged(self):
     def handleTextChanged(self):
+        """
+
+        :return:
+        :rtype:
+        """
         # enable = not self.ui.code_editor.document().isEmpty()
         # enable = not self.ui.code_editor.document().isEmpty()
         # self.ui.buttonPrint.setEnabled(enable)
         # self.ui.buttonPrint.setEnabled(enable)
         # self.ui.buttonPreview.setEnabled(enable)
         # self.ui.buttonPreview.setEnabled(enable)
@@ -97,7 +122,22 @@ class AppGCodeEditor(QtCore.QObject):
         self.buttonSave.setStyleSheet("QPushButton {color: red;}")
         self.buttonSave.setStyleSheet("QPushButton {color: red;}")
         self.buttonSave.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as_red.png'))
         self.buttonSave.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as_red.png'))
 
 
+    def insert_gcode(self):
+        """
+
+        :return:
+        :rtype:
+        """
+        pass
+
     def edit_fcgcode(self, cnc_obj):
     def edit_fcgcode(self, cnc_obj):
+        """
+
+        :param cnc_obj:
+        :type cnc_obj:
+        :return:
+        :rtype:
+        """
         assert isinstance(cnc_obj, CNCJobObject)
         assert isinstance(cnc_obj, CNCJobObject)
         self.gcode_obj = cnc_obj
         self.gcode_obj = cnc_obj
 
 
@@ -111,6 +151,11 @@ class AppGCodeEditor(QtCore.QObject):
         self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Editor'))
         self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Editor'))
 
 
     def update_fcgcode(self):
     def update_fcgcode(self):
+        """
+
+        :return:
+        :rtype:
+        """
         preamble = str(self.ui.prepend_text.get_value())
         preamble = str(self.ui.prepend_text.get_value())
         postamble = str(self.ui.append_text.get_value())
         postamble = str(self.ui.append_text.get_value())
         my_gcode = self.ui.gcode_editor_tab.code_editor.toPlainText()
         my_gcode = self.ui.gcode_editor_tab.code_editor.toPlainText()
@@ -120,7 +165,11 @@ class AppGCodeEditor(QtCore.QObject):
         self.ui.gcode_editor_tab.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
         self.ui.gcode_editor_tab.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
 
 
     def on_open_gcode(self):
     def on_open_gcode(self):
+        """
 
 
+        :return:
+        :rtype:
+        """
         _filter_ = "G-Code Files (*.nc);; G-Code Files (*.txt);; G-Code Files (*.tap);; G-Code Files (*.cnc);; " \
         _filter_ = "G-Code Files (*.nc);; G-Code Files (*.txt);; G-Code Files (*.tap);; G-Code Files (*.cnc);; " \
                    "All Files (*.*)"
                    "All Files (*.*)"
 
 

+ 29 - 3
appObjects/AppObject.py

@@ -223,11 +223,37 @@ class AppObject(QtCore.QObject):
 
 
         :return: None
         :return: None
         """
         """
+        outname = 'new_geo'
 
 
         def initialize(obj, app):
         def initialize(obj, app):
-            obj.multitool = False
-
-        self.new_object('geometry', 'new_geo', initialize, plot=False)
+            obj.multitool = True
+            obj.multigeo = True
+            # store here the default data for Geometry Data
+            default_data = {}
+
+            for opt_key, opt_val in app.options.items():
+                if opt_key.find('geometry' + "_") == 0:
+                    oname = opt_key[len('geometry') + 1:]
+                    default_data[oname] = self.app.options[opt_key]
+                if opt_key.find('tools_mill' + "_") == 0:
+                    oname = opt_key[len('tools_mill') + 1:]
+                    default_data[oname] = self.app.options[opt_key]
+
+            obj.tools = {}
+            obj.tools.update({
+                1: {
+                    'tooldia': float(app.defaults["geometry_cnctooldia"]),
+                    'offset': 'Path',
+                    'offset_value': 0.0,
+                    'type': _('Rough'),
+                    'tool_type': 'C1',
+                    'data': deepcopy(default_data),
+                    'solid_geometry': []
+                }
+            })
+            obj.tools[1]['data']['name'] = outname
+
+        self.new_object('geometry', outname, initialize, plot=False)
 
 
     def new_gerber_object(self):
     def new_gerber_object(self):
         """
         """

+ 15 - 4
app_Main.py

@@ -2193,9 +2193,10 @@ class App(QtCore.QObject):
                     if edited_object.tools[tool]['tooldia'] == selected_tooldia:
                     if edited_object.tools[tool]['tooldia'] == selected_tooldia:
                         multi_tool = tool
                         multi_tool = tool
                         break
                         break
-
+                log.debug("Editing MultiGeo Geometry with tool diameter: %s" % str(multi_tool))
                 self.geo_editor.edit_fcgeometry(edited_object, multigeo_tool=multi_tool)
                 self.geo_editor.edit_fcgeometry(edited_object, multigeo_tool=multi_tool)
             else:
             else:
+                log.debug("Editing SingleGeo Geometry with tool diameter.")
                 self.geo_editor.edit_fcgeometry(edited_object)
                 self.geo_editor.edit_fcgeometry(edited_object)
 
 
             # set call source to the Editor we go into
             # set call source to the Editor we go into
@@ -8780,9 +8781,15 @@ class App(QtCore.QObject):
         units = self.defaults['units'].upper()
         units = self.defaults['units'].upper()
 
 
         def obj_init(geo_obj, app_obj):
         def obj_init(geo_obj, app_obj):
-            geo_obj.import_svg(filename, obj_type, units=units)
-            geo_obj.multigeo = False
-            geo_obj.source_file = self.export_gerber(obj_name=name, filename=None, local_use=geo_obj, use_thread=False)
+            if obj_type == "geometry":
+                geo_obj.import_svg(filename, obj_type, units=units)
+            elif obj_type == "gerber":
+                geo_obj.import_svg(filename, obj_type, units=units)
+
+            geo_obj.multigeo = True
+            with open(filename) as f:
+                file_content = f.read()
+            geo_obj.source_file = file_content
 
 
         with self.proc_container.new(_("Importing SVG")) as proc:
         with self.proc_container.new(_("Importing SVG")) as proc:
 
 
@@ -8833,7 +8840,11 @@ class App(QtCore.QObject):
                 geo_obj.import_dxf_as_gerber(filename, units=units)
                 geo_obj.import_dxf_as_gerber(filename, units=units)
             else:
             else:
                 return "fail"
                 return "fail"
+
             geo_obj.multigeo = True
             geo_obj.multigeo = True
+            with open(filename) as f:
+                file_content = f.read()
+            geo_obj.source_file = file_content
 
 
         with self.proc_container.new(_("Importing DXF")):
         with self.proc_container.new(_("Importing DXF")):
 
 

+ 24 - 2
camlib.py

@@ -1058,6 +1058,7 @@ class Geometry(object):
             geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]
             geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]
 
 
         # trying to optimize the resulting geometry by merging contiguous lines
         # trying to optimize the resulting geometry by merging contiguous lines
+        geos = self.flatten(geos, reset=True, pathonly=True)
         geos = linemerge(geos)
         geos = linemerge(geos)
 
 
         # Add to object
         # Add to object
@@ -1081,12 +1082,31 @@ class Geometry(object):
             if flip:
             if flip:
                 # Change origin to bottom left
                 # Change origin to bottom left
                 for i in geos_text:
                 for i in geos_text:
-                    _, minimy, _, maximy = i.bounds
+                    __, minimy, __, maximy = i.bounds
                     h2 = (maximy - minimy) * 0.5
                     h2 = (maximy - minimy) * 0.5
                     geos_text_f.append(translate(scale(i, 1.0, -1.0, origin=(0, 0)), yoff=(h + h2)))
                     geos_text_f.append(translate(scale(i, 1.0, -1.0, origin=(0, 0)), yoff=(h + h2)))
             if geos_text_f:
             if geos_text_f:
                 self.solid_geometry = self.solid_geometry + geos_text_f
                 self.solid_geometry = self.solid_geometry + geos_text_f
 
 
+        tooldia = float(self.app.defaults["geometry_cnctooldia"])
+        tooldia = float('%.*f' % (self.decimals, tooldia))
+
+        new_data = {k: v for k, v in self.options.items()}
+
+        self.tools.update({
+            1: {
+                'tooldia': tooldia,
+                'offset': 'Path',
+                'offset_value': 0.0,
+                'type': _('Rough'),
+                'tool_type': 'C1',
+                'data': deepcopy(new_data),
+                'solid_geometry': self.solid_geometry
+            }
+        })
+
+        self.tools[1]['data']['name'] = self.options['name']
+
     def import_dxf_as_geo(self, filename, units='MM'):
     def import_dxf_as_geo(self, filename, units='MM'):
         """
         """
         Imports shapes from an DXF file into the object's geometry.
         Imports shapes from an DXF file into the object's geometry.
@@ -1103,6 +1123,7 @@ class Geometry(object):
         geos = getdxfgeo(dxf)
         geos = getdxfgeo(dxf)
 
 
         # trying to optimize the resulting geometry by merging contiguous lines
         # trying to optimize the resulting geometry by merging contiguous lines
+        geos = self.flatten(geos, reset=True, pathonly=True)
         geos = linemerge(geos)
         geos = linemerge(geos)
 
 
         # Add to object
         # Add to object
@@ -5176,7 +5197,8 @@ class CNCjob(Geometry):
 
 
         geo_storage = {}
         geo_storage = {}
         for geo in temp_solid_geometry:
         for geo in temp_solid_geometry:
-            geo_storage[geo.coords[0]] = geo
+            if not geo is None:
+                geo_storage[geo.coords[0]] = geo
         locations = list(geo_storage.keys())
         locations = list(geo_storage.keys())
 
 
         if opt_type == 'M':
         if opt_type == 'M':

+ 111 - 0
preprocessors/Z_laser.py

@@ -0,0 +1,111 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# File Author: Matthieu Berthomé                           #
+# Date: 5/26/2017                                          #
+# MIT Licence                                              #
+# ##########################################################
+
+from appPreProcessor import *
+
+# This post processor is configured to output code that
+# is compatible with almost any version of Grbl.
+
+
+class Z_laser(PreProc):
+
+    include_header = True
+    coordinate_format = "%.*f"
+    feedrate_format = '%.*f'
+
+    def start_code(self, p):
+        units = ' ' + str(p['units']).lower()
+        gcode = '(This preprocessor is used with a motion controller loaded with GRBL firmware. )\n'
+        gcode += '(It is for the case when it is used together with a LASER connected on the SPINDLE connector.)\n'
+        gcode += '(On toolchange event the laser will move to a defined Z height to change the laser dot size.)\n\n'
+
+        xmin = '%.*f' % (p.coords_decimals, p['options']['xmin'])
+        xmax = '%.*f' % (p.coords_decimals, p['options']['xmax'])
+        ymin = '%.*f' % (p.coords_decimals, p['options']['ymin'])
+        ymax = '%.*f' % (p.coords_decimals, p['options']['ymax'])
+
+        gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n'
+        gcode += '(Feedrate rapids: ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n'
+
+        gcode += '(Z Focus: ' + str(p['z_move']) + units + ')\n'
+
+        gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
+
+        if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
+            gcode += '(Preprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
+        else:
+            gcode += '(Preprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
+
+        gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
+        gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
+
+        gcode += '(Laser Power (Spindle Speed): ' + str(p['spindlespeed']) + ')\n\n'
+
+        gcode += ('G20' if p.units.upper() == 'IN' else 'G21') + "\n"
+        gcode += 'G90\n'
+        gcode += 'G17\n'
+        gcode += 'G94'
+
+        return gcode
+
+    def startz_code(self, p):
+        return ''
+
+    def lift_code(self, p):
+        return 'M5'
+
+    def down_code(self, p):
+        sdir = {'CW': 'M03', 'CCW': 'M04'}[p.spindledir]
+        if p.spindlespeed:
+            return '%s S%s' % (sdir, str(p.spindlespeed))
+        else:
+            return sdir
+
+    def toolchange_code(self, p):
+        return 'G00 Z' + self.coordinate_format % (p.coords_decimals, p.z_move)
+
+    def up_to_zero_code(self, p):
+        return 'M5'
+
+    def position_code(self, p):
+        return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \
+               (p.coords_decimals, p.x, p.coords_decimals, p.y)
+
+    def rapid_code(self, p):
+        return ('G00 ' + self.position_code(p)).format(**p)
+
+    def linear_code(self, p):
+        return ('G01 ' + self.position_code(p)).format(**p) + \
+               ' F' + str(self.feedrate_format % (p.fr_decimals, p.feedrate))
+
+    def end_code(self, p):
+        coords_xy = p['xy_end']
+        gcode = ('G00 Z' + self.feedrate_format % (p.fr_decimals, p.z_end) + "\n")
+
+        if coords_xy and coords_xy != '':
+            gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
+        return gcode
+
+    def feedrate_code(self, p):
+        return 'G01 F' + str(self.feedrate_format % (p.fr_decimals, p.feedrate))
+
+    def z_feedrate_code(self, p):
+        return 'G01 F' + str(self.feedrate_format % (p.fr_decimals, p.z_feedrate))
+
+    def spindle_code(self, p):
+        sdir = {'CW': 'M03', 'CCW': 'M04'}[p.spindledir]
+        if p.spindlespeed:
+            return '%s S%s' % (sdir, str(p.spindlespeed))
+        else:
+            return sdir
+
+    def dwell_code(self, p):
+        return ''
+
+    def spindle_stop_code(self, p):
+        return 'M5'