Преглед изворни кода

jpcgt/flatcam/Beta слито с Beta

Camellan пре 6 година
родитељ
комит
5784e4659e

+ 119 - 25
FlatCAMApp.py

@@ -56,7 +56,7 @@ from flatcamGUI.PlotCanvas import *
 from flatcamGUI.PlotCanvasLegacy import *
 from flatcamGUI.PlotCanvasLegacy import *
 from flatcamGUI.FlatCAMGUI import *
 from flatcamGUI.FlatCAMGUI import *
 
 
-from FlatCAMCommon import LoudDict, BookmarkManager, ToolsDB
+from FlatCAMCommon import LoudDict, BookmarkManager, ToolsDB, color_variant
 from FlatCAMPostProc import load_preprocessors
 from FlatCAMPostProc import load_preprocessors
 
 
 from flatcamEditors.FlatCAMGeoEditor import FlatCAMGeoEditor
 from flatcamEditors.FlatCAMGeoEditor import FlatCAMGeoEditor
@@ -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",
@@ -1038,6 +1040,7 @@ class App(QtCore.QObject):
         QtCore.QObject.__init__(self)
         QtCore.QObject.__init__(self)
 
 
         self.ui = FlatCAMGUI(self)
         self.ui = FlatCAMGUI(self)
+        self.on_grid_snap_triggered(state=True)
 
 
         theme_settings = QtCore.QSettings("Open Source", "FlatCAM")
         theme_settings = QtCore.QSettings("Open Source", "FlatCAM")
         if theme_settings.contains("theme"):
         if theme_settings.contains("theme"):
@@ -1431,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,
@@ -1939,6 +1944,10 @@ class App(QtCore.QObject):
 
 
         self.ui.popmenu_properties.triggered.connect(self.obj_properties)
         self.ui.popmenu_properties.triggered.connect(self.obj_properties)
 
 
+        # Project Context Menu -> Color Setting
+        for act in self.ui.menuprojectcolor.actions():
+            act.triggered.connect(self.on_set_color_action_triggered)
+
         # Preferences Plot Area TAB
         # Preferences Plot Area TAB
         self.ui.pref_save_button.clicked.connect(lambda: self.on_save_button(save_to_file=True))
         self.ui.pref_save_button.clicked.connect(lambda: self.on_save_button(save_to_file=True))
         self.ui.pref_apply_button.clicked.connect(lambda: self.on_save_button(save_to_file=False))
         self.ui.pref_apply_button.clicked.connect(lambda: self.on_save_button(save_to_file=False))
@@ -2023,7 +2032,7 @@ class App(QtCore.QObject):
         self.ui.general_defaults_form.general_gui_group.proj_color_dis_button.clicked.connect(
         self.ui.general_defaults_form.general_gui_group.proj_color_dis_button.clicked.connect(
             self.on_proj_color_dis_button)
             self.on_proj_color_dis_button)
 
 
-        # ############################# workspace setting signals #####################
+        # ############################# Workspace Setting Signals #####################
         self.ui.general_defaults_form.general_gui_group.wk_cb.currentIndexChanged.connect(self.on_workspace_modified)
         self.ui.general_defaults_form.general_gui_group.wk_cb.currentIndexChanged.connect(self.on_workspace_modified)
         self.ui.general_defaults_form.general_gui_group.wk_orientation_radio.activated_custom.connect(
         self.ui.general_defaults_form.general_gui_group.wk_orientation_radio.activated_custom.connect(
             self.on_workspace_modified
             self.on_workspace_modified
@@ -2153,6 +2162,8 @@ class App(QtCore.QObject):
         # signal emitted when a tab is closed in the Plot Area
         # signal emitted when a tab is closed in the Plot Area
         self.ui.plot_tab_area.tab_closed_signal.connect(self.on_plot_area_tab_closed)
         self.ui.plot_tab_area.tab_closed_signal.connect(self.on_plot_area_tab_closed)
 
 
+        self.ui.grid_snap_btn.triggered.connect(self.on_grid_snap_triggered)
+
         # #####################################################################################
         # #####################################################################################
         # ########### FINISHED CONNECTING SIGNALS #############################################
         # ########### FINISHED CONNECTING SIGNALS #############################################
         # #####################################################################################
         # #####################################################################################
@@ -4728,7 +4739,7 @@ class App(QtCore.QObject):
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % " "), 2, 3)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % " "), 2, 3)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "German"), 3, 0)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "German"), 3, 0)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Marius Stanciu (Google-Tr)"), 3, 1)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Marius Stanciu (Google-Tr)"), 3, 1)
-                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Jens Karstedt, @detlefeckardt"), 3, 2)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Jens Karstedt, Detlef Eckardt"), 3, 2)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % " "), 3, 3)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % " "), 3, 3)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Romanian"), 4, 0)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Romanian"), 4, 0)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Marius Stanciu"), 4, 1)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Marius Stanciu"), 4, 1)
@@ -6079,6 +6090,7 @@ class App(QtCore.QObject):
         self.report_usage("on_toggle_grid()")
         self.report_usage("on_toggle_grid()")
 
 
         self.ui.grid_snap_btn.trigger()
         self.ui.grid_snap_btn.trigger()
+        self.on_grid_snap_triggered(state=True)
 
 
     def on_toggle_grid_lines(self):
     def on_toggle_grid_lines(self):
         self.report_usage("on_toggle_grd_lines()")
         self.report_usage("on_toggle_grd_lines()")
@@ -7019,6 +7031,8 @@ class App(QtCore.QObject):
         self.connect_toolbar_signals()
         self.connect_toolbar_signals()
 
 
         self.ui.grid_snap_btn.setChecked(True)
         self.ui.grid_snap_btn.setChecked(True)
+        self.on_grid_snap_triggered(state=True)
+
         self.ui.grid_gap_x_entry.setText(str(self.defaults["global_gridx"]))
         self.ui.grid_gap_x_entry.setText(str(self.defaults["global_gridx"]))
         self.ui.grid_gap_y_entry.setText(str(self.defaults["global_gridy"]))
         self.ui.grid_gap_y_entry.setText(str(self.defaults["global_gridy"]))
         self.ui.snap_max_dist_entry.setText(str(self.defaults["global_snap_max"]))
         self.ui.snap_max_dist_entry.setText(str(self.defaults["global_snap_max"]))
@@ -8437,16 +8451,16 @@ class App(QtCore.QObject):
     def populate_cmenu_grids(self):
     def populate_cmenu_grids(self):
         units = self.defaults['units'].lower()
         units = self.defaults['units'].lower()
 
 
+        for act in self.ui.cmenu_gridmenu.actions():
+            act.triggered.disconnect()
         self.ui.cmenu_gridmenu.clear()
         self.ui.cmenu_gridmenu.clear()
+
         sorted_list = sorted(self.defaults["global_grid_context_menu"][str(units)])
         sorted_list = sorted(self.defaults["global_grid_context_menu"][str(units)])
 
 
         grid_toggle = self.ui.cmenu_gridmenu.addAction(QtGui.QIcon(self.resource_location + '/grid32_menu.png'),
         grid_toggle = self.ui.cmenu_gridmenu.addAction(QtGui.QIcon(self.resource_location + '/grid32_menu.png'),
                                                        _("Grid On/Off"))
                                                        _("Grid On/Off"))
         grid_toggle.setCheckable(True)
         grid_toggle.setCheckable(True)
-        if self.grid_status() == True:
-            grid_toggle.setChecked(True)
-        else:
-            grid_toggle.setChecked(False)
+        grid_toggle.setChecked(True) if self.grid_status() else grid_toggle.setChecked(False)
 
 
         self.ui.cmenu_gridmenu.addSeparator()
         self.ui.cmenu_gridmenu.addSeparator()
         for grid in sorted_list:
         for grid in sorted_list:
@@ -8842,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)
@@ -8894,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()
@@ -8922,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
@@ -8939,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)
 
 
@@ -8951,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':
@@ -10442,7 +10471,8 @@ class App(QtCore.QObject):
                                      mirror=None)
                                      mirror=None)
 
 
             if obj.kind.lower() == 'gerber':
             if obj.kind.lower() == 'gerber':
-                color = self.defaults["global_plot_fill"][:-2]
+                # color = self.defaults["global_plot_fill"][:-2]
+                color = obj.fill_color[:-2]
             elif obj.kind.lower() == 'excellon':
             elif obj.kind.lower() == 'excellon':
                 color = '#C40000'
                 color = '#C40000'
             elif obj.kind.lower() == 'geometry':
             elif obj.kind.lower() == 'geometry':
@@ -11501,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)
@@ -12350,6 +12382,68 @@ class App(QtCore.QObject):
         # Clear pool to free memory
         # Clear pool to free memory
         self.clear_pool()
         self.clear_pool()
 
 
+    def on_set_color_action_triggered(self):
+        new_color = self.defaults['global_plot_fill']
+        act_name = self.sender().text().lower()
+
+        sel_obj_list = self.collection.get_selected()
+
+        if not sel_obj_list:
+            return
+
+        if act_name == 'red':
+            new_color = '#FF0000' + \
+                        str(hex(self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.value())[2:])
+        if act_name == 'blue':
+            new_color = '#0000FF' + \
+                        str(hex(self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.value())[2:])
+        if act_name == 'yellow':
+            new_color = '#FFDF00' + \
+                        str(hex(self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.value())[2:])
+        if act_name == 'green':
+            new_color = '#00FF00' + \
+                        str(hex(self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.value())[2:])
+        if act_name == 'purple':
+            new_color = '#FF00FF' + \
+                        str(hex(self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.value())[2:])
+        if act_name == 'brown':
+            new_color = '#A52A2A' + \
+                        str(hex(self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.value())[2:])
+
+        if act_name == 'custom':
+            new_color = QtGui.QColor(self.defaults['global_plot_fill'][:7])
+            c_dialog = QtWidgets.QColorDialog()
+            plot_fill_color = c_dialog.getColor(initial=new_color)
+
+            if plot_fill_color.isValid() is False:
+                return
+
+            new_color = str(plot_fill_color.name()) + \
+                        str(hex(self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.value())[2:])
+
+        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.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):
+        if state:
+            self.ui.snap_infobar_label.setPixmap(QtGui.QPixmap(self.resource_location + '/snap_filled_16.png'))
+        else:
+            self.ui.snap_infobar_label.setPixmap(QtGui.QPixmap(self.resource_location + '/snap_16.png'))
+
     def generate_cnc_job(self, objects):
     def generate_cnc_job(self, objects):
         self.report_usage("generate_cnc_job()")
         self.report_usage("generate_cnc_job()")
 
 

+ 69 - 42
FlatCAMCommon.py

@@ -474,7 +474,7 @@ class ToolsDB(QtWidgets.QWidget):
         self.on_tool_request = callback_on_tool_request
         self.on_tool_request = callback_on_tool_request
 
 
         self.offset_item_options = ["Path", "In", "Out", "Custom"]
         self.offset_item_options = ["Path", "In", "Out", "Custom"]
-        self.type_item_options = [_("Iso"), _("Rough"), _("Finish")]
+        self.type_item_options = ["Iso", "Rough", "Finish"]
         self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
         self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
 
 
         '''
         '''
@@ -760,13 +760,16 @@ class ToolsDB(QtWidgets.QWidget):
         self.table_widget.setRowCount(len(self.db_tool_dict))
         self.table_widget.setRowCount(len(self.db_tool_dict))
 
 
         nr_crt = 0
         nr_crt = 0
+
         for toolid, dict_val in self.db_tool_dict.items():
         for toolid, dict_val in self.db_tool_dict.items():
             row = nr_crt
             row = nr_crt
             nr_crt += 1
             nr_crt += 1
 
 
             t_name = dict_val['name']
             t_name = dict_val['name']
-            self.add_tool_table_line(row, name=t_name, widget=self.table_widget, tooldict=dict_val)
-
+            try:
+                self.add_tool_table_line(row, name=t_name, widget=self.table_widget, tooldict=dict_val)
+            except Exception as e:
+                self.app.log.debug("ToolDB.build_db_ui.add_tool_table_line() --> %s" % str(e))
             vertical_header = self.table_widget.verticalHeader()
             vertical_header = self.table_widget.verticalHeader()
             vertical_header.hide()
             vertical_header.hide()
 
 
@@ -920,7 +923,7 @@ class ToolsDB(QtWidgets.QWidget):
 
 
         dwelltime_item = FCDoubleSpinner()
         dwelltime_item = FCDoubleSpinner()
         dwelltime_item.set_precision(self.decimals)
         dwelltime_item.set_precision(self.decimals)
-        dwelltime_item.set_range(0.0, 9999.9999)
+        dwelltime_item.set_range(0.0000, 9999.9999)
         dwelltime_item.set_value(float(data['dwelltime']))
         dwelltime_item.set_value(float(data['dwelltime']))
         widget.setCellWidget(row, 18, dwelltime_item)
         widget.setCellWidget(row, 18, dwelltime_item)
 
 
@@ -936,7 +939,7 @@ class ToolsDB(QtWidgets.QWidget):
 
 
         ecut_length_item = FCDoubleSpinner()
         ecut_length_item = FCDoubleSpinner()
         ecut_length_item.set_precision(self.decimals)
         ecut_length_item.set_precision(self.decimals)
-        ecut_length_item.set_range(0.0, 9999.9999)
+        ecut_length_item.set_range(0.0000, 9999.9999)
         ecut_length_item.set_value(data['extracut_length'])
         ecut_length_item.set_value(data['extracut_length'])
         widget.setCellWidget(row, 21, ecut_length_item)
         widget.setCellWidget(row, 21, ecut_length_item)
 
 
@@ -977,11 +980,8 @@ class ToolsDB(QtWidgets.QWidget):
         Add a tool in the DB Tool Table
         Add a tool in the DB Tool Table
         :return: None
         :return: None
         """
         """
-        new_toolid = len(self.db_tool_dict) + 1
 
 
-        dict_elem = dict()
         default_data = dict()
         default_data = dict()
-
         default_data.update({
         default_data.update({
             "cutz": float(self.app.defaults["geometry_cutz"]),
             "cutz": float(self.app.defaults["geometry_cutz"]),
             "multidepth": self.app.defaults["geometry_multidepth"],
             "multidepth": self.app.defaults["geometry_multidepth"],
@@ -997,7 +997,7 @@ class ToolsDB(QtWidgets.QWidget):
             "dwelltime": float(self.app.defaults["geometry_dwelltime"]),
             "dwelltime": float(self.app.defaults["geometry_dwelltime"]),
             "ppname_g": self.app.defaults["geometry_ppname_g"],
             "ppname_g": self.app.defaults["geometry_ppname_g"],
             "extracut": self.app.defaults["geometry_extracut"],
             "extracut": self.app.defaults["geometry_extracut"],
-            "extracut_length": self.app.defaults["geometry_extracut_length"],
+            "extracut_length": float(self.app.defaults["geometry_extracut_length"]),
             "toolchange": self.app.defaults["geometry_toolchange"],
             "toolchange": self.app.defaults["geometry_toolchange"],
             "toolchangexy": self.app.defaults["geometry_toolchangexy"],
             "toolchangexy": self.app.defaults["geometry_toolchangexy"],
             "toolchangez": float(self.app.defaults["geometry_toolchangez"]),
             "toolchangez": float(self.app.defaults["geometry_toolchangez"]),
@@ -1005,20 +1005,17 @@ class ToolsDB(QtWidgets.QWidget):
             "endz": float(self.app.defaults["geometry_endz"])
             "endz": float(self.app.defaults["geometry_endz"])
         })
         })
 
 
+        dict_elem = dict()
         dict_elem['name'] = 'new_tool'
         dict_elem['name'] = 'new_tool'
         dict_elem['tooldia'] = self.app.defaults["geometry_cnctooldia"]
         dict_elem['tooldia'] = self.app.defaults["geometry_cnctooldia"]
         dict_elem['offset'] = 'Path'
         dict_elem['offset'] = 'Path'
         dict_elem['offset_value'] = 0.0
         dict_elem['offset_value'] = 0.0
-        dict_elem['type'] = _('Rough')
+        dict_elem['type'] = 'Rough'
         dict_elem['tool_type'] = 'C1'
         dict_elem['tool_type'] = 'C1'
-
         dict_elem['data'] = default_data
         dict_elem['data'] = default_data
 
 
-        self.db_tool_dict.update(
-            {
-                new_toolid: deepcopy(dict_elem)
-            }
-        )
+        new_toolid = len(self.db_tool_dict) + 1
+        self.db_tool_dict[new_toolid] = deepcopy(dict_elem)
 
 
         # add the new entry to the Tools DB table
         # add the new entry to the Tools DB table
         self.build_db_ui()
         self.build_db_ui()
@@ -1253,59 +1250,59 @@ class ToolsDB(QtWidgets.QWidget):
             new_toolid = row + 1
             new_toolid = row + 1
             for col in range(self.table_widget.columnCount()):
             for col in range(self.table_widget.columnCount()):
                 column_header_text = self.table_widget.horizontalHeaderItem(col).text()
                 column_header_text = self.table_widget.horizontalHeaderItem(col).text()
-                if column_header_text == 'Tool Name':
+                if column_header_text == _('Tool Name'):
                     dict_elem['name'] = self.table_widget.item(row, col).text()
                     dict_elem['name'] = self.table_widget.item(row, col).text()
-                elif column_header_text == 'Tool Dia':
+                elif column_header_text == _('Tool Dia'):
                     dict_elem['tooldia'] = self.table_widget.cellWidget(row, col).get_value()
                     dict_elem['tooldia'] = self.table_widget.cellWidget(row, col).get_value()
-                elif column_header_text == 'Tool Offset':
+                elif column_header_text == _('Tool Offset'):
                     dict_elem['offset'] = self.table_widget.cellWidget(row, col).get_value()
                     dict_elem['offset'] = self.table_widget.cellWidget(row, col).get_value()
-                elif column_header_text == 'Custom Offset':
+                elif column_header_text == _('Custom Offset'):
                     dict_elem['offset_value'] = self.table_widget.cellWidget(row, col).get_value()
                     dict_elem['offset_value'] = self.table_widget.cellWidget(row, col).get_value()
-                elif column_header_text == 'Tool Type':
+                elif column_header_text == _('Tool Type'):
                     dict_elem['type'] = self.table_widget.cellWidget(row, col).get_value()
                     dict_elem['type'] = self.table_widget.cellWidget(row, col).get_value()
-                elif column_header_text == 'Tool Shape':
+                elif column_header_text == _('Tool Shape'):
                     dict_elem['tool_type'] = self.table_widget.cellWidget(row, col).get_value()
                     dict_elem['tool_type'] = self.table_widget.cellWidget(row, col).get_value()
                 else:
                 else:
-                    if column_header_text == 'Cut Z':
+                    if column_header_text == _('Cut Z'):
                         default_data['cutz'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['cutz'] = self.table_widget.cellWidget(row, col).get_value()
-                    elif column_header_text == 'MultiDepth':
+                    elif column_header_text == _('MultiDepth'):
                         default_data['multidepth'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['multidepth'] = self.table_widget.cellWidget(row, col).get_value()
-                    elif column_header_text == 'DPP':
+                    elif column_header_text == _('DPP'):
                         default_data['depthperpass'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['depthperpass'] = self.table_widget.cellWidget(row, col).get_value()
-                    elif column_header_text == 'V-Dia':
+                    elif column_header_text == _('V-Dia'):
                         default_data['vtipdia'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['vtipdia'] = self.table_widget.cellWidget(row, col).get_value()
-                    elif column_header_text == 'V-Angle':
+                    elif column_header_text == _('V-Angle'):
                         default_data['vtipangle'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['vtipangle'] = self.table_widget.cellWidget(row, col).get_value()
-                    elif column_header_text == 'Travel Z':
+                    elif column_header_text == _('Travel Z'):
                         default_data['travelz'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['travelz'] = self.table_widget.cellWidget(row, col).get_value()
-                    elif column_header_text == 'FR':
+                    elif column_header_text == _('FR'):
                         default_data['feedrate'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['feedrate'] = self.table_widget.cellWidget(row, col).get_value()
-                    elif column_header_text == 'FR Z':
+                    elif column_header_text == _('FR Z'):
                         default_data['feedrate_z'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['feedrate_z'] = self.table_widget.cellWidget(row, col).get_value()
-                    elif column_header_text == 'FR Rapids':
+                    elif column_header_text == _('FR Rapids'):
                         default_data['feedrate_rapid'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['feedrate_rapid'] = self.table_widget.cellWidget(row, col).get_value()
-                    elif column_header_text == 'Spindle Speed':
+                    elif column_header_text == _('Spindle Speed'):
                         default_data['spindlespeed'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['spindlespeed'] = self.table_widget.cellWidget(row, col).get_value()
-                    elif column_header_text == 'Dwell':
+                    elif column_header_text == _('Dwell'):
                         default_data['dwell'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['dwell'] = self.table_widget.cellWidget(row, col).get_value()
-                    elif column_header_text == 'Dwelltime':
+                    elif column_header_text == _('Dwelltime'):
                         default_data['dwelltime'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['dwelltime'] = self.table_widget.cellWidget(row, col).get_value()
-                    elif column_header_text == 'Preprocessor':
+                    elif column_header_text == _('Preprocessor'):
                         default_data['ppname_g'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['ppname_g'] = self.table_widget.cellWidget(row, col).get_value()
-                    elif column_header_text == 'ExtraCut':
+                    elif column_header_text == _('ExtraCut'):
                         default_data['extracut'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['extracut'] = self.table_widget.cellWidget(row, col).get_value()
-                    elif column_header_text == "E-Cut Length":
+                    elif column_header_text == _("E-Cut Length"):
                         default_data['extracut_length'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['extracut_length'] = self.table_widget.cellWidget(row, col).get_value()
-                    elif column_header_text == 'Toolchange':
+                    elif column_header_text == _('Toolchange'):
                         default_data['toolchange'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['toolchange'] = self.table_widget.cellWidget(row, col).get_value()
-                    elif column_header_text == 'Toolchange XY':
+                    elif column_header_text == _('Toolchange XY'):
                         default_data['toolchangexy'] = self.table_widget.item(row, col).text()
                         default_data['toolchangexy'] = self.table_widget.item(row, col).text()
-                    elif column_header_text == 'Toolchange Z':
+                    elif column_header_text == _('Toolchange Z'):
                         default_data['toolchangez'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['toolchangez'] = self.table_widget.cellWidget(row, col).get_value()
-                    elif column_header_text == 'Start Z':
+                    elif column_header_text == _('Start Z'):
                         default_data['startz'] = float(self.table_widget.item(row, col).text()) \
                         default_data['startz'] = float(self.table_widget.item(row, col).text()) \
                             if self.table_widget.item(row, col).text() is not '' else None
                             if self.table_widget.item(row, col).text() is not '' else None
-                    elif column_header_text == 'End Z':
+                    elif column_header_text == _('End Z'):
                         default_data['endz'] = self.table_widget.cellWidget(row, col).get_value()
                         default_data['endz'] = self.table_widget.cellWidget(row, col).get_value()
 
 
             dict_elem['data'] = default_data
             dict_elem['data'] = default_data
@@ -1355,3 +1352,33 @@ class ToolsDB(QtWidgets.QWidget):
 
 
     def closeEvent(self, QCloseEvent):
     def closeEvent(self, QCloseEvent):
         super().closeEvent(QCloseEvent)
         super().closeEvent(QCloseEvent)
+
+
+def color_variant(hex_color, bright_factor=1):
+    """
+    Takes a color in HEX format #FF00FF and produces a lighter or darker variant
+
+    :param hex_color:           color to change
+    :param bright_factor:   factor to change the color brightness [0 ... 1]
+    :return:                    modified color
+    """
+
+    if len(hex_color) != 7:
+        print("Color is %s, but needs to be in #FF00FF format. Returning original color." % hex_color)
+        return hex_color
+
+    if bright_factor > 1.0:
+        bright_factor = 1.0
+    if bright_factor < 0.0:
+        bright_factor = 0.0
+
+    rgb_hex = [hex_color[x:x + 2] for x in [1, 3, 5]]
+    new_rgb = list()
+    for hex_value in rgb_hex:
+        # adjust each color channel and turn it into a INT suitable as argument for hex()
+        mod_color = round(int(hex_value, 16) * bright_factor)
+        # make sure that each color channel has two digits without the 0x prefix
+        mod_color_hex = str(hex(mod_color)[2:]).zfill(2)
+        new_rgb.append(mod_color_hex)
+
+    return "#" + "".join([i for i in new_rgb])

+ 62 - 18
FlatCAMObj.py

@@ -15,7 +15,9 @@ from shapely.geometry import Point, Polygon, MultiPolygon, MultiLineString, Line
 from shapely.ops import cascaded_union
 from shapely.ops import cascaded_union
 import shapely.affinity as affinity
 import shapely.affinity as affinity
 
 
-from copy import deepcopy, copy
+from copy import deepcopy
+from copy import copy
+
 from io import StringIO
 from io import StringIO
 import traceback
 import traceback
 import inspect  # TODO: For debugging only.
 import inspect  # TODO: For debugging only.
@@ -30,6 +32,8 @@ from flatcamParsers.ParseGerber import Gerber
 from camlib import Geometry, CNCjob
 from camlib import Geometry, CNCjob
 import FlatCAMApp
 import FlatCAMApp
 
 
+from flatcamGUI.VisPyVisuals import ShapeCollection
+
 import tkinter as tk
 import tkinter as tk
 import os, sys, itertools
 import os, sys, itertools
 import ezdxf
 import ezdxf
@@ -104,6 +108,7 @@ class FlatCAMObj(QtCore.QObject):
 
 
         if self.app.is_legacy is False:
         if self.app.is_legacy is False:
             self.shapes = self.app.plotcanvas.new_shape_group()
             self.shapes = self.app.plotcanvas.new_shape_group()
+            # self.shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, pool=self.app.pool, layers=2)
         else:
         else:
             self.shapes = ShapeCollectionLegacy(obj=self, app=self.app, name=name)
             self.shapes = ShapeCollectionLegacy(obj=self, app=self.app, name=name)
 
 
@@ -123,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']
 
 
@@ -591,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"
@@ -649,10 +659,13 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
 
         self.units_found = self.app.defaults['units']
         self.units_found = self.app.defaults['units']
 
 
+        self.fill_color = self.app.defaults['global_plot_fill']
+        self.outline_color = self.app.defaults['global_plot_line']
+
         # Attributes to be included in serialization
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # Always append to it because it carries contents
         # from predecessors.
         # from predecessors.
-        self.ser_attrs += ['options', 'kind']
+        self.ser_attrs += ['options', 'kind', 'fill_color', 'outline_color']
 
 
     def set_ui(self, ui):
     def set_ui(self, ui):
         """
         """
@@ -736,6 +749,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             self.ui.aperture_table_visibility_cb.hide()
             self.ui.aperture_table_visibility_cb.hide()
             self.ui.milling_type_label.hide()
             self.ui.milling_type_label.hide()
             self.ui.milling_type_radio.hide()
             self.ui.milling_type_radio.hide()
+            self.ui.iso_type_label.hide()
             self.ui.iso_type_radio.hide()
             self.ui.iso_type_radio.hide()
 
 
             self.ui.follow_cb.hide()
             self.ui.follow_cb.hide()
@@ -1665,12 +1679,12 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         if 'color' in kwargs:
         if 'color' in kwargs:
             color = kwargs['color']
             color = kwargs['color']
         else:
         else:
-            color = self.app.defaults['global_plot_line']
+            color = self.outline_color
 
 
         if 'face_color' in kwargs:
         if 'face_color' in kwargs:
             face_color = kwargs['face_color']
             face_color = kwargs['face_color']
         else:
         else:
-            face_color = self.app.defaults['global_plot_fill']
+            face_color = self.fill_color
 
 
         if 'visible' not in kwargs:
         if 'visible' not in kwargs:
             visible = self.options['plot']
             visible = self.options['plot']
@@ -1740,7 +1754,10 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                         for el in g:
                         for el in g:
                             self.add_shape(shape=el, color=random_color() if self.options['multicolored'] else 'black',
                             self.add_shape(shape=el, color=random_color() if self.options['multicolored'] else 'black',
                                            visible=visible)
                                            visible=visible)
-            self.shapes.redraw()
+            self.shapes.redraw(
+                # update_colors=(self.fill_color, self.outline_color),
+                # indexes=self.app.plotcanvas.shape_collection.data.keys()
+            )
         except (ObjectDeleted, AttributeError):
         except (ObjectDeleted, AttributeError):
             self.shapes.clear(update=True)
             self.shapes.clear(update=True)
         except Exception as e:
         except Exception as e:
@@ -2184,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,
@@ -2202,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"
@@ -3530,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"
 
 
@@ -3612,6 +3638,9 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         # this variable can be updated by the Object that generates the geometry
         # this variable can be updated by the Object that generates the geometry
         self.tool_type = 'C1'
         self.tool_type = 'C1'
 
 
+        # save here the old value for the Cut Z before it is changed by selecting a V-shape type tool in the tool table
+        self.old_cutz = self.app.defaults["geometry_cutz"]
+
         # Attributes to be included in serialization
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # Always append to it because it carries contents
         # from predecessors.
         # from predecessors.
@@ -3760,6 +3789,9 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         self.ui.name_entry.set_value(self.options['name'])
         self.ui.name_entry.set_value(self.options['name'])
         self.ui_connect()
         self.ui_connect()
 
 
+        self.ui.e_cut_entry.setDisabled(False) if self.ui.extracut_cb.get_value() else \
+            self.ui.e_cut_entry.setDisabled(True)
+
     def set_ui(self, ui):
     def set_ui(self, ui):
         FlatCAMObj.set_ui(self, ui)
         FlatCAMObj.set_ui(self, ui)
 
 
@@ -3847,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
 
 
@@ -3936,7 +3971,9 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         else:
         else:
             self.ui.level.setText('<span style="color:red;"><b>%s</b></span>' % _('Advanced'))
             self.ui.level.setText('<span style="color:red;"><b>%s</b></span>' % _('Advanced'))
 
 
-        self.ui.e_cut_entry.setDisabled(True)
+        self.ui.e_cut_entry.setDisabled(False) if self.app.defaults['geometry_extracut'] else \
+            self.ui.e_cut_entry.setDisabled(True)
+        self.ui.extracut_cb.toggled.connect(lambda state: self.ui.e_cut_entry.setDisabled(not state))
 
 
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
         self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
         self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
@@ -3949,6 +3986,10 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
 
 
         self.ui.addtool_from_db_btn.clicked.connect(self.on_tool_add_from_db_clicked)
         self.ui.addtool_from_db_btn.clicked.connect(self.on_tool_add_from_db_clicked)
         self.ui.apply_param_to_all.clicked.connect(self.on_apply_param_to_all_clicked)
         self.ui.apply_param_to_all.clicked.connect(self.on_apply_param_to_all_clicked)
+        self.ui.cutz_entry.returnPressed.connect(self.on_cut_z_changed)
+
+    def on_cut_z_changed(self):
+        self.old_cutz = self.ui.cutz_entry.get_value()
 
 
     def set_tool_offset_visibility(self, current_row):
     def set_tool_offset_visibility(self, current_row):
         if current_row is None:
         if current_row is None:
@@ -4589,6 +4630,9 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                     if cb_txt == 'V':
                     if cb_txt == 'V':
                         idx = self.ui.geo_tools_table.cellWidget(cw_row, 3).findText(_('Iso'))
                         idx = self.ui.geo_tools_table.cellWidget(cw_row, 3).findText(_('Iso'))
                         self.ui.geo_tools_table.cellWidget(cw_row, 3).setCurrentIndex(idx)
                         self.ui.geo_tools_table.cellWidget(cw_row, 3).setCurrentIndex(idx)
+                    else:
+                        self.ui.cutz_entry.set_value(self.old_cutz)
+
                 self.ui_update_v_shape(tool_type_txt=self.ui.geo_tools_table.cellWidget(cw_row, 4).currentText())
                 self.ui_update_v_shape(tool_type_txt=self.ui.geo_tools_table.cellWidget(cw_row, 4).currentText())
 
 
     def update_form(self, dict_storage):
     def update_form(self, dict_storage):

+ 5 - 0
ObjectCollection.py

@@ -321,6 +321,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         sel = len(self.view.selectedIndexes()) > 0
         sel = len(self.view.selectedIndexes()) > 0
         self.app.ui.menuprojectenable.setEnabled(sel)
         self.app.ui.menuprojectenable.setEnabled(sel)
         self.app.ui.menuprojectdisable.setEnabled(sel)
         self.app.ui.menuprojectdisable.setEnabled(sel)
+        self.app.ui.menuprojectcolor.setEnabled(sel)
         self.app.ui.menuprojectviewsource.setEnabled(sel)
         self.app.ui.menuprojectviewsource.setEnabled(sel)
 
 
         self.app.ui.menuprojectcopy.setEnabled(sel)
         self.app.ui.menuprojectcopy.setEnabled(sel)
@@ -334,8 +335,12 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             self.app.ui.menuprojectedit.setVisible(True)
             self.app.ui.menuprojectedit.setVisible(True)
             self.app.ui.menuprojectsave.setVisible(True)
             self.app.ui.menuprojectsave.setVisible(True)
             self.app.ui.menuprojectviewsource.setVisible(True)
             self.app.ui.menuprojectviewsource.setVisible(True)
+            self.app.ui.menuprojectcolor.setEnabled(False)
 
 
             for obj in self.get_selected():
             for obj in self.get_selected():
+                if type(obj) == FlatCAMGerber:
+                    self.app.ui.menuprojectcolor.setEnabled(True)
+
                 if type(obj) != FlatCAMGeometry:
                 if type(obj) != FlatCAMGeometry:
                     self.app.ui.menuprojectgeneratecnc.setVisible(False)
                     self.app.ui.menuprojectgeneratecnc.setVisible(False)
                 if type(obj) != FlatCAMGeometry and type(obj) != FlatCAMExcellon and type(obj) != FlatCAMGerber:
                 if type(obj) != FlatCAMGeometry and type(obj) != FlatCAMExcellon and type(obj) != FlatCAMGerber:

+ 29 - 0
README.md

@@ -9,10 +9,39 @@ CAD program, and create G-Code for Isolation routing.
 
 
 =================================================
 =================================================
 
 
+23.12.2019
+
+- 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
+- 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
+- solved issue #356 - in Tools DB can not be added more than one tool if a translation is active 
+
+22.12.2019
+
+- added a new option for the Gerber objects: on the project context menu now can be chosen a color for the selected Gerber object
+- fixed issue in Gerber UI where a label was not hidden when in Basic mode
+- added the color parameters of the objects to the serializable attributes
+- fixed Gerber object color set for Legacy(2D) graphic engine; glitch on the OpenGL(3D) graphic engine
+- fixed the above mentioned glitch in the OpenGL(3D) graphic engine when an Gerber object has been set with a color
+
+21.12.2019
+
+- fixed a typo in Distance Tool
+
 20.12.2019
 20.12.2019
 
 
 - fixed a rare issue in the generation of non-copper-region geometry started from the Gerber Object UI (selected tab)
 - fixed a rare issue in the generation of non-copper-region geometry started from the Gerber Object UI (selected tab)
 - Print function is now printing a PDF file for a selection of objects in the colors from canvas 
 - Print function is now printing a PDF file for a selection of objects in the colors from canvas 
+- added an icon in the infobar that will show more clearly the status of the grid snapping
+- in Geometry Object UI (selected tab) when a tool type is changed from no matter what to V-shape, the cut_z value is saved and when the tool type is changed back to something different than V-shape, this saved cut-z value is restored
+- fixed re-cut length entry not staying disabled when the re-cut cb is not checked
 
 
 19.12.2019
 19.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):

+ 1 - 0
flatcamEditors/FlatCAMExcEditor.py

@@ -2744,6 +2744,7 @@ class FlatCAMExcEditor(QtCore.QObject):
         # start with GRID toolbar activated
         # start with GRID toolbar activated
         if self.app.ui.grid_snap_btn.isChecked() is False:
         if self.app.ui.grid_snap_btn.isChecked() is False:
             self.app.ui.grid_snap_btn.trigger()
             self.app.ui.grid_snap_btn.trigger()
+            self.app.on_grid_snap_triggered(state=True)
 
 
         self.app.ui.popmenu_disable.setVisible(False)
         self.app.ui.popmenu_disable.setVisible(False)
         self.app.ui.cmenu_newmenu.menuAction().setVisible(False)
         self.app.ui.cmenu_newmenu.menuAction().setVisible(False)

+ 1 - 0
flatcamEditors/FlatCAMGeoEditor.py

@@ -3679,6 +3679,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # start with GRID toolbar activated
         # start with GRID toolbar activated
         if self.app.ui.grid_snap_btn.isChecked() is False:
         if self.app.ui.grid_snap_btn.isChecked() is False:
             self.app.ui.grid_snap_btn.trigger()
             self.app.ui.grid_snap_btn.trigger()
+            self.app.on_grid_snap_triggered(state=True)
 
 
     def on_buffer_tool(self):
     def on_buffer_tool(self):
         buff_tool = BufferSelectionTool(self.app, self)
         buff_tool = BufferSelectionTool(self.app, self)

+ 1 - 0
flatcamEditors/FlatCAMGrbEditor.py

@@ -3530,6 +3530,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # start with GRID toolbar activated
         # start with GRID toolbar activated
         if self.app.ui.grid_snap_btn.isChecked() is False:
         if self.app.ui.grid_snap_btn.isChecked() is False:
             self.app.ui.grid_snap_btn.trigger()
             self.app.ui.grid_snap_btn.trigger()
+            self.app.on_grid_snap_triggered(state=True)
 
 
         # adjust the visibility of some of the canvas context menu
         # adjust the visibility of some of the canvas context menu
         self.app.ui.popmenu_edit.setVisible(False)
         self.app.ui.popmenu_edit.setVisible(False)

+ 32 - 0
flatcamGUI/FlatCAMGUI.py

@@ -634,11 +634,39 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         # ########################## Project Tab Context Menu # ##################
         # ########################## Project Tab Context Menu # ##################
         # ########################################################################
         # ########################################################################
         self.menuproject = QtWidgets.QMenu()
         self.menuproject = QtWidgets.QMenu()
+
         self.menuprojectenable = self.menuproject.addAction(
         self.menuprojectenable = self.menuproject.addAction(
             QtGui.QIcon(self.app.resource_location + '/replot32.png'), _('Enable Plot'))
             QtGui.QIcon(self.app.resource_location + '/replot32.png'), _('Enable Plot'))
         self.menuprojectdisable = self.menuproject.addAction(
         self.menuprojectdisable = self.menuproject.addAction(
             QtGui.QIcon(self.app.resource_location + '/clear_plot32.png'), _('Disable Plot'))
             QtGui.QIcon(self.app.resource_location + '/clear_plot32.png'), _('Disable Plot'))
         self.menuproject.addSeparator()
         self.menuproject.addSeparator()
+
+        self.menuprojectcolor = self.menuproject.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/set_color32.png'), _('Set Color'))
+
+        self.menuproject_red = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/red32.png'), _('Red'))
+
+        self.menuproject_blue = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/blue32.png'), _('Blue'))
+
+        self.menuproject_yellow = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/yellow32.png'), _('Yellow'))
+
+        self.menuproject_green = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/green32.png'), _('Green'))
+
+        self.menuproject_purple = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/violet32.png'), _('Purple'))
+
+        self.menuproject_brown = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/brown32.png'), _('Brown'))
+
+        self.menuproject_custom = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/set_color32.png'), _('Custom'))
+
+        self.menuproject.addSeparator()
+
         self.menuprojectgeneratecnc = self.menuproject.addAction(
         self.menuprojectgeneratecnc = self.menuproject.addAction(
             QtGui.QIcon(self.app.resource_location + '/cnc32.png'), _('Generate CNC'))
             QtGui.QIcon(self.app.resource_location + '/cnc32.png'), _('Generate CNC'))
         self.menuprojectviewsource = self.menuproject.addAction(
         self.menuprojectviewsource = self.menuproject.addAction(
@@ -2149,6 +2177,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.fcinfo = FlatCAMInfoBar(app=self.app)
         self.fcinfo = FlatCAMInfoBar(app=self.app)
         self.infobar.addWidget(self.fcinfo, stretch=1)
         self.infobar.addWidget(self.fcinfo, stretch=1)
 
 
+        self.snap_infobar_label = QtWidgets.QLabel()
+        self.snap_infobar_label.setPixmap(QtGui.QPixmap(self.app.resource_location + '/snap_16.png'))
+        self.infobar.addWidget(self.snap_infobar_label)
+
         self.rel_position_label = QtWidgets.QLabel(
         self.rel_position_label = QtWidgets.QLabel(
             "<b>Dx</b>: 0.0000&nbsp;&nbsp;   <b>Dy</b>: 0.0000&nbsp;&nbsp;&nbsp;&nbsp;")
             "<b>Dx</b>: 0.0000&nbsp;&nbsp;   <b>Dy</b>: 0.0000&nbsp;&nbsp;&nbsp;&nbsp;")
         self.rel_position_label.setMinimumWidth(110)
         self.rel_position_label.setMinimumWidth(110)

+ 1 - 3
flatcamGUI/ObjectUI.py

@@ -1577,7 +1577,7 @@ class GeometryObjectUI(ObjectUI):
         self.cncfeedrate_rapid_entry.hide()
         self.cncfeedrate_rapid_entry.hide()
 
 
         # Cut over 1st point in path
         # Cut over 1st point in path
-        self.extracut_cb = FCCheckBox('%s' % _('Re-cut'))
+        self.extracut_cb = FCCheckBox('%s:' % _('Re-cut'))
         self.extracut_cb.setToolTip(
         self.extracut_cb.setToolTip(
             _("In order to remove possible\n"
             _("In order to remove possible\n"
               "copper leftovers where first cut\n"
               "copper leftovers where first cut\n"
@@ -1599,8 +1599,6 @@ class GeometryObjectUI(ObjectUI):
         self.grid3.addWidget(self.extracut_cb, 13, 0)
         self.grid3.addWidget(self.extracut_cb, 13, 0)
         self.grid3.addWidget(self.e_cut_entry, 13, 1)
         self.grid3.addWidget(self.e_cut_entry, 13, 1)
 
 
-        self.ois_e_cut = OptionalInputSection(self.extracut_cb, [self.e_cut_entry])
-
         # Spindlespeed
         # Spindlespeed
         spdlabel = QtWidgets.QLabel('%s:' % _('Spindle speed'))
         spdlabel = QtWidgets.QLabel('%s:' % _('Spindle speed'))
         spdlabel.setToolTip(
         spdlabel.setToolTip(

+ 23 - 7
flatcamGUI/PlotCanvasLegacy.py

@@ -957,9 +957,17 @@ class ShapeCollectionLegacy:
         :param linewidth: the width of the line
         :param linewidth: the width of the line
         :return:
         :return:
         """
         """
-        self._color = color[:-2] if color is not None else None
-        self._face_color = face_color[:-2] if face_color is not None else None
-        self._alpha = int(face_color[-2:], 16) / 255 if face_color is not None else 0.75
+        self._color = color if color is not None else "#006E20"
+        self._face_color = face_color if face_color is not None else "#BBF268"
+
+        if len(self._color) > 7:
+            self._color = self._color[:7]
+
+        if len(self._face_color) > 7:
+            self._face_color = self._face_color[:7]
+            # self._alpha = int(self._face_color[-2:], 16) / 255
+
+        self._alpha = 0.75
 
 
         if alpha is not None:
         if alpha is not None:
             self._alpha = alpha
             self._alpha = alpha
@@ -1033,7 +1041,7 @@ class ShapeCollectionLegacy:
         if update is True:
         if update is True:
             self.redraw()
             self.redraw()
 
 
-    def redraw(self):
+    def redraw(self, update_colors=None):
         """
         """
         This draw the shapes in the shapes collection, on canvas
         This draw the shapes in the shapes collection, on canvas
 
 
@@ -1087,7 +1095,6 @@ class ShapeCollectionLegacy:
                         self.axes.plot(x, y, local_shapes[element]['color'],
                         self.axes.plot(x, y, local_shapes[element]['color'],
                                        linestyle='-',
                                        linestyle='-',
                                        linewidth=local_shapes[element]['linewidth'])
                                        linewidth=local_shapes[element]['linewidth'])
-
                 elif obj_type == 'gerber':
                 elif obj_type == 'gerber':
                     if self.obj.options["multicolored"]:
                     if self.obj.options["multicolored"]:
                         linespec = '-'
                         linespec = '-'
@@ -1095,16 +1102,25 @@ class ShapeCollectionLegacy:
                         linespec = 'k-'
                         linespec = 'k-'
 
 
                     if self.obj.options["solid"]:
                     if self.obj.options["solid"]:
+                        if update_colors:
+                            gerber_fill_color = update_colors[0]
+                            gerber_outline_color = update_colors[1]
+                        else:
+                            gerber_fill_color = local_shapes[element]['face_color']
+                            gerber_outline_color = local_shapes[element]['color']
+
                         try:
                         try:
                             patch = PolygonPatch(local_shapes[element]['shape'],
                             patch = PolygonPatch(local_shapes[element]['shape'],
-                                                 facecolor=local_shapes[element]['face_color'],
-                                                 edgecolor=local_shapes[element]['color'],
+                                                 facecolor=gerber_fill_color,
+                                                 edgecolor=gerber_outline_color,
                                                  alpha=local_shapes[element]['alpha'],
                                                  alpha=local_shapes[element]['alpha'],
                                                  zorder=2)
                                                  zorder=2)
                             self.axes.add_patch(patch)
                             self.axes.add_patch(patch)
                         except AssertionError:
                         except AssertionError:
                             FlatCAMApp.App.log.warning("A geometry component was not a polygon:")
                             FlatCAMApp.App.log.warning("A geometry component was not a polygon:")
                             FlatCAMApp.App.log.warning(str(element))
                             FlatCAMApp.App.log.warning(str(element))
+                        except Exception as e:
+                            FlatCAMApp.App.log.debug("PlotCanvasLegacy.ShepeCollectionLegacy.redraw() --> %s" % str(e))
                     else:
                     else:
                         x, y = local_shapes[element]['shape'].exterior.xy
                         x, y = local_shapes[element]['shape'].exterior.xy
                         self.axes.plot(x, y, linespec)
                         self.axes.plot(x, y, linespec)

+ 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()
 
 

+ 188 - 35
flatcamGUI/VisPyVisuals.py

@@ -17,10 +17,9 @@ from flatcamGUI.VisPyTesselators import GLUTess
 
 
 
 
 class FlatCAMLineVisual(LineVisual):
 class FlatCAMLineVisual(LineVisual):
-    def __init__(self, pos=None, color=(0.5, 0.5, 0.5, 1), width=1, connect='strip',
-                            method='gl', antialias=False):
-        LineVisual.__init__(self, pos=None, color=(0.5, 0.5, 0.5, 1), width=1, connect='strip',
-                            method='gl', antialias=True)
+    def __init__(self, pos=None, color=(0.5, 0.5, 0.5, 1), width=1, connect='strip', method='gl', antialias=False):
+        LineVisual.__init__(self, pos=pos, color=color, width=width, connect=connect,
+                            method=method, antialias=True)
 
 
     def clear_data(self):
     def clear_data(self):
         self._bounds = None
         self._bounds = None
@@ -46,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
@@ -158,11 +161,14 @@ class ShapeGroup(object):
         if update:
         if update:
             self._collection.redraw([])             # Skip waiting results
             self._collection.redraw([])             # Skip waiting results
 
 
-    def redraw(self):
+    def redraw(self, update_colors=None):
         """
         """
         Redraws shape collection
         Redraws shape collection
         """
         """
-        self._collection.redraw(self._indexes)
+        if update_colors:
+            self._collection.redraw(self._indexes, update_colors=update_colors)
+        else:
+            self._collection.redraw(self._indexes)
 
 
     @property
     @property
     def visible(self):
     def visible(self):
@@ -228,9 +234,9 @@ class ShapeCollectionVisual(CompoundVisual):
             pass
             pass
             m.set_gl_state(polygon_offset_fill=True, polygon_offset=(1, 1), cull_face=False)
             m.set_gl_state(polygon_offset_fill=True, polygon_offset=(1, 1), cull_face=False)
 
 
-        for l in self._lines:
+        for lne in self._lines:
             pass
             pass
-            l.set_gl_state(blend=True)
+            lne.set_gl_state(blend=True)
 
 
         self.freeze()
         self.freeze()
 
 
@@ -245,6 +251,8 @@ class ShapeCollectionVisual(CompoundVisual):
             Line/edge color
             Line/edge color
         :param face_color: str, tuple
         :param face_color: str, tuple
             Polygon face color
             Polygon face color
+        :param alpha: str
+            Polygon transparency
         :param visible: bool
         :param visible: bool
             Shape visibility
             Shape visibility
         :param update: bool
         :param update: bool
@@ -271,11 +279,11 @@ class ShapeCollectionVisual(CompoundVisual):
         # Add data to process pool if pool exists
         # Add data to process pool if pool exists
         try:
         try:
             self.results[key] = self.pool.map_async(_update_shape_buffers, [self.data[key]])
             self.results[key] = self.pool.map_async(_update_shape_buffers, [self.data[key]])
-        except Exception as e:
+        except Exception:
             self.data[key] = _update_shape_buffers(self.data[key])
             self.data[key] = _update_shape_buffers(self.data[key])
 
 
         if update:
         if update:
-            self.redraw()                       # redraw() waits for pool process end
+            self.redraw()   # redraw() waits for pool process end
 
 
         return key
         return key
 
 
@@ -309,6 +317,134 @@ class ShapeCollectionVisual(CompoundVisual):
         if update:
         if update:
             self.__update()
             self.__update()
 
 
+    def update_color(self, new_mesh_color=None, new_line_color=None, indexes=None):
+        if new_mesh_color is None and new_line_color is None:
+            return
+
+        if not self.data:
+            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
+        line_colors = [[] for _ in range(0, len(self._meshes))]     # Line colors
+        line_pts = [[] for _ in range(0, len(self._lines))]         # Vertices for line
+
+        # Lock sub-visuals updates
+        self.update_lock.acquire(True)
+        # Merge shapes buffers
+
+        if indexes is None:
+            for k, data in list(self.data.items()):
+                if data['visible'] and 'line_pts' in data:
+                    if new_mesh_color and new_mesh_color != '':
+                        dim_mesh_tris = (len(data['mesh_tris']) // 3)
+                        if dim_mesh_tris != 0:
+                            try:
+                                mesh_colors[data['layer']] += [mesh_color_rgba] * dim_mesh_tris
+                                self.data[k]['face_color'] = new_mesh_color
+
+                                data['mesh_colors'] = [mesh_color_rgba for __ in range(len(data['mesh_colors']))]
+                            except Exception as e:
+                                print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
+                                      "Create mesh colors --> Data error. %s" % str(e))
+
+                    if new_line_color and new_line_color != '':
+                        dim_line_pts = (len(data['line_pts']))
+                        if dim_line_pts != 0:
+                            try:
+                                line_pts[data['layer']] += data['line_pts']
+                                line_colors[data['layer']] += [line_color_rgba] * dim_line_pts
+                                self.data[k]['color'] = new_line_color
+
+                                data['line_colors'] = [mesh_color_rgba for __ in range(len(data['line_colors']))]
+                            except Exception as e:
+                                print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
+                                      "Create line colors --> Data error. %s" % str(e))
+        else:
+            for k, data in list(self.data.items()):
+                if data['visible'] and 'line_pts' in data:
+                    dim_mesh_tris = (len(data['mesh_tris']) // 3)
+                    dim_line_pts = (len(data['line_pts']))
+
+                    if k in indexes:
+                        if new_mesh_color and new_mesh_color != '':
+                            if dim_mesh_tris != 0:
+                                try:
+                                    mesh_colors[data['layer']] += [mesh_color_rgba] * dim_mesh_tris
+                                    self.data[k]['face_color'] = new_mesh_color
+
+                                    data['mesh_colors'] = [mesh_color_rgba for __ in range(len(data['mesh_colors']))]
+                                except Exception as e:
+                                    print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
+                                          "Create mesh colors --> Data error. %s" % str(e))
+                        if new_line_color and new_line_color != '':
+                            if dim_line_pts != 0:
+                                try:
+                                    line_pts[data['layer']] += data['line_pts']
+                                    line_colors[data['layer']] += [line_color_rgba] * dim_line_pts
+                                    self.data[k]['color'] = new_line_color
+
+                                    data['line_colors'] = [mesh_color_rgba for __ in range(len(data['line_colors']))]
+                                except Exception as e:
+                                    print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
+                                          "Create line colors --> Data error. %s" % str(e))
+                    else:
+                        if dim_mesh_tris != 0:
+                            try:
+                                mesh_colors[data['layer']] += [Color(data['face_color']).rgba] * dim_mesh_tris
+                            except Exception as e:
+                                print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
+                                      "Create mesh colors --> Data error. %s" % str(e))
+
+                        if dim_line_pts != 0:
+                            try:
+                                line_pts[data['layer']] += data['line_pts']
+                                line_colors[data['layer']] += [Color(data['color']).rgba] * dim_line_pts
+                            except Exception as e:
+                                print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
+                                      "Create line colors --> Data error. %s" % str(e))
+
+        # Updating meshes
+        if new_mesh_color and new_mesh_color != '':
+            for i, mesh in enumerate(self._meshes):
+                if mesh_colors[i]:
+                    try:
+                        mesh._meshdata.set_face_colors(colors=np.asarray(mesh_colors[i]))
+                        mesh.mesh_data_changed()
+                    except Exception as e:
+                        print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
+                              "Apply mesh colors --> Data error. %s" % str(e))
+
+        # Updating lines
+        if new_line_color and new_line_color != '':
+            for i, line in enumerate(self._lines):
+                if len(line_pts[i]) > 0:
+                    try:
+                        line._color = np.asarray(line_colors[i])
+                        line._changed['color'] = True
+                        line.update()
+                    except Exception as e:
+                        print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
+                              "Apply line colors --> Data error. %s" % str(e))
+                else:
+                    line.clear_data()
+
+        self.update_lock.release()
+
     def __update(self):
     def __update(self):
         """
         """
         Merges internal buffers, sets data to visuals, redraws collection on scene
         Merges internal buffers, sets data to visuals, redraws collection on scene
@@ -328,20 +464,23 @@ class ShapeCollectionVisual(CompoundVisual):
                 try:
                 try:
                     line_pts[data['layer']] += data['line_pts']
                     line_pts[data['layer']] += data['line_pts']
                     line_colors[data['layer']] += data['line_colors']
                     line_colors[data['layer']] += data['line_colors']
-                    mesh_tris[data['layer']] += [x + len(mesh_vertices[data['layer']])
-                                                 for x in data['mesh_tris']]
 
 
+                    mesh_tris[data['layer']] += [x + len(mesh_vertices[data['layer']]) for x in data['mesh_tris']]
                     mesh_vertices[data['layer']] += data['mesh_vertices']
                     mesh_vertices[data['layer']] += data['mesh_vertices']
                     mesh_colors[data['layer']] += data['mesh_colors']
                     mesh_colors[data['layer']] += data['mesh_colors']
                 except Exception as e:
                 except Exception as e:
-                    print("Data error", e)
+                    print("VisPyVisuals.ShapeCollectionVisual._update() --> Data error. %s" % str(e))
 
 
         # Updating meshes
         # Updating meshes
         for i, mesh in enumerate(self._meshes):
         for i, mesh in enumerate(self._meshes):
             if len(mesh_vertices[i]) > 0:
             if len(mesh_vertices[i]) > 0:
                 set_state(polygon_offset_fill=False)
                 set_state(polygon_offset_fill=False)
-                mesh.set_data(np.asarray(mesh_vertices[i]), np.asarray(mesh_tris[i], dtype=np.uint32)
-                              .reshape((-1, 3)), face_colors=np.asarray(mesh_colors[i]))
+                faces_array = np.asarray(mesh_tris[i], dtype=np.uint32)
+                mesh.set_data(
+                    vertices=np.asarray(mesh_vertices[i]),
+                    faces=faces_array.reshape((-1, 3)),
+                    face_colors=np.asarray(mesh_colors[i])
+                )
             else:
             else:
                 mesh.set_data()
                 mesh.set_data()
 
 
@@ -350,17 +489,20 @@ class ShapeCollectionVisual(CompoundVisual):
         # Updating lines
         # Updating lines
         for i, line in enumerate(self._lines):
         for i, line in enumerate(self._lines):
             if len(line_pts[i]) > 0:
             if len(line_pts[i]) > 0:
-                line.set_data(np.asarray(line_pts[i]), np.asarray(line_colors[i]), self._line_width, 'segments')
+                line.set_data(
+                    pos=np.asarray(line_pts[i]),
+                    color=np.asarray(line_colors[i]),
+                    width=self._line_width,
+                    connect='segments')
             else:
             else:
                 line.clear_data()
                 line.clear_data()
 
 
             line._bounds_changed()
             line._bounds_changed()
 
 
         self._bounds_changed()
         self._bounds_changed()
-
         self.update_lock.release()
         self.update_lock.release()
 
 
-    def redraw(self, indexes=None):
+    def redraw(self, indexes=None, update_colors=None):
         """
         """
         Redraws collection
         Redraws collection
         :param indexes: list
         :param indexes: list
@@ -369,19 +511,30 @@ class ShapeCollectionVisual(CompoundVisual):
         # Only one thread can update data
         # Only one thread can update data
         self.results_lock.acquire(True)
         self.results_lock.acquire(True)
 
 
-        for i in list(self.data.copy().keys()) if not indexes else indexes:
-            if i in list(self.results.copy().keys()):
+        for i in list(self.data.keys()) if not indexes else indexes:
+            if i in list(self.results.keys()):
                 try:
                 try:
                     self.results[i].wait()                                  # Wait for process results
                     self.results[i].wait()                                  # Wait for process results
                     if i in self.data:
                     if i in self.data:
                         self.data[i] = self.results[i].get()[0]             # Store translated data
                         self.data[i] = self.results[i].get()[0]             # Store translated data
                         del self.results[i]
                         del self.results[i]
                 except Exception as e:
                 except Exception as e:
-                    print(e, indexes)
+                    print("VisPyVisuals.ShapeCollectionVisual.redraw() --> Data error = %s. Indexes = %s" %
+                          (str(e), str(indexes)))
 
 
         self.results_lock.release()
         self.results_lock.release()
 
 
-        self.__update()
+        if update_colors is None:
+            self.__update()
+        else:
+            try:
+                self.update_color(
+                    new_mesh_color=update_colors[0],
+                    new_line_color=update_colors[1],
+                    indexes=indexes
+                )
+            except Exception as e:
+                print("VisPyVisuals.ShapeCollectionVisual.redraw() --> Update colors error = %s." % str(e))
 
 
     def lock_updates(self):
     def lock_updates(self):
         self.update_lock.acquire(True)
         self.update_lock.acquire(True)
@@ -489,7 +642,7 @@ class TextCollectionVisual(TextVisual):
         self.lock.release()
         self.lock.release()
 
 
         # Prepare data for translation
         # Prepare data for translation
-        self.data[key] = {'text': text, 'pos': pos, 'visible': visible,'font_size': font_size, 'color': color}
+        self.data[key] = {'text': text, 'pos': pos, 'visible': visible, 'font_size': font_size, 'color': color}
 
 
         if update:
         if update:
             self.redraw()
             self.redraw()
@@ -537,7 +690,7 @@ class TextCollectionVisual(TextVisual):
                     font_s = data['font_size']
                     font_s = data['font_size']
                     color = data['color']
                     color = data['color']
                 except Exception as e:
                 except Exception as e:
-                    print("Data error", e)
+                    print("VisPyVisuals.TextCollectionVisual._update() --> Data error. %s" % str(e))
 
 
         # Updating text
         # Updating text
         if len(labels) > 0:
         if len(labels) > 0:

+ 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):
     """
     """

+ 1 - 1
flatcamTools/ToolDistance.py

@@ -349,7 +349,7 @@ class Distance(FlatCAMTool):
                 d = math.sqrt(dx ** 2 + dy ** 2)
                 d = math.sqrt(dx ** 2 + dy ** 2)
                 self.stop_entry.set_value("(%.*f, %.*f)" % (self.decimals, pos[0], self.decimals, pos[1]))
                 self.stop_entry.set_value("(%.*f, %.*f)" % (self.decimals, pos[0], self.decimals, pos[1]))
 
 
-                self.app.inform.emit("{tx1}: {tx2} D(x) = {d_x} | D(y) = {d_y} | (tx3} = {d_z}".format(
+                self.app.inform.emit("{tx1}: {tx2} D(x) = {d_x} | D(y) = {d_y} | {tx3} = {d_z}".format(
                     tx1=_("MEASURING"),
                     tx1=_("MEASURING"),
                     tx2=_("Result"),
                     tx2=_("Result"),
                     tx3=_("Distance"),
                     tx3=_("Distance"),

+ 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


BIN
share/brown32.png


BIN
share/green32.png



BIN
share/set_color16.png


BIN
share/set_color32.png


BIN
share/snap_16.png


BIN
share/snap_filled_16.png


BIN
share/violet32.png


BIN
share/yellow32.png