Просмотр исходного кода

Merged in marius_stanciu/flatcam_beta/beta_8.916 (pull request #146)

Beta 8.916
Marius Stanciu 6 лет назад
Родитель
Сommit
cd1da70147

+ 289 - 29
FlatCAMApp.py

@@ -94,8 +94,8 @@ class App(QtCore.QObject):
     log.addHandler(handler)
     log.addHandler(handler)
 
 
     # Version
     # Version
-    version = 8.915
-    version_date = "2019/05/1"
+    version = 8.916
+    version_date = "2019/05/10"
     beta = True
     beta = True
 
 
     # current date now
     # current date now
@@ -322,6 +322,7 @@ class App(QtCore.QObject):
             "global_project_autohide": self.ui.general_defaults_form.general_app_group.project_autohide_cb,
             "global_project_autohide": self.ui.general_defaults_form.general_app_group.project_autohide_cb,
             "global_toggle_tooltips": self.ui.general_defaults_form.general_app_group.toggle_tooltips_cb,
             "global_toggle_tooltips": self.ui.general_defaults_form.general_app_group.toggle_tooltips_cb,
             "global_worker_number": self.ui.general_defaults_form.general_app_group.worker_number_sb,
             "global_worker_number": self.ui.general_defaults_form.general_app_group.worker_number_sb,
+            "global_tolerance": self.ui.general_defaults_form.general_app_group.tol_entry,
 
 
             "global_compression_level": self.ui.general_defaults_form.general_app_group.compress_combo,
             "global_compression_level": self.ui.general_defaults_form.general_app_group.compress_combo,
             "global_save_compressed": self.ui.general_defaults_form.general_app_group.save_type_cb,
             "global_save_compressed": self.ui.general_defaults_form.general_app_group.save_type_cb,
@@ -372,6 +373,12 @@ class App(QtCore.QObject):
             "gerber_aperture_buffer_factor": self.ui.gerber_defaults_form.gerber_adv_opt_group.buffer_aperture_entry,
             "gerber_aperture_buffer_factor": self.ui.gerber_defaults_form.gerber_adv_opt_group.buffer_aperture_entry,
             "gerber_follow": self.ui.gerber_defaults_form.gerber_adv_opt_group.follow_cb,
             "gerber_follow": self.ui.gerber_defaults_form.gerber_adv_opt_group.follow_cb,
 
 
+            # Gerber Export
+            "gerber_exp_units": self.ui.gerber_defaults_form.gerber_exp_group.gerber_units_radio,
+            "gerber_exp_integer": self.ui.gerber_defaults_form.gerber_exp_group.format_whole_entry,
+            "gerber_exp_decimals": self.ui.gerber_defaults_form.gerber_exp_group.format_dec_entry,
+            "gerber_exp_zeros": self.ui.gerber_defaults_form.gerber_exp_group.zeros_radio,
+
             # Excellon General
             # Excellon General
             "excellon_plot": self.ui.excellon_defaults_form.excellon_gen_group.plot_cb,
             "excellon_plot": self.ui.excellon_defaults_form.excellon_gen_group.plot_cb,
             "excellon_solid": self.ui.excellon_defaults_form.excellon_gen_group.solid_cb,
             "excellon_solid": self.ui.excellon_defaults_form.excellon_gen_group.solid_cb,
@@ -594,6 +601,7 @@ class App(QtCore.QObject):
             "global_project_autohide": True,
             "global_project_autohide": True,
             "global_toggle_tooltips": True,
             "global_toggle_tooltips": True,
             "global_worker_number": 2,
             "global_worker_number": 2,
+            "global_tolerance": 0.01,
             "global_compression_level": 3,
             "global_compression_level": 3,
             "global_save_compressed": True,
             "global_save_compressed": True,
 
 
@@ -680,6 +688,12 @@ class App(QtCore.QObject):
             "gerber_aperture_buffer_factor": 0.0,
             "gerber_aperture_buffer_factor": 0.0,
             "gerber_follow": False,
             "gerber_follow": False,
 
 
+            # Gerber Export
+            "gerber_exp_units": 'IN',
+            "gerber_exp_integer": 2,
+            "gerber_exp_decimals": 4,
+            "gerber_exp_zeros": 'L',
+
             # Excellon General
             # Excellon General
             "excellon_plot": True,
             "excellon_plot": True,
             "excellon_solid": True,
             "excellon_solid": True,
@@ -700,7 +714,7 @@ class App(QtCore.QObject):
             "excellon_dwell": False,
             "excellon_dwell": False,
             "excellon_dwelltime": 1,
             "excellon_dwelltime": 1,
             "excellon_toolchange": False,
             "excellon_toolchange": False,
-            "excellon_toolchangez": 1.0,
+            "excellon_toolchangez": 0.5,
             "excellon_ppname_e": 'default',
             "excellon_ppname_e": 'default',
             "excellon_tooldia": 0.016,
             "excellon_tooldia": 0.016,
             "excellon_slot_tooldia": 0.016,
             "excellon_slot_tooldia": 0.016,
@@ -710,7 +724,7 @@ class App(QtCore.QObject):
             "excellon_offset": 0.0,
             "excellon_offset": 0.0,
             "excellon_toolchangexy": "0.0, 0.0",
             "excellon_toolchangexy": "0.0, 0.0",
             "excellon_startz": None,
             "excellon_startz": None,
-            "excellon_endz": 2.0,
+            "excellon_endz": 0.5,
             "excellon_feedrate_rapid": 3.0,
             "excellon_feedrate_rapid": 3.0,
             "excellon_z_pdepth": -0.02,
             "excellon_z_pdepth": -0.02,
             "excellon_feedrate_probe": 3.0,
             "excellon_feedrate_probe": 3.0,
@@ -735,7 +749,7 @@ class App(QtCore.QObject):
             "geometry_depthperpass": 0.002,
             "geometry_depthperpass": 0.002,
             "geometry_travelz": 0.1,
             "geometry_travelz": 0.1,
             "geometry_toolchange": False,
             "geometry_toolchange": False,
-            "geometry_toolchangez": 1.0,
+            "geometry_toolchangez": 0.5,
             "geometry_feedrate": 3.0,
             "geometry_feedrate": 3.0,
             "geometry_feedrate_z": 3.0,
             "geometry_feedrate_z": 3.0,
             "geometry_spindlespeed": None,
             "geometry_spindlespeed": None,
@@ -746,7 +760,7 @@ class App(QtCore.QObject):
             # Geometry Advanced Options
             # Geometry Advanced Options
             "geometry_toolchangexy": "0.0, 0.0",
             "geometry_toolchangexy": "0.0, 0.0",
             "geometry_startz": None,
             "geometry_startz": None,
-            "geometry_endz": 2.0,
+            "geometry_endz": 0.5,
             "geometry_feedrate_rapid": 3.0,
             "geometry_feedrate_rapid": 3.0,
             "geometry_extracut": False,
             "geometry_extracut": False,
             "geometry_z_pdepth": -0.02,
             "geometry_z_pdepth": -0.02,
@@ -1297,7 +1311,7 @@ class App(QtCore.QObject):
         self.ui.menufileexportsvg.triggered.connect(self.on_file_exportsvg)
         self.ui.menufileexportsvg.triggered.connect(self.on_file_exportsvg)
         self.ui.menufileexportpng.triggered.connect(self.on_file_exportpng)
         self.ui.menufileexportpng.triggered.connect(self.on_file_exportpng)
         self.ui.menufileexportexcellon.triggered.connect(self.on_file_exportexcellon)
         self.ui.menufileexportexcellon.triggered.connect(self.on_file_exportexcellon)
-
+        self.ui.menufileexportgerber.triggered.connect(self.on_file_exportgerber)
 
 
         self.ui.menufileexportdxf.triggered.connect(self.on_file_exportdxf)
         self.ui.menufileexportdxf.triggered.connect(self.on_file_exportdxf)
 
 
@@ -1320,7 +1334,9 @@ class App(QtCore.QObject):
         self.ui.menueditdelete.triggered.connect(self.on_delete)
         self.ui.menueditdelete.triggered.connect(self.on_delete)
 
 
         self.ui.menueditcopyobject.triggered.connect(self.on_copy_object)
         self.ui.menueditcopyobject.triggered.connect(self.on_copy_object)
-        self.ui.menueditcopyobjectasgeom.triggered.connect(self.on_copy_object_as_geometry)
+        self.ui.menueditconvert_any2geo.triggered.connect(self.convert_any2geo)
+        self.ui.menueditconvert_any2gerber.triggered.connect(self.convert_any2gerber)
+
         self.ui.menueditorigin.triggered.connect(self.on_set_origin)
         self.ui.menueditorigin.triggered.connect(self.on_set_origin)
         self.ui.menueditjump.triggered.connect(self.on_jump_to)
         self.ui.menueditjump.triggered.connect(self.on_jump_to)
 
 
@@ -1416,7 +1432,8 @@ class App(QtCore.QObject):
         self.ui.general_defaults_form.general_app_group.language_apply_btn.clicked.connect(
         self.ui.general_defaults_form.general_app_group.language_apply_btn.clicked.connect(
             lambda: fcTranslate.on_language_apply_click(self, restart=True)
             lambda: fcTranslate.on_language_apply_click(self, restart=True)
         )
         )
-        self.ui.general_defaults_form.general_app_group.units_radio.activated_custom.connect(self.on_toggle_units)
+        self.ui.general_defaults_form.general_app_group.units_radio.activated_custom.connect(
+            lambda :self.on_toggle_units(no_pref=False))
 
 
         ###############################
         ###############################
         ### GUI PREFERENCES SIGNALS ###
         ### GUI PREFERENCES SIGNALS ###
@@ -2169,6 +2186,9 @@ class App(QtCore.QObject):
             # set call source to the Editor we go into
             # set call source to the Editor we go into
             self.call_source = 'exc_editor'
             self.call_source = 'exc_editor'
 
 
+            if self.ui.splitter.sizes()[0] == 0:
+                self.ui.splitter.setSizes([1, 1])
+
         elif isinstance(edited_object, FlatCAMGerber):
         elif isinstance(edited_object, FlatCAMGerber):
             # store the Gerber Editor Toolbar visibility before entering in the Editor
             # store the Gerber Editor Toolbar visibility before entering in the Editor
             self.grb_editor.toolbar_old_state = True if self.ui.grb_edit_toolbar.isVisible() else False
             self.grb_editor.toolbar_old_state = True if self.ui.grb_edit_toolbar.isVisible() else False
@@ -2177,6 +2197,9 @@ class App(QtCore.QObject):
             # set call source to the Editor we go into
             # set call source to the Editor we go into
             self.call_source = 'grb_editor'
             self.call_source = 'grb_editor'
 
 
+            if self.ui.splitter.sizes()[0] == 0:
+                self.ui.splitter.setSizes([1, 1])
+
         # # make sure that we can't select another object while in Editor Mode:
         # # make sure that we can't select another object while in Editor Mode:
         # self.collection.view.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
         # self.collection.view.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
         self.ui.project_frame.setDisabled(True)
         self.ui.project_frame.setDisabled(True)
@@ -2249,7 +2272,7 @@ class App(QtCore.QObject):
                         self.grb_editor.deactivate_grb_editor()
                         self.grb_editor.deactivate_grb_editor()
 
 
                         # delete the old object (the source object) if it was an empty one
                         # delete the old object (the source object) if it was an empty one
-                        if edited_obj.solid_geometry.is_empty:
+                        if not edited_obj.solid_geometry:
                             old_name = edited_obj.options['name']
                             old_name = edited_obj.options['name']
                             self.collection.set_active(old_name)
                             self.collection.set_active(old_name)
                             self.collection.delete_active()
                             self.collection.delete_active()
@@ -3513,7 +3536,7 @@ class App(QtCore.QObject):
     def set_screen_units(self, units):
     def set_screen_units(self, units):
         self.ui.units_label.setText("[" + self.defaults["units"].lower() + "]")
         self.ui.units_label.setText("[" + self.defaults["units"].lower() + "]")
 
 
-    def on_toggle_units(self):
+    def on_toggle_units(self, no_pref=False):
         """
         """
         Callback for the Units radio-button change in the Options tab.
         Callback for the Units radio-button change in the Options tab.
         Changes the application's default units or the current project's units.
         Changes the application's default units or the current project's units.
@@ -3651,13 +3674,14 @@ class App(QtCore.QObject):
         response = msgbox.clickedButton()
         response = msgbox.clickedButton()
 
 
         if response == bt_ok:
         if response == bt_ok:
-            self.options_read_form()
-            scale_options(factor)
-            self.options_write_form()
+            if no_pref is False:
+                self.options_read_form()
+                scale_options(factor)
+                self.options_write_form()
 
 
-            self.defaults_read_form()
-            scale_defaults(factor)
-            self.defaults_write_form()
+                self.defaults_read_form()
+                scale_defaults(factor)
+                self.defaults_write_form()
 
 
             self.should_we_save = True
             self.should_we_save = True
 
 
@@ -3669,8 +3693,8 @@ class App(QtCore.QObject):
             self.ui.grid_gap_x_entry.set_value(float(self.ui.grid_gap_x_entry.get_value()) * factor)
             self.ui.grid_gap_x_entry.set_value(float(self.ui.grid_gap_x_entry.get_value()) * factor)
             self.ui.grid_gap_y_entry.set_value(float(self.ui.grid_gap_y_entry.get_value()) * factor)
             self.ui.grid_gap_y_entry.set_value(float(self.ui.grid_gap_y_entry.get_value()) * factor)
 
 
+            units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
             for obj in self.collection.get_list():
             for obj in self.collection.get_list():
-                units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
                 obj.convert_units(units)
                 obj.convert_units(units)
 
 
                 # make that the properties stored in the object are also updated
                 # make that the properties stored in the object are also updated
@@ -3684,9 +3708,9 @@ class App(QtCore.QObject):
                     current.to_form()
                     current.to_form()
 
 
             self.plot_all()
             self.plot_all()
-            self.inform.emit(_("[success] Converted units to %s") % self.defaults["units"])
+            self.inform.emit(_("[success] Converted units to %s") % units)
             # self.ui.units_label.setText("[" + self.options["units"] + "]")
             # self.ui.units_label.setText("[" + self.options["units"] + "]")
-            self.set_screen_units(self.defaults["units"])
+            self.set_screen_units(units)
         else:
         else:
             # Undo toggling
             # Undo toggling
             self.toggle_units_ignore = True
             self.toggle_units_ignore = True
@@ -3701,11 +3725,14 @@ class App(QtCore.QObject):
         self.defaults_read_form()
         self.defaults_read_form()
 
 
     def on_toggle_units_click(self):
     def on_toggle_units_click(self):
-        if self.options["units"] == 'MM':
+        self.ui.general_defaults_form.general_app_group.units_radio.activated_custom.disconnect()
+        if self.defaults["units"] == 'MM':
             self.ui.general_defaults_form.general_app_group.units_radio.set_value("IN")
             self.ui.general_defaults_form.general_app_group.units_radio.set_value("IN")
         else:
         else:
             self.ui.general_defaults_form.general_app_group.units_radio.set_value("MM")
             self.ui.general_defaults_form.general_app_group.units_radio.set_value("MM")
-        self.on_toggle_units()
+        self.on_toggle_units(no_pref=True)
+        self.ui.general_defaults_form.general_app_group.units_radio.activated_custom.connect(
+            lambda: self.on_toggle_units(no_pref=False))
 
 
     def on_fullscreen(self):
     def on_fullscreen(self):
         self.report_usage("on_fullscreen()")
         self.report_usage("on_fullscreen()")
@@ -4552,6 +4579,13 @@ class App(QtCore.QObject):
                 self.report_usage("on_delete")
                 self.report_usage("on_delete")
 
 
                 while (self.collection.get_active()):
                 while (self.collection.get_active()):
+                    obj_active = self.collection.get_active()
+                    # if the deleted object is FlatCAMGerber then make sure to delete the possbile mark shapes
+                    if isinstance(obj_active, FlatCAMGerber):
+                        for el in obj_active.mark_shapes:
+                            obj_active.mark_shapes[el].clear(update=True)
+                            obj_active.mark_shapes[el].enabled = False
+                            obj_active.mark_shapes[el] = None
                     self.delete_first_selected()
                     self.delete_first_selected()
 
 
                 self.inform.emit(_("Object(s) deleted ..."))
                 self.inform.emit(_("Object(s) deleted ..."))
@@ -4713,8 +4747,8 @@ class App(QtCore.QObject):
                 except:
                 except:
                     log.warning("Could not rename the object in the list")
                     log.warning("Could not rename the object in the list")
 
 
-    def on_copy_object_as_geometry(self):
-        self.report_usage("on_copy_object_as_geometry()")
+    def convert_any2geo(self):
+        self.report_usage("convert_any2geo()")
 
 
         def initialize(obj_init, app):
         def initialize(obj_init, app):
             obj_init.solid_geometry = obj.solid_geometry
             obj_init.solid_geometry = obj.solid_geometry
@@ -4727,8 +4761,11 @@ class App(QtCore.QObject):
             except:
             except:
                 pass
                 pass
 
 
-            if obj.tools:
-                obj_init.tools = obj.tools
+            try:
+                if obj.tools:
+                    obj_init.tools = obj.tools
+            except AttributeError:
+                pass
 
 
         def initialize_excellon(obj_init, app):
         def initialize_excellon(obj_init, app):
             # objs = self.collection.get_selected()
             # objs = self.collection.get_selected()
@@ -4739,15 +4776,82 @@ class App(QtCore.QObject):
                     solid_geo.append(geo)
                     solid_geo.append(geo)
             obj_init.solid_geometry = deepcopy(solid_geo)
             obj_init.solid_geometry = deepcopy(solid_geo)
 
 
+        if not self.collection.get_selected():
+            log.warning("App.convert_any2geo --> No object selected")
+            self.inform.emit(_("[WARNING_NOTCL] No object is selected. Select an object and try again."))
+            return
+
+        for obj in self.collection.get_selected():
+
+            obj_name = obj.options["name"]
+
+            try:
+                if isinstance(obj, FlatCAMExcellon):
+                    self.new_object("geometry", str(obj_name) + "_conv", initialize_excellon)
+                else:
+                    self.new_object("geometry", str(obj_name) + "_conv", initialize)
+
+            except Exception as e:
+                return "Operation failed: %s" % str(e)
+
+    def convert_any2gerber(self):
+        self.report_usage("convert_any2gerber()")
+
+        def initialize(obj_init, app):
+            apertures = {}
+            apid = 0
+
+            apertures[str(apid)] = {}
+            apertures[str(apid)]['solid_geometry'] = []
+            apertures[str(apid)]['solid_geometry'] = deepcopy(obj.solid_geometry)
+            apertures[str(apid)]['size'] = 0.0
+            apertures[str(apid)]['type'] = 'C'
+
+            obj_init.solid_geometry = deepcopy(obj.solid_geometry)
+            obj_init.apertures = deepcopy(apertures)
+
+        def initialize_excellon(obj_init, app):
+            apertures = {}
+
+            apid = 10
+            for tool in obj.tools:
+                apertures[str(apid)] = {}
+                apertures[str(apid)]['solid_geometry'] = []
+                for geo in obj.tools[tool]['solid_geometry']:
+                    apertures[str(apid)]['solid_geometry'].append(geo)
+
+                apertures[str(apid)]['size'] = float(obj.tools[tool]['C'])
+                apertures[str(apid)]['type'] = 'C'
+                apid += 1
+
+            # create solid_geometry
+            solid_geometry = []
+            for apid in apertures:
+                for geo in apertures[apid]['solid_geometry']:
+                    solid_geometry.append(geo)
+
+            solid_geometry = MultiPolygon(solid_geometry)
+            solid_geometry = solid_geometry.buffer(0.0000001)
+
+            obj_init.solid_geometry = deepcopy(solid_geometry)
+            obj_init.apertures = deepcopy(apertures)
+            # clear the working objects (perhaps not necessary due of Python GC)
+            apertures.clear()
+
+        if not self.collection.get_selected():
+            log.warning("App.convert_any2gerber --> No object selected")
+            self.inform.emit(_("[WARNING_NOTCL] No object is selected. Select an object and try again."))
+            return
+
         for obj in self.collection.get_selected():
         for obj in self.collection.get_selected():
 
 
             obj_name = obj.options["name"]
             obj_name = obj.options["name"]
 
 
             try:
             try:
                 if isinstance(obj, FlatCAMExcellon):
                 if isinstance(obj, FlatCAMExcellon):
-                    self.new_object("geometry", str(obj_name) + "_gcopy", initialize_excellon)
+                    self.new_object("gerber", str(obj_name) + "_conv", initialize_excellon)
                 else:
                 else:
-                    self.new_object("geometry", str(obj_name) + "_gcopy", initialize)
+                    self.new_object("gerber", str(obj_name) + "_conv", initialize)
 
 
             except Exception as e:
             except Exception as e:
                 return "Operation failed: %s" % str(e)
                 return "Operation failed: %s" % str(e)
@@ -4776,6 +4880,7 @@ class App(QtCore.QObject):
                 obj.options['ymax'] = d
                 obj.options['ymax'] = d
             # self.plot_all(zoom=False)
             # self.plot_all(zoom=False)
             self.inform.emit(_('[success] Origin set ...'))
             self.inform.emit(_('[success] Origin set ...'))
+            self.plotcanvas.fit_view()
             self.plotcanvas.vis_disconnect('mouse_press', self.on_set_zero_click)
             self.plotcanvas.vis_disconnect('mouse_press', self.on_set_zero_click)
             self.should_we_save = True
             self.should_we_save = True
 
 
@@ -6079,7 +6184,7 @@ class App(QtCore.QObject):
 
 
     def on_file_exportexcellon(self):
     def on_file_exportexcellon(self):
         """
         """
-        Callback for menu item File->Export SVG.
+        Callback for menu item File->Export->Excellon.
 
 
         :return: None
         :return: None
         """
         """
@@ -6116,6 +6221,45 @@ class App(QtCore.QObject):
             self.export_excellon(name, filename)
             self.export_excellon(name, filename)
             self.file_saved.emit("Excellon", filename)
             self.file_saved.emit("Excellon", filename)
 
 
+    def on_file_exportgerber(self):
+        """
+        Callback for menu item File->Export->Gerber.
+
+        :return: None
+        """
+        self.report_usage("on_file_exportgerber")
+        App.log.debug("on_file_exportgerber()")
+
+        obj = self.collection.get_active()
+        if obj is None:
+            self.inform.emit(_("[WARNING_NOTCL] No object selected. Please Select an Gerber object to export."))
+            return
+
+        # Check for more compatible types and add as required
+        if not isinstance(obj, FlatCAMGerber):
+            self.inform.emit(_("[ERROR_NOTCL] Failed. Only Gerber objects can be saved as Gerber files..."))
+            return
+
+        name = self.collection.get_active().options["name"]
+
+        filter = "Gerber File (*.GBR);;All Files (*.*)"
+        try:
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(
+                caption=_("Export Gerber"),
+                directory=self.get_last_save_folder() + '/' + name,
+                filter=filter)
+        except TypeError:
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export Gerber"), filter=filter)
+
+        filename = str(filename)
+
+        if filename == "":
+            self.inform.emit(_("[WARNING_NOTCL] Export Gerber cancelled."))
+            return
+        else:
+            self.export_gerber(name, filename)
+            self.file_saved.emit("Gerber", filename)
+
     def on_file_exportdxf(self):
     def on_file_exportdxf(self):
         """
         """
                 Callback for menu item File->Export DXF.
                 Callback for menu item File->Export DXF.
@@ -6917,6 +7061,122 @@ class App(QtCore.QObject):
                 self.inform.emit(_('[ERROR_NOTCL] Could not export Excellon file.'))
                 self.inform.emit(_('[ERROR_NOTCL] Could not export Excellon file.'))
                 return
                 return
 
 
+    def export_gerber(self, obj_name, filename, use_thread=True):
+        """
+        Exports a Gerber Object to an Gerber file.
+
+        :param filename: Path to the Gerber file to save to.
+        :return:
+        """
+        self.report_usage("export_gerber()")
+
+        if filename is None:
+            filename = self.defaults["global_last_save_folder"]
+
+        self.log.debug("export_gerber()")
+
+        try:
+            obj = self.collection.get_by_name(str(obj_name))
+        except:
+            # TODO: The return behavior has not been established... should raise exception?
+            return "Could not retrieve object: %s" % obj_name
+
+        # updated units
+        gunits = self.defaults["gerber_exp_units"]
+        gwhole = self.defaults["gerber_exp_integer"]
+        gfract = self.defaults["gerber_exp_decimals"]
+        gzeros = self.defaults["gerber_exp_zeros"]
+
+        fc_units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        if fc_units == 'MM':
+            factor = 1 if gunits == 'MM' else 0.03937
+        else:
+            factor = 25.4 if gunits == 'MM' else 1
+
+        def make_gerber():
+            try:
+                time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
+
+                header = 'G04*\n'
+                header += 'G04 RS-274X GERBER GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s*\n' % \
+                          (str(self.version), str(self.version_date))
+
+                header += 'G04 Filename: %s*' % str(obj_name) + '\n'
+                header += 'G04 Created on : %s*' % time_str + '\n'
+                header += '%%FS%sAX%s%sY%s%s*%%\n' % (gzeros, gwhole, gfract, gwhole, gfract)
+                header += "%MO{units}*%\n".format(units=gunits)
+
+                for apid in obj.apertures:
+                    if obj.apertures[apid]['type'] == 'C':
+                        header += "%ADD{apid}{type},{size}*%\n".format(
+                            apid=str(apid),
+                            type='C',
+                            size=(factor * obj.apertures[apid]['size'])
+                        )
+                    elif obj.apertures[apid]['type'] == 'R':
+                        header += "%ADD{apid}{type},{width}X{height}*%\n".format(
+                            apid=str(apid),
+                            type='R',
+                            width=(factor * obj.apertures[apid]['width']),
+                            height=(factor * obj.apertures[apid]['height'])
+                        )
+                    elif obj.apertures[apid]['type'] == 'O':
+                        header += "%ADD{apid}{type},{width}X{height}*%\n".format(
+                            apid=str(apid),
+                            type='O',
+                            width=(factor * obj.apertures[apid]['width']),
+                            height=(factor * obj.apertures[apid]['height'])
+                        )
+
+                header += '\n'
+
+                # obsolete units but some software may need it
+                if gunits == 'IN':
+                    header += 'G70*\n'
+                else:
+                    header += 'G71*\n'
+
+                # Absolute Mode
+                header += 'G90*\n'
+
+                header += 'G01*\n'
+                # positive polarity
+                header += '%LPD*%\n'
+
+                footer = 'M02*\n'
+
+                gerber_code = obj.export_gerber(gwhole, gfract, g_zeros=gzeros, factor=factor)
+
+                exported_gerber = header
+                exported_gerber += gerber_code
+                exported_gerber += footer
+
+                with open(filename, 'w') as fp:
+                    fp.write(exported_gerber)
+
+                self.file_saved.emit("Gerber", filename)
+                self.inform.emit(_("[success] Gerber file exported to %s") % filename)
+            except Exception as e:
+                log.debug("App.export_gerber.make_gerber() --> %s" % str(e))
+                return 'fail'
+
+        if use_thread is True:
+
+            with self.proc_container.new(_("Exporting Gerber")) as proc:
+
+                def job_thread_exc(app_obj):
+                    ret = make_gerber()
+                    if ret == 'fail':
+                        self.inform.emit(_('[ERROR_NOTCL] Could not export Gerber file.'))
+                        return
+
+                self.worker_task.emit({'fcn': job_thread_exc, 'params': [self]})
+        else:
+            ret = make_gerber()
+            if ret == 'fail':
+                self.inform.emit(_('[ERROR_NOTCL] Could not export Gerber file.'))
+                return
+
     def export_dxf(self, obj_name, filename, use_thread=True):
     def export_dxf(self, obj_name, filename, use_thread=True):
         """
         """
         Exports a Geometry Object to an DXF file.
         Exports a Geometry Object to an DXF file.

+ 275 - 58
FlatCAMObj.py

@@ -73,14 +73,19 @@ class FlatCAMObj(QtCore.QObject):
         # self.shapes = ShapeCollection(parent=self.app.plotcanvas.vispy_canvas.view.scene)
         # self.shapes = ShapeCollection(parent=self.app.plotcanvas.vispy_canvas.view.scene)
         self.shapes = self.app.plotcanvas.new_shape_group()
         self.shapes = self.app.plotcanvas.new_shape_group()
 
 
-        self.mark_shapes = self.app.plotcanvas.new_shape_collection(layers=2)
+        # self.mark_shapes = self.app.plotcanvas.new_shape_collection(layers=2)
+        self.mark_shapes = {}
 
 
         self.item = None  # Link with project view item
         self.item = None  # Link with project view item
 
 
         self.muted_ui = False
         self.muted_ui = False
         self.deleted = False
         self.deleted = False
 
 
-        self._drawing_tolerance = 0.01
+        try:
+            self._drawing_tolerance = float(self.app.defaults["global_tolerance"]) if \
+                self.app.defaults["global_tolerance"] else 0.01
+        except ValueError:
+            self._drawing_tolerance = 0.01
 
 
         self.isHovering = False
         self.isHovering = False
         self.notHovering = True
         self.notHovering = True
@@ -321,11 +326,11 @@ class FlatCAMObj(QtCore.QObject):
             key = self.shapes.add(tolerance=self.drawing_tolerance, **kwargs)
             key = self.shapes.add(tolerance=self.drawing_tolerance, **kwargs)
         return key
         return key
 
 
-    def add_mark_shape(self, **kwargs):
+    def add_mark_shape(self, apid, **kwargs):
         if self.deleted:
         if self.deleted:
             raise ObjectDeleted()
             raise ObjectDeleted()
         else:
         else:
-            key = self.mark_shapes.add(tolerance=self.drawing_tolerance, **kwargs)
+            key = self.mark_shapes[apid].add(tolerance=self.drawing_tolerance, **kwargs)
         return key
         return key
 
 
     @property
     @property
@@ -555,6 +560,10 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             ))
             ))
             self.ui.padding_area_label.hide()
             self.ui.padding_area_label.hide()
 
 
+        # add the shapes storage for marking apertures
+        for ap_code in self.apertures:
+            self.mark_shapes[ap_code] = self.app.plotcanvas.new_shape_collection(layers=2)
+
         # set initial state of the aperture table and associated widgets
         # set initial state of the aperture table and associated widgets
         self.on_aperture_table_visibility_change()
         self.on_aperture_table_visibility_change()
 
 
@@ -576,12 +585,13 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             sort.append(int(k))
             sort.append(int(k))
         sorted_apertures = sorted(sort)
         sorted_apertures = sorted(sort)
 
 
-        sort = []
-        for k, v in list(self.aperture_macros.items()):
-            sort.append(k)
-        sorted_macros = sorted(sort)
+        # sort = []
+        # for k, v in list(self.aperture_macros.items()):
+        #     sort.append(k)
+        # sorted_macros = sorted(sort)
 
 
-        n = len(sorted_apertures) + len(sorted_macros)
+        # n = len(sorted_apertures) + len(sorted_macros)
+        n = len(sorted_apertures)
         self.ui.apertures_table.setRowCount(n)
         self.ui.apertures_table.setRowCount(n)
 
 
         for ap_code in sorted_apertures:
         for ap_code in sorted_apertures:
@@ -639,28 +649,28 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
 
             self.apertures_row += 1
             self.apertures_row += 1
 
 
-        for ap_code in sorted_macros:
-            ap_code = str(ap_code)
-
-            ap_id_item = QtWidgets.QTableWidgetItem('%d' % int(self.apertures_row + 1))
-            ap_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-            self.ui.apertures_table.setItem(self.apertures_row, 0, ap_id_item)  # Tool name/id
-
-            ap_code_item = QtWidgets.QTableWidgetItem(ap_code)
-
-            ap_type_item = QtWidgets.QTableWidgetItem('AM')
-            ap_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
-
-            mark_item = FCCheckBox()
-            mark_item.setLayoutDirection(QtCore.Qt.RightToLeft)
-            # if self.ui.aperture_table_visibility_cb.isChecked():
-            #     mark_item.setChecked(True)
-
-            self.ui.apertures_table.setItem(self.apertures_row, 1, ap_code_item)  # Aperture Code
-            self.ui.apertures_table.setItem(self.apertures_row, 2, ap_type_item)  # Aperture Type
-            self.ui.apertures_table.setCellWidget(self.apertures_row, 5, mark_item)
-
-            self.apertures_row += 1
+        # for ap_code in sorted_macros:
+        #     ap_code = str(ap_code)
+        #
+        #     ap_id_item = QtWidgets.QTableWidgetItem('%d' % int(self.apertures_row + 1))
+        #     ap_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        #     self.ui.apertures_table.setItem(self.apertures_row, 0, ap_id_item)  # Tool name/id
+        #
+        #     ap_code_item = QtWidgets.QTableWidgetItem(ap_code)
+        #
+        #     ap_type_item = QtWidgets.QTableWidgetItem('AM')
+        #     ap_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
+        #
+        #     mark_item = FCCheckBox()
+        #     mark_item.setLayoutDirection(QtCore.Qt.RightToLeft)
+        #     # if self.ui.aperture_table_visibility_cb.isChecked():
+        #     #     mark_item.setChecked(True)
+        #
+        #     self.ui.apertures_table.setItem(self.apertures_row, 1, ap_code_item)  # Aperture Code
+        #     self.ui.apertures_table.setItem(self.apertures_row, 2, ap_type_item)  # Aperture Type
+        #     self.ui.apertures_table.setCellWidget(self.apertures_row, 5, mark_item)
+        #
+        #     self.apertures_row += 1
 
 
         self.ui.apertures_table.selectColumn(0)
         self.ui.apertures_table.selectColumn(0)
         self.ui.apertures_table.resizeColumnsToContents()
         self.ui.apertures_table.resizeColumnsToContents()
@@ -692,7 +702,10 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         # update the 'mark' checkboxes state according with what is stored in the self.marked_rows list
         # update the 'mark' checkboxes state according with what is stored in the self.marked_rows list
         if self.marked_rows:
         if self.marked_rows:
             for row in range(self.ui.apertures_table.rowCount()):
             for row in range(self.ui.apertures_table.rowCount()):
-                self.ui.apertures_table.cellWidget(row, 5).set_value(self.marked_rows[row])
+                try:
+                    self.ui.apertures_table.cellWidget(row, 5).set_value(self.marked_rows[row])
+                except IndexError:
+                    pass
 
 
         self.ui_connect()
         self.ui_connect()
 
 
@@ -999,6 +1012,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
     def on_aperture_table_visibility_change(self):
     def on_aperture_table_visibility_change(self):
         if self.ui.aperture_table_visibility_cb.isChecked():
         if self.ui.aperture_table_visibility_cb.isChecked():
             self.ui.apertures_table.setVisible(True)
             self.ui.apertures_table.setVisible(True)
+            for ap in self.mark_shapes:
+                self.mark_shapes[ap].enabled = True
 
 
             self.ui.mark_all_cb.setVisible(True)
             self.ui.mark_all_cb.setVisible(True)
             self.ui.mark_all_cb.setChecked(False)
             self.ui.mark_all_cb.setChecked(False)
@@ -1012,6 +1027,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                 self.ui.apertures_table.cellWidget(row, 5).set_value(False)
                 self.ui.apertures_table.cellWidget(row, 5).set_value(False)
             self.clear_plot_apertures()
             self.clear_plot_apertures()
 
 
+            for ap in self.mark_shapes:
+                self.mark_shapes[ap].enabled = False
+
     def convert_units(self, units):
     def convert_units(self, units):
         """
         """
         Converts the units of the object by scaling dimensions in all geometry
         Converts the units of the object by scaling dimensions in all geometry
@@ -1098,14 +1116,14 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             self.shapes.clear(update=True)
             self.shapes.clear(update=True)
 
 
     # experimental plot() when the solid_geometry is stored in the self.apertures
     # experimental plot() when the solid_geometry is stored in the self.apertures
-    def plot_apertures(self, **kwargs):
+    def plot_aperture(self, **kwargs):
         """
         """
 
 
         :param kwargs: color and face_color
         :param kwargs: color and face_color
         :return:
         :return:
         """
         """
 
 
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMGerber.plot_apertures()")
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMGerber.plot_aperture()")
 
 
         # Does all the required setup and returns False
         # Does all the required setup and returns False
         # if the 'ptint' option is set to False.
         # if the 'ptint' option is set to False.
@@ -1135,7 +1153,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
 
             def job_thread(app_obj):
             def job_thread(app_obj):
                 self.app.progress.emit(30)
                 self.app.progress.emit(30)
-
                 try:
                 try:
                     if aperture_to_plot_mark in self.apertures:
                     if aperture_to_plot_mark in self.apertures:
                         if type(self.apertures[aperture_to_plot_mark]['solid_geometry']) is not list:
                         if type(self.apertures[aperture_to_plot_mark]['solid_geometry']) is not list:
@@ -1143,12 +1160,14 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                                 [self.apertures[aperture_to_plot_mark]['solid_geometry']]
                                 [self.apertures[aperture_to_plot_mark]['solid_geometry']]
                         for geo in self.apertures[aperture_to_plot_mark]['solid_geometry']:
                         for geo in self.apertures[aperture_to_plot_mark]['solid_geometry']:
                             if type(geo) == Polygon or type(geo) == LineString:
                             if type(geo) == Polygon or type(geo) == LineString:
-                                self.add_mark_shape(shape=geo, color=color, face_color=color, visible=visibility)
+                                self.add_mark_shape(apid=aperture_to_plot_mark, shape=geo, color=color,
+                                                    face_color=color, visible=visibility)
                             else:
                             else:
                                 for el in geo:
                                 for el in geo:
-                                    self.add_mark_shape(shape=el, color=color, face_color=color, visible=visibility)
+                                    self.add_mark_shape(apid=aperture_to_plot_mark, shape=el, color=color,
+                                                        face_color=color, visible=visibility)
 
 
-                    self.mark_shapes.redraw()
+                    self.mark_shapes[aperture_to_plot_mark].redraw()
                     self.app.progress.emit(100)
                     self.app.progress.emit(100)
 
 
                 except (ObjectDeleted, AttributeError):
                 except (ObjectDeleted, AttributeError):
@@ -1156,34 +1175,47 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
 
             self.app.worker_task.emit({'fcn': job_thread, 'params': [self]})
             self.app.worker_task.emit({'fcn': job_thread, 'params': [self]})
 
 
-    def clear_plot_apertures(self):
-        self.mark_shapes.clear(update=True)
+    def clear_plot_apertures(self, aperture='all'):
+        """
+
+        :param aperture: string; aperture for which to clear the mark shapes
+        :return:
+        """
+        if aperture == 'all':
+            for apid in self.apertures:
+                self.mark_shapes[apid].clear(update=True)
+        else:
+            self.mark_shapes[aperture].clear(update=True)
 
 
     def clear_mark_all(self):
     def clear_mark_all(self):
         self.ui.mark_all_cb.set_value(False)
         self.ui.mark_all_cb.set_value(False)
         self.marked_rows[:] = []
         self.marked_rows[:] = []
 
 
     def on_mark_cb_click_table(self):
     def on_mark_cb_click_table(self):
+        """
+        Will mark aperture geometries on canvas or delete the markings depending on the checkbox state
+        :return:
+        """
+
         self.ui_disconnect()
         self.ui_disconnect()
-        # cw = self.sender()
-        # cw_index = self.ui.apertures_table.indexAt(cw.pos())
-        # cw_row = cw_index.row()
-        check_row = 0
+        cw = self.sender()
+        try:
+            cw_index = self.ui.apertures_table.indexAt(cw.pos())
+            cw_row = cw_index.row()
+        except AttributeError:
+            cw_row = 0
 
 
-        self.clear_plot_apertures()
         self.marked_rows[:] = []
         self.marked_rows[:] = []
+        aperture = self.ui.apertures_table.item(cw_row, 1).text()
 
 
-        for row in range(self.ui.apertures_table.rowCount()):
-            if self.ui.apertures_table.cellWidget(row, 5).isChecked():
-                self.marked_rows.append(True)
-
-                aperture = self.ui.apertures_table.item(row, 1).text()
-                # self.plot_apertures(color='#2d4606bf', marked_aperture=aperture, visible=True)
-                self.plot_apertures(color=self.app.defaults['global_sel_draw_color'], marked_aperture=aperture, visible=True)
-            else:
-                self.marked_rows.append(False)
-
-        self.mark_shapes.redraw()
+        if self.ui.apertures_table.cellWidget(cw_row, 5).isChecked():
+            self.marked_rows.append(True)
+            # self.plot_aperture(color='#2d4606bf', marked_aperture=aperture, visible=True)
+            self.plot_aperture(color=self.app.defaults['global_sel_draw_color'], marked_aperture=aperture, visible=True)
+            self.mark_shapes[aperture].redraw()
+        else:
+            self.marked_rows.append(False)
+            self.clear_plot_apertures(aperture=aperture)
 
 
         # make sure that the Mark All is disabled if one of the row mark's are disabled and
         # make sure that the Mark All is disabled if one of the row mark's are disabled and
         # if all the row mark's are enabled also enable the Mark All checkbox
         # if all the row mark's are enabled also enable the Mark All checkbox
@@ -1215,13 +1247,198 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
 
         if mark_all:
         if mark_all:
             for aperture in self.apertures:
             for aperture in self.apertures:
-                # self.plot_apertures(color='#2d4606bf', marked_aperture=aperture, visible=True)
-                self.plot_apertures(color=self.app.defaults['global_sel_draw_color'], marked_aperture=aperture, visible=True)
+                # self.plot_aperture(color='#2d4606bf', marked_aperture=aperture, visible=True)
+                self.plot_aperture(color=self.app.defaults['global_sel_draw_color'],
+                                   marked_aperture=aperture, visible=True)
+            # HACK: enable/disable the grid for a better look
+            self.app.ui.grid_snap_btn.trigger()
+            self.app.ui.grid_snap_btn.trigger()
         else:
         else:
             self.clear_plot_apertures()
             self.clear_plot_apertures()
 
 
         self.ui_connect()
         self.ui_connect()
 
 
+    def export_gerber(self, whole, fract, g_zeros='L', factor=1):
+        """
+
+        :return: Gerber_code
+        """
+
+        def tz_format(x, y ,fac):
+            x_c = x * fac
+            y_c = y * fac
+
+            x_form = "{:.{dec}f}".format(x_c, dec=fract)
+            y_form = "{:.{dec}f}".format(y_c, dec=fract)
+
+            # extract whole part and decimal part
+            x_form = x_form.partition('.')
+            y_form = y_form.partition('.')
+
+            # left padd the 'whole' part with zeros
+            x_whole = x_form[0].rjust(whole, '0')
+            y_whole = y_form[0].rjust(whole, '0')
+
+            # restore the coordinate padded in the left with 0 and added the decimal part
+            # without the decinal dot
+            x_form = x_whole + x_form[2]
+            y_form = y_whole + y_form[2]
+            return x_form, y_form
+
+        def lz_format(x, y, fac):
+            x_c = x * fac
+            y_c = y * fac
+
+            x_form = "{:.{dec}f}".format(x_c, dec=fract).replace('.', '')
+            y_form = "{:.{dec}f}".format(y_c, dec=fract).replace('.', '')
+
+            # pad with rear zeros
+            x_form.ljust(length, '0')
+            y_form.ljust(length, '0')
+
+            return x_form, y_form
+
+        # Gerber code is stored here
+        gerber_code = ''
+
+        # apertures processing
+        try:
+            length = whole + fract
+            if '0' in self.apertures:
+                if 'solid_geometry' in self.apertures['0']:
+                    for geo in self.apertures['0']['solid_geometry']:
+                        gerber_code += 'G36*\n'
+                        geo_coords = list(geo.exterior.coords)
+                        # first command is a move with pen-up D02 at the beginning of the geo
+                        if g_zeros == 'T':
+                            x_formatted, y_formatted = tz_format(geo_coords[0][0], geo_coords[0][1], factor)
+                            gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
+                                                                           yform=y_formatted)
+                        else:
+                            x_formatted, y_formatted = lz_format(geo_coords[0][0], geo_coords[0][1], factor)
+                            gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
+                                                                           yform=y_formatted)
+                        for coord in geo_coords[1:]:
+                            if g_zeros == 'T':
+                                x_formatted, y_formatted = tz_format(coord[0], coord[1], factor)
+                                gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
+                                                                               yform=y_formatted)
+                            else:
+                                x_formatted, y_formatted = lz_format(coord[0], coord[1], factor)
+                                gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
+                                                                               yform=y_formatted)
+                        gerber_code += 'D02*\n'
+                        gerber_code += 'G37*\n'
+
+                        clear_list = list(geo.interiors)
+                        if clear_list:
+                            gerber_code += '%LPC*%\n'
+                            for clear_geo in clear_list:
+                                gerber_code += 'G36*\n'
+                                geo_coords = list(clear_geo.coords)
+
+                                # first command is a move with pen-up D02 at the beginning of the geo
+                                if g_zeros == 'T':
+                                    x_formatted, y_formatted = tz_format(geo_coords[0][0], geo_coords[0][1], factor)
+                                    gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
+                                                                                   yform=y_formatted)
+                                else:
+                                    x_formatted, y_formatted = lz_format(geo_coords[0][0], geo_coords[0][1], factor)
+                                    gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
+                                                                                   yform=y_formatted)
+                                for coord in geo_coords[1:]:
+                                    if g_zeros == 'T':
+                                        x_formatted, y_formatted = tz_format(coord[0], coord[1], factor)
+                                        gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
+                                                                                       yform=y_formatted)
+                                    else:
+                                        x_formatted, y_formatted = lz_format(coord[0], coord[1], factor)
+                                        gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
+                                                                                       yform=y_formatted)
+                                gerber_code += 'D02*\n'
+                                gerber_code += 'G37*\n'
+                            gerber_code += '%LPD*%\n'
+
+            for apid in self.apertures:
+                if apid == '0':
+                    continue
+                else:
+                    gerber_code += 'D%s*\n' % str(apid)
+
+                    if 'follow_geometry' in self.apertures[apid]:
+                        for geo in self.apertures[apid]['follow_geometry']:
+                            if isinstance(geo, Point):
+                                if g_zeros == 'T':
+                                    x_formatted, y_formatted = tz_format(geo.x, geo.y, factor)
+                                    gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted,
+                                                                                   yform=y_formatted)
+                                else:
+                                    x_formatted, y_formatted = lz_format(geo.x, geo.y, factor)
+                                    gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted,
+                                                                                   yform=y_formatted)
+                            else:
+                                geo_coords = list(geo.coords)
+                                # first command is a move with pen-up D02 at the beginning of the geo
+                                if g_zeros == 'T':
+                                    x_formatted, y_formatted = tz_format(geo_coords[0][0], geo_coords[0][1], factor)
+                                    gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
+                                                                                   yform=y_formatted)
+                                else:
+                                    x_formatted, y_formatted = lz_format(geo_coords[0][0], geo_coords[0][1], factor)
+                                    gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
+                                                                                   yform=y_formatted)
+                                for coord in geo_coords[1:]:
+                                    if g_zeros == 'T':
+                                        x_formatted, y_formatted = tz_format(coord[0], coord[1], factor)
+                                        gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
+                                                                                       yform=y_formatted)
+                                    else:
+                                        x_formatted, y_formatted = lz_format(coord[0], coord[1], factor)
+                                        gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
+                                                                                       yform=y_formatted)
+                    if 'clear_follow_geometry' in self.apertures[apid]:
+                        gerber_code += '%LPC*%\n'
+                        for geo in self.apertures[apid]['clear_follow_geometry']:
+                            if isinstance(geo, Point):
+                                if g_zeros == 'T':
+                                    x_formatted, y_formatted = tz_format(geo.x, geo.y, factor)
+                                    gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted,
+                                                                                   yform=y_formatted)
+                                else:
+                                    x_formatted, y_formatted = lz_format(geo.x, geo.y, factor)
+                                    gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted,
+                                                                                   yform=y_formatted)
+                            else:
+                                geo_coords = list(geo.coords)
+                                # first command is a move with pen-up D02 at the beginning of the geo
+                                if g_zeros == 'T':
+                                    x_formatted, y_formatted = tz_format(geo_coords[0][0], geo_coords[0][1], factor)
+                                    gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
+                                                                                   yform=y_formatted)
+                                else:
+                                    x_formatted, y_formatted = lz_format(geo_coords[0][0], geo_coords[0][1], factor)
+                                    gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
+                                                                                   yform=y_formatted)
+                                for coord in geo_coords[1:]:
+                                    if g_zeros == 'T':
+                                        x_formatted, y_formatted = tz_format(coord[0], coord[1], factor)
+                                        gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
+                                                                                       yform=y_formatted)
+                                    else:
+                                        x_formatted, y_formatted = lz_format(coord[0], coord[1], factor)
+                                        gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
+                                                                                       yform=y_formatted)
+                        gerber_code += '%LPD*%\n'
+
+        except Exception as e:
+            log.debug("FlatCAMObj.FlatCAMGerber.export_gerber() --> %s" % str(e))
+
+        if not self.apertures:
+            log.debug("FlatCAMObj.FlatCAMGerber.export_gerber() --> Gerber Object is empty: no apertures.")
+            return 'fail'
+
+        return gerber_code
+
     def mirror(self, axis, point):
     def mirror(self, axis, point):
         Gerber.mirror(self, axis=axis, point=point)
         Gerber.mirror(self, axis=axis, point=point)
         self.replotApertures.emit()
         self.replotApertures.emit()

+ 52 - 2
README.md

@@ -9,6 +9,56 @@ CAD program, and create G-Code for Isolation routing.
 
 
 =================================================
 =================================================
 
 
+10.05.2019
+
+- made sure that only units toggle done in Edit -> Preferences will toggle the data in Preferences. THe menu entry Edit -> Toggle Units and the shortcut key 'Q' will change only the display units in the app
+- optimized Transform tool
+
+8.05.2019
+
+- added zoom fit for Set Origin command
+- added move action for solid_geometry stored in the gerber_obj.apertures
+- fixed camlib.Gerber skew, rotate, offset, mirror functions to work for geometry stored in the Gerber apertures
+- fixed Gerber Editor follow_geometry reconstruction
+- Geometry Editor: made the tool to be able to continuously move until the tool is exited either by ESC key or by right mouse button click
+- Geometry Editor Move Tool: if no shape is selected when triggering this tool, now it is possible to make the selection inside the tool
+- Gerber editor Move Tool: fixed a bug that repeated the plotting function unnecessarily 
+- Gerber editor Move Tool: if no shape is selected the tool will exit
+
+7.05.2019
+
+- remade the Tool Panelize GUI
+- work in Gerber Export: finished the header export
+- fixed the Gerber Object and Gerber Editor Apertures Table to not show extra rows when there are aperture macros in the object
+- work in Gerber Export: finished the body export but have some errors with clear geometry (LPC)
+- Gerber Export - finished
+
+6.05.2019
+
+- made units change from shortcut key 'Q' not to affect the preferences
+- made units change from Edit -> Toggle Units not to affect the preferences
+- remade the way the aperture marks are plotted in Gerber Object
+- fixed some bugs related to moving an Gerber object with the aperture table in view
+- added a new parameter in the Edit -> Preferences -> App Preferences named Geo Tolerance. This parameter control the level of geometric detail throughout FlatCAM. It directly influence the effect of Circle Steps parameter.
+- solved a bug in Excellon Editor that caused app crash when trying to edit a tool in Tool Table due of missing a tool offset
+- updated the ToolPanelize tool so the Gerber panel of type FlatCAMGerber can be isolated like any other FlatCAMGerber object
+- updated the ToolPanelize tool so it can be edited
+- modified the default values for toolchangez and endz parameters so they are now safe in all cases
+
+5.05.2019
+
+- another fix for bug in clear geometry processing for Gerber apertures
+- added a protection for the case that the aperture table is part of a deleted object
+- in Script Editor added support for auto-add closing parenthesis, brace and bracket
+- in Script Editor added support for "CTRL + / " key combo to comment/uncomment line
+
+4.05.2019
+
+- fixed bug in camlib.parse_lines() in the clear_geometry processing section for self.apertures
+- fixed bug in parsing Gerber regions (a point was added unnecessary)
+- renamed the menu entry Edit -> Copy as Geo to Convert Any to Geo and moved it in the Edit -> Conversion
+- created a new function named Convert Any to Gerber and installed it in Edit -> Conversion. It's doing what the name say: it will convert an Geometry or Excellon FlatCAM object to a Gerber object.
+
 01.05.2019
 01.05.2019
 
 
 - the project items color is now controlled from Foreground Role in ObjectCollection.data()
 - the project items color is now controlled from Foreground Role in ObjectCollection.data()
@@ -463,7 +513,7 @@ CAD program, and create G-Code for Isolation routing.
 - fixed mouse selection on canvas, mouse drag, mouse click and mouse double click
 - fixed mouse selection on canvas, mouse drag, mouse click and mouse double click
 - fixed Gerber Aperture Table dimensions
 - fixed Gerber Aperture Table dimensions
 - added a Mark All button in the Gerber aperture table.
 - added a Mark All button in the Gerber aperture table.
-- because adding shapes to the shapes collection (when doing Mark or Mark All) is time consuming I made the plot_apertures() threaded.
+- because adding shapes to the shapes collection (when doing Mark or Mark All) is time consuming I made the plot_aperture() threaded.
 - made the polygon fusing in modified Gerber creation, a list comprehension in an attempt for optimization
 - made the polygon fusing in modified Gerber creation, a list comprehension in an attempt for optimization
 - when right clicking the files in Project tab, the Save option for Excellon no longer export it but really save the original. 
 - when right clicking the files in Project tab, the Save option for Excellon no longer export it but really save the original. 
 - in ToolChange Custom Code replacement, the Text Box in the CNCJob Selected tab will be active only if there is a 'toolchange_custom' in the name of the postprocessor file. This assume that it is, or was created having as template the Toolchange Custom postprocessor file.
 - in ToolChange Custom Code replacement, the Text Box in the CNCJob Selected tab will be active only if there is a 'toolchange_custom' in the name of the postprocessor file. This assume that it is, or was created having as template the Toolchange Custom postprocessor file.
@@ -635,7 +685,7 @@ CAD program, and create G-Code for Isolation routing.
 - finished Gerber aperture table display
 - finished Gerber aperture table display
 - made the Gerber aperture table not visible as default and added a checkbox that can toggle the visibility
 - made the Gerber aperture table not visible as default and added a checkbox that can toggle the visibility
 - fixed issue with plotting in CNCJob; with Plot kind set to something else than 'all' when toggling Plot, it was defaulting to kind = 'all'
 - fixed issue with plotting in CNCJob; with Plot kind set to something else than 'all' when toggling Plot, it was defaulting to kind = 'all'
-- added (and commented) an experimental FlatCAMObj.FlatCAMGerber.plot_apertures()
+- added (and commented) an experimental FlatCAMObj.FlatCAMGerber.plot_aperture()
 
 
 12.02.2019
 12.02.2019
 
 

+ 101 - 36
camlib.py

@@ -2446,6 +2446,14 @@ class Gerber (Geometry):
                                         self.apertures[current_aperture]['clear_geometry'] = []
                                         self.apertures[current_aperture]['clear_geometry'] = []
                                         self.apertures[current_aperture]['clear_geometry'].append(flash)
                                         self.apertures[current_aperture]['clear_geometry'].append(flash)
                                 else:
                                 else:
+                                    try:
+                                        self.apertures[current_aperture]['follow_geometry'].append(Point(
+                                            current_x, current_y))
+                                    except KeyError:
+                                        self.apertures[current_aperture]['follow_geometry'] = []
+                                        self.apertures[current_aperture]['follow_geometry'].append(Point(
+                                            current_x, current_y))
+
                                     try:
                                     try:
                                         self.apertures[current_aperture]['solid_geometry'].append(flash)
                                         self.apertures[current_aperture]['solid_geometry'].append(flash)
                                     except KeyError:
                                     except KeyError:
@@ -2478,17 +2486,18 @@ class Gerber (Geometry):
                             # do nothing because 'R' type moving aperture is none at once
                             # do nothing because 'R' type moving aperture is none at once
                             pass
                             pass
                         else:
                         else:
-                            # --- Buffered ----
-                            width = self.apertures[last_path_aperture]["size"]
+
                             geo = LineString(path)
                             geo = LineString(path)
                             if not geo.is_empty:
                             if not geo.is_empty:
                                 follow_buffer.append(geo)
                                 follow_buffer.append(geo)
                                 try:
                                 try:
-                                    self.apertures[current_aperture]['follow_geometry'].append(geo)
+                                    self.apertures[last_path_aperture]['follow_geometry'].append(geo)
                                 except KeyError:
                                 except KeyError:
-                                    self.apertures[current_aperture]['follow_geometry'] = []
-                                    self.apertures[current_aperture]['follow_geometry'].append(geo)
+                                    self.apertures[last_path_aperture]['follow_geometry'] = []
+                                    self.apertures[last_path_aperture]['follow_geometry'].append(geo)
 
 
+                            # --- Buffered ----
+                            width = self.apertures[last_path_aperture]["size"]
                             geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
                             geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
                             if not geo.is_empty:
                             if not geo.is_empty:
                                 poly_buffer.append(geo)
                                 poly_buffer.append(geo)
@@ -2692,11 +2701,10 @@ class Gerber (Geometry):
                     # Pen down: add segment
                     # Pen down: add segment
                     if current_operation_code == 1:
                     if current_operation_code == 1:
                         # if linear_x or linear_y are None, ignore those
                         # if linear_x or linear_y are None, ignore those
-                        if linear_x is not None and linear_y is not None:
+                        if current_x is not None and current_y is not None:
                             # only add the point if it's a new one otherwise skip it (harder to process)
                             # only add the point if it's a new one otherwise skip it (harder to process)
-                            if path[-1] != [linear_x, linear_y]:
-                                path.append([linear_x, linear_y])
-
+                            if path[-1] != [current_x, current_y]:
+                                path.append([current_x, current_y])
                             if making_region is False:
                             if making_region is False:
                                 # if the aperture is rectangle then add a rectangular shape having as parameters the
                                 # if the aperture is rectangle then add a rectangular shape having as parameters the
                                 # coordinates of the start and end point and also the width and height
                                 # coordinates of the start and end point and also the width and height
@@ -2791,9 +2799,9 @@ class Gerber (Geometry):
                                         self.apertures['0']['size'] = 0.0
                                         self.apertures['0']['size'] = 0.0
                                         self.apertures['0']['solid_geometry'] = []
                                         self.apertures['0']['solid_geometry'] = []
                                     last_path_aperture = '0'
                                     last_path_aperture = '0'
-                                elem = [linear_x, linear_y]
-                                if elem != path[-1]:
-                                    path.append([linear_x, linear_y])
+                                # elem = [current_x, current_y]
+                                # if elem != path[-1]:
+                                #     path.append([current_x, current_y])
 
 
                                 try:
                                 try:
                                     geo = Polygon(path)
                                     geo = Polygon(path)
@@ -2862,17 +2870,18 @@ class Gerber (Geometry):
                                     if self.apertures[last_path_aperture]["type"] != 'R':
                                     if self.apertures[last_path_aperture]["type"] != 'R':
                                         follow_buffer.append(geo)
                                         follow_buffer.append(geo)
                                         try:
                                         try:
-                                            self.apertures[current_aperture]['follow_geometry'].append(geo)
+                                            self.apertures[last_path_aperture]['follow_geometry'].append(geo)
                                         except KeyError:
                                         except KeyError:
-                                            self.apertures[current_aperture]['follow_geometry'] = []
-                                            self.apertures[current_aperture]['follow_geometry'].append(geo)
-                                except:
+                                            self.apertures[last_path_aperture]['follow_geometry'] = []
+                                            self.apertures[last_path_aperture]['follow_geometry'].append(geo)
+                                except Exception as e:
+                                    log.debug("camlib.Gerber.parse_lines() --> G01 match D03 --> %s" % str(e))
                                     follow_buffer.append(geo)
                                     follow_buffer.append(geo)
                                     try:
                                     try:
-                                        self.apertures[current_aperture]['follow_geometry'].append(geo)
+                                        self.apertures[last_path_aperture]['follow_geometry'].append(geo)
                                     except KeyError:
                                     except KeyError:
-                                        self.apertures[current_aperture]['follow_geometry'] = []
-                                        self.apertures[current_aperture]['follow_geometry'].append(geo)
+                                        self.apertures[last_path_aperture]['follow_geometry'] = []
+                                        self.apertures[last_path_aperture]['follow_geometry'].append(geo)
 
 
                             # this treats the case when we are storing geometry as solids
                             # this treats the case when we are storing geometry as solids
                             width = self.apertures[last_path_aperture]["size"]
                             width = self.apertures[last_path_aperture]["size"]
@@ -3200,24 +3209,45 @@ class Gerber (Geometry):
 
 
             conversion_factor = 25.4 if file_units == 'IN' else (1/25.4) if file_units != app_units else 1
             conversion_factor = 25.4 if file_units == 'IN' else (1/25.4) if file_units != app_units else 1
 
 
-            # first check if we have any clear_geometry (LPC) and if yes then we need to substract it
-            # from the apertures solid_geometry
-            temp_geo = []
+            # --- the following section is useful for Gerber editor only --- #
+            log.warning("Applying clear geometry in the apertures dict.")
+            # list of clear geos that are to be applied to the entire file
+            global_clear_geo = []
+
             for apid in self.apertures:
             for apid in self.apertures:
+                # first check if we have any clear_geometry (LPC) and if yes added it to the global_clear_geo
                 if 'clear_geometry' in self.apertures[apid]:
                 if 'clear_geometry' in self.apertures[apid]:
-                    clear_geo = MultiPolygon(self.apertures[apid]['clear_geometry'])
+                    for pol in self.apertures[apid]['clear_geometry']:
+                        global_clear_geo.append(pol)
+                self.apertures[apid].pop('clear_geometry', None)
+            log.warning("Found %d clear polygons." % len(global_clear_geo))
+
+            temp_geo = []
+            for apid in self.apertures:
+                if 'solid_geometry' in self.apertures[apid]:
                     for solid_geo in self.apertures[apid]['solid_geometry']:
                     for solid_geo in self.apertures[apid]['solid_geometry']:
-                        if clear_geo.intersects(solid_geo):
-                            res_geo = solid_geo.difference(clear_geo)
-                            temp_geo.append(res_geo)
-                        else:
+                        for clear_geo in global_clear_geo:
+                            # Make sure that the clear_geo is within the solid_geo otherwise we loose
+                            # the solid_geometry. We want for clear_geometry just to cut into solid_geometry not to
+                            # delete it
+                            if clear_geo.within(solid_geo):
+                                solid_geo = solid_geo.difference(clear_geo)
+                        try:
+                            for poly in solid_geo:
+                                temp_geo.append(poly)
+                        except TypeError:
                             temp_geo.append(solid_geo)
                             temp_geo.append(solid_geo)
+
                     self.apertures[apid]['solid_geometry'] = deepcopy(temp_geo)
                     self.apertures[apid]['solid_geometry'] = deepcopy(temp_geo)
-                    self.apertures[apid].pop('clear_geometry', None)
+                    temp_geo = []
+            log.warning("Polygon difference done for %d apertures." % len(self.apertures))
 
 
+            for apid in self.apertures:
+                # scale de aperture geometries according to the used units
                 for k, v in self.apertures[apid].items():
                 for k, v in self.apertures[apid].items():
                     if k == 'size' or k == 'width' or k == 'height':
                     if k == 'size' or k == 'width' or k == 'height':
                         self.apertures[apid][k] = v * conversion_factor
                         self.apertures[apid][k] = v * conversion_factor
+            # -------------------------------------------------------------
 
 
             # --- Apply buffer ---
             # --- Apply buffer ---
             # this treats the case when we are storing geometry as paths
             # this treats the case when we are storing geometry as paths
@@ -3228,7 +3258,7 @@ class Gerber (Geometry):
 
 
             if len(poly_buffer) == 0:
             if len(poly_buffer) == 0:
                 log.error("Object is not Gerber file or empty. Aborting Object creation.")
                 log.error("Object is not Gerber file or empty. Aborting Object creation.")
-                return
+                return 'fail'
 
 
             if self.use_buffer_for_union:
             if self.use_buffer_for_union:
                 log.debug("Union by buffer...")
                 log.debug("Union by buffer...")
@@ -3467,9 +3497,15 @@ class Gerber (Geometry):
         # we need to scale the geometry stored in the Gerber apertures, too
         # we need to scale the geometry stored in the Gerber apertures, too
         try:
         try:
             for apid in self.apertures:
             for apid in self.apertures:
-                self.apertures[apid]['solid_geometry'] = scale_geom(self.apertures[apid]['solid_geometry'])
+                if 'solid_geometry' in self.apertures[apid]:
+                    self.apertures[apid]['solid_geometry'] = scale_geom(self.apertures[apid]['solid_geometry'])
+                if 'follow_geometry' in self.apertures[apid]:
+                    self.apertures[apid]['follow_geometry'] = scale_geom(self.apertures[apid]['follow_geometry'])
+                if 'clear_geometry' in self.apertures[apid]:
+                    self.apertures[apid]['clear_geometry'] = scale_geom(self.apertures[apid]['clear_geometry'])
         except Exception as e:
         except Exception as e:
-            log.debug('FlatCAMGeometry.scale() --> %s' % str(e))
+            log.debug('camlib.Gerber.scale() Exception --> %s' % str(e))
+            return 'fail'
 
 
         self.app.inform.emit(_("[success] Gerber Scale done."))
         self.app.inform.emit(_("[success] Gerber Scale done."))
 
 
@@ -3526,7 +3562,14 @@ class Gerber (Geometry):
             for apid in self.apertures:
             for apid in self.apertures:
                 self.apertures[apid]['solid_geometry'] = offset_geom(self.apertures[apid]['solid_geometry'])
                 self.apertures[apid]['solid_geometry'] = offset_geom(self.apertures[apid]['solid_geometry'])
         except Exception as e:
         except Exception as e:
-            log.debug('FlatCAMGeometry.offset() --> %s' % str(e))
+            log.debug('camlib.Gerber.offset() --> %s' % str(e))
+            return 'fail'
+        try:
+            for apid in self.apertures:
+                self.apertures[apid]['follow_geometry'] = offset_geom(self.apertures[apid]['follow_geometry'])
+        except Exception as e:
+            log.debug('camlib.Gerber.offset() --> %s' % str(e))
+            return 'fail'
 
 
         self.app.inform.emit(_("[success] Gerber Offset done."))
         self.app.inform.emit(_("[success] Gerber Offset done."))
 
 
@@ -3572,7 +3615,14 @@ class Gerber (Geometry):
             for apid in self.apertures:
             for apid in self.apertures:
                 self.apertures[apid]['solid_geometry'] = mirror_geom(self.apertures[apid]['solid_geometry'])
                 self.apertures[apid]['solid_geometry'] = mirror_geom(self.apertures[apid]['solid_geometry'])
         except Exception as e:
         except Exception as e:
-            log.debug('FlatCAMGeometry.mirror() --> %s' % str(e))
+            log.debug('camlib.Gerber.mirror() --> %s' % str(e))
+            return 'fail'
+        try:
+            for apid in self.apertures:
+                self.apertures[apid]['follow_geometry'] = mirror_geom(self.apertures[apid]['follow_geometry'])
+        except Exception as e:
+            log.debug('camlib.Gerber.mirror() --> %s' % str(e))
+            return 'fail'
 
 
         #  It's a cascaded union of objects.
         #  It's a cascaded union of objects.
         # self.solid_geometry = affinity.scale(self.solid_geometry,
         # self.solid_geometry = affinity.scale(self.solid_geometry,
@@ -3612,7 +3662,15 @@ class Gerber (Geometry):
             for apid in self.apertures:
             for apid in self.apertures:
                 self.apertures[apid]['solid_geometry'] = skew_geom(self.apertures[apid]['solid_geometry'])
                 self.apertures[apid]['solid_geometry'] = skew_geom(self.apertures[apid]['solid_geometry'])
         except Exception as e:
         except Exception as e:
-            log.debug('FlatCAMGeometry.skew() --> %s' % str(e))
+            log.debug('camlib.Gerber.skew() --> %s' % str(e))
+            return 'fail'
+        try:
+            for apid in self.apertures:
+                self.apertures[apid]['follow_geometry'] = skew_geom(self.apertures[apid]['follow_geometry'])
+        except Exception as e:
+            log.debug('camlib.Gerber.skew() --> %s' % str(e))
+            return 'fail'
+
         # self.solid_geometry = affinity.skew(self.solid_geometry, angle_x, angle_y, origin=(px, py))
         # self.solid_geometry = affinity.skew(self.solid_geometry, angle_x, angle_y, origin=(px, py))
 
 
     def rotate(self, angle, point):
     def rotate(self, angle, point):
@@ -3642,7 +3700,14 @@ class Gerber (Geometry):
             for apid in self.apertures:
             for apid in self.apertures:
                 self.apertures[apid]['solid_geometry'] = rotate_geom(self.apertures[apid]['solid_geometry'])
                 self.apertures[apid]['solid_geometry'] = rotate_geom(self.apertures[apid]['solid_geometry'])
         except Exception as e:
         except Exception as e:
-            log.debug('FlatCAMGeometry.rotate() --> %s' % str(e))
+            log.debug('camlib.Gerber.rotate() --> %s' % str(e))
+            return 'fail'
+        try:
+            for apid in self.apertures:
+                self.apertures[apid]['follow_geometry'] = rotate_geom(self.apertures[apid]['follow_geometry'])
+        except Exception as e:
+            log.debug('camlib.Gerber.rotate() --> %s' % str(e))
+            return 'fail'
         # self.solid_geometry = affinity.rotate(self.solid_geometry, angle, origin=(px, py))
         # self.solid_geometry = affinity.rotate(self.solid_geometry, angle, origin=(px, py))
 
 
 
 
@@ -7204,13 +7269,13 @@ class CNCjob(Geometry):
 
 
         self.create_geometry()
         self.create_geometry()
 
 
+
 def get_bounds(geometry_list):
 def get_bounds(geometry_list):
     xmin = Inf
     xmin = Inf
     ymin = Inf
     ymin = Inf
     xmax = -Inf
     xmax = -Inf
     ymax = -Inf
     ymax = -Inf
 
 
-    #print "Getting bounds of:", str(geometry_set)
     for gs in geometry_list:
     for gs in geometry_list:
         try:
         try:
             gxmin, gymin, gxmax, gymax = gs.bounds()
             gxmin, gymin, gxmax, gymax = gs.bounds()

+ 3 - 2
flatcamEditors/FlatCAMExcEditor.py

@@ -1565,8 +1565,9 @@ class FlatCAMExcEditor(QtCore.QObject):
             self.tool2tooldia[key_in_tool2tooldia] = current_table_dia_edited
             self.tool2tooldia[key_in_tool2tooldia] = current_table_dia_edited
 
 
             # update the tool offset
             # update the tool offset
-            modified_offset = self.exc_obj.tool_offset.pop(dia_changed)
-            self.exc_obj.tool_offset[current_table_dia_edited] = modified_offset
+            modified_offset = self.exc_obj.tool_offset.pop(dia_changed ,None)
+            if modified_offset is not None:
+                self.exc_obj.tool_offset[current_table_dia_edited] = modified_offset
 
 
             self.replot()
             self.replot()
         else:
         else:

+ 88 - 39
flatcamEditors/FlatCAMGeoEditor.py

@@ -18,6 +18,7 @@ from FlatCAMTool import FlatCAMTool
 from flatcamGUI.ObjectUI import LengthEntry, RadioSet
 from flatcamGUI.ObjectUI import LengthEntry, RadioSet
 
 
 from shapely.geometry import LineString, LinearRing, MultiLineString
 from shapely.geometry import LineString, LinearRing, MultiLineString
+# from shapely.geometry import mapping
 from shapely.ops import cascaded_union, unary_union
 from shapely.ops import cascaded_union, unary_union
 import shapely.affinity as affinity
 import shapely.affinity as affinity
 
 
@@ -29,6 +30,7 @@ from flatcamGUI.GUIElements import OptionalInputSection, FCCheckBox, FCEntry, FC
     FCTable, FCDoubleSpinner, FCButton, EvalEntry2, FCInputDialog
     FCTable, FCDoubleSpinner, FCButton, EvalEntry2, FCInputDialog
 from flatcamParsers.ParseFont import *
 from flatcamParsers.ParseFont import *
 
 
+# from vispy.io import read_png
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
 
 
@@ -1861,7 +1863,6 @@ class DrawTool(object):
     def __init__(self, draw_app):
     def __init__(self, draw_app):
         self.draw_app = draw_app
         self.draw_app = draw_app
         self.complete = False
         self.complete = False
-        self.start_msg = "Click on 1st point..."
         self.points = []
         self.points = []
         self.geometry = None  # DrawToolShape or None
         self.geometry = None  # DrawToolShape or None
 
 
@@ -1939,7 +1940,6 @@ class FCCircle(FCShapeTool):
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_circle_geo.png'))
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_circle_geo.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
 
 
-        self.start_msg = _("Click on Center point ...")
         self.draw_app.app.inform.emit(_("Click on Center point ..."))
         self.draw_app.app.inform.emit(_("Click on Center point ..."))
         self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"]
         self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"]
 
 
@@ -1991,7 +1991,6 @@ class FCArc(FCShapeTool):
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_arc.png'))
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_arc.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
 
 
-        self.start_msg = _("Click on Center point ...")
         self.draw_app.app.inform.emit(_("Click on Center point ..."))
         self.draw_app.app.inform.emit(_("Click on Center point ..."))
 
 
         # Direction of rotation between point 1 and 2.
         # Direction of rotation between point 1 and 2.
@@ -2210,12 +2209,13 @@ class FCRectangle(FCShapeTool):
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero.png'))
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
 
 
-        self.start_msg = _("Click on 1st corner ...")
+        self.draw_app.app.inform.emit( _("Click on 1st corner ..."))
 
 
     def click(self, point):
     def click(self, point):
         self.points.append(point)
         self.points.append(point)
 
 
         if len(self.points) == 1:
         if len(self.points) == 1:
+            self.draw_app.app.inform.emit(_("Click on opposite corner to complete ..."))
             return "Click on opposite corner to complete ..."
             return "Click on opposite corner to complete ..."
 
 
         if len(self.points) == 2:
         if len(self.points) == 2:
@@ -2262,14 +2262,14 @@ class FCPolygon(FCShapeTool):
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero.png'))
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
 
 
-        self.start_msg = _("Click on 1st point ...")
+        self.draw_app.app.inform.emit(_("Click on 1st corner ..."))
 
 
     def click(self, point):
     def click(self, point):
         self.draw_app.in_action = True
         self.draw_app.in_action = True
         self.points.append(point)
         self.points.append(point)
 
 
         if len(self.points) > 0:
         if len(self.points) > 0:
-            self.draw_app.app.inform.emit(_("Click on next Point or click Right mouse button to complete ..."))
+            self.draw_app.app.inform.emit(_("Click on next Point or click right mouse button to complete ..."))
             return "Click on next point or hit ENTER to complete ..."
             return "Click on next point or hit ENTER to complete ..."
 
 
         return ""
         return ""
@@ -2445,21 +2445,33 @@ class FCMove(FCShapeTool):
         FCShapeTool.__init__(self, draw_app)
         FCShapeTool.__init__(self, draw_app)
         self.name = 'move'
         self.name = 'move'
 
 
-        # self.shape_buffer = self.draw_app.shape_buffer
-        if not self.draw_app.selected:
-            self.draw_app.app.inform.emit(_("[WARNING_NOTCL] Move cancelled. No shape selected."))
-            return
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+
+        self.storage = self.draw_app.storage
+
         self.origin = None
         self.origin = None
         self.destination = None
         self.destination = None
-        self.start_msg = _("Click on reference point.")
+
+        if len(self.draw_app.get_selected()) == 0:
+            self.draw_app.app.inform.emit(_("[WARNING_NOTCL] MOVE: No shape selected. Select a shape to move ..."))
+        else:
+            self.draw_app.app.inform.emit(_(" MOVE: Click on reference point ..."))
 
 
     def set_origin(self, origin):
     def set_origin(self, origin):
-        self.draw_app.app.inform.emit(_("Click on destination point."))
+        self.draw_app.app.inform.emit(_(" Click on destination point ..."))
         self.origin = origin
         self.origin = origin
 
 
     def click(self, point):
     def click(self, point):
         if len(self.draw_app.get_selected()) == 0:
         if len(self.draw_app.get_selected()) == 0:
-            return "Nothing to move."
+            # self.complete = True
+            # self.draw_app.app.inform.emit(_("[WARNING_NOTCL] Move cancelled. No shape selected."))
+            self.select_shapes(point)
+            self.draw_app.replot()
+            self.draw_app.app.inform.emit(_(" MOVE: Click on reference point ..."))
+            return
 
 
         if self.origin is None:
         if self.origin is None:
             self.set_origin(point)
             self.set_origin(point)
@@ -2517,6 +2529,58 @@ class FCMove(FCShapeTool):
         # return DrawToolUtilityShape([affinity.translate(geom.geo, xoff=dx, yoff=dy)
         # return DrawToolUtilityShape([affinity.translate(geom.geo, xoff=dx, yoff=dy)
         #                              for geom in self.draw_app.get_selected()])
         #                              for geom in self.draw_app.get_selected()])
 
 
+    def select_shapes(self, pos):
+        # list where we store the overlapped shapes under our mouse left click position
+        over_shape_list = []
+
+        try:
+            _, closest_shape = self.storage.nearest(pos)
+        except StopIteration:
+            return ""
+
+        over_shape_list.append(closest_shape)
+
+        try:
+            # if there is no shape under our click then deselect all shapes
+            # it will not work for 3rd method of click selection
+            if not over_shape_list:
+                self.draw_app.selected = []
+                self.draw_app.draw_shape_idx = -1
+            else:
+                # if there are shapes under our click then advance through the list of them, one at the time in a
+                # circular way
+                self.draw_app.draw_shape_idx = (FlatCAMGeoEditor.draw_shape_idx + 1) % len(over_shape_list)
+                try:
+                    obj_to_add = over_shape_list[int(FlatCAMGeoEditor.draw_shape_idx)]
+                except IndexError:
+                    return
+
+                key_modifier = QtWidgets.QApplication.keyboardModifiers()
+                if self.draw_app.app.defaults["global_mselect_key"] == 'Control':
+                    # if CONTROL key is pressed then we add to the selected list the current shape but if it's
+                    # already in the selected list, we removed it. Therefore first click selects, second deselects.
+                    if key_modifier == Qt.ControlModifier:
+                        if obj_to_add in self.draw_app.selected:
+                            self.draw_app.selected.remove(obj_to_add)
+                        else:
+                            self.draw_app.selected.append(obj_to_add)
+                    else:
+                        self.draw_app.selected = []
+                        self.draw_app.selected.append(obj_to_add)
+                else:
+                    if key_modifier == Qt.ShiftModifier:
+                        if obj_to_add in self.draw_app.selected:
+                            self.draw_app.selected.remove(obj_to_add)
+                        else:
+                            self.draw_app.selected.append(obj_to_add)
+                    else:
+                        self.draw_app.selected = []
+                        self.draw_app.selected.append(obj_to_add)
+
+        except Exception as e:
+            log.error("[ERROR] Something went bad. %s" % str(e))
+            raise
+
 
 
 class FCCopy(FCMove):
 class FCCopy(FCMove):
     def __init__(self, draw_app):
     def __init__(self, draw_app):
@@ -2550,7 +2614,7 @@ class FCText(FCShapeTool):
         self.draw_app = draw_app
         self.draw_app = draw_app
         self.app = draw_app.app
         self.app = draw_app.app
 
 
-        self.start_msg = _("Click on the Destination point...")
+        self.draw_app.app.inform.emit(_("Click on 1st corner ..."))
         self.origin = (0, 0)
         self.origin = (0, 0)
 
 
         self.text_gui = TextInputTool(self.app)
         self.text_gui = TextInputTool(self.app)
@@ -2602,7 +2666,7 @@ class FCBuffer(FCShapeTool):
         self.draw_app = draw_app
         self.draw_app = draw_app
         self.app = draw_app.app
         self.app = draw_app.app
 
 
-        self.start_msg = _("Create buffer geometry ...")
+        self.draw_app.app.inform.emit(_("Create buffer geometry ..."))
         self.origin = (0, 0)
         self.origin = (0, 0)
         self.buff_tool = BufferSelectionTool(self.app, self.draw_app)
         self.buff_tool = BufferSelectionTool(self.app, self.draw_app)
         self.buff_tool.run()
         self.buff_tool.run()
@@ -2720,7 +2784,7 @@ class FCPaint(FCShapeTool):
         self.draw_app = draw_app
         self.draw_app = draw_app
         self.app = draw_app.app
         self.app = draw_app.app
 
 
-        self.start_msg = _("Create Paint geometry ...")
+        self.draw_app.app.inform.emit(_("Create Paint geometry ..."))
         self.origin = (0, 0)
         self.origin = (0, 0)
         self.draw_app.paint_tool.run()
         self.draw_app.paint_tool.run()
 
 
@@ -2734,7 +2798,7 @@ class FCTransform(FCShapeTool):
         self.draw_app = draw_app
         self.draw_app = draw_app
         self.app = draw_app.app
         self.app = draw_app.app
 
 
-        self.start_msg = _("Shape transformations ...")
+        self.draw_app.app.infrom.emit(_("Shape transformations ..."))
         self.origin = (0, 0)
         self.origin = (0, 0)
         self.draw_app.transform_tool.run()
         self.draw_app.transform_tool.run()
 
 
@@ -3159,6 +3223,9 @@ class FlatCAMGeoEditor(QtCore.QObject):
         :return: None
         :return: None
         """
         """
 
 
+        if shape is None:
+            return
+
         # List of DrawToolShape?
         # List of DrawToolShape?
         if isinstance(shape, list):
         if isinstance(shape, list):
             for subshape in shape:
             for subshape in shape:
@@ -3280,8 +3347,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
                         self.tools[t]["button"].setChecked(False)
                         self.tools[t]["button"].setChecked(False)
 
 
                 self.active_tool = self.tools[tool]["constructor"](self)
                 self.active_tool = self.tools[tool]["constructor"](self)
-                if not isinstance(self.active_tool, FCSelect):
-                    self.app.inform.emit(self.active_tool.start_msg)
             else:
             else:
                 self.app.log.debug("%s is NOT checked." % tool)
                 self.app.log.debug("%s is NOT checked." % tool)
                 for t in self.tools:
                 for t in self.tools:
@@ -3341,6 +3406,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
 
             # Selection with left mouse button
             # Selection with left mouse button
             if self.active_tool is not None and event.button is 1:
             if self.active_tool is not None and event.button is 1:
+
                 # Dispatch event to active_tool
                 # Dispatch event to active_tool
                 msg = self.active_tool.click(self.snap(self.pos[0], self.pos[1]))
                 msg = self.active_tool.click(self.snap(self.pos[0], self.pos[1]))
 
 
@@ -3348,28 +3414,11 @@ class FlatCAMGeoEditor(QtCore.QObject):
                 if isinstance(self.active_tool, FCShapeTool) and self.active_tool.complete:
                 if isinstance(self.active_tool, FCShapeTool) and self.active_tool.complete:
                     self.on_shape_complete()
                     self.on_shape_complete()
 
 
-                    # MS: always return to the Select Tool if modifier key is not pressed
-                    # else return to the current tool
-                    key_modifier = QtWidgets.QApplication.keyboardModifiers()
-                    if self.app.defaults["global_mselect_key"] == 'Control':
-                        modifier_to_use = Qt.ControlModifier
-                    else:
-                        modifier_to_use = Qt.ShiftModifier
-
                     if isinstance(self.active_tool, FCText):
                     if isinstance(self.active_tool, FCText):
                         self.select_tool("select")
                         self.select_tool("select")
                     else:
                     else:
                         self.select_tool(self.active_tool.name)
                         self.select_tool(self.active_tool.name)
 
 
-
-                    # if modifier key is pressed then we add to the selected list the current shape but if
-                    # it's already in the selected list, we removed it. Therefore first click selects, second deselects.
-                    # if key_modifier == modifier_to_use:
-                    #     self.select_tool(self.active_tool.name)
-                    # else:
-                    #     self.select_tool("select")
-                    #     return
-
                 if isinstance(self.active_tool, FCSelect):
                 if isinstance(self.active_tool, FCSelect):
                     # self.app.log.debug("Replotting after click.")
                     # self.app.log.debug("Replotting after click.")
                     self.replot()
                     self.replot()
@@ -3616,13 +3665,13 @@ class FlatCAMGeoEditor(QtCore.QObject):
             self.selected.remove(shape)  # TODO: Check performance
             self.selected.remove(shape)  # TODO: Check performance
 
 
     def on_move(self):
     def on_move(self):
+        # if not self.selected:
+        #     self.app.inform.emit(_("[WARNING_NOTCL] Move cancelled. No shape selected."))
+        #     return
         self.app.ui.geo_move_btn.setChecked(True)
         self.app.ui.geo_move_btn.setChecked(True)
         self.on_tool_select('move')
         self.on_tool_select('move')
 
 
     def on_move_click(self):
     def on_move_click(self):
-        if not self.selected:
-            self.app.inform.emit(_("[WARNING_NOTCL] Move cancelled. No shape selected."))
-            return
         self.on_move()
         self.on_move()
         self.active_tool.set_origin(self.snap(self.x, self.y))
         self.active_tool.set_origin(self.snap(self.x, self.y))
 
 

+ 120 - 43
flatcamEditors/FlatCAMGrbEditor.py

@@ -2,6 +2,7 @@ from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5.QtCore import Qt, QSettings
 from PyQt5.QtCore import Qt, QSettings
 
 
 from shapely.geometry import LineString, LinearRing, MultiLineString
 from shapely.geometry import LineString, LinearRing, MultiLineString
+# from shapely.geometry import mapping
 from shapely.ops import cascaded_union, unary_union
 from shapely.ops import cascaded_union, unary_union
 import shapely.affinity as affinity
 import shapely.affinity as affinity
 
 
@@ -19,6 +20,9 @@ from FlatCAMTool import FlatCAMTool
 
 
 from numpy.linalg import norm as numpy_norm
 from numpy.linalg import norm as numpy_norm
 
 
+# from vispy.io import read_png
+# import pngcanvas
+
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
 
 
@@ -1449,11 +1453,18 @@ class FCApertureMove(FCShapeTool):
         self.destination = None
         self.destination = None
         self.selected_apertures = []
         self.selected_apertures = []
 
 
+        if len(self.draw_app.get_selected()) == 0:
+            self.draw_app.app.inform.emit(_("[WARNING_NOTCL] Nothing selected to move ..."))
+            self.complete = True
+            self.draw_app.select_tool("select")
+            return
+
         if self.draw_app.launched_from_shortcuts is True:
         if self.draw_app.launched_from_shortcuts is True:
             self.draw_app.launched_from_shortcuts = False
             self.draw_app.launched_from_shortcuts = False
             self.draw_app.app.inform.emit(_("Click on target location ..."))
             self.draw_app.app.inform.emit(_("Click on target location ..."))
         else:
         else:
             self.draw_app.app.inform.emit(_("Click on reference location ..."))
             self.draw_app.app.inform.emit(_("Click on reference location ..."))
+
         self.current_storage = None
         self.current_storage = None
         self.geometry = []
         self.geometry = []
 
 
@@ -1485,6 +1496,37 @@ class FCApertureMove(FCShapeTool):
             self.draw_app.select_tool("select")
             self.draw_app.select_tool("select")
             return
             return
 
 
+    # def create_png(self):
+    #     """
+    #     Create a PNG file out of a list of Shapely polygons
+    #     :return:
+    #     """
+    #     if len(self.draw_app.get_selected()) == 0:
+    #         return None
+    #
+    #     geo_list = [geoms.geo for geoms in self.draw_app.get_selected()]
+    #     xmin, ymin, xmax, ymax = get_shapely_list_bounds(geo_list)
+    #
+    #     iwidth = (xmax - xmin)
+    #     iwidth = int(round(iwidth))
+    #     iheight = (ymax - ymin)
+    #     iheight = int(round(iheight))
+    #     c = pngcanvas.PNGCanvas(iwidth, iheight)
+    #
+    #     pixels = []
+    #     for geom in self.draw_app.get_selected():
+    #         m = mapping(geom.geo.exterior)
+    #         pixels += [[coord[0], coord[1]] for coord in m['coordinates']]
+    #         for g in geom.geo.interiors:
+    #             m = mapping(g)
+    #             pixels += [[coord[0], coord[1]] for coord in m['coordinates']]
+    #         c.polyline(pixels)
+    #         pixels = []
+    #
+    #     f = open("%s.png" % 'D:\\shapely_image', "wb")
+    #     f.write(c.dump())
+    #     f.close()
+
     def make(self):
     def make(self):
         # Create new geometry
         # Create new geometry
         dx = self.destination[0] - self.origin[0]
         dx = self.destination[0] - self.origin[0]
@@ -1499,13 +1541,14 @@ class FCApertureMove(FCShapeTool):
                     self.geometry.append(DrawToolShape(affinity.translate(select_shape.geo, xoff=dx, yoff=dy)))
                     self.geometry.append(DrawToolShape(affinity.translate(select_shape.geo, xoff=dx, yoff=dy)))
                     self.current_storage.remove(select_shape)
                     self.current_storage.remove(select_shape)
                     sel_shapes_to_be_deleted.append(select_shape)
                     sel_shapes_to_be_deleted.append(select_shape)
-                    self.draw_app.on_grb_shape_complete(self.current_storage)
+                    self.draw_app.on_grb_shape_complete(self.current_storage, noplot=True)
                     self.geometry = []
                     self.geometry = []
 
 
             for shp in sel_shapes_to_be_deleted:
             for shp in sel_shapes_to_be_deleted:
                 self.draw_app.selected.remove(shp)
                 self.draw_app.selected.remove(shp)
             sel_shapes_to_be_deleted = []
             sel_shapes_to_be_deleted = []
 
 
+        self.draw_app.plot_all()
         self.draw_app.build_ui()
         self.draw_app.build_ui()
         self.draw_app.app.inform.emit(_("[success] Done. Apertures Move completed."))
         self.draw_app.app.inform.emit(_("[success] Done. Apertures Move completed."))
 
 
@@ -1531,8 +1574,9 @@ class FCApertureMove(FCShapeTool):
 
 
         dx = data[0] - self.origin[0]
         dx = data[0] - self.origin[0]
         dy = data[1] - self.origin[1]
         dy = data[1] - self.origin[1]
-        for geom in self.draw_app.get_selected():
-            geo_list.append(affinity.translate(geom.geo, xoff=dx, yoff=dy))
+        # for geom in self.draw_app.get_selected():
+        #     geo_list.append(affinity.translate(geom.geo, xoff=dx, yoff=dy))
+        geo_list = [affinity.translate(geom.geo, xoff=dx, yoff=dy) for geom in self.draw_app.get_selected()]
         return DrawToolUtilityShape(geo_list)
         return DrawToolUtilityShape(geo_list)
 
 
 
 
@@ -1582,7 +1626,11 @@ class FCApertureSelect(DrawTool):
         # bending modes using in FCRegion and FCTrack
         # bending modes using in FCRegion and FCTrack
         self.draw_app.bend_mode = 1
         self.draw_app.bend_mode = 1
 
 
-        self.grb_editor_app.apertures_table.clearSelection()
+        try:
+            self.grb_editor_app.apertures_table.clearSelection()
+        except Exception as e:
+            log.error("FlatCAMGerbEditor.FCApertureSelect.__init__() --> %s" % str(e))
+
         self.grb_editor_app.hide_tool('all')
         self.grb_editor_app.hide_tool('all')
         self.grb_editor_app.hide_tool('select')
         self.grb_editor_app.hide_tool('select')
 
 
@@ -2297,12 +2345,13 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
 
         sorted_apertures = sorted(sort)
         sorted_apertures = sorted(sort)
 
 
-        sort = []
-        for k, v in list(self.gerber_obj.aperture_macros.items()):
-            sort.append(k)
-        sorted_macros = sorted(sort)
+        # sort = []
+        # for k, v in list(self.gerber_obj.aperture_macros.items()):
+        #     sort.append(k)
+        # sorted_macros = sorted(sort)
 
 
-        n = len(sorted_apertures) + len(sorted_macros)
+        # n = len(sorted_apertures) + len(sorted_macros)
+        n = len(sorted_apertures)
         self.apertures_table.setRowCount(n)
         self.apertures_table.setRowCount(n)
 
 
         for ap_code in sorted_apertures:
         for ap_code in sorted_apertures:
@@ -2355,25 +2404,25 @@ class FlatCAMGrbEditor(QtCore.QObject):
                 # set now the last aperture selected
                 # set now the last aperture selected
                 self.last_aperture_selected = ap_code
                 self.last_aperture_selected = ap_code
 
 
-        for ap_code in sorted_macros:
-            ap_code = str(ap_code)
-
-            ap_id_item = QtWidgets.QTableWidgetItem('%d' % int(self.apertures_row + 1))
-            ap_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-            self.apertures_table.setItem(self.apertures_row, 0, ap_id_item)  # Tool name/id
-
-            ap_code_item = QtWidgets.QTableWidgetItem(ap_code)
-
-            ap_type_item = QtWidgets.QTableWidgetItem('AM')
-            ap_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
-
-            self.apertures_table.setItem(self.apertures_row, 1, ap_code_item)  # Aperture Code
-            self.apertures_table.setItem(self.apertures_row, 2, ap_type_item)  # Aperture Type
-
-            self.apertures_row += 1
-            if first_run is True:
-                # set now the last aperture selected
-                self.last_aperture_selected = ap_code
+        # for ap_code in sorted_macros:
+        #     ap_code = str(ap_code)
+        #
+        #     ap_id_item = QtWidgets.QTableWidgetItem('%d' % int(self.apertures_row + 1))
+        #     ap_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        #     self.apertures_table.setItem(self.apertures_row, 0, ap_id_item)  # Tool name/id
+        #
+        #     ap_code_item = QtWidgets.QTableWidgetItem(ap_code)
+        #
+        #     ap_type_item = QtWidgets.QTableWidgetItem('AM')
+        #     ap_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
+        #
+        #     self.apertures_table.setItem(self.apertures_row, 1, ap_code_item)  # Aperture Code
+        #     self.apertures_table.setItem(self.apertures_row, 2, ap_type_item)  # Aperture Type
+        #
+        #     self.apertures_row += 1
+        #     if first_run is True:
+        #         # set now the last aperture selected
+        #         self.last_aperture_selected = ap_code
 
 
         self.apertures_table.selectColumn(0)
         self.apertures_table.selectColumn(0)
         self.apertures_table.resizeColumnsToContents()
         self.apertures_table.resizeColumnsToContents()
@@ -2947,6 +2996,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
                                 if geo is not None:
                                 if geo is not None:
                                     self.add_gerber_shape(DrawToolShape(geo), follow_storage_elem)
                                     self.add_gerber_shape(DrawToolShape(geo), follow_storage_elem)
                             self.storage_dict[apid][k] = follow_storage_elem
                             self.storage_dict[apid][k] = follow_storage_elem
+                        elif k == 'clear_geometry':
+                            continue
                         else:
                         else:
                             self.storage_dict[apid][k] = v
                             self.storage_dict[apid][k] = v
                     except Exception as e:
                     except Exception as e:
@@ -3062,8 +3113,13 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
 
                     elif k == 'follow_geometry':
                     elif k == 'follow_geometry':
                         grb_obj.apertures[storage_apid][k] = []
                         grb_obj.apertures[storage_apid][k] = []
-                        for geo in v:
-                            new_geo = deepcopy(geo.geo)
+                        for geo_f in v:
+                            if isinstance(geo_f.geo, Polygon):
+                                buff_val = -(int(storage_apid) / 2)
+                                geo_f = geo_f.geo.buffer(buff_val).exterior
+                                new_geo = deepcopy(geo_f)
+                            else:
+                                new_geo = deepcopy(geo_f.geo)
                             grb_obj.apertures[storage_apid][k].append(new_geo)
                             grb_obj.apertures[storage_apid][k].append(new_geo)
                             follow_buffer.append(new_geo)
                             follow_buffer.append(new_geo)
                     else:
                     else:
@@ -3176,7 +3232,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.options[key] = self.sender().isChecked()
         self.options[key] = self.sender().isChecked()
         return self.options[key]
         return self.options[key]
 
 
-    def on_grb_shape_complete(self, storage=None, specific_shape=None):
+    def on_grb_shape_complete(self, storage=None, specific_shape=None, noplot=False):
         self.app.log.debug("on_shape_complete()")
         self.app.log.debug("on_shape_complete()")
 
 
         if specific_shape:
         if specific_shape:
@@ -3197,8 +3253,9 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.delete_utility_geometry()
         self.delete_utility_geometry()
         self.tool_shape.clear(update=True)
         self.tool_shape.clear(update=True)
 
 
-        # Replot and reset tool.
-        self.plot_all()
+        if noplot is False:
+            # Replot and reset tool.
+            self.plot_all()
 
 
     def add_gerber_shape(self, shape, storage):
     def add_gerber_shape(self, shape, storage):
         """
         """
@@ -3857,16 +3914,17 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
 
     def hide_tool(self, tool_name):
     def hide_tool(self, tool_name):
         # self.app.ui.notebook.setTabText(2, _("Tools"))
         # self.app.ui.notebook.setTabText(2, _("Tools"))
-
-        if tool_name == 'all':
-            self.apertures_frame.hide()
-        if tool_name == 'select':
-            self.apertures_frame.show()
-        if tool_name == 'buffer' or tool_name == 'all':
-            self.buffer_tool_frame.hide()
-        if tool_name == 'scale' or tool_name == 'all':
-            self.scale_tool_frame.hide()
-
+        try:
+            if tool_name == 'all':
+                self.apertures_frame.hide()
+            if tool_name == 'select':
+                self.apertures_frame.show()
+            if tool_name == 'buffer' or tool_name == 'all':
+                self.buffer_tool_frame.hide()
+            if tool_name == 'scale' or tool_name == 'all':
+                self.scale_tool_frame.hide()
+        except Exception as e:
+            log.debug("FlatCAMGrbEditor.hide_tool() --> %s" % str(e))
         self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
         self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
 
 
 
 
@@ -4847,3 +4905,22 @@ class TransformEditorTool(FlatCAMTool):
         else:
         else:
             self.app.inform.emit(
             self.app.inform.emit(
                 _("[WARNING_NOTCL] Geometry shape skew Y cancelled..."))
                 _("[WARNING_NOTCL] Geometry shape skew Y cancelled..."))
+
+
+def get_shapely_list_bounds(geometry_list):
+    xmin = Inf
+    ymin = Inf
+    xmax = -Inf
+    ymax = -Inf
+
+    for gs in geometry_list:
+        try:
+            gxmin, gymin, gxmax, gymax = gs.bounds
+            xmin = min([xmin, gxmin])
+            ymin = min([ymin, gymin])
+            xmax = max([xmax, gxmax])
+            ymax = max([ymax, gymax])
+        except:
+            log.warning("DEVELOPMENT: Tried to get bounds of empty geometry.")
+
+    return [xmin, ymin, xmax, ymax]

+ 143 - 9
flatcamGUI/FlatCAMGUI.py

@@ -183,6 +183,14 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         )
         )
         self.menufileexport.addAction(self.menufileexportexcellon)
         self.menufileexport.addAction(self.menufileexportexcellon)
 
 
+        self.menufileexportgerber = QtWidgets.QAction(QtGui.QIcon('share/flatcam_icon32.png'), _('Export &Gerber ...'),
+                                                        self)
+        self.menufileexportgerber.setToolTip(
+            _("Will export an Gerber Object as Gerber file,\n"
+              "the coordinates format, the file units and zeros\n"
+              "are set in Preferences -> Gerber Export.")
+        )
+        self.menufileexport.addAction(self.menufileexportgerber)
 
 
         # Separator
         # Separator
         self.menufile.addSeparator()
         self.menufile.addSeparator()
@@ -265,13 +273,18 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
            _( "Will convert a Geometry object from multi_geometry type\n"
            _( "Will convert a Geometry object from multi_geometry type\n"
             "to a single_geometry type.")
             "to a single_geometry type.")
         )
         )
+        # Separator
+        self.menuedit_convert.addSeparator()
+        self.menueditconvert_any2geo = self.menuedit_convert.addAction(QtGui.QIcon('share/copy_geo.png'),
+                                                                _('Convert Any to Geo'))
+        self.menueditconvert_any2gerber = self.menuedit_convert.addAction(QtGui.QIcon('share/copy_geo.png'),
+                                                                       _('Convert Any to Gerber'))
         self.menuedit_convert.setToolTipsVisible(True)
         self.menuedit_convert.setToolTipsVisible(True)
 
 
         # Separator
         # Separator
         self.menuedit.addSeparator()
         self.menuedit.addSeparator()
-        self.menueditcopyobject = self.menuedit.addAction(QtGui.QIcon('share/copy.png'), _('&Copy Object\tCTRL+C'))
-        self.menueditcopyobjectasgeom = self.menuedit.addAction(QtGui.QIcon('share/copy_geo.png'),
-                                                                _('Copy as &Geom'))
+        self.menueditcopyobject = self.menuedit.addAction(QtGui.QIcon('share/copy.png'), _('&Copy\tCTRL+C'))
+
         # Separator
         # Separator
         self.menuedit.addSeparator()
         self.menuedit.addSeparator()
         self.menueditdelete = self.menuedit.addAction(QtGui.QIcon('share/trash16.png'), _('&Delete\tDEL'))
         self.menueditdelete = self.menuedit.addAction(QtGui.QIcon('share/trash16.png'), _('&Delete\tDEL'))
@@ -2281,11 +2294,12 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
 
                 # Change Units
                 # Change Units
                 if key == QtCore.Qt.Key_Q:
                 if key == QtCore.Qt.Key_Q:
-                    if self.app.defaults["units"] == 'MM':
-                        self.app.ui.general_defaults_form.general_app_group.units_radio.set_value("IN")
-                    else:
-                        self.app.ui.general_defaults_form.general_app_group.units_radio.set_value("MM")
-                    self.app.on_toggle_units()
+                    # if self.app.defaults["units"] == 'MM':
+                    #     self.app.ui.general_defaults_form.general_app_group.units_radio.set_value("IN")
+                    # else:
+                    #     self.app.ui.general_defaults_form.general_app_group.units_radio.set_value("MM")
+                    # self.app.on_toggle_units(no_pref=True)
+                    self.app.on_toggle_units_click()
 
 
                 # Rotate Object by 90 degree CW
                 # Rotate Object by 90 degree CW
                 if key == QtCore.Qt.Key_R:
                 if key == QtCore.Qt.Key_R:
@@ -3131,11 +3145,17 @@ class GerberPreferencesUI(QtWidgets.QWidget):
         self.gerber_gen_group.setFixedWidth(250)
         self.gerber_gen_group.setFixedWidth(250)
         self.gerber_opt_group = GerberOptPrefGroupUI()
         self.gerber_opt_group = GerberOptPrefGroupUI()
         self.gerber_opt_group.setFixedWidth(230)
         self.gerber_opt_group.setFixedWidth(230)
+        self.gerber_exp_group = GerberExpPrefGroupUI()
+        self.gerber_exp_group.setFixedWidth(230)
         self.gerber_adv_opt_group = GerberAdvOptPrefGroupUI()
         self.gerber_adv_opt_group = GerberAdvOptPrefGroupUI()
         self.gerber_adv_opt_group.setFixedWidth(200)
         self.gerber_adv_opt_group.setFixedWidth(200)
 
 
+        self.vlay = QtWidgets.QVBoxLayout()
+        self.vlay.addWidget(self.gerber_opt_group)
+        self.vlay.addWidget(self.gerber_exp_group)
+
         self.layout.addWidget(self.gerber_gen_group)
         self.layout.addWidget(self.gerber_gen_group)
-        self.layout.addWidget(self.gerber_opt_group)
+        self.layout.addLayout(self.vlay)
         self.layout.addWidget(self.gerber_adv_opt_group)
         self.layout.addWidget(self.gerber_adv_opt_group)
 
 
         self.layout.addStretch()
         self.layout.addStretch()
@@ -3861,6 +3881,25 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
         )
         )
         self.worker_number_sb.set_range(2, 16)
         self.worker_number_sb.set_range(2, 16)
 
 
+        # Geometric tolerance
+        tol_label = QtWidgets.QLabel("Geo Tolerance:")
+        tol_label.setToolTip(_(
+            "This value can counter the effect of the Circle Steps\n"
+            "parameter. Default value is 0.01.\n"
+            "A lower value will increase the detail both in image\n"
+            "and in Gcode for the circles, with a higher cost in\n"
+            "performance. Higher value will provide more\n"
+            "performance at the expense of level of detail."
+        ))
+        self.tol_entry = FCEntry()
+        self.tol_entry.setToolTip(_(
+            "This value can counter the effect of the Circle Steps\n"
+            "parameter. Default value is 0.01.\n"
+            "A lower value will increase the detail both in image\n"
+            "and in Gcode for the circles, with a higher cost in\n"
+            "performance. Higher value will provide more\n"
+            "performance at the expense of level of detail."
+        ))
         # Just to add empty rows
         # Just to add empty rows
         self.spacelabel = QtWidgets.QLabel('')
         self.spacelabel = QtWidgets.QLabel('')
 
 
@@ -3881,6 +3920,7 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
         self.form_box.addRow(self.project_autohide_label, self.project_autohide_cb)
         self.form_box.addRow(self.project_autohide_label, self.project_autohide_cb)
         self.form_box.addRow(self.toggle_tooltips_label, self.toggle_tooltips_cb)
         self.form_box.addRow(self.toggle_tooltips_label, self.toggle_tooltips_cb)
         self.form_box.addRow(self.worker_number_label, self.worker_number_sb)
         self.form_box.addRow(self.worker_number_label, self.worker_number_sb)
+        self.form_box.addRow(tol_label, self.tol_entry)
 
 
         self.form_box.addRow(self.spacelabel, self.spacelabel)
         self.form_box.addRow(self.spacelabel, self.spacelabel)
 
 
@@ -4166,6 +4206,100 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
         self.layout.addStretch()
         self.layout.addStretch()
 
 
 
 
+class GerberExpPrefGroupUI(OptionsGroupUI):
+
+    def __init__(self, parent=None):
+        super(GerberExpPrefGroupUI, self).__init__(self)
+
+        self.setTitle(str(_("Gerber Export")))
+
+        # Plot options
+        self.export_options_label = QtWidgets.QLabel(_("<b>Export Options:</b>"))
+        self.export_options_label.setToolTip(
+            _("The parameters set here are used in the file exported\n"
+            "when using the File -> Export -> Export Gerber menu entry.")
+        )
+        self.layout.addWidget(self.export_options_label)
+
+        form = QtWidgets.QFormLayout()
+        self.layout.addLayout(form)
+
+        # Gerber Units
+        self.gerber_units_label = QtWidgets.QLabel(_('<b>Units</b>:'))
+        self.gerber_units_label.setToolTip(
+            _("The units used in the Gerber file.")
+        )
+
+        self.gerber_units_radio = RadioSet([{'label': 'INCH', 'value': 'IN'},
+                                              {'label': 'MM', 'value': 'MM'}])
+        self.gerber_units_radio.setToolTip(
+            _("The units used in the Gerber file.")
+        )
+
+        form.addRow(self.gerber_units_label, self.gerber_units_radio)
+
+        # Gerber format
+        self.digits_label = QtWidgets.QLabel(_("<b>Int/Decimals:</b>"))
+        self.digits_label.setToolTip(
+            _("The number of digits in the whole part of the number\n"
+              "and in the fractional part of the number.")
+        )
+
+        hlay1 = QtWidgets.QHBoxLayout()
+
+        self.format_whole_entry = IntEntry()
+        self.format_whole_entry.setMaxLength(1)
+        self.format_whole_entry.setAlignment(QtCore.Qt.AlignRight)
+        self.format_whole_entry.setFixedWidth(30)
+        self.format_whole_entry.setToolTip(
+            _("This numbers signify the number of digits in\n"
+            "the whole part of Gerber coordinates.")
+        )
+        hlay1.addWidget(self.format_whole_entry, QtCore.Qt.AlignLeft)
+
+        gerber_separator_label= QtWidgets.QLabel(':')
+        gerber_separator_label.setFixedWidth(5)
+        hlay1.addWidget(gerber_separator_label, QtCore.Qt.AlignLeft)
+
+        self.format_dec_entry = IntEntry()
+        self.format_dec_entry.setMaxLength(1)
+        self.format_dec_entry.setAlignment(QtCore.Qt.AlignRight)
+        self.format_dec_entry.setFixedWidth(30)
+        self.format_dec_entry.setToolTip(
+            _("This numbers signify the number of digits in\n"
+              "the decimal part of Gerber coordinates.")
+        )
+        hlay1.addWidget(self.format_dec_entry, QtCore.Qt.AlignLeft)
+        hlay1.addStretch()
+
+        form.addRow(self.digits_label, hlay1)
+
+        # Gerber Zeros
+        self.zeros_label = QtWidgets.QLabel(_('<b>Zeros</b>:'))
+        self.zeros_label.setAlignment(QtCore.Qt.AlignLeft)
+        self.zeros_label.setToolTip(
+            _("This sets the type of Gerber zeros.\n"
+              "If LZ then Leading Zeros are removed and\n"
+              "Trailing Zeros are kept.\n"
+              "If TZ is checked then Trailing Zeros are removed\n"
+              "and Leading Zeros are kept.")
+        )
+
+        self.zeros_radio = RadioSet([{'label': 'LZ', 'value': 'L'},
+                                     {'label': 'TZ', 'value': 'T'}])
+        self.zeros_radio.setToolTip(
+            _("This sets the type of Gerber zeros.\n"
+              "If LZ then Leading Zeros are removed and\n"
+              "Trailing Zeros are kept.\n"
+              "If TZ is checked then Trailing Zeros are removed\n"
+              "and Leading Zeros are kept.")
+        )
+
+        form.addRow(self.zeros_label, self.zeros_radio)
+
+        self.layout.addStretch()
+
+
 class ExcellonGenPrefGroupUI(OptionsGroupUI):
 class ExcellonGenPrefGroupUI(OptionsGroupUI):
 
 
     def __init__(self, parent=None):
     def __init__(self, parent=None):

+ 64 - 0
flatcamGUI/GUIElements.py

@@ -571,11 +571,48 @@ class FCTextAreaExtended(QtWidgets.QTextEdit):
                 clip_text = clip_text.replace('\\', '/')
                 clip_text = clip_text.replace('\\', '/')
                 self.insertPlainText(clip_text)
                 self.insertPlainText(clip_text)
 
 
+        if modifier & Qt.ControlModifier and key == Qt.Key_Slash:
+            self.comment()
+
         tc = self.textCursor()
         tc = self.textCursor()
         if (key == Qt.Key_Tab or key == Qt.Key_Enter or key == Qt.Key_Return) and self.completer.popup().isVisible():
         if (key == Qt.Key_Tab or key == Qt.Key_Enter or key == Qt.Key_Return) and self.completer.popup().isVisible():
             self.completer.insertText.emit(self.completer.getSelected())
             self.completer.insertText.emit(self.completer.getSelected())
             self.completer.setCompletionMode(QCompleter.PopupCompletion)
             self.completer.setCompletionMode(QCompleter.PopupCompletion)
             return
             return
+        elif key == Qt.Key_BraceLeft:
+            tc.insertText('{}')
+            self.moveCursor(QtGui.QTextCursor.Left)
+        elif key == Qt.Key_BracketLeft:
+            tc.insertText('[]')
+            self.moveCursor(QtGui.QTextCursor.Left)
+        elif key == Qt.Key_ParenLeft:
+            tc.insertText('()')
+            self.moveCursor(QtGui.QTextCursor.Left)
+
+        elif key == Qt.Key_BraceRight:
+            tc.select(QtGui.QTextCursor.WordUnderCursor)
+            if tc.selectedText() == '}':
+                tc.movePosition(QTextCursor.Right)
+                self.setTextCursor(tc)
+            else:
+                tc.clearSelection()
+                self.textCursor().insertText('}')
+        elif key == Qt.Key_BracketRight:
+            tc.select(QtGui.QTextCursor.WordUnderCursor)
+            if tc.selectedText() == ']':
+                tc.movePosition(QTextCursor.Right)
+                self.setTextCursor(tc)
+            else:
+                tc.clearSelection()
+                self.textCursor().insertText(']')
+        elif key == Qt.Key_ParenRight:
+            tc.select(QtGui.QTextCursor.WordUnderCursor)
+            if tc.selectedText() == ')':
+                tc.movePosition(QTextCursor.Right)
+                self.setTextCursor(tc)
+            else:
+                tc.clearSelection()
+                self.textCursor().insertText(')')
         else:
         else:
             super(FCTextAreaExtended, self).keyPressEvent(event)
             super(FCTextAreaExtended, self).keyPressEvent(event)
 
 
@@ -594,6 +631,33 @@ class FCTextAreaExtended(QtWidgets.QTextEdit):
             else:
             else:
                 self.completer.popup().hide()
                 self.completer.popup().hide()
 
 
+    def comment(self):
+        """
+        Got it from here:
+        https://stackoverflow.com/questions/49898820/how-to-get-text-next-to-cursor-in-qtextedit-in-pyqt4
+        :return:
+        """
+        pos = self.textCursor().position()
+        self.moveCursor(QtGui.QTextCursor.StartOfLine)
+        line_text = self.textCursor().block().text()
+        if self.textCursor().block().text().startswith(" "):
+            # skip the white space
+            self.moveCursor(QtGui.QTextCursor.NextWord)
+        self.moveCursor(QtGui.QTextCursor.NextCharacter,QtGui.QTextCursor.KeepAnchor)
+        character = self.textCursor().selectedText()
+        if character == "#":
+            # delete #
+            self.textCursor().deletePreviousChar()
+            # delete white space 
+            self.moveCursor(QtGui.QTextCursor.NextWord,QtGui.QTextCursor.KeepAnchor)
+            self.textCursor().removeSelectedText()
+        else:
+            self.moveCursor(QtGui.QTextCursor.PreviousCharacter,QtGui.QTextCursor.KeepAnchor)
+            self.textCursor().insertText("# ")
+        cursor = QtGui.QTextCursor(self.textCursor())
+        cursor.setPosition(pos)
+        self.setTextCursor(cursor)
+
 
 
 class FCComboBox(QtWidgets.QComboBox):
 class FCComboBox(QtWidgets.QComboBox):
 
 

+ 1 - 1
flatcamTools/ToolMove.py

@@ -137,7 +137,7 @@ class ToolMove(FlatCAMTool):
                             else:
                             else:
                                 for sel_obj in obj_list:
                                 for sel_obj in obj_list:
 
 
-                                    # offset
+                                    # offset solid_geometry
                                     sel_obj.offset((dx, dy))
                                     sel_obj.offset((dx, dy))
                                     sel_obj.plot()
                                     sel_obj.plot()
 
 

+ 162 - 53
flatcamTools/ToolPanelize.py

@@ -13,9 +13,9 @@ import time
 
 
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
+import builtins
 
 
 fcTranslate.apply_language('strings')
 fcTranslate.apply_language('strings')
-import builtins
 if '_' not in builtins.__dict__:
 if '_' not in builtins.__dict__:
     _ = gettext.gettext
     _ = gettext.gettext
 
 
@@ -39,11 +39,11 @@ class Panelize(FlatCAMTool):
                         """)
                         """)
         self.layout.addWidget(title_label)
         self.layout.addWidget(title_label)
 
 
-        ## Form Layout
-        form_layout = QtWidgets.QFormLayout()
-        self.layout.addLayout(form_layout)
+        # Form Layout
+        form_layout_0 = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_layout_0)
 
 
-        ## Type of object to be panelized
+        # Type of object to be panelized
         self.type_obj_combo = QtWidgets.QComboBox()
         self.type_obj_combo = QtWidgets.QComboBox()
         self.type_obj_combo.addItem("Gerber")
         self.type_obj_combo.addItem("Gerber")
         self.type_obj_combo.addItem("Excellon")
         self.type_obj_combo.addItem("Excellon")
@@ -56,13 +56,13 @@ class Panelize(FlatCAMTool):
         self.type_obj_combo_label = QtWidgets.QLabel(_("Object Type:"))
         self.type_obj_combo_label = QtWidgets.QLabel(_("Object Type:"))
         self.type_obj_combo_label.setToolTip(
         self.type_obj_combo_label.setToolTip(
             _("Specify the type of object to be panelized\n"
             _("Specify the type of object to be panelized\n"
-            "It can be of type: Gerber, Excellon or Geometry.\n"
-            "The selection here decide the type of objects that will be\n"
-            "in the Object combobox.")
+              "It can be of type: Gerber, Excellon or Geometry.\n"
+              "The selection here decide the type of objects that will be\n"
+              "in the Object combobox.")
         )
         )
-        form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo)
+        form_layout_0.addRow(self.type_obj_combo_label, self.type_obj_combo)
 
 
-        ## Object to be panelized
+        # Object to be panelized
         self.object_combo = QtWidgets.QComboBox()
         self.object_combo = QtWidgets.QComboBox()
         self.object_combo.setModel(self.app.collection)
         self.object_combo.setModel(self.app.collection)
         self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
@@ -71,11 +71,33 @@ class Panelize(FlatCAMTool):
         self.object_label = QtWidgets.QLabel(_("Object:"))
         self.object_label = QtWidgets.QLabel(_("Object:"))
         self.object_label.setToolTip(
         self.object_label.setToolTip(
             _("Object to be panelized. This means that it will\n"
             _("Object to be panelized. This means that it will\n"
-            "be duplicated in an array of rows and columns.")
+              "be duplicated in an array of rows and columns.")
         )
         )
-        form_layout.addRow(self.object_label, self.object_combo)
+        form_layout_0.addRow(self.object_label, self.object_combo)
+        form_layout_0.addRow(QtWidgets.QLabel(""))
 
 
-        ## Type of Box Object to be used as an envelope for panelization
+        # Form Layout
+        form_layout = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_layout)
+
+        # Type of box Panel object
+        self.reference_radio = RadioSet([{'label': 'Object', 'value': 'object'},
+                                         {'label': 'Bounding Box', 'value': 'bbox'}])
+        self.box_label = QtWidgets.QLabel(_("<b>Penelization Reference:</b>"))
+        self.box_label.setToolTip(
+            _("Choose the reference for panelization:\n"
+              "- Object = the bounding box of a different object\n"
+              "- Bounding Box = the bounding box of the object to be panelized\n"
+              "\n"
+              "The reference is useful when doing panelization for more than one\n"
+              "object. The spacings (really offsets) will be applied in reference\n"
+              "to this reference object therefore maintaining the panelized\n"
+              "objects in sync.")
+        )
+        form_layout.addRow(self.box_label)
+        form_layout.addRow(self.reference_radio)
+
+        # Type of Box Object to be used as an envelope for panelization
         self.type_box_combo = QtWidgets.QComboBox()
         self.type_box_combo = QtWidgets.QComboBox()
         self.type_box_combo.addItem("Gerber")
         self.type_box_combo.addItem("Gerber")
         self.type_box_combo.addItem("Excellon")
         self.type_box_combo.addItem("Excellon")
@@ -89,13 +111,13 @@ class Panelize(FlatCAMTool):
         self.type_box_combo_label = QtWidgets.QLabel(_("Box Type:"))
         self.type_box_combo_label = QtWidgets.QLabel(_("Box Type:"))
         self.type_box_combo_label.setToolTip(
         self.type_box_combo_label.setToolTip(
             _("Specify the type of object to be used as an container for\n"
             _("Specify the type of object to be used as an container for\n"
-            "panelization. It can be: Gerber or Geometry type.\n"
-            "The selection here decide the type of objects that will be\n"
-            "in the Box Object combobox.")
+              "panelization. It can be: Gerber or Geometry type.\n"
+              "The selection here decide the type of objects that will be\n"
+              "in the Box Object combobox.")
         )
         )
         form_layout.addRow(self.type_box_combo_label, self.type_box_combo)
         form_layout.addRow(self.type_box_combo_label, self.type_box_combo)
 
 
-        ## Box
+        # Box
         self.box_combo = QtWidgets.QComboBox()
         self.box_combo = QtWidgets.QComboBox()
         self.box_combo.setModel(self.app.collection)
         self.box_combo.setModel(self.app.collection)
         self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
@@ -104,29 +126,41 @@ class Panelize(FlatCAMTool):
         self.box_combo_label = QtWidgets.QLabel(_("Box Object:"))
         self.box_combo_label = QtWidgets.QLabel(_("Box Object:"))
         self.box_combo_label.setToolTip(
         self.box_combo_label.setToolTip(
             _("The actual object that is used a container for the\n "
             _("The actual object that is used a container for the\n "
-            "selected object that is to be panelized.")
+              "selected object that is to be panelized.")
         )
         )
         form_layout.addRow(self.box_combo_label, self.box_combo)
         form_layout.addRow(self.box_combo_label, self.box_combo)
+        form_layout.addRow(QtWidgets.QLabel(""))
+
+        panel_data_label = QtWidgets.QLabel(_("<b>Panel Data:</b>"))
+        panel_data_label.setToolTip(
+            _("This informations will shape the resulting panel.\n"
+              "The number of rows and columns will set how many\n"
+              "duplicates of the original geometry will be generated.\n"
+              "\n"
+              "The spacings will set the distance between any two\n"
+              "elements of the panel array.")
+        )
+        form_layout.addRow(panel_data_label)
 
 
-        ## Spacing Columns
+        # Spacing Columns
         self.spacing_columns = FCEntry()
         self.spacing_columns = FCEntry()
         self.spacing_columns_label = QtWidgets.QLabel(_("Spacing cols:"))
         self.spacing_columns_label = QtWidgets.QLabel(_("Spacing cols:"))
         self.spacing_columns_label.setToolTip(
         self.spacing_columns_label.setToolTip(
             _("Spacing between columns of the desired panel.\n"
             _("Spacing between columns of the desired panel.\n"
-            "In current units.")
+              "In current units.")
         )
         )
         form_layout.addRow(self.spacing_columns_label, self.spacing_columns)
         form_layout.addRow(self.spacing_columns_label, self.spacing_columns)
 
 
-        ## Spacing Rows
+        # Spacing Rows
         self.spacing_rows = FCEntry()
         self.spacing_rows = FCEntry()
         self.spacing_rows_label = QtWidgets.QLabel(_("Spacing rows:"))
         self.spacing_rows_label = QtWidgets.QLabel(_("Spacing rows:"))
         self.spacing_rows_label.setToolTip(
         self.spacing_rows_label.setToolTip(
             _("Spacing between rows of the desired panel.\n"
             _("Spacing between rows of the desired panel.\n"
-            "In current units.")
+              "In current units.")
         )
         )
         form_layout.addRow(self.spacing_rows_label, self.spacing_rows)
         form_layout.addRow(self.spacing_rows_label, self.spacing_rows)
 
 
-        ## Columns
+        # Columns
         self.columns = FCEntry()
         self.columns = FCEntry()
         self.columns_label = QtWidgets.QLabel(_("Columns:"))
         self.columns_label = QtWidgets.QLabel(_("Columns:"))
         self.columns_label.setToolTip(
         self.columns_label.setToolTip(
@@ -134,34 +168,35 @@ class Panelize(FlatCAMTool):
         )
         )
         form_layout.addRow(self.columns_label, self.columns)
         form_layout.addRow(self.columns_label, self.columns)
 
 
-        ## Rows
+        # Rows
         self.rows = FCEntry()
         self.rows = FCEntry()
         self.rows_label = QtWidgets.QLabel(_("Rows:"))
         self.rows_label = QtWidgets.QLabel(_("Rows:"))
         self.rows_label.setToolTip(
         self.rows_label.setToolTip(
             _("Number of rows of the desired panel")
             _("Number of rows of the desired panel")
         )
         )
         form_layout.addRow(self.rows_label, self.rows)
         form_layout.addRow(self.rows_label, self.rows)
+        form_layout.addRow(QtWidgets.QLabel(""))
 
 
-        ## Type of resulting Panel object
+        # Type of resulting Panel object
         self.panel_type_radio = RadioSet([{'label': 'Gerber', 'value': 'gerber'},
         self.panel_type_radio = RadioSet([{'label': 'Gerber', 'value': 'gerber'},
-                                     {'label': 'Geometry', 'value': 'geometry'}])
-        self.panel_type_label = QtWidgets.QLabel(_("Panel Type:"))
+                                          {'label': 'Geometry', 'value': 'geometry'}])
+        self.panel_type_label = QtWidgets.QLabel(_("<b>Panel Type:</b>"))
         self.panel_type_label.setToolTip(
         self.panel_type_label.setToolTip(
             _("Choose the type of object for the panel object:\n"
             _("Choose the type of object for the panel object:\n"
-            "- Geometry\n"
-            "- Gerber")
+              "- Geometry\n"
+              "- Gerber")
         )
         )
         form_layout.addRow(self.panel_type_label)
         form_layout.addRow(self.panel_type_label)
         form_layout.addRow(self.panel_type_radio)
         form_layout.addRow(self.panel_type_radio)
 
 
-        ## Constrains
+        # Constrains
         self.constrain_cb = FCCheckBox(_("Constrain panel within:"))
         self.constrain_cb = FCCheckBox(_("Constrain panel within:"))
         self.constrain_cb.setToolTip(
         self.constrain_cb.setToolTip(
             _("Area define by DX and DY within to constrain the panel.\n"
             _("Area define by DX and DY within to constrain the panel.\n"
-            "DX and DY values are in current units.\n"
-            "Regardless of how many columns and rows are desired,\n"
-            "the final panel will have as many columns and rows as\n"
-            "they fit completely within selected area.")
+              "DX and DY values are in current units.\n"
+              "Regardless of how many columns and rows are desired,\n"
+              "the final panel will have as many columns and rows as\n"
+              "they fit completely within selected area.")
         )
         )
         form_layout.addRow(self.constrain_cb)
         form_layout.addRow(self.constrain_cb)
 
 
@@ -169,7 +204,7 @@ class Panelize(FlatCAMTool):
         self.x_width_lbl = QtWidgets.QLabel(_("Width (DX):"))
         self.x_width_lbl = QtWidgets.QLabel(_("Width (DX):"))
         self.x_width_lbl.setToolTip(
         self.x_width_lbl.setToolTip(
             _("The width (DX) within which the panel must fit.\n"
             _("The width (DX) within which the panel must fit.\n"
-            "In current units.")
+              "In current units.")
         )
         )
         form_layout.addRow(self.x_width_lbl, self.x_width_entry)
         form_layout.addRow(self.x_width_lbl, self.x_width_entry)
 
 
@@ -177,14 +212,14 @@ class Panelize(FlatCAMTool):
         self.y_height_lbl = QtWidgets.QLabel(_("Height (DY):"))
         self.y_height_lbl = QtWidgets.QLabel(_("Height (DY):"))
         self.y_height_lbl.setToolTip(
         self.y_height_lbl.setToolTip(
             _("The height (DY)within which the panel must fit.\n"
             _("The height (DY)within which the panel must fit.\n"
-            "In current units.")
+              "In current units.")
         )
         )
         form_layout.addRow(self.y_height_lbl, self.y_height_entry)
         form_layout.addRow(self.y_height_lbl, self.y_height_entry)
 
 
         self.constrain_sel = OptionalInputSection(
         self.constrain_sel = OptionalInputSection(
             self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry])
             self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry])
 
 
-        ## Buttons
+        # Buttons
         hlay_2 = QtWidgets.QHBoxLayout()
         hlay_2 = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay_2)
         self.layout.addLayout(hlay_2)
 
 
@@ -192,14 +227,15 @@ class Panelize(FlatCAMTool):
         self.panelize_object_button = QtWidgets.QPushButton(_("Panelize Object"))
         self.panelize_object_button = QtWidgets.QPushButton(_("Panelize Object"))
         self.panelize_object_button.setToolTip(
         self.panelize_object_button.setToolTip(
             _("Panelize the specified object around the specified box.\n"
             _("Panelize the specified object around the specified box.\n"
-            "In other words it creates multiple copies of the source object,\n"
-            "arranged in a 2D array of rows and columns.")
+              "In other words it creates multiple copies of the source object,\n"
+              "arranged in a 2D array of rows and columns.")
         )
         )
         hlay_2.addWidget(self.panelize_object_button)
         hlay_2.addWidget(self.panelize_object_button)
 
 
         self.layout.addStretch()
         self.layout.addStretch()
 
 
-        ## Signals
+        # Signals
+        self.reference_radio.activated_custom.connect(self.on_reference_radio_changed)
         self.panelize_object_button.clicked.connect(self.on_panelize)
         self.panelize_object_button.clicked.connect(self.on_panelize)
         self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
         self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
         self.type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
         self.type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
@@ -241,6 +277,8 @@ class Panelize(FlatCAMTool):
     def set_tool_ui(self):
     def set_tool_ui(self):
         self.reset_fields()
         self.reset_fields()
 
 
+        self.reference_radio.set_value('bbox')
+
         sp_c = self.app.defaults["tools_panelize_spacing_columns"] if \
         sp_c = self.app.defaults["tools_panelize_spacing_columns"] if \
             self.app.defaults["tools_panelize_spacing_columns"] else 0.0
             self.app.defaults["tools_panelize_spacing_columns"] else 0.0
         self.spacing_columns.set_value(float(sp_c))
         self.spacing_columns.set_value(float(sp_c))
@@ -278,11 +316,32 @@ class Panelize(FlatCAMTool):
         self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
         self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
         self.object_combo.setCurrentIndex(0)
         self.object_combo.setCurrentIndex(0)
 
 
+        # hide the panel type for Excellons, the panel can be only of type Geometry
+        if self.type_obj_combo.currentText() != 'Excellon':
+            self.panel_type_label.setDisabled(False)
+            self.panel_type_radio.setDisabled(False)
+        else:
+            self.panel_type_label.setDisabled(True)
+            self.panel_type_radio.setDisabled(True)
+            self.panel_type_radio.set_value('geometry')
+
     def on_type_box_index_changed(self):
     def on_type_box_index_changed(self):
         obj_type = self.type_box_combo.currentIndex()
         obj_type = self.type_box_combo.currentIndex()
         self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
         self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
         self.box_combo.setCurrentIndex(0)
         self.box_combo.setCurrentIndex(0)
 
 
+    def on_reference_radio_changed(self, current_val):
+        if current_val == 'object':
+            self.type_box_combo.setDisabled(False)
+            self.type_box_combo_label.setDisabled(False)
+            self.box_combo.setDisabled(False)
+            self.box_combo_label.setDisabled(False)
+        else:
+            self.type_box_combo.setDisabled(True)
+            self.type_box_combo_label.setDisabled(True)
+            self.box_combo.setDisabled(True)
+            self.box_combo_label.setDisabled(True)
+
     def on_panelize(self):
     def on_panelize(self):
         name = self.object_combo.currentText()
         name = self.object_combo.currentText()
 
 
@@ -308,7 +367,10 @@ class Panelize(FlatCAMTool):
             return "Could not retrieve object: %s" % boxname
             return "Could not retrieve object: %s" % boxname
 
 
         if box is None:
         if box is None:
-            self.app.inform.emit(_("[WARNING]No object Box. Using instead %s") % panel_obj)
+            self.app.inform.emit(_("[WARNING_NOTCL]No object Box. Using instead %s") % panel_obj)
+            self.reference_radio.set_value('bbox')
+
+        if self.reference_radio.get_value() == 'bbox':
             box = panel_obj
             box = panel_obj
 
 
         self.outname = name + '_panelized'
         self.outname = name + '_panelized'
@@ -387,7 +449,6 @@ class Panelize(FlatCAMTool):
 
 
         panel_type = str(self.panel_type_radio.get_value())
         panel_type = str(self.panel_type_radio.get_value())
 
 
-
         if 0 in {columns, rows}:
         if 0 in {columns, rows}:
             self.app.inform.emit(_("[ERROR_NOTCL] Columns or Rows are zero value. Change them to a positive integer."))
             self.app.inform.emit(_("[ERROR_NOTCL] Columns or Rows are zero value. Change them to a positive integer."))
             return "Columns or Rows are zero value. Change them to a positive integer."
             return "Columns or Rows are zero value. Change them to a positive integer."
@@ -471,7 +532,11 @@ class Panelize(FlatCAMTool):
                         if type(geom) == list:
                         if type(geom) == list:
                             geoms = list()
                             geoms = list()
                             for local_geom in geom:
                             for local_geom in geom:
-                                geoms.append(translate_recursion(local_geom))
+                                res_geo = translate_recursion(local_geom)
+                                try:
+                                    geoms += (res_geo)
+                                except TypeError:
+                                    geoms.append(res_geo)
                             return geoms
                             return geoms
                         else:
                         else:
                             return affinity.translate(geom, xoff=currentx, yoff=currenty)
                             return affinity.translate(geom, xoff=currentx, yoff=currenty)
@@ -485,6 +550,16 @@ class Panelize(FlatCAMTool):
                             for tool in panel_obj.tools:
                             for tool in panel_obj.tools:
                                 obj_fin.tools[tool]['solid_geometry'][:] = []
                                 obj_fin.tools[tool]['solid_geometry'][:] = []
 
 
+                    if isinstance(panel_obj, FlatCAMGerber):
+                        obj_fin.apertures = deepcopy(panel_obj.apertures)
+                        for ap in obj_fin.apertures:
+                            if 'solid_geometry' in obj_fin.apertures[ap]:
+                                obj_fin.apertures[ap]['solid_geometry'] = []
+                            if 'clear_geometry' in obj_fin.apertures[ap]:
+                                obj_fin.apertures[ap]['clear_geometry'] = []
+                            if 'follow_geometry' in obj_fin.apertures[ap]:
+                                obj_fin.apertures[ap]['follow_geometry'] = []
+
                     self.app.progress.emit(0)
                     self.app.progress.emit(0)
                     for row in range(rows):
                     for row in range(rows):
                         currentx = 0.0
                         currentx = 0.0
@@ -493,21 +568,54 @@ class Panelize(FlatCAMTool):
                             if isinstance(panel_obj, FlatCAMGeometry):
                             if isinstance(panel_obj, FlatCAMGeometry):
                                 if panel_obj.multigeo is True:
                                 if panel_obj.multigeo is True:
                                     for tool in panel_obj.tools:
                                     for tool in panel_obj.tools:
-                                        obj_fin.tools[tool]['solid_geometry'].append(translate_recursion(
-                                            panel_obj.tools[tool]['solid_geometry'])
-                                        )
+                                        geo = translate_recursion(panel_obj.tools[tool]['solid_geometry'])
+                                        if isinstance(geo, list):
+                                            obj_fin.tools[tool]['solid_geometry'] += geo
+                                        else:
+                                            obj_fin.tools[tool]['solid_geometry'].append(geo)
                                 else:
                                 else:
-                                    obj_fin.solid_geometry.append(
-                                        translate_recursion(panel_obj.solid_geometry)
-                                    )
+                                    geo = translate_recursion(panel_obj.solid_geometry)
+                                    if isinstance(geo, list):
+                                        obj_fin.solid_geometry += geo
+                                    else:
+                                        obj_fin.solid_geometry.append(geo)
                             else:
                             else:
-                                obj_fin.solid_geometry.append(
-                                    translate_recursion(panel_obj.solid_geometry)
-                                )
+                                geo = translate_recursion(panel_obj.solid_geometry)
+                                if isinstance(geo, list):
+                                    obj_fin.solid_geometry += geo
+                                else:
+                                    obj_fin.solid_geometry.append(geo)
+
+                                for apid in panel_obj.apertures:
+                                    if 'solid_geometry' in panel_obj.apertures[apid]:
+                                        geo_aper = translate_recursion(panel_obj.apertures[apid]['solid_geometry'])
+                                        if isinstance(geo_aper, list):
+                                            obj_fin.apertures[apid]['solid_geometry'] += geo_aper
+                                        else:
+                                            obj_fin.apertures[apid]['solid_geometry'].append(geo_aper)
+
+                                    if 'clear_geometry' in panel_obj.apertures[apid]:
+                                        geo_aper = translate_recursion(panel_obj.apertures[apid]['clear_geometry'])
+                                        if isinstance(geo_aper, list):
+                                            obj_fin.apertures[apid]['clear_geometry'] += geo_aper
+                                        else:
+                                            obj_fin.apertures[apid]['clear_geometry'].append(geo_aper)
+
+                                    if 'follow_geometry' in panel_obj.apertures[apid]:
+                                        geo_aper = translate_recursion(panel_obj.apertures[apid]['follow_geometry'])
+                                        if isinstance(geo_aper, list):
+                                            obj_fin.apertures[apid]['follow_geometry'] += geo_aper
+                                        else:
+                                            obj_fin.apertures[apid]['follow_geometry'].append(geo_aper)
 
 
                             currentx += lenghtx
                             currentx += lenghtx
                         currenty += lenghty
                         currenty += lenghty
 
 
+                    app_obj.log.debug("Found %s geometries. Creating a panel geometry cascaded union ..." %
+                                      len(obj_fin.solid_geometry))
+                    obj_fin.solid_geometry = cascaded_union(obj_fin.solid_geometry)
+                    app_obj.log.debug("Finished creating a cascaded union for the panel.")
+
                 if isinstance(panel_obj, FlatCAMExcellon):
                 if isinstance(panel_obj, FlatCAMExcellon):
                     self.app.progress.emit(50)
                     self.app.progress.emit(50)
                     self.app.new_object("excellon", self.outname, job_init_excellon, plot=True, autoselected=True)
                     self.app.new_object("excellon", self.outname, job_init_excellon, plot=True, autoselected=True)
@@ -520,7 +628,8 @@ class Panelize(FlatCAMTool):
             self.app.inform.emit(_("[success] Panel done..."))
             self.app.inform.emit(_("[success] Panel done..."))
         else:
         else:
             self.constrain_flag = False
             self.constrain_flag = False
-            self.app.inform.emit(_("[WARNING] Too big for the constrain area. Final panel has {col} columns and {row} rows").format(
+            self.app.inform.emit(_("[WARNING] Too big for the constrain area. "
+                                   "Final panel has {col} columns and {row} rows").format(
                 col=columns, row=rows))
                 col=columns, row=rows))
 
 
         proc = self.app.proc_container.new(_("Generating panel ... Please wait."))
         proc = self.app.proc_container.new(_("Generating panel ... Please wait."))

+ 40 - 54
flatcamTools/ToolTransform.py

@@ -658,19 +658,18 @@ class ToolTransform(FlatCAMTool):
 
 
                     self.app.progress.emit(20)
                     self.app.progress.emit(20)
 
 
+                    px = 0.5 * (xminimal + xmaximal)
+                    py = 0.5 * (yminimal + ymaximal)
                     for sel_obj in obj_list:
                     for sel_obj in obj_list:
-                        px = 0.5 * (xminimal + xmaximal)
-                        py = 0.5 * (yminimal + ymaximal)
                         if isinstance(sel_obj, FlatCAMCNCjob):
                         if isinstance(sel_obj, FlatCAMCNCjob):
                             self.app.inform.emit(_("CNCJob objects can't be rotated."))
                             self.app.inform.emit(_("CNCJob objects can't be rotated."))
                         else:
                         else:
                             sel_obj.rotate(-num, point=(px, py))
                             sel_obj.rotate(-num, point=(px, py))
-                            sel_obj.plot()
                             self.app.object_changed.emit(sel_obj)
                             self.app.object_changed.emit(sel_obj)
 
 
                         # add information to the object that it was changed and how much
                         # add information to the object that it was changed and how much
                         sel_obj.options['rotate'] = num
                         sel_obj.options['rotate'] = num
-
+                        sel_obj.plot()
                     self.app.inform.emit(_('[success] Rotate done ...'))
                     self.app.inform.emit(_('[success] Rotate done ...'))
                     self.app.progress.emit(100)
                     self.app.progress.emit(100)
 
 
@@ -719,31 +718,30 @@ class ToolTransform(FlatCAMTool):
                     self.app.progress.emit(20)
                     self.app.progress.emit(20)
 
 
                     # execute mirroring
                     # execute mirroring
-                    for obj in obj_list:
-                        if isinstance(obj, FlatCAMCNCjob):
+                    for sel_obj in obj_list:
+                        if isinstance(sel_obj, FlatCAMCNCjob):
                             self.app.inform.emit(_("CNCJob objects can't be mirrored/flipped."))
                             self.app.inform.emit(_("CNCJob objects can't be mirrored/flipped."))
                         else:
                         else:
                             if axis is 'X':
                             if axis is 'X':
-                                obj.mirror('X', (px, py))
+                                sel_obj.mirror('X', (px, py))
                                 # add information to the object that it was changed and how much
                                 # add information to the object that it was changed and how much
                                 # the axis is reversed because of the reference
                                 # the axis is reversed because of the reference
-                                if 'mirror_y' in obj.options:
-                                    obj.options['mirror_y'] = not obj.options['mirror_y']
+                                if 'mirror_y' in sel_obj.options:
+                                    sel_obj.options['mirror_y'] = not sel_obj.options['mirror_y']
                                 else:
                                 else:
-                                    obj.options['mirror_y'] = True
-                                obj.plot()
+                                    sel_obj.options['mirror_y'] = True
                                 self.app.inform.emit(_('[success] Flip on the Y axis done ...'))
                                 self.app.inform.emit(_('[success] Flip on the Y axis done ...'))
                             elif axis is 'Y':
                             elif axis is 'Y':
-                                obj.mirror('Y', (px, py))
+                                sel_obj.mirror('Y', (px, py))
                                 # add information to the object that it was changed and how much
                                 # add information to the object that it was changed and how much
                                 # the axis is reversed because of the reference
                                 # the axis is reversed because of the reference
-                                if 'mirror_x' in obj.options:
-                                    obj.options['mirror_x'] = not obj.options['mirror_x']
+                                if 'mirror_x' in sel_obj.options:
+                                    sel_obj.options['mirror_x'] = not sel_obj.options['mirror_x']
                                 else:
                                 else:
-                                    obj.options['mirror_x'] = True
-                                obj.plot()
+                                    sel_obj.options['mirror_x'] = True
                                 self.app.inform.emit(_('[success] Flip on the X axis done ...'))
                                 self.app.inform.emit(_('[success] Flip on the X axis done ...'))
-                            self.app.object_changed.emit(obj)
+                            self.app.object_changed.emit(sel_obj)
+                        sel_obj.plot()
                     self.app.progress.emit(100)
                     self.app.progress.emit(100)
 
 
                 except Exception as e:
                 except Exception as e:
@@ -776,20 +774,20 @@ class ToolTransform(FlatCAMTool):
 
 
                     self.app.progress.emit(20)
                     self.app.progress.emit(20)
 
 
-                    for obj in obj_list:
-                        if isinstance(obj, FlatCAMCNCjob):
+                    for sel_obj in obj_list:
+                        if isinstance(sel_obj, FlatCAMCNCjob):
                             self.app.inform.emit(_("CNCJob objects can't be skewed."))
                             self.app.inform.emit(_("CNCJob objects can't be skewed."))
                         else:
                         else:
                             if axis is 'X':
                             if axis is 'X':
-                                obj.skew(num, 0, point=(xminimal, yminimal))
+                                sel_obj.skew(num, 0, point=(xminimal, yminimal))
                                 # add information to the object that it was changed and how much
                                 # add information to the object that it was changed and how much
-                                obj.options['skew_x'] = num
+                                sel_obj.options['skew_x'] = num
                             elif axis is 'Y':
                             elif axis is 'Y':
-                                obj.skew(0, num, point=(xminimal, yminimal))
+                                sel_obj.skew(0, num, point=(xminimal, yminimal))
                                 # add information to the object that it was changed and how much
                                 # add information to the object that it was changed and how much
-                                obj.options['skew_y'] = num
-                            obj.plot()
-                            self.app.object_changed.emit(obj)
+                                sel_obj.options['skew_y'] = num
+                            self.app.object_changed.emit(sel_obj)
+                        sel_obj.plot()
                     self.app.inform.emit(_('[success] Skew on the %s axis done ...') % str(axis))
                     self.app.inform.emit(_('[success] Skew on the %s axis done ...') % str(axis))
                     self.app.progress.emit(100)
                     self.app.progress.emit(100)
 
 
@@ -836,16 +834,17 @@ class ToolTransform(FlatCAMTool):
                         px = 0
                         px = 0
                         py = 0
                         py = 0
 
 
-                    for obj in obj_list:
-                        if isinstance(obj, FlatCAMCNCjob):
+                    for sel_obj in obj_list:
+                        if isinstance(sel_obj, FlatCAMCNCjob):
                             self.app.inform.emit(_("CNCJob objects can't be scaled."))
                             self.app.inform.emit(_("CNCJob objects can't be scaled."))
                         else:
                         else:
-                            obj.scale(xfactor, yfactor, point=(px, py))
+                            sel_obj.scale(xfactor, yfactor, point=(px, py))
                             # add information to the object that it was changed and how much
                             # add information to the object that it was changed and how much
-                            obj.options['scale_x'] = xfactor
-                            obj.options['scale_y'] = yfactor
-                            obj.plot()
-                            self.app.object_changed.emit(obj)
+                            sel_obj.options['scale_x'] = xfactor
+                            sel_obj.options['scale_y'] = yfactor
+                            self.app.object_changed.emit(sel_obj)
+                        sel_obj.plot()
+
                     self.app.inform.emit(_('[success] Scale on the %s axis done ...') % str(axis))
                     self.app.inform.emit(_('[success] Scale on the %s axis done ...') % str(axis))
                     self.app.progress.emit(100)
                     self.app.progress.emit(100)
                 except Exception as e:
                 except Exception as e:
@@ -854,8 +853,6 @@ class ToolTransform(FlatCAMTool):
 
 
     def on_offset(self, axis, num):
     def on_offset(self, axis, num):
         obj_list = self.app.collection.get_selected()
         obj_list = self.app.collection.get_selected()
-        xminlist = []
-        yminlist = []
 
 
         if not obj_list:
         if not obj_list:
             self.app.inform.emit(_("[WARNING_NOTCL] No object selected. Please Select an object to offset!"))
             self.app.inform.emit(_("[WARNING_NOTCL] No object selected. Please Select an object to offset!"))
@@ -863,34 +860,23 @@ class ToolTransform(FlatCAMTool):
         else:
         else:
             with self.app.proc_container.new(_("Applying Offset")):
             with self.app.proc_container.new(_("Applying Offset")):
                 try:
                 try:
-                    # first get a bounding box to fit all
-                    for obj in obj_list:
-                        if isinstance(obj, FlatCAMCNCjob):
-                            pass
-                        else:
-                            xmin, ymin, xmax, ymax = obj.bounds()
-                            xminlist.append(xmin)
-                            yminlist.append(ymin)
-
-                    # get the minimum x,y and maximum x,y for all objects selected
-                    xminimal = min(xminlist)
-                    yminimal = min(yminlist)
                     self.app.progress.emit(20)
                     self.app.progress.emit(20)
 
 
-                    for obj in obj_list:
-                        if isinstance(obj, FlatCAMCNCjob):
+                    for sel_obj in obj_list:
+                        if isinstance(sel_obj, FlatCAMCNCjob):
                             self.app.inform.emit(_("CNCJob objects can't be offseted."))
                             self.app.inform.emit(_("CNCJob objects can't be offseted."))
                         else:
                         else:
                             if axis is 'X':
                             if axis is 'X':
-                                obj.offset((num, 0))
+                                sel_obj.offset((num, 0))
                                 # add information to the object that it was changed and how much
                                 # add information to the object that it was changed and how much
-                                obj.options['offset_x'] = num
+                                sel_obj.options['offset_x'] = num
                             elif axis is 'Y':
                             elif axis is 'Y':
-                                obj.offset((0, num))
+                                sel_obj.offset((0, num))
                                 # add information to the object that it was changed and how much
                                 # add information to the object that it was changed and how much
-                                obj.options['offset_y'] = num
-                            obj.plot()
-                            self.app.object_changed.emit(obj)
+                                sel_obj.options['offset_y'] = num
+                            self.app.object_changed.emit(sel_obj)
+                        sel_obj.plot()
+
                     self.app.inform.emit(_('[success] Offset on the %s axis done ...') % str(axis))
                     self.app.inform.emit(_('[success] Offset on the %s axis done ...') % str(axis))
                     self.app.progress.emit(100)
                     self.app.progress.emit(100)