Explorar el Código

Merged in marius_stanciu/flatcam_beta/Beta (pull request #274)

Beta
Marius Stanciu hace 6 años
padre
commit
ac54dd9bd7

+ 56 - 32
FlatCAMApp.py

@@ -830,6 +830,8 @@ class App(QtCore.QObject):
             "tools_transform_offset_y": 0.0,
             "tools_transform_offset_y": 0.0,
             "tools_transform_mirror_reference": False,
             "tools_transform_mirror_reference": False,
             "tools_transform_mirror_point": (0, 0),
             "tools_transform_mirror_point": (0, 0),
+            "tools_transform_buffer_dis": 0.0,
+            "tools_transform_buffer_corner": True,
 
 
             # SolderPaste Tool
             # SolderPaste Tool
             "tools_solderpaste_tools": "1.0, 0.3",
             "tools_solderpaste_tools": "1.0, 0.3",
@@ -1432,6 +1434,8 @@ class App(QtCore.QObject):
             "tools_transform_offset_y": self.ui.tools_defaults_form.tools_transform_group.offy_entry,
             "tools_transform_offset_y": self.ui.tools_defaults_form.tools_transform_group.offy_entry,
             "tools_transform_mirror_reference": self.ui.tools_defaults_form.tools_transform_group.mirror_reference_cb,
             "tools_transform_mirror_reference": self.ui.tools_defaults_form.tools_transform_group.mirror_reference_cb,
             "tools_transform_mirror_point": self.ui.tools_defaults_form.tools_transform_group.flip_ref_entry,
             "tools_transform_mirror_point": self.ui.tools_defaults_form.tools_transform_group.flip_ref_entry,
+            "tools_transform_buffer_dis": self.ui.tools_defaults_form.tools_transform_group.buffer_entry,
+            "tools_transform_buffer_corner": self.ui.tools_defaults_form.tools_transform_group.buffer_rounded_cb,
 
 
             # SolderPaste Dispensing Tool
             # SolderPaste Dispensing Tool
             "tools_solderpaste_tools": self.ui.tools_defaults_form.tools_solderpaste_group.nozzle_tool_dia_entry,
             "tools_solderpaste_tools": self.ui.tools_defaults_form.tools_solderpaste_group.nozzle_tool_dia_entry,
@@ -8852,12 +8856,14 @@ class App(QtCore.QObject):
                             # create the selection box around the selected object
                             # create the selection box around the selected object
                             if self.defaults['global_selection_shape'] is True:
                             if self.defaults['global_selection_shape'] is True:
                                 self.draw_selection_shape(obj)
                                 self.draw_selection_shape(obj)
+                                obj.selection_shape_drawn = True
                             self.collection.set_active(obj.options['name'])
                             self.collection.set_active(obj.options['name'])
                     else:
                     else:
                         if poly_selection.intersects(poly_obj):
                         if poly_selection.intersects(poly_obj):
                             # create the selection box around the selected object
                             # create the selection box around the selected object
                             if self.defaults['global_selection_shape'] is True:
                             if self.defaults['global_selection_shape'] is True:
                                 self.draw_selection_shape(obj)
                                 self.draw_selection_shape(obj)
+                                obj.selection_shape_drawn = True
                             self.collection.set_active(obj.options['name'])
                             self.collection.set_active(obj.options['name'])
             except Exception as e:
             except Exception as e:
                 # the Exception here will happen if we try to select on screen and we have an newly (and empty)
                 # the Exception here will happen if we try to select on screen and we have an newly (and empty)
@@ -8904,20 +8910,26 @@ class App(QtCore.QObject):
                         # create the selection box around the selected object
                         # create the selection box around the selected object
                         if self.defaults['global_selection_shape'] is True:
                         if self.defaults['global_selection_shape'] is True:
                             self.draw_selection_shape(curr_sel_obj)
                             self.draw_selection_shape(curr_sel_obj)
+                            curr_sel_obj.selection_shape_drawn = True
 
 
-                    elif self.collection.get_active().options['name'] not in objects_under_the_click_list:
+                    elif curr_sel_obj.options['name'] not in objects_under_the_click_list:
                         self.on_objects_selection(False)
                         self.on_objects_selection(False)
                         self.delete_selection_shape()
                         self.delete_selection_shape()
+                        curr_sel_obj.selection_shape_drawn = False
 
 
                         self.collection.set_active(objects_under_the_click_list[0])
                         self.collection.set_active(objects_under_the_click_list[0])
                         curr_sel_obj = self.collection.get_active()
                         curr_sel_obj = self.collection.get_active()
-
                         # create the selection box around the selected object
                         # create the selection box around the selected object
                         if self.defaults['global_selection_shape'] is True:
                         if self.defaults['global_selection_shape'] is True:
                             self.draw_selection_shape(curr_sel_obj)
                             self.draw_selection_shape(curr_sel_obj)
+                            curr_sel_obj.selection_shape_drawn = True
 
 
                         self.selected_message(curr_sel_obj=curr_sel_obj)
                         self.selected_message(curr_sel_obj=curr_sel_obj)
 
 
+                    elif curr_sel_obj.selection_shape_drawn is False:
+                        if self.defaults['global_selection_shape'] is True:
+                            self.draw_selection_shape(curr_sel_obj)
+                            curr_sel_obj.selection_shape_drawn = True
                     else:
                     else:
                         self.on_objects_selection(False)
                         self.on_objects_selection(False)
                         self.delete_selection_shape()
                         self.delete_selection_shape()
@@ -8932,6 +8944,7 @@ class App(QtCore.QObject):
                     # make active the first element of the overlapped objects list
                     # make active the first element of the overlapped objects list
                     if self.collection.get_active() is None:
                     if self.collection.get_active() is None:
                         self.collection.set_active(objects_under_the_click_list[0])
                         self.collection.set_active(objects_under_the_click_list[0])
+                        objects_under_the_click_list[0].selection_shape_drawn = True
 
 
                     name_sel_obj = self.collection.get_active().options['name']
                     name_sel_obj = self.collection.get_active().options['name']
                     # In case that there is a selected object but it is not in the overlapped object list
                     # In case that there is a selected object but it is not in the overlapped object list
@@ -8949,9 +8962,12 @@ class App(QtCore.QObject):
                     curr_sel_obj = self.collection.get_active()
                     curr_sel_obj = self.collection.get_active()
                     # delete the possible selection box around a possible selected object
                     # delete the possible selection box around a possible selected object
                     self.delete_selection_shape()
                     self.delete_selection_shape()
+                    curr_sel_obj.selection_shape_drawn = False
+
                     # create the selection box around the selected object
                     # create the selection box around the selected object
                     if self.defaults['global_selection_shape'] is True:
                     if self.defaults['global_selection_shape'] is True:
                         self.draw_selection_shape(curr_sel_obj)
                         self.draw_selection_shape(curr_sel_obj)
+                        curr_sel_obj.selection_shape_drawn = True
 
 
                     self.selected_message(curr_sel_obj=curr_sel_obj)
                     self.selected_message(curr_sel_obj=curr_sel_obj)
 
 
@@ -8961,6 +8977,9 @@ class App(QtCore.QObject):
                 # delete the possible selection box around a possible selected object
                 # delete the possible selection box around a possible selected object
                 self.delete_selection_shape()
                 self.delete_selection_shape()
 
 
+                for o in self.collection.get_list():
+                    o.selection_shape_drawn = False
+
                 # and as a convenience move the focus to the Project tab because Selected tab is now empty but
                 # and as a convenience move the focus to the Project tab because Selected tab is now empty but
                 # only when working on App
                 # only when working on App
                 if self.call_source == 'app':
                 if self.call_source == 'app':
@@ -11512,26 +11531,28 @@ class App(QtCore.QObject):
         App.log.debug(" **************** Started PROEJCT loading... **************** ")
         App.log.debug(" **************** Started PROEJCT loading... **************** ")
 
 
         for obj in d['objs']:
         for obj in d['objs']:
-            def obj_init(obj_inst, app_inst):
+            try:
+                def obj_init(obj_inst, app_inst):
 
 
-                obj_inst.from_dict(obj)
+                    obj_inst.from_dict(obj)
 
 
-            App.log.debug("Recreating from opened project an %s object: %s" %
-                          (obj['kind'].capitalize(), obj['options']['name']))
+                App.log.debug("Recreating from opened project an %s object: %s" %
+                              (obj['kind'].capitalize(), obj['options']['name']))
 
 
-            # for some reason, setting ui_title does not work when this method is called from Tcl Shell
-            # it's because the TclCommand is run in another thread (it inherit TclCommandSignaled)
-            if cli is None:
-                self.set_ui_title(name="{} {}: {}".format(_("Loading Project ... restoring"),
-                                                          obj['kind'].upper(),
-                                                          obj['options']['name']
-                                                          )
-                                  )
+                # for some reason, setting ui_title does not work when this method is called from Tcl Shell
+                # it's because the TclCommand is run in another thread (it inherit TclCommandSignaled)
+                if cli is None:
+                    self.set_ui_title(name="{} {}: {}".format(_("Loading Project ... restoring"),
+                                                              obj['kind'].upper(),
+                                                              obj['options']['name']
+                                                              )
+                                      )
 
 
-            self.new_object(obj['kind'], obj['options']['name'], obj_init, active=False, fit=False, plot=plot)
+                self.new_object(obj['kind'], obj['options']['name'], obj_init, active=False, fit=False, plot=plot)
+            except Exception as e:
+                print('App.open_project() --> ' + str(e))
 
 
-        self.inform.emit('[success] %s: %s' %
-                         (_("Project loaded from"), filename))
+        self.inform.emit('[success] %s: %s' % (_("Project loaded from"), filename))
 
 
         self.should_we_save = False
         self.should_we_save = False
         self.file_opened.emit("project", filename)
         self.file_opened.emit("project", filename)
@@ -12365,7 +12386,10 @@ class App(QtCore.QObject):
         new_color = self.defaults['global_plot_fill']
         new_color = self.defaults['global_plot_fill']
         act_name = self.sender().text().lower()
         act_name = self.sender().text().lower()
 
 
-        sel_obj = self.collection.get_active()
+        sel_obj_list = self.collection.get_selected()
+
+        if not sel_obj_list:
+            return
 
 
         if act_name == 'red':
         if act_name == 'red':
             new_color = '#FF0000' + \
             new_color = '#FF0000' + \
@@ -12397,22 +12421,22 @@ class App(QtCore.QObject):
             new_color = str(plot_fill_color.name()) + \
             new_color = str(plot_fill_color.name()) + \
                         str(hex(self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.value())[2:])
                         str(hex(self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.value())[2:])
 
 
-        if self.is_legacy is False:
-            new_line_color = color_variant(new_color[:7], 0.7)
-            sel_obj.fill_color = new_color
-            sel_obj.outline_color = new_line_color
+        new_line_color = color_variant(new_color[:7], 0.7)
 
 
-            sel_obj.shapes.redraw(
-                update_colors=(new_color, new_line_color)
-            )
-        else:
-            new_line_color = color_variant(new_color[:7], 0.7)
+        for sel_obj in sel_obj_list:
+            if self.is_legacy is False:
+                sel_obj.fill_color = new_color
+                sel_obj.outline_color = new_line_color
 
 
-            sel_obj.fill_color = new_color
-            sel_obj.outline_color = new_line_color
-            sel_obj.shapes.redraw(
-                update_colors=(new_color, new_line_color)
-            )
+                sel_obj.shapes.redraw(
+                    update_colors=(new_color, new_line_color)
+                )
+            else:
+                sel_obj.fill_color = new_color
+                sel_obj.outline_color = new_line_color
+                sel_obj.shapes.redraw(
+                    update_colors=(new_color, new_line_color)
+                )
 
 
     def on_grid_snap_triggered(self, state):
     def on_grid_snap_triggered(self, state):
         if state:
         if state:

+ 29 - 12
FlatCAMObj.py

@@ -128,6 +128,9 @@ class FlatCAMObj(QtCore.QObject):
         self.isHovering = False
         self.isHovering = False
         self.notHovering = True
         self.notHovering = True
 
 
+        # Flag to show if a selection shape is drawn
+        self.selection_shape_drawn = False
+
         # self.units = 'IN'
         # self.units = 'IN'
         self.units = self.app.defaults['units']
         self.units = self.app.defaults['units']
 
 
@@ -596,7 +599,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
     def __init__(self, name):
     def __init__(self, name):
         self.decimals = self.app.decimals
         self.decimals = self.app.decimals
 
 
-        Gerber.__init__(self, steps_per_circle=int(self.app.defaults["gerber_circle_steps"]))
+        self.circle_steps = int(self.app.defaults["gerber_circle_steps"])
+
+        Gerber.__init__(self, steps_per_circle=self.circle_steps)
         FlatCAMObj.__init__(self, name)
         FlatCAMObj.__init__(self, name)
 
 
         self.kind = "gerber"
         self.kind = "gerber"
@@ -2196,6 +2201,10 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         Gerber.skew(self, angle_x=angle_x, angle_y=angle_y, point=point)
         Gerber.skew(self, angle_x=angle_x, angle_y=angle_y, point=point)
         self.replotApertures.emit()
         self.replotApertures.emit()
 
 
+    def buffer(self, distance, join):
+        Gerber.buffer(self, distance=distance, join=join)
+        self.replotApertures.emit()
+
     def serialize(self):
     def serialize(self):
         return {
         return {
             "options": self.options,
             "options": self.options,
@@ -2214,7 +2223,9 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
     def __init__(self, name):
     def __init__(self, name):
         self.decimals = self.app.decimals
         self.decimals = self.app.decimals
 
 
-        Excellon.__init__(self, geo_steps_per_circle=int(self.app.defaults["geometry_circle_steps"]))
+        self.circle_steps = int(self.app.defaults["geometry_circle_steps"])
+
+        Excellon.__init__(self, geo_steps_per_circle=self.circle_steps)
         FlatCAMObj.__init__(self, name)
         FlatCAMObj.__init__(self, name)
 
 
         self.kind = "excellon"
         self.kind = "excellon"
@@ -3542,8 +3553,11 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
 
 
     def __init__(self, name):
     def __init__(self, name):
         self.decimals = self.app.decimals
         self.decimals = self.app.decimals
+
+        self.circle_steps = int(self.app.defaults["geometry_circle_steps"])
+
         FlatCAMObj.__init__(self, name)
         FlatCAMObj.__init__(self, name)
-        Geometry.__init__(self, geo_steps_per_circle=int(self.app.defaults["geometry_circle_steps"]))
+        Geometry.__init__(self, geo_steps_per_circle=self.circle_steps)
 
 
         self.kind = "geometry"
         self.kind = "geometry"
 
 
@@ -3865,15 +3879,18 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 if def_key == opt_key:
                 if def_key == opt_key:
                     self.default_data[def_key] = deepcopy(opt_val)
                     self.default_data[def_key] = deepcopy(opt_val)
 
 
-        try:
-            temp_tools = self.options["cnctooldia"].split(",")
-            tools_list = [
-                float(eval(dia)) for dia in temp_tools if dia != ''
-            ]
-        except Exception as e:
-            log.error("At least one tool diameter needed. Verify in Edit -> Preferences -> Geometry General -> "
-                      "Tool dia. %s" % str(e))
-            return
+        if type(self.options["cnctooldia"]) == float:
+            tools_list = [self.options["cnctooldia"]]
+        else:
+            try:
+                temp_tools = self.options["cnctooldia"].split(",")
+                tools_list = [
+                    float(eval(dia)) for dia in temp_tools if dia != ''
+                ]
+            except Exception as e:
+                log.error("FlatCAMGeometry.set_ui() -> At least one tool diameter needed. "
+                          "Verify in Edit -> Preferences -> Geometry General -> Tool dia. %s" % str(e))
+                return
 
 
         self.tooluid += 1
         self.tooluid += 1
 
 

+ 7 - 0
README.md

@@ -14,6 +14,13 @@ CAD program, and create G-Code for Isolation routing.
 - some fixes in the Legacy(2D) graphic mode regarding the possibility of changing the color of the Gerber objects
 - some fixes in the Legacy(2D) graphic mode regarding the possibility of changing the color of the Gerber objects
 - added a method to darken the outline color for Gerber objects when they have the color set
 - added a method to darken the outline color for Gerber objects when they have the color set
 - when Printing as PDF Gerber objects now the rendered color is the print color
 - when Printing as PDF Gerber objects now the rendered color is the print color
+- speed up the plotting in OpenGL(3D) graphic mode
+- spped up the color setting for Gerber object when using the OpenGL(3D) graphic mode
+- setting color for Gerber objects work on a selection of Gerber objects
+- ~~when the selection is changed in the Project Tree the selection shape on canvas is deleted~~
+- if an object is selected on Project Tree and it does not have the selection shape drawn, first click on canvas over it will draw the selection shape 
+- in Tool Transform added a new feature named 'Buffer'. For Geometry and Gerber objects will create (and replace) a geometry at a distance from the original geometry and for Excellon will adjust the Tool diameters
+- solved issue #355 - when the tool diameter field in the Edit → Preferences → Geometry → Geometry General → Tools → Tool dia is only one the app failed to read it
 
 
 22.12.2019
 22.12.2019
 
 

+ 63 - 0
camlib.py

@@ -2118,6 +2118,69 @@ class Geometry(object):
         #     self.solid_geometry = affinity.skew(self.solid_geometry, angle_x, angle_y,
         #     self.solid_geometry = affinity.skew(self.solid_geometry, angle_x, angle_y,
         #                                         origin=(px, py))
         #                                         origin=(px, py))
 
 
+    def buffer(self, distance, join):
+        """
+
+        :param distance:
+        :param join:
+        :return:
+        """
+
+        log.debug("camlib.Geometry.buffer()")
+
+        if distance == 0:
+            return
+
+        def buffer_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(buffer_geom(g))
+                return new_obj
+            else:
+                try:
+                    self.el_count += 1
+                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+                    if self.old_disp_number < disp_number <= 100:
+                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                        self.old_disp_number = disp_number
+
+                    return obj.buffer(distance, resolution=self.geo_steps_per_circle, join_style=join)
+                except AttributeError:
+                    return obj
+
+        try:
+            if self.multigeo is True:
+                for tool in self.tools:
+                    # variables to display the percentage of work done
+                    self.geo_len = 0
+                    try:
+                        for __ in self.tools[tool]['solid_geometry']:
+                            self.geo_len += 1
+                    except TypeError:
+                        self.geo_len = 1
+                    self.old_disp_number = 0
+                    self.el_count = 0
+
+                    self.tools[tool]['solid_geometry'] = buffer_geom(self.tools[tool]['solid_geometry'])
+
+            # variables to display the percentage of work done
+            self.geo_len = 0
+            try:
+                for __ in self.solid_geometry:
+                    self.geo_len += 1
+            except TypeError:
+                self.geo_len = 1
+            self.old_disp_number = 0
+            self.el_count = 0
+
+            self.solid_geometry = buffer_geom(self.solid_geometry)
+
+            self.app.inform.emit('[success] %s...' %  _('Object was buffered'))
+        except AttributeError:
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed to buffer. No object selected"))
+
+        self.app.proc_container.new_text = ''
 
 
 class AttrDict(dict):
 class AttrDict(dict):
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):

+ 44 - 6
flatcamGUI/PreferencesUI.py

@@ -5378,7 +5378,7 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.skewy_label, 4, 0)
         grid0.addWidget(self.skewy_label, 4, 0)
         grid0.addWidget(self.skewy_entry, 4, 1)
         grid0.addWidget(self.skewy_entry, 4, 1)
 
 
-        # ## Scale factor on X axis
+        # ## Scale
         scale_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _("Scale"))
         scale_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _("Scale"))
         grid0.addWidget(scale_title_lbl, 5, 0, 1, 2)
         grid0.addWidget(scale_title_lbl, 5, 0, 1, 2)
 
 
@@ -5425,7 +5425,7 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI):
         )
         )
         grid0.addWidget(self.reference_cb, 8, 1)
         grid0.addWidget(self.reference_cb, 8, 1)
 
 
-        # ## Offset distance on X axis
+        # ## Offset
         offset_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _("Offset"))
         offset_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _("Offset"))
         grid0.addWidget(offset_title_lbl, 9, 0, 1, 2)
         grid0.addWidget(offset_title_lbl, 9, 0, 1, 2)
 
 
@@ -5454,6 +5454,10 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.offy_label, 11, 0)
         grid0.addWidget(self.offy_label, 11, 0)
         grid0.addWidget(self.offy_entry, 11, 1)
         grid0.addWidget(self.offy_entry, 11, 1)
 
 
+        # ## Mirror
+        mirror_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _("Mirror"))
+        grid0.addWidget(mirror_title_lbl, 12, 0, 1, 2)
+
         # ## Mirror (Flip) Reference Point
         # ## Mirror (Flip) Reference Point
         self.mirror_reference_cb = FCCheckBox('%s' % _("Mirror Reference"))
         self.mirror_reference_cb = FCCheckBox('%s' % _("Mirror Reference"))
         self.mirror_reference_cb.setToolTip(
         self.mirror_reference_cb.setToolTip(
@@ -5466,9 +5470,9 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI):
               "Then click Add button to insert coordinates.\n"
               "Then click Add button to insert coordinates.\n"
               "Or enter the coords in format (x, y) in the\n"
               "Or enter the coords in format (x, y) in the\n"
               "Point Entry field and click Flip on X(Y)"))
               "Point Entry field and click Flip on X(Y)"))
-        grid0.addWidget(self.mirror_reference_cb, 12, 0, 1, 2)
+        grid0.addWidget(self.mirror_reference_cb, 13, 0, 1, 2)
 
 
-        self.flip_ref_label = QtWidgets.QLabel('<b>%s</b>' % _("Mirror Reference point"))
+        self.flip_ref_label = QtWidgets.QLabel('%s' % _("Mirror Reference point"))
         self.flip_ref_label.setToolTip(
         self.flip_ref_label.setToolTip(
             _("Coordinates in format (x, y) used as reference for mirroring.\n"
             _("Coordinates in format (x, y) used as reference for mirroring.\n"
               "The 'x' in (x, y) will be used when using Flip on X and\n"
               "The 'x' in (x, y) will be used when using Flip on X and\n"
@@ -5476,8 +5480,42 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI):
         )
         )
         self.flip_ref_entry = EvalEntry2("(0, 0)")
         self.flip_ref_entry = EvalEntry2("(0, 0)")
 
 
-        grid0.addWidget(self.flip_ref_label, 13, 0, 1, 2)
-        grid0.addWidget(self.flip_ref_entry, 14, 0, 1, 2)
+        grid0.addWidget(self.flip_ref_label, 14, 0, 1, 2)
+        grid0.addWidget(self.flip_ref_entry, 15, 0, 1, 2)
+
+        # ## Buffer
+        buffer_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _("Buffer"))
+        grid0.addWidget(buffer_title_lbl, 16, 0, 1, 2)
+
+        self.buffer_label = QtWidgets.QLabel('%s:' % _("Distance"))
+        self.buffer_label.setToolTip(
+            _("A positive value will create the effect of dilation,\n"
+              "while a negative value will create the effect of erosion.\n"
+              "Each geometry element of the object will be increased\n"
+              "or decreased with the 'distance'.")
+        )
+
+        self.buffer_entry = FCDoubleSpinner()
+        self.buffer_entry.set_precision(self.decimals)
+        self.buffer_entry.setSingleStep(0.1)
+        self.buffer_entry.setWrapping(True)
+        self.buffer_entry.set_range(-9999.9999, 9999.9999)
+
+        grid0.addWidget(self.buffer_label, 17, 0)
+        grid0.addWidget(self.buffer_entry, 17, 1)
+
+        self.buffer_rounded_cb = FCCheckBox()
+        self.buffer_rounded_cb.setText('%s' % _("Rounded"))
+        self.buffer_rounded_cb.setToolTip(
+            _("If checked then the buffer will surround the buffered shape,\n"
+              "every corner will be rounded.\n"
+              "If not checked then the buffer will follow the exact geometry\n"
+              "of the buffered shape.")
+        )
+
+        grid0.addWidget(self.buffer_rounded_cb, 18, 0, 1, 2)
+
+        grid0.addWidget(QtWidgets.QLabel(''), 19, 0, 1, 2)
 
 
         self.layout.addStretch()
         self.layout.addStretch()
 
 

+ 39 - 32
flatcamGUI/VisPyVisuals.py

@@ -45,44 +45,48 @@ def _update_shape_buffers(data, triangulation='glu'):
     geo, color, face_color, tolerance = data['geometry'], data['color'], data['face_color'], data['tolerance']
     geo, color, face_color, tolerance = data['geometry'], data['color'], data['face_color'], data['tolerance']
 
 
     if geo is not None and not geo.is_empty:
     if geo is not None and not geo.is_empty:
-        simple = geo.simplify(tolerance) if tolerance else geo      # Simplified shape
-        pts = []                                                    # Shape line points
-        tri_pts = []                                                # Mesh vertices
-        tri_tris = []                                               # Mesh faces
+        simplified_geo = geo.simplify(tolerance) if tolerance else geo      # Simplified shape
+        pts = []                                                            # Shape line points
+        tri_pts = []                                                        # Mesh vertices
+        tri_tris = []                                                       # Mesh faces
 
 
         if type(geo) == LineString:
         if type(geo) == LineString:
             # Prepare lines
             # Prepare lines
-            pts = _linestring_to_segments(list(simple.coords))
+            pts = _linestring_to_segments(list(simplified_geo.coords))
 
 
         elif type(geo) == LinearRing:
         elif type(geo) == LinearRing:
             # Prepare lines
             # Prepare lines
-            pts = _linearring_to_segments(list(simple.coords))
+            pts = _linearring_to_segments(list(simplified_geo.coords))
 
 
         elif type(geo) == Polygon:
         elif type(geo) == Polygon:
             # Prepare polygon faces
             # Prepare polygon faces
             if face_color is not None:
             if face_color is not None:
                 if triangulation == 'glu':
                 if triangulation == 'glu':
                     gt = GLUTess()
                     gt = GLUTess()
-                    tri_tris, tri_pts = gt.triangulate(simple)
+                    tri_tris, tri_pts = gt.triangulate(simplified_geo)
                 else:
                 else:
                     print("Triangulation type '%s' isn't implemented. Drawing only edges." % triangulation)
                     print("Triangulation type '%s' isn't implemented. Drawing only edges." % triangulation)
 
 
             # Prepare polygon edges
             # Prepare polygon edges
             if color is not None:
             if color is not None:
-                pts = _linearring_to_segments(list(simple.exterior.coords))
-                for ints in simple.interiors:
+                pts = _linearring_to_segments(list(simplified_geo.exterior.coords))
+                for ints in simplified_geo.interiors:
                     pts += _linearring_to_segments(list(ints.coords))
                     pts += _linearring_to_segments(list(ints.coords))
 
 
         # Appending data for mesh
         # Appending data for mesh
         if len(tri_pts) > 0 and len(tri_tris) > 0:
         if len(tri_pts) > 0 and len(tri_tris) > 0:
             mesh_tris += tri_tris
             mesh_tris += tri_tris
             mesh_vertices += tri_pts
             mesh_vertices += tri_pts
-            mesh_colors += [Color(face_color).rgba] * (len(tri_tris) // 3)
+            face_color_rgba = Color(face_color).rgba
+            # mesh_colors += [face_color_rgba] * (len(tri_tris) // 3)
+            mesh_colors += [face_color_rgba for __ in range(len(tri_tris) // 3)]
 
 
         # Appending data for line
         # Appending data for line
         if len(pts) > 0:
         if len(pts) > 0:
             line_pts += pts
             line_pts += pts
-            line_colors += [Color(color).rgba] * len(pts)
+            colo_rgba = Color(color).rgba
+            # line_colors += [colo_rgba] * len(pts)
+            line_colors += [colo_rgba for __ in range(len(pts))]
 
 
     # Store buffers
     # Store buffers
     data['line_pts'] = line_pts
     data['line_pts'] = line_pts
@@ -314,12 +318,27 @@ class ShapeCollectionVisual(CompoundVisual):
             self.__update()
             self.__update()
 
 
     def update_color(self, new_mesh_color=None, new_line_color=None, indexes=None):
     def update_color(self, new_mesh_color=None, new_line_color=None, indexes=None):
-        if (new_mesh_color is None or new_mesh_color == '') and (new_line_color is None or new_line_color == ''):
+        if new_mesh_color is None and new_line_color is None:
             return
             return
 
 
         if not self.data:
         if not self.data:
             return
             return
 
 
+        # if a new color is empty string then make it None so it will not be updated
+        # if a new color is valid then transform it here in a format palatable
+        mesh_color_rgba = None
+        line_color_rgba = None
+        if new_mesh_color:
+            if new_mesh_color != '':
+                mesh_color_rgba = Color(new_mesh_color).rgba
+            else:
+                new_mesh_color = None
+        if new_line_color:
+            if new_line_color != '':
+                line_color_rgba = Color(new_line_color).rgba
+            else:
+                new_line_color = None
+
         mesh_colors = [[] for _ in range(0, len(self._meshes))]     # Face colors
         mesh_colors = [[] for _ in range(0, len(self._meshes))]     # Face colors
         line_colors = [[] for _ in range(0, len(self._meshes))]     # Line colors
         line_colors = [[] for _ in range(0, len(self._meshes))]     # Line colors
         line_pts = [[] for _ in range(0, len(self._lines))]         # Vertices for line
         line_pts = [[] for _ in range(0, len(self._lines))]         # Vertices for line
@@ -335,13 +354,10 @@ class ShapeCollectionVisual(CompoundVisual):
                         dim_mesh_tris = (len(data['mesh_tris']) // 3)
                         dim_mesh_tris = (len(data['mesh_tris']) // 3)
                         if dim_mesh_tris != 0:
                         if dim_mesh_tris != 0:
                             try:
                             try:
-                                mesh_colors[data['layer']] += [Color(new_mesh_color).rgba] * dim_mesh_tris
+                                mesh_colors[data['layer']] += [mesh_color_rgba] * dim_mesh_tris
                                 self.data[k]['face_color'] = new_mesh_color
                                 self.data[k]['face_color'] = new_mesh_color
 
 
-                                new_temp = list()
-                                for i in range(len(data['mesh_colors'])):
-                                    new_temp.append(Color(new_mesh_color).rgba)
-                                data['mesh_colors'] = new_temp
+                                data['mesh_colors'] = [mesh_color_rgba for __ in range(len(data['mesh_colors']))]
                             except Exception as e:
                             except Exception as e:
                                 print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
                                 print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
                                       "Create mesh colors --> Data error. %s" % str(e))
                                       "Create mesh colors --> Data error. %s" % str(e))
@@ -351,13 +367,10 @@ class ShapeCollectionVisual(CompoundVisual):
                         if dim_line_pts != 0:
                         if dim_line_pts != 0:
                             try:
                             try:
                                 line_pts[data['layer']] += data['line_pts']
                                 line_pts[data['layer']] += data['line_pts']
-                                line_colors[data['layer']] += [Color(new_line_color).rgba] * dim_line_pts
+                                line_colors[data['layer']] += [line_color_rgba] * dim_line_pts
                                 self.data[k]['color'] = new_line_color
                                 self.data[k]['color'] = new_line_color
 
 
-                                new_temp = list()
-                                for i in range(len(data['line_colors'])):
-                                    new_temp.append(Color(new_line_color).rgba)
-                                data['line_colors'] = new_temp
+                                data['line_colors'] = [mesh_color_rgba for __ in range(len(data['line_colors']))]
                             except Exception as e:
                             except Exception as e:
                                 print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
                                 print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
                                       "Create line colors --> Data error. %s" % str(e))
                                       "Create line colors --> Data error. %s" % str(e))
@@ -371,13 +384,10 @@ class ShapeCollectionVisual(CompoundVisual):
                         if new_mesh_color and new_mesh_color != '':
                         if new_mesh_color and new_mesh_color != '':
                             if dim_mesh_tris != 0:
                             if dim_mesh_tris != 0:
                                 try:
                                 try:
-                                    mesh_colors[data['layer']] += [Color(new_mesh_color).rgba] * dim_mesh_tris
+                                    mesh_colors[data['layer']] += [mesh_color_rgba] * dim_mesh_tris
                                     self.data[k]['face_color'] = new_mesh_color
                                     self.data[k]['face_color'] = new_mesh_color
 
 
-                                    new_temp = list()
-                                    for i in range(len(data['mesh_colors'])):
-                                        new_temp.append(Color(new_mesh_color).rgba)
-                                    data['mesh_colors'] = new_temp
+                                    data['mesh_colors'] = [mesh_color_rgba for __ in range(len(data['mesh_colors']))]
                                 except Exception as e:
                                 except Exception as e:
                                     print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
                                     print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
                                           "Create mesh colors --> Data error. %s" % str(e))
                                           "Create mesh colors --> Data error. %s" % str(e))
@@ -385,13 +395,10 @@ class ShapeCollectionVisual(CompoundVisual):
                             if dim_line_pts != 0:
                             if dim_line_pts != 0:
                                 try:
                                 try:
                                     line_pts[data['layer']] += data['line_pts']
                                     line_pts[data['layer']] += data['line_pts']
-                                    line_colors[data['layer']] += [Color(new_line_color).rgba] * dim_line_pts
+                                    line_colors[data['layer']] += [line_color_rgba] * dim_line_pts
                                     self.data[k]['color'] = new_line_color
                                     self.data[k]['color'] = new_line_color
 
 
-                                    new_temp = list()
-                                    for i in range(len(data['line_colors'])):
-                                        new_temp.append(Color(new_line_color).rgba)
-                                    data['line_colors'] = new_temp
+                                    data['line_colors'] = [mesh_color_rgba for __ in range(len(data['line_colors']))]
                                 except Exception as e:
                                 except Exception as e:
                                     print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
                                     print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
                                           "Create line colors --> Data error. %s" % str(e))
                                           "Create line colors --> Data error. %s" % str(e))

+ 32 - 1
flatcamParsers/ParseExcellon.py

@@ -1457,4 +1457,35 @@ class Excellon(Geometry):
                 slot['start'] = affinity.rotate(slot['start'], angle, origin=(px, py))
                 slot['start'] = affinity.rotate(slot['start'], angle, origin=(px, py))
 
 
         self.create_geometry()
         self.create_geometry()
-        self.app.proc_container.new_text = ''
+        self.app.proc_container.new_text = ''
+
+    def buffer(self, distance, join):
+        """
+
+        :param distance:
+        :param join:
+        :return:
+        """
+        log.debug("flatcamParsers.ParseExcellon.Excellon.buffer()")
+
+        if distance == 0:
+            return
+
+        def buffer_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(buffer_geom(g))
+                return new_obj
+            else:
+                try:
+                    return obj.buffer(distance, resolution=self.geo_steps_per_circle)
+                except AttributeError:
+                    return obj
+
+        # buffer solid_geometry
+        for tool, tool_dict in list(self.tools.items()):
+            self.tools[tool]['solid_geometry'] = buffer_geom(tool_dict['solid_geometry'])
+            self.tools[tool]['C'] += distance
+
+        self.create_geometry()

+ 81 - 0
flatcamParsers/ParseGerber.py

@@ -2169,6 +2169,87 @@ class Gerber(Geometry):
                              _("Gerber Rotate done."))
                              _("Gerber Rotate done."))
         self.app.proc_container.new_text = ''
         self.app.proc_container.new_text = ''
 
 
+    def buffer(self, distance, join):
+        """
+
+        :param distance:
+        :return:
+        """
+        log.debug("parseGerber.Gerber.buffer()")
+
+        if distance == 0:
+            return
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for __ in self.solid_geometry:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        def buffer_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(buffer_geom(g))
+                return new_obj
+            else:
+                try:
+                    self.el_count += 1
+                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+                    if self.old_disp_number < disp_number <= 100:
+                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                        self.old_disp_number = disp_number
+
+                    return obj.buffer(distance, resolution=self.steps_per_circle, join_style=join)
+                except AttributeError:
+                    return obj
+
+        self.solid_geometry = buffer_geom(self.solid_geometry)
+
+        # we need to buffer the geometry stored in the Gerber apertures, too
+        try:
+            for apid in self.apertures:
+                new_geometry = list()
+                if 'geometry' in self.apertures[apid]:
+                    for geo_el in self.apertures[apid]['geometry']:
+                        new_geo_el = dict()
+                        if 'solid' in geo_el:
+                            new_geo_el['solid'] = buffer_geom(geo_el['solid'])
+                        if 'follow' in geo_el:
+                            new_geo_el['follow'] = buffer_geom(geo_el['follow'])
+                        if 'clear' in geo_el:
+                            new_geo_el['clear'] = buffer_geom(geo_el['clear'])
+                        new_geometry.append(new_geo_el)
+
+                self.apertures[apid]['geometry'] = deepcopy(new_geometry)
+
+                try:
+                    if str(self.apertures[apid]['type']) == 'R' or str(self.apertures[apid]['type']) == 'O':
+                        self.apertures[apid]['width'] += (distance * 2)
+                        self.apertures[apid]['height'] += (distance * 2)
+                    elif str(self.apertures[apid]['type']) == 'P':
+                        self.apertures[apid]['diam'] += (distance * 2)
+                        self.apertures[apid]['nVertices'] += (distance * 2)
+                except KeyError:
+                    pass
+
+                try:
+                    if self.apertures[apid]['size'] is not None:
+                        self.apertures[apid]['size'] = float(self.apertures[apid]['size'] + (distance * 2))
+                except KeyError:
+                    pass
+        except Exception as e:
+            log.debug('camlib.Gerber.buffer() Exception --> %s' % str(e))
+            return 'fail'
+
+        self.app.inform.emit('[success] %s' % _("Gerber Buffer done."))
+        self.app.proc_container.new_text = ''
+
 
 
 def parse_gerber_number(strnumber, int_digits, frac_digits, zeros):
 def parse_gerber_number(strnumber, int_digits, frac_digits, zeros):
     """
     """

+ 119 - 17
flatcamTools/ToolTransform.py

@@ -27,6 +27,7 @@ class ToolTransform(FlatCAMTool):
     scaleName = _("Scale")
     scaleName = _("Scale")
     flipName = _("Mirror (Flip)")
     flipName = _("Mirror (Flip)")
     offsetName = _("Offset")
     offsetName = _("Offset")
+    bufferName = _("Buffer")
 
 
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
@@ -255,11 +256,11 @@ class ToolTransform(FlatCAMTool):
         grid0.addWidget(self.offy_entry, 14, 1)
         grid0.addWidget(self.offy_entry, 14, 1)
         grid0.addWidget(self.offy_button, 14, 2)
         grid0.addWidget(self.offy_button, 14, 2)
 
 
-        grid0.addWidget(QtWidgets.QLabel(''))
+        grid0.addWidget(QtWidgets.QLabel(''), 15, 0, 1, 3)
 
 
         # ## Flip Title
         # ## Flip Title
         flip_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.flipName)
         flip_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.flipName)
-        self.transform_lay.addWidget(flip_title_label)
+        grid0.addWidget(flip_title_label, 16, 0, 1, 3)
 
 
         self.flipx_button = FCButton()
         self.flipx_button = FCButton()
         self.flipx_button.set_value(_("Flip on X"))
         self.flipx_button.set_value(_("Flip on X"))
@@ -274,7 +275,7 @@ class ToolTransform(FlatCAMTool):
         )
         )
 
 
         hlay0 = QtWidgets.QHBoxLayout()
         hlay0 = QtWidgets.QHBoxLayout()
-        self.transform_lay.addLayout(hlay0)
+        grid0.addLayout(hlay0, 17, 0, 1, 3)
 
 
         hlay0.addWidget(self.flipx_button)
         hlay0.addWidget(self.flipx_button)
         hlay0.addWidget(self.flipy_button)
         hlay0.addWidget(self.flipy_button)
@@ -293,7 +294,7 @@ class ToolTransform(FlatCAMTool):
               "Or enter the coords in format (x, y) in the\n"
               "Or enter the coords in format (x, y) in the\n"
               "Point Entry field and click Flip on X(Y)"))
               "Point Entry field and click Flip on X(Y)"))
 
 
-        self.transform_lay.addWidget(self.flip_ref_cb)
+        grid0.addWidget(self.flip_ref_cb, 18, 0, 1, 3)
 
 
         self.flip_ref_label = QtWidgets.QLabel('%s:' % _("Ref. Point"))
         self.flip_ref_label = QtWidgets.QLabel('%s:' % _("Ref. Point"))
         self.flip_ref_label.setToolTip(
         self.flip_ref_label.setToolTip(
@@ -315,12 +316,60 @@ class ToolTransform(FlatCAMTool):
         self.ois_flip = OptionalInputSection(self.flip_ref_cb, [self.flip_ref_entry, self.flip_ref_button], logic=True)
         self.ois_flip = OptionalInputSection(self.flip_ref_cb, [self.flip_ref_entry, self.flip_ref_button], logic=True)
 
 
         hlay1 = QtWidgets.QHBoxLayout()
         hlay1 = QtWidgets.QHBoxLayout()
-        self.transform_lay.addLayout(hlay1)
+        grid0.addLayout(hlay1, 19, 0, 1, 3)
 
 
         hlay1.addWidget(self.flip_ref_label)
         hlay1.addWidget(self.flip_ref_label)
         hlay1.addWidget(self.flip_ref_entry)
         hlay1.addWidget(self.flip_ref_entry)
 
 
-        self.transform_lay.addWidget(self.flip_ref_button)
+        grid0.addWidget(self.flip_ref_button, 20, 0, 1, 3)
+
+        grid0.addWidget(QtWidgets.QLabel(''), 21, 0, 1, 3)
+
+        # ## Buffer Title
+        buffer_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.bufferName)
+        grid0.addWidget(buffer_title_label, 22, 0, 1, 3)
+
+        self.buffer_label = QtWidgets.QLabel('%s:' % _("Distance"))
+        self.buffer_label.setToolTip(
+            _("A positive value will create the effect of dilation,\n"
+              "while a negative value will create the effect of erosion.\n"
+              "Each geometry element of the object will be increased\n"
+              "or decreased with the 'distance'.")
+        )
+
+        self.buffer_entry = FCDoubleSpinner()
+        self.buffer_entry.set_precision(self.decimals)
+        self.buffer_entry.setSingleStep(0.1)
+        self.buffer_entry.setWrapping(True)
+        self.buffer_entry.set_range(-9999.9999, 9999.9999)
+
+        # self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+
+        self.buffer_button = FCButton()
+        self.buffer_button.set_value(_("Buffer"))
+        self.buffer_button.setToolTip(
+            _("Create the buffer effect on each geometry,\n"
+              "element from the selected object.")
+        )
+        self.buffer_button.setMinimumWidth(90)
+
+        grid0.addWidget(self.buffer_label, 23, 0)
+        grid0.addWidget(self.buffer_entry, 23, 1)
+        grid0.addWidget(self.buffer_button, 23, 2)
+
+        self.buffer_rounded_cb = FCCheckBox()
+        self.buffer_rounded_cb.setText('%s' % _("Rounded"))
+        self.buffer_rounded_cb.setToolTip(
+            _("If checked then the buffer will surround the buffered shape,\n"
+              "every corner will be rounded.\n"
+              "If not checked then the buffer will follow the exact geometry\n"
+              "of the buffered shape.")
+        )
+
+        grid0.addWidget(self.buffer_rounded_cb, 24, 0, 1, 3)
+
+        grid0.addWidget(QtWidgets.QLabel(''), 25, 0, 1, 3)
+
         self.transform_lay.addStretch()
         self.transform_lay.addStretch()
 
 
         # ## Signals
         # ## Signals
@@ -334,14 +383,16 @@ class ToolTransform(FlatCAMTool):
         self.flipx_button.clicked.connect(self.on_flipx)
         self.flipx_button.clicked.connect(self.on_flipx)
         self.flipy_button.clicked.connect(self.on_flipy)
         self.flipy_button.clicked.connect(self.on_flipy)
         self.flip_ref_button.clicked.connect(self.on_flip_add_coords)
         self.flip_ref_button.clicked.connect(self.on_flip_add_coords)
+        self.buffer_button.clicked.connect(self.on_buffer)
 
 
-        self.rotate_entry.returnPressed.connect(self.on_rotate)
-        self.skewx_entry.returnPressed.connect(self.on_skewx)
-        self.skewy_entry.returnPressed.connect(self.on_skewy)
-        self.scalex_entry.returnPressed.connect(self.on_scalex)
-        self.scaley_entry.returnPressed.connect(self.on_scaley)
-        self.offx_entry.returnPressed.connect(self.on_offx)
-        self.offy_entry.returnPressed.connect(self.on_offy)
+        # self.rotate_entry.returnPressed.connect(self.on_rotate)
+        # self.skewx_entry.returnPressed.connect(self.on_skewx)
+        # self.skewy_entry.returnPressed.connect(self.on_skewy)
+        # self.scalex_entry.returnPressed.connect(self.on_scalex)
+        # self.scaley_entry.returnPressed.connect(self.on_scaley)
+        # self.offx_entry.returnPressed.connect(self.on_offx)
+        # self.offy_entry.returnPressed.connect(self.on_offy)
+        # self.buffer_entry.returnPressed.connect(self.on_buffer)
 
 
     def run(self, toggle=True):
     def run(self, toggle=True):
         self.app.report_usage("ToolTransform()")
         self.app.report_usage("ToolTransform()")
@@ -430,6 +481,16 @@ class ToolTransform(FlatCAMTool):
         else:
         else:
             self.flip_ref_entry.set_value((0, 0))
             self.flip_ref_entry.set_value((0, 0))
 
 
+        if self.app.defaults["tools_transform_buffer_dis"]:
+            self.buffer_entry.set_value(self.app.defaults["tools_transform_buffer_dis"])
+        else:
+            self.buffer_entry.set_value(0.0)
+
+        if self.app.defaults["tools_transform_buffer_corner"]:
+            self.buffer_rounded_cb.set_value(self.app.defaults["tools_transform_buffer_corner"])
+        else:
+            self.buffer_rounded_cb.set_value(True)
+
     def on_rotate(self):
     def on_rotate(self):
         value = float(self.rotate_entry.get_value())
         value = float(self.rotate_entry.get_value())
         if value == 0:
         if value == 0:
@@ -511,8 +572,7 @@ class ToolTransform(FlatCAMTool):
     def on_offx(self):
     def on_offx(self):
         value = float(self.offx_entry.get_value())
         value = float(self.offx_entry.get_value())
         if value == 0:
         if value == 0:
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("Offset transformation can not be done for a value of 0."))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Offset transformation can not be done for a value of 0."))
             return
             return
         axis = 'X'
         axis = 'X'
 
 
@@ -522,14 +582,20 @@ class ToolTransform(FlatCAMTool):
     def on_offy(self):
     def on_offy(self):
         value = float(self.offy_entry.get_value())
         value = float(self.offy_entry.get_value())
         if value == 0:
         if value == 0:
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("Offset transformation can not be done for a value of 0."))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Offset transformation can not be done for a value of 0."))
             return
             return
         axis = 'Y'
         axis = 'Y'
 
 
         self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]})
         self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]})
         return
         return
 
 
+    def on_buffer(self):
+        value = self.buffer_entry.get_value()
+        join = 1 if self.buffer_rounded_cb.get_value() else 2
+
+        self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join]})
+        return
+
     def on_rotate_action(self, num):
     def on_rotate_action(self, num):
         obj_list = self.app.collection.get_selected()
         obj_list = self.app.collection.get_selected()
         xminlist = []
         xminlist = []
@@ -808,4 +874,40 @@ class ToolTransform(FlatCAMTool):
                                          (_("Due of"), str(e),  _("action was not executed.")))
                                          (_("Due of"), str(e),  _("action was not executed.")))
                     return
                     return
 
 
+    def on_buffer_action(self, value, join):
+        obj_list = self.app.collection.get_selected()
+
+        if not obj_list:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to buffer!"))
+            return
+        else:
+            with self.app.proc_container.new(_("Applying Buffer")):
+                try:
+                    for sel_obj in obj_list:
+                        if isinstance(sel_obj, FlatCAMCNCjob):
+                            self.app.inform.emit(_("CNCJob objects can't be buffered."))
+                        elif sel_obj.kind.lower() == 'gerber':
+                            sel_obj.buffer(value, join)
+                            sel_obj.source_file = self.app.export_gerber(obj_name=sel_obj.options['name'],
+                                                                         filename=None, local_use=sel_obj,
+                                                                         use_thread=False)
+                        elif sel_obj.kind.lower() == 'excellon':
+                            sel_obj.buffer(value, join)
+                            sel_obj.source_file = self.app.export_excellon(obj_name=sel_obj.options['name'],
+                                                                           filename=None, local_use=sel_obj,
+                                                                           use_thread=False)
+                        elif sel_obj.kind.lower() == 'geometry':
+                            sel_obj.buffer(value, join)
+
+                        self.app.object_changed.emit(sel_obj)
+                        sel_obj.plot()
+
+                    self.app.inform.emit('[success] %s...' % _('Buffer done'))
+
+                except Exception as e:
+                    self.app.log.debug("ToolTransform.on_buffer_action() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s %s, %s.' %
+                                         (_("Due of"), str(e),  _("action was not executed.")))
+                    return
+
 # end of file
 # end of file