فهرست منبع

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

Beta 8.916
Marius Stanciu 6 سال پیش
والد
کامیت
cd1da70147

+ 289 - 29
FlatCAMApp.py

@@ -94,8 +94,8 @@ class App(QtCore.QObject):
     log.addHandler(handler)
 
     # Version
-    version = 8.915
-    version_date = "2019/05/1"
+    version = 8.916
+    version_date = "2019/05/10"
     beta = True
 
     # 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_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_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_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_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_plot": self.ui.excellon_defaults_form.excellon_gen_group.plot_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_toggle_tooltips": True,
             "global_worker_number": 2,
+            "global_tolerance": 0.01,
             "global_compression_level": 3,
             "global_save_compressed": True,
 
@@ -680,6 +688,12 @@ class App(QtCore.QObject):
             "gerber_aperture_buffer_factor": 0.0,
             "gerber_follow": False,
 
+            # Gerber Export
+            "gerber_exp_units": 'IN',
+            "gerber_exp_integer": 2,
+            "gerber_exp_decimals": 4,
+            "gerber_exp_zeros": 'L',
+
             # Excellon General
             "excellon_plot": True,
             "excellon_solid": True,
@@ -700,7 +714,7 @@ class App(QtCore.QObject):
             "excellon_dwell": False,
             "excellon_dwelltime": 1,
             "excellon_toolchange": False,
-            "excellon_toolchangez": 1.0,
+            "excellon_toolchangez": 0.5,
             "excellon_ppname_e": 'default',
             "excellon_tooldia": 0.016,
             "excellon_slot_tooldia": 0.016,
@@ -710,7 +724,7 @@ class App(QtCore.QObject):
             "excellon_offset": 0.0,
             "excellon_toolchangexy": "0.0, 0.0",
             "excellon_startz": None,
-            "excellon_endz": 2.0,
+            "excellon_endz": 0.5,
             "excellon_feedrate_rapid": 3.0,
             "excellon_z_pdepth": -0.02,
             "excellon_feedrate_probe": 3.0,
@@ -735,7 +749,7 @@ class App(QtCore.QObject):
             "geometry_depthperpass": 0.002,
             "geometry_travelz": 0.1,
             "geometry_toolchange": False,
-            "geometry_toolchangez": 1.0,
+            "geometry_toolchangez": 0.5,
             "geometry_feedrate": 3.0,
             "geometry_feedrate_z": 3.0,
             "geometry_spindlespeed": None,
@@ -746,7 +760,7 @@ class App(QtCore.QObject):
             # Geometry Advanced Options
             "geometry_toolchangexy": "0.0, 0.0",
             "geometry_startz": None,
-            "geometry_endz": 2.0,
+            "geometry_endz": 0.5,
             "geometry_feedrate_rapid": 3.0,
             "geometry_extracut": False,
             "geometry_z_pdepth": -0.02,
@@ -1297,7 +1311,7 @@ class App(QtCore.QObject):
         self.ui.menufileexportsvg.triggered.connect(self.on_file_exportsvg)
         self.ui.menufileexportpng.triggered.connect(self.on_file_exportpng)
         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)
 
@@ -1320,7 +1334,9 @@ class App(QtCore.QObject):
         self.ui.menueditdelete.triggered.connect(self.on_delete)
 
         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.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(
             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 ###
@@ -2169,6 +2186,9 @@ class App(QtCore.QObject):
             # set call source to the Editor we go into
             self.call_source = 'exc_editor'
 
+            if self.ui.splitter.sizes()[0] == 0:
+                self.ui.splitter.setSizes([1, 1])
+
         elif isinstance(edited_object, FlatCAMGerber):
             # 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
@@ -2177,6 +2197,9 @@ class App(QtCore.QObject):
             # set call source to the Editor we go into
             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:
         # self.collection.view.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
         self.ui.project_frame.setDisabled(True)
@@ -2249,7 +2272,7 @@ class App(QtCore.QObject):
                         self.grb_editor.deactivate_grb_editor()
 
                         # 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']
                             self.collection.set_active(old_name)
                             self.collection.delete_active()
@@ -3513,7 +3536,7 @@ class App(QtCore.QObject):
     def set_screen_units(self, units):
         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.
         Changes the application's default units or the current project's units.
@@ -3651,13 +3674,14 @@ class App(QtCore.QObject):
         response = msgbox.clickedButton()
 
         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
 
@@ -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_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():
-                units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
                 obj.convert_units(units)
 
                 # make that the properties stored in the object are also updated
@@ -3684,9 +3708,9 @@ class App(QtCore.QObject):
                     current.to_form()
 
             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.set_screen_units(self.defaults["units"])
+            self.set_screen_units(units)
         else:
             # Undo toggling
             self.toggle_units_ignore = True
@@ -3701,11 +3725,14 @@ class App(QtCore.QObject):
         self.defaults_read_form()
 
     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")
         else:
             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):
         self.report_usage("on_fullscreen()")
@@ -4552,6 +4579,13 @@ class App(QtCore.QObject):
                 self.report_usage("on_delete")
 
                 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.inform.emit(_("Object(s) deleted ..."))
@@ -4713,8 +4747,8 @@ class App(QtCore.QObject):
                 except:
                     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):
             obj_init.solid_geometry = obj.solid_geometry
@@ -4727,8 +4761,11 @@ class App(QtCore.QObject):
             except:
                 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):
             # objs = self.collection.get_selected()
@@ -4739,15 +4776,82 @@ class App(QtCore.QObject):
                     solid_geo.append(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():
 
             obj_name = obj.options["name"]
 
             try:
                 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:
-                    self.new_object("geometry", str(obj_name) + "_gcopy", initialize)
+                    self.new_object("gerber", str(obj_name) + "_conv", initialize)
 
             except Exception as e:
                 return "Operation failed: %s" % str(e)
@@ -4776,6 +4880,7 @@ class App(QtCore.QObject):
                 obj.options['ymax'] = d
             # self.plot_all(zoom=False)
             self.inform.emit(_('[success] Origin set ...'))
+            self.plotcanvas.fit_view()
             self.plotcanvas.vis_disconnect('mouse_press', self.on_set_zero_click)
             self.should_we_save = True
 
@@ -6079,7 +6184,7 @@ class App(QtCore.QObject):
 
     def on_file_exportexcellon(self):
         """
-        Callback for menu item File->Export SVG.
+        Callback for menu item File->Export->Excellon.
 
         :return: None
         """
@@ -6116,6 +6221,45 @@ class App(QtCore.QObject):
             self.export_excellon(name, 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):
         """
                 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.'))
                 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):
         """
         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 = 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.muted_ui = 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.notHovering = True
@@ -321,11 +326,11 @@ class FlatCAMObj(QtCore.QObject):
             key = self.shapes.add(tolerance=self.drawing_tolerance, **kwargs)
         return key
 
-    def add_mark_shape(self, **kwargs):
+    def add_mark_shape(self, apid, **kwargs):
         if self.deleted:
             raise ObjectDeleted()
         else:
-            key = self.mark_shapes.add(tolerance=self.drawing_tolerance, **kwargs)
+            key = self.mark_shapes[apid].add(tolerance=self.drawing_tolerance, **kwargs)
         return key
 
     @property
@@ -555,6 +560,10 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             ))
             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
         self.on_aperture_table_visibility_change()
 
@@ -576,12 +585,13 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             sort.append(int(k))
         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)
 
         for ap_code in sorted_apertures:
@@ -639,28 +649,28 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
             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.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
         if self.marked_rows:
             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()
 
@@ -999,6 +1012,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
     def on_aperture_table_visibility_change(self):
         if self.ui.aperture_table_visibility_cb.isChecked():
             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.setChecked(False)
@@ -1012,6 +1027,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                 self.ui.apertures_table.cellWidget(row, 5).set_value(False)
             self.clear_plot_apertures()
 
+            for ap in self.mark_shapes:
+                self.mark_shapes[ap].enabled = False
+
     def convert_units(self, units):
         """
         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)
 
     # 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
         :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
         # if the 'ptint' option is set to False.
@@ -1135,7 +1153,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
             def job_thread(app_obj):
                 self.app.progress.emit(30)
-
                 try:
                     if aperture_to_plot_mark in self.apertures:
                         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']]
                         for geo in self.apertures[aperture_to_plot_mark]['solid_geometry']:
                             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:
                                 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)
 
                 except (ObjectDeleted, AttributeError):
@@ -1156,34 +1175,47 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
             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):
         self.ui.mark_all_cb.set_value(False)
         self.marked_rows[:] = []
 
     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()
-        # 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[:] = []
+        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
         # 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:
             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:
             self.clear_plot_apertures()
 
         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):
         Gerber.mirror(self, axis=axis, point=point)
         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
 
 - 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 Gerber Aperture Table dimensions
 - 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
 - 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.
@@ -635,7 +685,7 @@ CAD program, and create G-Code for Isolation routing.
 - finished Gerber aperture table display
 - 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'
-- added (and commented) an experimental FlatCAMObj.FlatCAMGerber.plot_apertures()
+- added (and commented) an experimental FlatCAMObj.FlatCAMGerber.plot_aperture()
 
 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'].append(flash)
                                 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:
                                         self.apertures[current_aperture]['solid_geometry'].append(flash)
                                     except KeyError:
@@ -2478,17 +2486,18 @@ class Gerber (Geometry):
                             # do nothing because 'R' type moving aperture is none at once
                             pass
                         else:
-                            # --- Buffered ----
-                            width = self.apertures[last_path_aperture]["size"]
+
                             geo = LineString(path)
                             if not geo.is_empty:
                                 follow_buffer.append(geo)
                                 try:
-                                    self.apertures[current_aperture]['follow_geometry'].append(geo)
+                                    self.apertures[last_path_aperture]['follow_geometry'].append(geo)
                                 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))
                             if not geo.is_empty:
                                 poly_buffer.append(geo)
@@ -2692,11 +2701,10 @@ class Gerber (Geometry):
                     # Pen down: add segment
                     if current_operation_code == 1:
                         # 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)
-                            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 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
@@ -2791,9 +2799,9 @@ class Gerber (Geometry):
                                         self.apertures['0']['size'] = 0.0
                                         self.apertures['0']['solid_geometry'] = []
                                     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:
                                     geo = Polygon(path)
@@ -2862,17 +2870,18 @@ class Gerber (Geometry):
                                     if self.apertures[last_path_aperture]["type"] != 'R':
                                         follow_buffer.append(geo)
                                         try:
-                                            self.apertures[current_aperture]['follow_geometry'].append(geo)
+                                            self.apertures[last_path_aperture]['follow_geometry'].append(geo)
                                         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)
                                     try:
-                                        self.apertures[current_aperture]['follow_geometry'].append(geo)
+                                        self.apertures[last_path_aperture]['follow_geometry'].append(geo)
                                     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
                             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
 
-            # 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:
+                # 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]:
-                    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']:
-                        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)
+
                     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():
                     if k == 'size' or k == 'width' or k == 'height':
                         self.apertures[apid][k] = v * conversion_factor
+            # -------------------------------------------------------------
 
             # --- Apply buffer ---
             # this treats the case when we are storing geometry as paths
@@ -3228,7 +3258,7 @@ class Gerber (Geometry):
 
             if len(poly_buffer) == 0:
                 log.error("Object is not Gerber file or empty. Aborting Object creation.")
-                return
+                return 'fail'
 
             if self.use_buffer_for_union:
                 log.debug("Union by buffer...")
@@ -3467,9 +3497,15 @@ class Gerber (Geometry):
         # we need to scale the geometry stored in the Gerber apertures, too
         try:
             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:
-            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."))
 
@@ -3526,7 +3562,14 @@ class Gerber (Geometry):
             for apid in self.apertures:
                 self.apertures[apid]['solid_geometry'] = offset_geom(self.apertures[apid]['solid_geometry'])
         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."))
 
@@ -3572,7 +3615,14 @@ class Gerber (Geometry):
             for apid in self.apertures:
                 self.apertures[apid]['solid_geometry'] = mirror_geom(self.apertures[apid]['solid_geometry'])
         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.
         # self.solid_geometry = affinity.scale(self.solid_geometry,
@@ -3612,7 +3662,15 @@ class Gerber (Geometry):
             for apid in self.apertures:
                 self.apertures[apid]['solid_geometry'] = skew_geom(self.apertures[apid]['solid_geometry'])
         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))
 
     def rotate(self, angle, point):
@@ -3642,7 +3700,14 @@ class Gerber (Geometry):
             for apid in self.apertures:
                 self.apertures[apid]['solid_geometry'] = rotate_geom(self.apertures[apid]['solid_geometry'])
         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))
 
 
@@ -7204,13 +7269,13 @@ class CNCjob(Geometry):
 
         self.create_geometry()
 
+
 def get_bounds(geometry_list):
     xmin = Inf
     ymin = Inf
     xmax = -Inf
     ymax = -Inf
 
-    #print "Getting bounds of:", str(geometry_set)
     for gs in geometry_list:
         try:
             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
 
             # 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()
         else:

+ 88 - 39
flatcamEditors/FlatCAMGeoEditor.py

@@ -18,6 +18,7 @@ from FlatCAMTool import FlatCAMTool
 from flatcamGUI.ObjectUI import LengthEntry, RadioSet
 
 from shapely.geometry import LineString, LinearRing, MultiLineString
+# from shapely.geometry import mapping
 from shapely.ops import cascaded_union, unary_union
 import shapely.affinity as affinity
 
@@ -29,6 +30,7 @@ from flatcamGUI.GUIElements import OptionalInputSection, FCCheckBox, FCEntry, FC
     FCTable, FCDoubleSpinner, FCButton, EvalEntry2, FCInputDialog
 from flatcamParsers.ParseFont import *
 
+# from vispy.io import read_png
 import gettext
 import FlatCAMTranslation as fcTranslate
 
@@ -1861,7 +1863,6 @@ class DrawTool(object):
     def __init__(self, draw_app):
         self.draw_app = draw_app
         self.complete = False
-        self.start_msg = "Click on 1st point..."
         self.points = []
         self.geometry = None  # DrawToolShape or None
 
@@ -1939,7 +1940,6 @@ class FCCircle(FCShapeTool):
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_circle_geo.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
 
-        self.start_msg = _("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"]
 
@@ -1991,7 +1991,6 @@ class FCArc(FCShapeTool):
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_arc.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
 
-        self.start_msg = _("Click on Center point ...")
         self.draw_app.app.inform.emit(_("Click on Center point ..."))
 
         # Direction of rotation between point 1 and 2.
@@ -2210,12 +2209,13 @@ class FCRectangle(FCShapeTool):
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero.png'))
         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):
         self.points.append(point)
 
         if len(self.points) == 1:
+            self.draw_app.app.inform.emit(_("Click on opposite corner to complete ..."))
             return "Click on opposite corner to complete ..."
 
         if len(self.points) == 2:
@@ -2262,14 +2262,14 @@ class FCPolygon(FCShapeTool):
         self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero.png'))
         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):
         self.draw_app.in_action = True
         self.points.append(point)
 
         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 ""
@@ -2445,21 +2445,33 @@ class FCMove(FCShapeTool):
         FCShapeTool.__init__(self, draw_app)
         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.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):
-        self.draw_app.app.inform.emit(_("Click on destination point."))
+        self.draw_app.app.inform.emit(_(" Click on destination point ..."))
         self.origin = origin
 
     def click(self, point):
         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:
             self.set_origin(point)
@@ -2517,6 +2529,58 @@ class FCMove(FCShapeTool):
         # return DrawToolUtilityShape([affinity.translate(geom.geo, xoff=dx, yoff=dy)
         #                              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):
     def __init__(self, draw_app):
@@ -2550,7 +2614,7 @@ class FCText(FCShapeTool):
         self.draw_app = draw_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.text_gui = TextInputTool(self.app)
@@ -2602,7 +2666,7 @@ class FCBuffer(FCShapeTool):
         self.draw_app = draw_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.buff_tool = BufferSelectionTool(self.app, self.draw_app)
         self.buff_tool.run()
@@ -2720,7 +2784,7 @@ class FCPaint(FCShapeTool):
         self.draw_app = draw_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.draw_app.paint_tool.run()
 
@@ -2734,7 +2798,7 @@ class FCTransform(FCShapeTool):
         self.draw_app = draw_app
         self.app = draw_app.app
 
-        self.start_msg = _("Shape transformations ...")
+        self.draw_app.app.infrom.emit(_("Shape transformations ..."))
         self.origin = (0, 0)
         self.draw_app.transform_tool.run()
 
@@ -3159,6 +3223,9 @@ class FlatCAMGeoEditor(QtCore.QObject):
         :return: None
         """
 
+        if shape is None:
+            return
+
         # List of DrawToolShape?
         if isinstance(shape, list):
             for subshape in shape:
@@ -3280,8 +3347,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
                         self.tools[t]["button"].setChecked(False)
 
                 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:
                 self.app.log.debug("%s is NOT checked." % tool)
                 for t in self.tools:
@@ -3341,6 +3406,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
             # Selection with left mouse button
             if self.active_tool is not None and event.button is 1:
+
                 # Dispatch event to active_tool
                 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:
                     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):
                         self.select_tool("select")
                     else:
                         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):
                     # self.app.log.debug("Replotting after click.")
                     self.replot()
@@ -3616,13 +3665,13 @@ class FlatCAMGeoEditor(QtCore.QObject):
             self.selected.remove(shape)  # TODO: Check performance
 
     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.on_tool_select('move')
 
     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.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 shapely.geometry import LineString, LinearRing, MultiLineString
+# from shapely.geometry import mapping
 from shapely.ops import cascaded_union, unary_union
 import shapely.affinity as affinity
 
@@ -19,6 +20,9 @@ from FlatCAMTool import FlatCAMTool
 
 from numpy.linalg import norm as numpy_norm
 
+# from vispy.io import read_png
+# import pngcanvas
+
 import gettext
 import FlatCAMTranslation as fcTranslate
 
@@ -1449,11 +1453,18 @@ class FCApertureMove(FCShapeTool):
         self.destination = None
         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:
             self.draw_app.launched_from_shortcuts = False
             self.draw_app.app.inform.emit(_("Click on target location ..."))
         else:
             self.draw_app.app.inform.emit(_("Click on reference location ..."))
+
         self.current_storage = None
         self.geometry = []
 
@@ -1485,6 +1496,37 @@ class FCApertureMove(FCShapeTool):
             self.draw_app.select_tool("select")
             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):
         # Create new geometry
         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.current_storage.remove(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 = []
 
             for shp in sel_shapes_to_be_deleted:
                 self.draw_app.selected.remove(shp)
             sel_shapes_to_be_deleted = []
 
+        self.draw_app.plot_all()
         self.draw_app.build_ui()
         self.draw_app.app.inform.emit(_("[success] Done. Apertures Move completed."))
 
@@ -1531,8 +1574,9 @@ class FCApertureMove(FCShapeTool):
 
         dx = data[0] - self.origin[0]
         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)
 
 
@@ -1582,7 +1626,11 @@ class FCApertureSelect(DrawTool):
         # bending modes using in FCRegion and FCTrack
         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('select')
 
@@ -2297,12 +2345,13 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
         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)
 
         for ap_code in sorted_apertures:
@@ -2355,25 +2404,25 @@ class FlatCAMGrbEditor(QtCore.QObject):
                 # 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
+        # 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.resizeColumnsToContents()
@@ -2947,6 +2996,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
                                 if geo is not None:
                                     self.add_gerber_shape(DrawToolShape(geo), follow_storage_elem)
                             self.storage_dict[apid][k] = follow_storage_elem
+                        elif k == 'clear_geometry':
+                            continue
                         else:
                             self.storage_dict[apid][k] = v
                     except Exception as e:
@@ -3062,8 +3113,13 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
                     elif k == 'follow_geometry':
                         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)
                             follow_buffer.append(new_geo)
                     else:
@@ -3176,7 +3232,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.options[key] = self.sender().isChecked()
         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()")
 
         if specific_shape:
@@ -3197,8 +3253,9 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.delete_utility_geometry()
         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):
         """
@@ -3857,16 +3914,17 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
     def hide_tool(self, tool_name):
         # 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)
 
 
@@ -4847,3 +4905,22 @@ class TransformEditorTool(FlatCAMTool):
         else:
             self.app.inform.emit(
                 _("[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.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
         self.menufile.addSeparator()
@@ -265,13 +273,18 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
            _( "Will convert a Geometry object from multi_geometry type\n"
             "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)
 
         # Separator
         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
         self.menuedit.addSeparator()
         self.menueditdelete = self.menuedit.addAction(QtGui.QIcon('share/trash16.png'), _('&Delete\tDEL'))
@@ -2281,11 +2294,12 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
                 # Change Units
                 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
                 if key == QtCore.Qt.Key_R:
@@ -3131,11 +3145,17 @@ class GerberPreferencesUI(QtWidgets.QWidget):
         self.gerber_gen_group.setFixedWidth(250)
         self.gerber_opt_group = GerberOptPrefGroupUI()
         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.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_opt_group)
+        self.layout.addLayout(self.vlay)
         self.layout.addWidget(self.gerber_adv_opt_group)
 
         self.layout.addStretch()
@@ -3861,6 +3881,25 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
         )
         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
         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.toggle_tooltips_label, self.toggle_tooltips_cb)
         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)
 
@@ -4166,6 +4206,100 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
         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):
 
     def __init__(self, parent=None):

+ 64 - 0
flatcamGUI/GUIElements.py

@@ -571,11 +571,48 @@ class FCTextAreaExtended(QtWidgets.QTextEdit):
                 clip_text = clip_text.replace('\\', '/')
                 self.insertPlainText(clip_text)
 
+        if modifier & Qt.ControlModifier and key == Qt.Key_Slash:
+            self.comment()
+
         tc = self.textCursor()
         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.setCompletionMode(QCompleter.PopupCompletion)
             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:
             super(FCTextAreaExtended, self).keyPressEvent(event)
 
@@ -594,6 +631,33 @@ class FCTextAreaExtended(QtWidgets.QTextEdit):
             else:
                 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):
 

+ 1 - 1
flatcamTools/ToolMove.py

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

+ 162 - 53
flatcamTools/ToolPanelize.py

@@ -13,9 +13,9 @@ import time
 
 import gettext
 import FlatCAMTranslation as fcTranslate
+import builtins
 
 fcTranslate.apply_language('strings')
-import builtins
 if '_' not in builtins.__dict__:
     _ = gettext.gettext
 
@@ -39,11 +39,11 @@ class Panelize(FlatCAMTool):
                         """)
         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.addItem("Gerber")
         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.setToolTip(
             _("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.setModel(self.app.collection)
         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.setToolTip(
             _("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.addItem("Gerber")
         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.setToolTip(
             _("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)
 
-        ## Box
+        # Box
         self.box_combo = QtWidgets.QComboBox()
         self.box_combo.setModel(self.app.collection)
         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.setToolTip(
             _("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(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_label = QtWidgets.QLabel(_("Spacing cols:"))
         self.spacing_columns_label.setToolTip(
             _("Spacing between columns of the desired panel.\n"
-            "In current units.")
+              "In current units.")
         )
         form_layout.addRow(self.spacing_columns_label, self.spacing_columns)
 
-        ## Spacing Rows
+        # Spacing Rows
         self.spacing_rows = FCEntry()
         self.spacing_rows_label = QtWidgets.QLabel(_("Spacing rows:"))
         self.spacing_rows_label.setToolTip(
             _("Spacing between rows of the desired panel.\n"
-            "In current units.")
+              "In current units.")
         )
         form_layout.addRow(self.spacing_rows_label, self.spacing_rows)
 
-        ## Columns
+        # Columns
         self.columns = FCEntry()
         self.columns_label = QtWidgets.QLabel(_("Columns:"))
         self.columns_label.setToolTip(
@@ -134,34 +168,35 @@ class Panelize(FlatCAMTool):
         )
         form_layout.addRow(self.columns_label, self.columns)
 
-        ## Rows
+        # Rows
         self.rows = FCEntry()
         self.rows_label = QtWidgets.QLabel(_("Rows:"))
         self.rows_label.setToolTip(
             _("Number of rows of the desired panel")
         )
         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'},
-                                     {'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(
             _("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_radio)
 
-        ## Constrains
+        # Constrains
         self.constrain_cb = FCCheckBox(_("Constrain panel within:"))
         self.constrain_cb.setToolTip(
             _("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)
 
@@ -169,7 +204,7 @@ class Panelize(FlatCAMTool):
         self.x_width_lbl = QtWidgets.QLabel(_("Width (DX):"))
         self.x_width_lbl.setToolTip(
             _("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)
 
@@ -177,14 +212,14 @@ class Panelize(FlatCAMTool):
         self.y_height_lbl = QtWidgets.QLabel(_("Height (DY):"))
         self.y_height_lbl.setToolTip(
             _("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)
 
         self.constrain_sel = OptionalInputSection(
             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()
         self.layout.addLayout(hlay_2)
 
@@ -192,14 +227,15 @@ class Panelize(FlatCAMTool):
         self.panelize_object_button = QtWidgets.QPushButton(_("Panelize Object"))
         self.panelize_object_button.setToolTip(
             _("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)
 
         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.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_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):
         self.reset_fields()
 
+        self.reference_radio.set_value('bbox')
+
         sp_c = self.app.defaults["tools_panelize_spacing_columns"] if \
             self.app.defaults["tools_panelize_spacing_columns"] else 0.0
         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.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):
         obj_type = self.type_box_combo.currentIndex()
         self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
         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):
         name = self.object_combo.currentText()
 
@@ -308,7 +367,10 @@ class Panelize(FlatCAMTool):
             return "Could not retrieve object: %s" % boxname
 
         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
 
         self.outname = name + '_panelized'
@@ -387,7 +449,6 @@ class Panelize(FlatCAMTool):
 
         panel_type = str(self.panel_type_radio.get_value())
 
-
         if 0 in {columns, rows}:
             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."
@@ -471,7 +532,11 @@ class Panelize(FlatCAMTool):
                         if type(geom) == list:
                             geoms = list()
                             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
                         else:
                             return affinity.translate(geom, xoff=currentx, yoff=currenty)
@@ -485,6 +550,16 @@ class Panelize(FlatCAMTool):
                             for tool in panel_obj.tools:
                                 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)
                     for row in range(rows):
                         currentx = 0.0
@@ -493,21 +568,54 @@ class Panelize(FlatCAMTool):
                             if isinstance(panel_obj, FlatCAMGeometry):
                                 if panel_obj.multigeo is True:
                                     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:
-                                    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:
-                                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
                         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):
                     self.app.progress.emit(50)
                     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..."))
         else:
             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))
 
         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)
 
+                    px = 0.5 * (xminimal + xmaximal)
+                    py = 0.5 * (yminimal + ymaximal)
                     for sel_obj in obj_list:
-                        px = 0.5 * (xminimal + xmaximal)
-                        py = 0.5 * (yminimal + ymaximal)
                         if isinstance(sel_obj, FlatCAMCNCjob):
                             self.app.inform.emit(_("CNCJob objects can't be rotated."))
                         else:
                             sel_obj.rotate(-num, point=(px, py))
-                            sel_obj.plot()
                             self.app.object_changed.emit(sel_obj)
 
                         # add information to the object that it was changed and how much
                         sel_obj.options['rotate'] = num
-
+                        sel_obj.plot()
                     self.app.inform.emit(_('[success] Rotate done ...'))
                     self.app.progress.emit(100)
 
@@ -719,31 +718,30 @@ class ToolTransform(FlatCAMTool):
                     self.app.progress.emit(20)
 
                     # 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."))
                         else:
                             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
                                 # 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:
-                                    obj.options['mirror_y'] = True
-                                obj.plot()
+                                    sel_obj.options['mirror_y'] = True
                                 self.app.inform.emit(_('[success] Flip on the Y axis done ...'))
                             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
                                 # 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:
-                                    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.object_changed.emit(obj)
+                            self.app.object_changed.emit(sel_obj)
+                        sel_obj.plot()
                     self.app.progress.emit(100)
 
                 except Exception as e:
@@ -776,20 +774,20 @@ class ToolTransform(FlatCAMTool):
 
                     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."))
                         else:
                             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
-                                obj.options['skew_x'] = num
+                                sel_obj.options['skew_x'] = num
                             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
-                                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.progress.emit(100)
 
@@ -836,16 +834,17 @@ class ToolTransform(FlatCAMTool):
                         px = 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."))
                         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
-                            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.progress.emit(100)
                 except Exception as e:
@@ -854,8 +853,6 @@ class ToolTransform(FlatCAMTool):
 
     def on_offset(self, axis, num):
         obj_list = self.app.collection.get_selected()
-        xminlist = []
-        yminlist = []
 
         if not obj_list:
             self.app.inform.emit(_("[WARNING_NOTCL] No object selected. Please Select an object to offset!"))
@@ -863,34 +860,23 @@ class ToolTransform(FlatCAMTool):
         else:
             with self.app.proc_container.new(_("Applying Offset")):
                 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)
 
-                    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."))
                         else:
                             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
-                                obj.options['offset_x'] = num
+                                sel_obj.options['offset_x'] = num
                             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
-                                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.progress.emit(100)