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

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

Camellan 6 лет назад
Родитель
Сommit
07b4046d4f

+ 8 - 0
FlatCAM.py

@@ -47,6 +47,14 @@ if __name__ == '__main__':
     else:
     else:
         os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "0"
         os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "0"
 
 
+    # if hdpi_support == 2:
+    #     tst_screen = QtWidgets.QApplication(sys.argv)
+    #     if tst_screen.screens()[0].geometry().width() > 1930 or tst_screen.screens()[1].geometry().width() > 1930:
+    #         QGuiApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
+    #         del tst_screen
+    # else:
+    #     QGuiApplication.setAttribute(Qt.AA_EnableHighDpiScaling, False)
+
     app = QtWidgets.QApplication(sys.argv)
     app = QtWidgets.QApplication(sys.argv)
 
 
     # apply style
     # apply style

+ 264 - 34
FlatCAMApp.py

@@ -31,6 +31,7 @@ from reportlab.pdfgen import canvas
 from reportlab.graphics import renderPM
 from reportlab.graphics import renderPM
 from reportlab.lib.units import inch, mm
 from reportlab.lib.units import inch, mm
 from reportlab.lib.pagesizes import landscape, portrait
 from reportlab.lib.pagesizes import landscape, portrait
+from svglib.svglib import svg2rlg
 
 
 from contextlib import contextmanager
 from contextlib import contextmanager
 import gc
 import gc
@@ -415,6 +416,13 @@ class App(QtCore.QObject):
             "global_serial": 0,
             "global_serial": 0,
             "global_stats": dict(),
             "global_stats": dict(),
             "global_tabs_detachable": True,
             "global_tabs_detachable": True,
+            "global_jump_ref": 'abs',
+            "global_tpdf_tmargin": 15.0,
+            "global_tpdf_bmargin": 10.0,
+            "global_tpdf_lmargin": 20.0,
+            "global_tpdf_rmargin": 20.0,
+
+            # General
             "global_graphic_engine": '3D',
             "global_graphic_engine": '3D',
             "global_app_level": 'b',
             "global_app_level": 'b',
             "global_portable": False,
             "global_portable": False,
@@ -511,6 +519,9 @@ class App(QtCore.QObject):
             "gerber_multicolored": False,
             "gerber_multicolored": False,
             "gerber_circle_steps": 64,
             "gerber_circle_steps": 64,
             "gerber_use_buffer_for_union": True,
             "gerber_use_buffer_for_union": True,
+            "gerber_clean_apertures": True,
+            "gerber_extra_buffering": True,
+
             "gerber_def_units": 'IN',
             "gerber_def_units": 'IN',
             "gerber_def_zeros": 'L',
             "gerber_def_zeros": 'L',
             "gerber_save_filters": "Gerber File (*.gbr);;Gerber File (*.bot);;Gerber File (*.bsm);;"
             "gerber_save_filters": "Gerber File (*.gbr);;Gerber File (*.bot);;Gerber File (*.bsm);;"
@@ -972,6 +983,10 @@ class App(QtCore.QObject):
 
 
         self.current_units = self.defaults['units']
         self.current_units = self.defaults['units']
 
 
+        # store here the current self.defaults so it can be restored if Preferences changes are cancelled
+        self.current_defaults = dict()
+        self.current_defaults.update(self.defaults)
+
         # #############################################################################
         # #############################################################################
         # ##################### CREATE MULTIPROCESSING POOL ###########################
         # ##################### CREATE MULTIPROCESSING POOL ###########################
         # #############################################################################
         # #############################################################################
@@ -1079,6 +1094,11 @@ class App(QtCore.QObject):
             "global_bookmarks_limit": self.ui.general_defaults_form.general_app_group.bm_limit_spinner,
             "global_bookmarks_limit": self.ui.general_defaults_form.general_app_group.bm_limit_spinner,
             "global_machinist_setting": self.ui.general_defaults_form.general_app_group.machinist_cb,
             "global_machinist_setting": self.ui.general_defaults_form.general_app_group.machinist_cb,
 
 
+            "global_tpdf_tmargin": self.ui.general_defaults_form.general_app_group.tmargin_entry,
+            "global_tpdf_bmargin": self.ui.general_defaults_form.general_app_group.bmargin_entry,
+            "global_tpdf_lmargin": self.ui.general_defaults_form.general_app_group.lmargin_entry,
+            "global_tpdf_rmargin": self.ui.general_defaults_form.general_app_group.rmargin_entry,
+
             # General GUI Preferences
             # General GUI Preferences
             "global_gridx": self.ui.general_defaults_form.general_gui_group.gridx_entry,
             "global_gridx": self.ui.general_defaults_form.general_gui_group.gridx_entry,
             "global_gridy": self.ui.general_defaults_form.general_gui_group.gridy_entry,
             "global_gridy": self.ui.general_defaults_form.general_gui_group.gridy_entry,
@@ -1121,6 +1141,8 @@ class App(QtCore.QObject):
             "gerber_circle_steps": self.ui.gerber_defaults_form.gerber_gen_group.circle_steps_entry,
             "gerber_circle_steps": self.ui.gerber_defaults_form.gerber_gen_group.circle_steps_entry,
             "gerber_def_units": self.ui.gerber_defaults_form.gerber_gen_group.gerber_units_radio,
             "gerber_def_units": self.ui.gerber_defaults_form.gerber_gen_group.gerber_units_radio,
             "gerber_def_zeros": self.ui.gerber_defaults_form.gerber_gen_group.gerber_zeros_radio,
             "gerber_def_zeros": self.ui.gerber_defaults_form.gerber_gen_group.gerber_zeros_radio,
+            "gerber_clean_apertures": self.ui.gerber_defaults_form.gerber_gen_group.gerber_clean_cb,
+            "gerber_extra_buffering": self.ui.gerber_defaults_form.gerber_gen_group.gerber_extra_buffering,
 
 
             # Gerber Options
             # Gerber Options
             "gerber_isotooldia": self.ui.gerber_defaults_form.gerber_opt_group.iso_tool_dia_entry,
             "gerber_isotooldia": self.ui.gerber_defaults_form.gerber_opt_group.iso_tool_dia_entry,
@@ -1673,6 +1695,7 @@ class App(QtCore.QObject):
         self.mr = None
         self.mr = None
         self.mdc = None
         self.mdc = None
         self.mp_zc = None
         self.mp_zc = None
+        self.kp = None
 
 
         # Matplotlib axis
         # Matplotlib axis
         self.axes = None
         self.axes = None
@@ -1791,10 +1814,11 @@ class App(QtCore.QObject):
 
 
         self.ui.menufileexportdxf.triggered.connect(self.on_file_exportdxf)
         self.ui.menufileexportdxf.triggered.connect(self.on_file_exportdxf)
 
 
+        self.ui.menufile_print.triggered.connect(lambda: self.on_file_save_objects_pdf(use_thread=True))
+
         self.ui.menufilesaveproject.triggered.connect(self.on_file_saveproject)
         self.ui.menufilesaveproject.triggered.connect(self.on_file_saveproject)
         self.ui.menufilesaveprojectas.triggered.connect(self.on_file_saveprojectas)
         self.ui.menufilesaveprojectas.triggered.connect(self.on_file_saveprojectas)
         self.ui.menufilesaveprojectcopy.triggered.connect(lambda: self.on_file_saveprojectas(make_copy=True))
         self.ui.menufilesaveprojectcopy.triggered.connect(lambda: self.on_file_saveprojectas(make_copy=True))
-        self.ui.menufilesave_object_pdf.triggered.connect(self.on_file_save_object_pdf)
         self.ui.menufilesavedefaults.triggered.connect(self.on_file_savedefaults)
         self.ui.menufilesavedefaults.triggered.connect(self.on_file_savedefaults)
 
 
         self.ui.menufileexportpref.triggered.connect(self.on_export_preferences)
         self.ui.menufileexportpref.triggered.connect(self.on_export_preferences)
@@ -2931,17 +2955,25 @@ class App(QtCore.QObject):
             except Exception as e:
             except Exception as e:
                 log.debug("App.defaults_read_form() --> %s" % str(e))
                 log.debug("App.defaults_read_form() --> %s" % str(e))
 
 
-    def defaults_write_form(self, factor=None, fl_units=None):
+    def defaults_write_form(self, factor=None, fl_units=None, source_dict=None):
         """
         """
         Will set the values for all the GUI elements in Preferences GUI based on the values found in the
         Will set the values for all the GUI elements in Preferences GUI based on the values found in the
         self.defaults dictionary.
         self.defaults dictionary.
 
 
         :param factor: will apply a factor to the values that written in the GUI elements
         :param factor: will apply a factor to the values that written in the GUI elements
         :param fl_units: current measuring units in FlatCAM: Metric or Inch
         :param fl_units: current measuring units in FlatCAM: Metric or Inch
+        :param source_dict: the repository of options, usually is the self.defaults
         :return: None
         :return: None
         """
         """
-        for option in self.defaults:
-            self.defaults_write_form_field(option, factor=factor, units=fl_units)
+
+        options_storage = self.defaults if source_dict is None else source_dict
+
+        for option in options_storage:
+            if source_dict:
+                self.defaults_write_form_field(option, factor=factor, units=fl_units, defaults_dict=source_dict)
+            else:
+                self.defaults_write_form_field(option, factor=factor, units=fl_units)
+
             # try:
             # try:
             #     self.defaults_form_fields[option].set_value(self.defaults[option])
             #     self.defaults_form_fields[option].set_value(self.defaults[option])
             # except KeyError:
             # except KeyError:
@@ -2949,7 +2981,7 @@ class App(QtCore.QObject):
             #     # TODO: Rethink this?
             #     # TODO: Rethink this?
             #     pass
             #     pass
 
 
-    def defaults_write_form_field(self, field, factor=None, units=None):
+    def defaults_write_form_field(self, field, factor=None, units=None, defaults_dict=None):
         """
         """
         Basically it is the worker in the self.defaults_write_form()
         Basically it is the worker in the self.defaults_write_form()
 
 
@@ -2958,21 +2990,23 @@ class App(QtCore.QObject):
         :param units: current FLatCAM measuring units
         :param units: current FLatCAM measuring units
         :return: None, it updates GUI elements
         :return: None, it updates GUI elements
         """
         """
+
+        def_dict = self.defaults if defaults_dict is None else defaults_dict
         try:
         try:
             if factor is None:
             if factor is None:
                 if units is None:
                 if units is None:
-                    self.defaults_form_fields[field].set_value(self.defaults[field])
+                    self.defaults_form_fields[field].set_value(def_dict[field])
                 elif units == 'IN' and (field == 'global_gridx' or field == 'global_gridy'):
                 elif units == 'IN' and (field == 'global_gridx' or field == 'global_gridy'):
-                    self.defaults_form_fields[field].set_value(self.defaults[field])
+                    self.defaults_form_fields[field].set_value(def_dict[field])
                 elif units == 'MM' and (field == 'global_gridx' or field == 'global_gridy'):
                 elif units == 'MM' and (field == 'global_gridx' or field == 'global_gridy'):
-                    self.defaults_form_fields[field].set_value(self.defaults[field])
+                    self.defaults_form_fields[field].set_value(def_dict[field])
             else:
             else:
                 if units is None:
                 if units is None:
-                    self.defaults_form_fields[field].set_value(self.defaults[field] * factor)
+                    self.defaults_form_fields[field].set_value(def_dict[field] * factor)
                 elif units == 'IN' and (field == 'global_gridx' or field == 'global_gridy'):
                 elif units == 'IN' and (field == 'global_gridx' or field == 'global_gridy'):
-                    self.defaults_form_fields[field].set_value((self.defaults[field] * factor))
+                    self.defaults_form_fields[field].set_value((def_dict[field] * factor))
                 elif units == 'MM' and (field == 'global_gridx' or field == 'global_gridy'):
                 elif units == 'MM' and (field == 'global_gridx' or field == 'global_gridy'):
-                    self.defaults_form_fields[field].set_value((self.defaults[field] * factor))
+                    self.defaults_form_fields[field].set_value((def_dict[field] * factor))
         except KeyError:
         except KeyError:
             # self.log.debug("defaults_write_form(): No field for: %s" % option)
             # self.log.debug("defaults_write_form(): No field for: %s" % option)
             # TODO: Rethink this?
             # TODO: Rethink this?
@@ -3882,6 +3916,10 @@ class App(QtCore.QObject):
                                  _("Failed to parse defaults file."))
                                  _("Failed to parse defaults file."))
                 return
                 return
             self.defaults.update(defaults_from_file)
             self.defaults.update(defaults_from_file)
+            # update the dict that is used to restore the values in the defaults form if Cancel is clicked in the
+            # Preferences window
+            self.current_defaults.update(defaults_from_file)
+
             self.on_preferences_edited()
             self.on_preferences_edited()
             self.inform.emit('[success] %s: %s' %
             self.inform.emit('[success] %s: %s' %
                              (_("Imported Defaults from"), filename))
                              (_("Imported Defaults from"), filename))
@@ -4161,16 +4199,10 @@ class App(QtCore.QObject):
         try:
         try:
             return_value = initialize(obj, self)
             return_value = initialize(obj, self)
         except Exception as e:
         except Exception as e:
-            msg = '[ERROR_NOTCL] %s' % \
-                  _("An internal error has ocurred. See shell.\n")
+            msg = '[ERROR_NOTCL] %s' % _("An internal error has occurred. See shell.\n")
             msg += _("Object ({kind}) failed because: {error} \n\n").format(kind=kind, error=str(e))
             msg += _("Object ({kind}) failed because: {error} \n\n").format(kind=kind, error=str(e))
             msg += traceback.format_exc()
             msg += traceback.format_exc()
             self.inform.emit(msg)
             self.inform.emit(msg)
-
-            # if str(e) == "Empty Geometry":
-            #     self.inform.emit("[ERROR_NOTCL] )
-            # else:
-            #     self.inform.emit("[ERROR] Object (%s) failed because: %s" % (kind, str(e)))
             return "fail"
             return "fail"
 
 
         t2 = time.time()
         t2 = time.time()
@@ -5761,14 +5793,15 @@ class App(QtCore.QObject):
                       "tools_cr_trace_size_val", "tools_cr_c2c_val", "tools_cr_c2o_val", "tools_cr_s2s_val",
                       "tools_cr_trace_size_val", "tools_cr_c2c_val", "tools_cr_c2o_val", "tools_cr_s2s_val",
                       "tools_cr_s2sm_val", "tools_cr_s2o_val", "tools_cr_sm2sm_val", "tools_cr_ri_val",
                       "tools_cr_s2sm_val", "tools_cr_s2o_val", "tools_cr_sm2sm_val", "tools_cr_ri_val",
                       "tools_cr_h2h_val", "tools_cr_dh_val", "tools_fiducials_dia", "tools_fiducials_margin",
                       "tools_cr_h2h_val", "tools_cr_dh_val", "tools_fiducials_dia", "tools_fiducials_margin",
-                      "tools_fiducials_mode", "tools_fiducials_second_pos", "tools_fiducials_type",
                       "tools_fiducials_line_thickness",
                       "tools_fiducials_line_thickness",
                       "tools_copper_thieving_clearance", "tools_copper_thieving_margin",
                       "tools_copper_thieving_clearance", "tools_copper_thieving_margin",
                       "tools_copper_thieving_dots_dia", "tools_copper_thieving_dots_spacing",
                       "tools_copper_thieving_dots_dia", "tools_copper_thieving_dots_spacing",
                       "tools_copper_thieving_squares_size", "tools_copper_thieving_squares_spacing",
                       "tools_copper_thieving_squares_size", "tools_copper_thieving_squares_spacing",
                       "tools_copper_thieving_lines_size", "tools_copper_thieving_lines_spacing",
                       "tools_copper_thieving_lines_size", "tools_copper_thieving_lines_spacing",
                       "tools_copper_thieving_rb_margin", "tools_copper_thieving_rb_thickness",
                       "tools_copper_thieving_rb_margin", "tools_copper_thieving_rb_thickness",
-                      'global_gridx', 'global_gridy', 'global_snap_max', "global_tolerance"]
+
+                      'global_gridx', 'global_gridy', 'global_snap_max', "global_tolerance",
+                      'global_tpdf_bmargin', 'global_tpdf_tmargin', 'global_tpdf_rmargin', 'global_tpdf_lmargin']
 
 
         def scale_defaults(sfactor):
         def scale_defaults(sfactor):
             for dim in dimensions:
             for dim in dimensions:
@@ -5793,6 +5826,7 @@ class App(QtCore.QObject):
                         tools_diameters = [eval(a) for a in tools_string if a != '']
                         tools_diameters = [eval(a) for a in tools_string if a != '']
                     except Exception as e:
                     except Exception as e:
                         log.debug("App.on_toggle_units().scale_options() --> %s" % str(e))
                         log.debug("App.on_toggle_units().scale_options() --> %s" % str(e))
+                        continue
 
 
                     self.defaults['geometry_cnctooldia'] = ''
                     self.defaults['geometry_cnctooldia'] = ''
                     for t in range(len(tools_diameters)):
                     for t in range(len(tools_diameters)):
@@ -5805,6 +5839,7 @@ class App(QtCore.QObject):
                         ncctools = [eval(a) for a in tools_string if a != '']
                         ncctools = [eval(a) for a in tools_string if a != '']
                     except Exception as e:
                     except Exception as e:
                         log.debug("App.on_toggle_units().scale_options() --> %s" % str(e))
                         log.debug("App.on_toggle_units().scale_options() --> %s" % str(e))
+                        continue
 
 
                     self.defaults['tools_ncctools'] = ''
                     self.defaults['tools_ncctools'] = ''
                     for t in range(len(ncctools)):
                     for t in range(len(ncctools)):
@@ -5817,6 +5852,7 @@ class App(QtCore.QObject):
                         sptools = [eval(a) for a in tools_string if a != '']
                         sptools = [eval(a) for a in tools_string if a != '']
                     except Exception as e:
                     except Exception as e:
                         log.debug("App.on_toggle_units().scale_options() --> %s" % str(e))
                         log.debug("App.on_toggle_units().scale_options() --> %s" % str(e))
+                        continue
 
 
                     self.defaults['tools_solderpaste_tools'] = ""
                     self.defaults['tools_solderpaste_tools'] = ""
                     for t in range(len(sptools)):
                     for t in range(len(sptools)):
@@ -5836,6 +5872,7 @@ class App(QtCore.QObject):
                             val = float(self.defaults[dim]) * sfactor
                             val = float(self.defaults[dim]) * sfactor
                         except Exception as e:
                         except Exception as e:
                             log.debug('App.on_toggle_units().scale_defaults() --> %s' % str(e))
                             log.debug('App.on_toggle_units().scale_defaults() --> %s' % str(e))
+                            continue
 
 
                         self.defaults[dim] = float('%.*f' % (self.decimals, val))
                         self.defaults[dim] = float('%.*f' % (self.decimals, val))
                     else:
                     else:
@@ -5844,6 +5881,7 @@ class App(QtCore.QObject):
                             val = float(self.defaults[dim]) * sfactor
                             val = float(self.defaults[dim]) * sfactor
                         except Exception as e:
                         except Exception as e:
                             log.debug('App.on_toggle_units().scale_defaults() --> %s' % str(e))
                             log.debug('App.on_toggle_units().scale_defaults() --> %s' % str(e))
+                            continue
 
 
                         self.defaults[dim] = float('%.*f' % (self.decimals, val))
                         self.defaults[dim] = float('%.*f' % (self.decimals, val))
                 else:
                 else:
@@ -5852,7 +5890,8 @@ class App(QtCore.QObject):
                         try:
                         try:
                             val = float(self.defaults[dim]) * sfactor
                             val = float(self.defaults[dim]) * sfactor
                         except Exception as e:
                         except Exception as e:
-                            log.debug('App.on_toggle_units().scale_defaults() --> %s' % str(e))
+                            log.debug('App.on_toggle_units().scale_defaults() --> Value: %s %s' % (str(dim), str(e)))
+                            continue
 
 
                         self.defaults[dim] = val
                         self.defaults[dim] = val
 
 
@@ -7026,6 +7065,9 @@ class App(QtCore.QObject):
 
 
         self.inform.emit('%s' % _("Preferences applied."))
         self.inform.emit('%s' % _("Preferences applied."))
 
 
+        # make sure we update the self.current_defaults dict used to undo changes to self.defaults
+        self.current_defaults.update(self.defaults)
+
         if save_to_file:
         if save_to_file:
             self.save_defaults(silent=False)
             self.save_defaults(silent=False)
             # load the defaults so they are updated into the app
             # load the defaults so they are updated into the app
@@ -7064,7 +7106,18 @@ class App(QtCore.QObject):
         except TypeError:
         except TypeError:
             pass
             pass
 
 
-        self.defaults_write_form()
+        try:
+            self.ui.general_defaults_form.general_app_group.units_radio.activated_custom.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        self.defaults_write_form(source_dict=self.current_defaults)
+        self.ui.general_defaults_form.general_app_group.units_radio.activated_custom.connect(
+            lambda: self.on_toggle_units(no_pref=False))
+        self.defaults.update(self.current_defaults)
+
+        # shared_items = {k: self.defaults[k] for k in self.defaults if k in self.current_defaults and
+        #                 self.defaults[k] == self.current_defaults[k]}
+        # print(len(self.defaults), len(shared_items))
 
 
         # Preferences save, update the color of the Preferences Tab text
         # Preferences save, update the color of the Preferences Tab text
         for idx in range(self.ui.plot_tab_area.count()):
         for idx in range(self.ui.plot_tab_area.count()):
@@ -7379,7 +7432,8 @@ class App(QtCore.QObject):
             dia_box = DialogBoxRadio(title=_("Jump to ..."),
             dia_box = DialogBoxRadio(title=_("Jump to ..."),
                                      label=_("Enter the coordinates in format X,Y:"),
                                      label=_("Enter the coordinates in format X,Y:"),
                                      icon=QtGui.QIcon(self.resource_location + '/jump_to16.png'),
                                      icon=QtGui.QIcon(self.resource_location + '/jump_to16.png'),
-                                     initial_text=dia_box_location)
+                                     initial_text=dia_box_location,
+                                     reference=self.defaults['global_jump_ref'])
 
 
             if dia_box.ok is True:
             if dia_box.ok is True:
                 try:
                 try:
@@ -7393,7 +7447,7 @@ class App(QtCore.QObject):
                         rel_x = self.mouse[0] + location[0]
                         rel_x = self.mouse[0] + location[0]
                         rel_y = self.mouse[1] + location[1]
                         rel_y = self.mouse[1] + location[1]
                         location = (rel_x, rel_y)
                         location = (rel_x, rel_y)
-
+                    self.defaults['global_jump_ref'] = dia_box.reference
                 except Exception:
                 except Exception:
                     return
                     return
             else:
             else:
@@ -7793,8 +7847,7 @@ class App(QtCore.QObject):
                     pass
                     pass
 
 
     def on_preferences_edited(self):
     def on_preferences_edited(self):
-        self.inform.emit('[WARNING_NOTCL] %s' %
-                         _("Preferences edited but not saved."))
+        self.inform.emit('[WARNING_NOTCL] %s' % _("Preferences edited but not saved."))
 
 
         for idx in range(self.ui.plot_tab_area.count()):
         for idx in range(self.ui.plot_tab_area.count()):
             if self.ui.plot_tab_area.tabText(idx) == _("Preferences"):
             if self.ui.plot_tab_area.tabText(idx) == _("Preferences"):
@@ -9930,7 +9983,7 @@ class App(QtCore.QObject):
 
 
     def init_code_editor(self, name):
     def init_code_editor(self, name):
 
 
-        self.text_editor_tab = TextEditor(app=self)
+        self.text_editor_tab = TextEditor(app=self, plain_text=True)
 
 
         # add the tab if it was closed
         # add the tab if it was closed
         self.ui.plot_tab_area.addTab(self.text_editor_tab, '%s' % name)
         self.ui.plot_tab_area.addTab(self.text_editor_tab, '%s' % name)
@@ -10271,19 +10324,26 @@ class App(QtCore.QObject):
         self.set_ui_title(name=self.project_filename)
         self.set_ui_title(name=self.project_filename)
         self.should_we_save = False
         self.should_we_save = False
 
 
-    def on_file_save_object_pdf(self, use_thread=True):
+    def on_file_save_objects_pdf(self, use_thread=True):
         self.date = str(datetime.today()).rpartition('.')[0]
         self.date = str(datetime.today()).rpartition('.')[0]
         self.date = ''.join(c for c in self.date if c not in ':-')
         self.date = ''.join(c for c in self.date if c not in ':-')
         self.date = self.date.replace(' ', '_')
         self.date = self.date.replace(' ', '_')
 
 
         try:
         try:
-            obj_active = self.collection.get_active()
-            obj_name = _(str(obj_active.options['name']))
+            obj_selection = self.collection.get_selected()
+            if len(obj_selection) == 1:
+                obj_name = str(obj_selection[0].options['name'])
+            else:
+                obj_name = _("FlatCAM objects print")
         except AttributeError as err:
         except AttributeError as err:
             log.debug("App.on_file_save_object_pdf() --> %s" % str(err))
             log.debug("App.on_file_save_object_pdf() --> %s" % str(err))
             self.inform.emit('[ERROR_NOTCL] %s' % _("No object selected."))
             self.inform.emit('[ERROR_NOTCL] %s' % _("No object selected."))
             return
             return
 
 
+        if not obj_selection:
+            self.inform.emit('[ERROR_NOTCL] %s' % _("No object selected."))
+            return
+
         filter_ = "PDF File (*.PDF);; All Files (*.*)"
         filter_ = "PDF File (*.PDF);; All Files (*.*)"
         try:
         try:
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(
@@ -10303,17 +10363,185 @@ class App(QtCore.QObject):
             return
             return
 
 
         if use_thread is True:
         if use_thread is True:
-            self.worker_task.emit({'fcn': self.save_pdf, 'params': [filename, obj_name]})
+            proc = self.proc_container.new(_("Printing PDF ... Please wait."))
+            self.worker_task.emit({'fcn': self.save_pdf, 'params': [filename, obj_selection]})
         else:
         else:
-            self.save_pdf(filename, obj_name)
+            self.save_pdf(filename, obj_selection)
 
 
         # self.save_project(filename)
         # self.save_project(filename)
         if self.defaults["global_open_style"] is False:
         if self.defaults["global_open_style"] is False:
             self.file_opened.emit("pdf", filename)
             self.file_opened.emit("pdf", filename)
         self.file_saved.emit("pdf", filename)
         self.file_saved.emit("pdf", filename)
 
 
-    def save_pdf(self, file_name, obj_name):
-        self.film_tool.export_positive(obj_name=obj_name, box_name=obj_name, filename=file_name, ftype='pdf')
+    def save_pdf(self, file_name, obj_selection):
+
+        p_size = self.defaults['global_workspaceT']
+        orientation = self.defaults['global_workspace_orientation']
+        color = 'black'
+        transparency_level = 1.0
+
+        self.pagesize = dict()
+        self.pagesize.update(
+            {
+                'Bounds': None,
+                'A0': (841*mm, 1189*mm),
+                'A1': (594*mm, 841*mm),
+                'A2': (420*mm, 594*mm),
+                'A3': (297*mm, 420*mm),
+                'A4': (210*mm, 297*mm),
+                'A5': (148*mm, 210*mm),
+                'A6': (105*mm, 148*mm),
+                'A7': (74*mm, 105*mm),
+                'A8': (52*mm, 74*mm),
+                'A9': (37*mm, 52*mm),
+                'A10': (26*mm, 37*mm),
+
+                'B0': (1000*mm, 1414*mm),
+                'B1': (707*mm, 1000*mm),
+                'B2': (500*mm, 707*mm),
+                'B3': (353*mm, 500*mm),
+                'B4': (250*mm, 353*mm),
+                'B5': (176*mm, 250*mm),
+                'B6': (125*mm, 176*mm),
+                'B7': (88*mm, 125*mm),
+                'B8': (62*mm, 88*mm),
+                'B9': (44*mm, 62*mm),
+                'B10': (31*mm, 44*mm),
+
+                'C0': (917*mm, 1297*mm),
+                'C1': (648*mm, 917*mm),
+                'C2': (458*mm, 648*mm),
+                'C3': (324*mm, 458*mm),
+                'C4': (229*mm, 324*mm),
+                'C5': (162*mm, 229*mm),
+                'C6': (114*mm, 162*mm),
+                'C7': (81*mm, 114*mm),
+                'C8': (57*mm, 81*mm),
+                'C9': (40*mm, 57*mm),
+                'C10': (28*mm, 40*mm),
+
+                # American paper sizes
+                'LETTER': (8.5*inch, 11*inch),
+                'LEGAL': (8.5*inch, 14*inch),
+                'ELEVENSEVENTEEN': (11*inch, 17*inch),
+
+                # From https://en.wikipedia.org/wiki/Paper_size
+                'JUNIOR_LEGAL': (5*inch, 8*inch),
+                'HALF_LETTER': (5.5*inch, 8*inch),
+                'GOV_LETTER': (8*inch, 10.5*inch),
+                'GOV_LEGAL': (8.5*inch, 13*inch),
+                'LEDGER': (17*inch, 11*inch),
+            }
+        )
+
+        exported_svg = list()
+        for obj in obj_selection:
+            svg_obj = obj.export_svg(scale_stroke_factor=0.0,
+                                     scale_factor_x=None, scale_factor_y=None,
+                                     skew_factor_x=None, skew_factor_y=None,
+                                     mirror=None)
+
+            if obj.kind.lower() == 'gerber':
+                color = self.defaults["global_plot_fill"][:-2]
+            elif obj.kind.lower() == 'excellon':
+                color = '#C40000'
+            elif obj.kind.lower() == 'geometry':
+                color = self.defaults["global_draw_color"]
+
+            # Change the attributes of the exported SVG
+            # We don't need stroke-width
+            # We set opacity to maximum
+            # We set the colour to WHITE
+            root = ET.fromstring(svg_obj)
+            for child in root:
+                child.set('fill', str(color))
+                child.set('opacity', str(transparency_level))
+                child.set('stroke', str(color))
+
+            exported_svg.append(ET.tostring(root))
+
+        xmin = Inf
+        ymin = Inf
+        xmax = -Inf
+        ymax = -Inf
+
+        for obj in obj_selection:
+            try:
+                gxmin, gymin, gxmax, gymax = obj.bounds()
+                xmin = min([xmin, gxmin])
+                ymin = min([ymin, gymin])
+                xmax = max([xmax, gxmax])
+                ymax = max([ymax, gymax])
+            except Exception as e:
+                log.warning("DEV WARNING: Tried to get bounds of empty geometry in App.save_pdf(). %s" % str(e))
+
+        # Determine bounding area for svg export
+        bounds = [xmin, ymin, xmax, ymax]
+        size = bounds[2] - bounds[0], bounds[3] - bounds[1]
+
+        # This contain the measure units
+        uom = obj_selection[0].units.lower()
+
+        # Define a boundary around SVG of about 1.0mm (~39mils)
+        if uom in "mm":
+            boundary = 1.0
+        else:
+            boundary = 0.0393701
+
+        # Convert everything to strings for use in the xml doc
+        svgwidth = str(size[0] + (2 * boundary))
+        svgheight = str(size[1] + (2 * boundary))
+        minx = str(bounds[0] - boundary)
+        miny = str(bounds[1] + boundary + size[1])
+
+        # Add a SVG Header and footer to the svg output from shapely
+        # The transform flips the Y Axis so that everything renders
+        # properly within svg apps such as inkscape
+        svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
+                     'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
+        svg_header += 'width="' + svgwidth + uom + '" '
+        svg_header += 'height="' + svgheight + uom + '" '
+        svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
+        svg_header += '>'
+        svg_header += '<g transform="scale(1,-1)">'
+        svg_footer = '</g> </svg>'
+
+        svg_elem = str(svg_header)
+        for svg_item in exported_svg:
+            svg_elem += str(svg_item)
+        svg_elem += str(svg_footer)
+
+        # Parse the xml through a xml parser just to add line feeds
+        # and to make it look more pretty for the output
+        doc = parse_xml_string(svg_elem)
+        doc_final = doc.toprettyxml()
+
+        try:
+            if self.defaults['units'].upper() == 'IN':
+                unit = inch
+            else:
+                unit = mm
+
+            doc_final = StringIO(doc_final)
+            drawing = svg2rlg(doc_final)
+
+            if p_size == 'Bounds':
+                renderPDF.drawToFile(drawing, file_name)
+            else:
+                if orientation == 'p':
+                    page_size = portrait(self.pagesize[p_size])
+                else:
+                    page_size = landscape(self.pagesize[p_size])
+
+                my_canvas = canvas.Canvas(file_name, pagesize=page_size)
+                my_canvas.translate(bounds[0] * unit, bounds[1] * unit)
+                renderPDF.draw(drawing, my_canvas, 0, 0)
+                my_canvas.save()
+        except Exception as e:
+            log.debug("App.save_pdf() --> PDF output --> %s" % str(e))
+            return 'fail'
+
+        self.inform.emit('[success] %s: %s' % (_("PDF file saved to"), file_name))
 
 
     def export_svg(self, obj_name, filename, scale_stroke_factor=0.00):
     def export_svg(self, obj_name, filename, scale_stroke_factor=0.00):
         """
         """
@@ -11261,6 +11489,7 @@ class App(QtCore.QObject):
 
 
         # Project options
         # Project options
         self.options.update(d['options'])
         self.options.update(d['options'])
+
         self.project_filename = filename
         self.project_filename = filename
 
 
         # for some reason, setting ui_title does not work when this method is called from Tcl Shell
         # for some reason, setting ui_title does not work when this method is called from Tcl Shell
@@ -11273,6 +11502,7 @@ class App(QtCore.QObject):
 
 
         for obj in d['objs']:
         for obj in d['objs']:
             def obj_init(obj_inst, app_inst):
             def obj_init(obj_inst, app_inst):
+
                 obj_inst.from_dict(obj)
                 obj_inst.from_dict(obj)
 
 
             App.log.debug("Recreating from opened project an %s object: %s" %
             App.log.debug("Recreating from opened project an %s object: %s" %

+ 5 - 5
FlatCAMObj.py

@@ -981,6 +981,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
 
         def geo_init(geo_obj, app_obj):
         def geo_init(geo_obj, app_obj):
             assert isinstance(geo_obj, FlatCAMGeometry)
             assert isinstance(geo_obj, FlatCAMGeometry)
+            if isinstance(self.solid_geometry, list):
+                self.solid_geometry = cascaded_union(self.solid_geometry)
+
             bounding_box = self.solid_geometry.envelope.buffer(float(self.options["noncoppermargin"]))
             bounding_box = self.solid_geometry.envelope.buffer(float(self.options["noncoppermargin"]))
             if not self.options["noncopperrounded"]:
             if not self.options["noncopperrounded"]:
                 bounding_box = bounding_box.envelope
                 bounding_box = bounding_box.envelope
@@ -5173,8 +5176,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                     if self.tools[tooluid_key]['solid_geometry'] is None:
                     if self.tools[tooluid_key]['solid_geometry'] is None:
                         a += 1
                         a += 1
                 if a == len(self.tools):
                 if a == len(self.tools):
-                    self.app.inform.emit('[ERROR_NOTCL] %s...' %
-                                         _('Cancelled. Empty file, it has no geometry'))
+                    self.app.inform.emit('[ERROR_NOTCL] %s...' % _('Cancelled. Empty file, it has no geometry'))
                     return 'fail'
                     return 'fail'
 
 
             for tooluid_key in list(tools_dict.keys()):
             for tooluid_key in list(tools_dict.keys()):
@@ -5292,10 +5294,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
 
 
         if use_thread:
         if use_thread:
             # To be run in separate thread
             # To be run in separate thread
-            # The idea is that if there is a solid_geometry in the file "root" then most likely thare are no
-            # separate solid_geometry in the self.tools dictionary
             def job_thread(app_obj):
             def job_thread(app_obj):
-                if self.solid_geometry:
+                if self.multigeo is False:
                     with self.app.proc_container.new(_("Generating CNC Code")):
                     with self.app.proc_container.new(_("Generating CNC Code")):
                         if app_obj.new_object("cncjob", outname, job_init_single_geometry, plot=plot) != 'fail':
                         if app_obj.new_object("cncjob", outname, job_init_single_geometry, plot=plot) != 'fail':
                             app_obj.inform.emit('[success] %s: %s' % (_("CNCjob created"), outname))
                             app_obj.inform.emit('[success] %s: %s' % (_("CNCjob created"), outname))

+ 26 - 0
README.md

@@ -9,6 +9,31 @@ CAD program, and create G-Code for Isolation routing.
 
 
 =================================================
 =================================================
 
 
+20.12.2019
+
+- fixed a rare issue in the generation of non-copper-region geometry started from the Gerber Object UI (selected tab)
+- Print function is now printing a PDF file for a selection of objects in the colors from canvas 
+
+19.12.2019
+
+- in 2-Sided Tool added a way to calculate the bounding box values for a selection of objects, and also the centroid
+- in 2-Sided Tool fixed the Reset Tool button handler to reset the bounds value too; changed a string
+- added Preferences values for PDF margins when saving text in Code Editor as PDF
+- when clicking Cancel in Preferences now the values are reverted to what they used to be before opening Preferences tab and start changing values
+- starting to work to a general Print function; for now it will generate PDF files; currently it works only for one object not for a selection
+- added shortcut key CTRL+P for printing to PDF method
+
+18.12.2019
+
+- added new parameters to improve Gerber parsing
+- small optimizations in the Preferences UI
+- the Jump To function reference is now saving it's last used value
+- added the ability to use the Jump To method in the Gerber Editor
+- improved the loading of Config File by using the advanced code editor
+- fixed a bug in the new feature 'extra buffering'
+- fixed the creation of CNCJob objects out of multigeo Geometry objects (objects with multiple tools)
+- optimized the NCC Tool
+
 17.12.2019
 17.12.2019
 
 
 - more optimizations in NCC Tool
 - more optimizations in NCC Tool
@@ -21,6 +46,7 @@ CAD program, and create G-Code for Isolation routing.
 - added ability to save the Source File as PDF - fixed page size and added line breaks
 - added ability to save the Source File as PDF - fixed page size and added line breaks
 - more mods to generate_from_geometry_2() method
 - more mods to generate_from_geometry_2() method
 - fixed bug saving the FlatCAM project saying the file is used by another application
 - fixed bug saving the FlatCAM project saying the file is used by another application
+- fixed issue #347 - a Gerber generated by Sprint Layout with copper pour ON will not have rendered the copper pour
 
 
 16.12.2019
 16.12.2019
 
 

+ 0 - 1
flatcamEditors/FlatCAMGeoEditor.py

@@ -1880,7 +1880,6 @@ class DrawTool(object):
         return ""
         return ""
 
 
     def on_key(self, key):
     def on_key(self, key):
-
         # Jump to coords
         # Jump to coords
         if key == QtCore.Qt.Key_J or key == 'J':
         if key == QtCore.Qt.Key_J or key == 'J':
             self.draw_app.app.on_jump_to()
             self.draw_app.app.on_jump_to()

+ 58 - 24
flatcamEditors/FlatCAMGrbEditor.py

@@ -139,7 +139,9 @@ class DrawTool(object):
         return ""
         return ""
 
 
     def on_key(self, key):
     def on_key(self, key):
-        return None
+        # Jump to coords
+        if key == QtCore.Qt.Key_J or key == 'J':
+            self.draw_app.app.on_jump_to()
 
 
     def utility_geometry(self, data=None):
     def utility_geometry(self, data=None):
         return None
         return None
@@ -874,9 +876,11 @@ class FCRegion(FCShapeTool):
         except Exception as e:
         except Exception as e:
             log.debug("FlatCAMGrbEditor.FCRegion --> %s" % str(e))
             log.debug("FlatCAMGrbEditor.FCRegion --> %s" % str(e))
 
 
-        self.cursor = QtGui.QCursor(QtGui.QPixmap(self.app.resource_location + '/aero.png'))
+        self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
 
 
+        self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
+
         self.draw_app.app.inform.emit(_('Corner Mode 1: 45 degrees ...'))
         self.draw_app.app.inform.emit(_('Corner Mode 1: 45 degrees ...'))
 
 
         self.start_msg = _("Click on 1st point ...")
         self.start_msg = _("Click on 1st point ...")
@@ -1064,8 +1068,10 @@ class FCRegion(FCShapeTool):
             self.geometry = DrawToolShape(new_geo_el)
             self.geometry = DrawToolShape(new_geo_el)
         self.draw_app.in_action = False
         self.draw_app.in_action = False
         self.complete = True
         self.complete = True
-        self.draw_app.app.inform.emit('[success] %s' %
-                                      _("Done."))
+
+        self.draw_app.app.jump_signal.disconnect()
+
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
 
 
     def clean_up(self):
     def clean_up(self):
         self.draw_app.selected = []
         self.draw_app.selected = []
@@ -1073,6 +1079,10 @@ class FCRegion(FCShapeTool):
         self.draw_app.plot_all()
         self.draw_app.plot_all()
 
 
     def on_key(self, key):
     def on_key(self, key):
+        # Jump to coords
+        if key == QtCore.Qt.Key_J or key == 'J':
+            self.draw_app.app.on_jump_to()
+
         if key == 'Backspace' or key == QtCore.Qt.Key_Backspace:
         if key == 'Backspace' or key == QtCore.Qt.Key_Backspace:
             if len(self.points) > 0:
             if len(self.points) > 0:
                 if self.draw_app.bend_mode == 5:
                 if self.draw_app.bend_mode == 5:
@@ -1148,9 +1158,12 @@ class FCTrack(FCRegion):
         except Exception as e:
         except Exception as e:
             log.debug("FlatCAMGrbEditor.FCTrack.__init__() --> %s" % str(e))
             log.debug("FlatCAMGrbEditor.FCTrack.__init__() --> %s" % str(e))
 
 
-        self.cursor = QtGui.QCursor(QtGui.QPixmap(self.app.resource_location + '/aero_path%s.png' % self.draw_app.bend_mode))
+        self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location +
+                                                  '/aero_path%s.png' % self.draw_app.bend_mode))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
 
 
+        self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
+
         self.draw_app.app.inform.emit(_('Track Mode 1: 45 degrees ...'))
         self.draw_app.app.inform.emit(_('Track Mode 1: 45 degrees ...'))
 
 
     def make(self):
     def make(self):
@@ -1168,8 +1181,10 @@ class FCTrack(FCRegion):
 
 
         self.draw_app.in_action = False
         self.draw_app.in_action = False
         self.complete = True
         self.complete = True
-        self.draw_app.app.inform.emit('[success] %s' %
-                                      _("Done."))
+
+        self.draw_app.app.jump_signal.disconnect()
+
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
 
 
     def clean_up(self):
     def clean_up(self):
         self.draw_app.selected = []
         self.draw_app.selected = []
@@ -1287,6 +1302,10 @@ class FCTrack(FCRegion):
                 self.draw_app.draw_utility_geometry(geo=geo)
                 self.draw_app.draw_utility_geometry(geo=geo)
                 return _("Backtracked one point ...")
                 return _("Backtracked one point ...")
 
 
+        # Jump to coords
+        if key == QtCore.Qt.Key_J or key == 'J':
+            self.draw_app.app.on_jump_to()
+
         if key == 'T' or key == QtCore.Qt.Key_T:
         if key == 'T' or key == QtCore.Qt.Key_T:
             try:
             try:
                 QtGui.QGuiApplication.restoreOverrideCursor()
                 QtGui.QGuiApplication.restoreOverrideCursor()
@@ -1396,6 +1415,8 @@ class FCDisc(FCShapeTool):
 
 
         self.draw_app.app.inform.emit(_("Click on Center point ..."))
         self.draw_app.app.inform.emit(_("Click on Center point ..."))
 
 
+        self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
+
         self.steps_per_circ = self.draw_app.app.defaults["gerber_circle_steps"]
         self.steps_per_circ = self.draw_app.app.defaults["gerber_circle_steps"]
 
 
     def click(self, point):
     def click(self, point):
@@ -1442,8 +1463,10 @@ class FCDisc(FCShapeTool):
 
 
         self.draw_app.in_action = False
         self.draw_app.in_action = False
         self.complete = True
         self.complete = True
-        self.draw_app.app.inform.emit('[success] %s' %
-                                      _("Done."))
+
+        self.draw_app.app.jump_signal.disconnect()
+
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
 
 
     def clean_up(self):
     def clean_up(self):
         self.draw_app.selected = []
         self.draw_app.selected = []
@@ -1490,6 +1513,7 @@ class FCSemiDisc(FCShapeTool):
             self.storage_obj = self.draw_app.storage_dict['0']['geometry']
             self.storage_obj = self.draw_app.storage_dict['0']['geometry']
 
 
         self.steps_per_circ = self.draw_app.app.defaults["gerber_circle_steps"]
         self.steps_per_circ = self.draw_app.app.defaults["gerber_circle_steps"]
+        self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
 
 
     def click(self, point):
     def click(self, point):
         self.points.append(point)
         self.points.append(point)
@@ -1523,6 +1547,10 @@ class FCSemiDisc(FCShapeTool):
             self.direction = 'cw' if self.direction == 'ccw' else 'ccw'
             self.direction = 'cw' if self.direction == 'ccw' else 'ccw'
             return '%s: %s' % (_('Direction'), self.direction.upper())
             return '%s: %s' % (_('Direction'), self.direction.upper())
 
 
+        # Jump to coords
+        if key == QtCore.Qt.Key_J or key == 'J':
+            self.draw_app.app.on_jump_to()
+
         if key == 'M' or key == QtCore.Qt.Key_M:
         if key == 'M' or key == QtCore.Qt.Key_M:
             # delete the possible points made before this action; we want to start anew
             # delete the possible points made before this action; we want to start anew
             self.points = []
             self.points = []
@@ -1700,8 +1728,10 @@ class FCSemiDisc(FCShapeTool):
 
 
         self.draw_app.in_action = False
         self.draw_app.in_action = False
         self.complete = True
         self.complete = True
-        self.draw_app.app.inform.emit('[success] %s' %
-                                      _("Done."))
+
+        self.draw_app.app.jump_signal.disconnect()
+
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
 
 
     def clean_up(self):
     def clean_up(self):
         self.draw_app.selected = []
         self.draw_app.selected = []
@@ -4517,6 +4547,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.snap_x = x
         self.snap_x = x
         self.snap_y = y
         self.snap_y = y
 
 
+        self.app.mouse = [x, y]
+
         # update the position label in the infobar since the APP mouse event handlers are disconnected
         # update the position label in the infobar since the APP mouse event handlers are disconnected
         self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   " 
         self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   " 
                                            "<b>Y</b>: %.4f" % (x, y))
                                            "<b>Y</b>: %.4f" % (x, y))
@@ -4594,19 +4626,21 @@ class FlatCAMGrbEditor(QtCore.QObject):
             self.shapes.clear(update=True)
             self.shapes.clear(update=True)
 
 
             for storage in self.storage_dict:
             for storage in self.storage_dict:
-                for elem in self.storage_dict[storage]['geometry']:
-                    if 'solid' in elem.geo:
-                        geometric_data = elem.geo['solid']
-                        if geometric_data is None:
-                            continue
-
-                        if elem in self.selected:
-                            self.plot_shape(geometry=geometric_data,
-                                            color=self.app.defaults['global_sel_draw_color'] + 'FF',
-                                            linewidth=2)
-                        else:
-                            self.plot_shape(geometry=geometric_data,
-                                            color=self.app.defaults['global_draw_color'] + 'FF')
+                # fix for apertures with now geometry inside
+                if 'geometry' in self.storage_dict[storage]:
+                    for elem in self.storage_dict[storage]['geometry']:
+                        if 'solid' in elem.geo:
+                            geometric_data = elem.geo['solid']
+                            if geometric_data is None:
+                                continue
+
+                            if elem in self.selected:
+                                self.plot_shape(geometry=geometric_data,
+                                                color=self.app.defaults['global_sel_draw_color'] + 'FF',
+                                                linewidth=2)
+                            else:
+                                self.plot_shape(geometry=geometric_data,
+                                                color=self.app.defaults['global_draw_color'] + 'FF')
 
 
             if self.utility:
             if self.utility:
                 for elem in self.utility:
                 for elem in self.utility:

+ 17 - 6
flatcamEditors/FlatCAMTextEditor.py

@@ -196,7 +196,7 @@ class TextEditor(QtWidgets.QWidget):
             _filter_ = filt
             _filter_ = filt
         else:
         else:
             _filter_ = "G-Code Files (*.nc);; G-Code Files (*.txt);; G-Code Files (*.tap);; G-Code Files (*.cnc);; " \
             _filter_ = "G-Code Files (*.nc);; G-Code Files (*.txt);; G-Code Files (*.tap);; G-Code Files (*.cnc);; " \
-                       "All Files (*.*)"
+                       "PDF Files (*.pdf);;All Files (*.*)"
 
 
         if name:
         if name:
             obj_name = name
             obj_name = name
@@ -206,7 +206,7 @@ class TextEditor(QtWidgets.QWidget):
             except AttributeError:
             except AttributeError:
                 obj_name = 'file'
                 obj_name = 'file'
                 if filt is None:
                 if filt is None:
-                    _filter_ = "FlatConfig Files (*.FlatConfig);;All Files (*.*)"
+                    _filter_ = "FlatConfig Files (*.FlatConfig);;PDF Files (*.pdf);;All Files (*.*)"
 
 
         try:
         try:
             filename = str(QtWidgets.QFileDialog.getSaveFileName(
             filename = str(QtWidgets.QFileDialog.getSaveFileName(
@@ -237,13 +237,24 @@ class TextEditor(QtWidgets.QWidget):
                     styleH = styles['Heading1']
                     styleH = styles['Heading1']
                     story = []
                     story = []
 
 
+                    if self.app.defaults['units'].lower() == 'mm':
+                        bmargin = self.app.defaults['global_tpdf_bmargin'] * mm
+                        tmargin = self.app.defaults['global_tpdf_tmargin'] * mm
+                        rmargin = self.app.defaults['global_tpdf_rmargin'] * mm
+                        lmargin = self.app.defaults['global_tpdf_lmargin'] * mm
+                    else:
+                        bmargin = self.app.defaults['global_tpdf_bmargin'] * inch
+                        tmargin = self.app.defaults['global_tpdf_tmargin'] * inch
+                        rmargin = self.app.defaults['global_tpdf_rmargin'] * inch
+                        lmargin = self.app.defaults['global_tpdf_lmargin'] * inch
+
                     doc = SimpleDocTemplate(
                     doc = SimpleDocTemplate(
                         filename,
                         filename,
                         pagesize=page_size,
                         pagesize=page_size,
-                        bottomMargin=0.4 * inch,
-                        topMargin=0.6 * inch,
-                        rightMargin=0.8 * inch,
-                        leftMargin=0.8 * inch)
+                        bottomMargin=bmargin,
+                        topMargin=tmargin,
+                        rightMargin=rmargin,
+                        leftMargin=lmargin)
 
 
                     P = Paragraph(lined_gcode, styleN)
                     P = Paragraph(lined_gcode, styleN)
                     story.append(P)
                     story.append(P)

+ 20 - 14
flatcamGUI/FlatCAMGUI.py

@@ -240,6 +240,9 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
 
         # Separator
         # Separator
         self.menufile.addSeparator()
         self.menufile.addSeparator()
+        self.menufile_print = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/printer32.png'), '%s\tCTRL+P' % _('Print (PDF)'))
+        self.menufile.addAction(self.menufile_print)
 
 
         self.menufile_save = self.menufile.addMenu(QtGui.QIcon(self.app.resource_location + '/save_as.png'), _('Save'))
         self.menufile_save = self.menufile.addMenu(QtGui.QIcon(self.app.resource_location + '/save_as.png'), _('Save'))
 
 
@@ -260,11 +263,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
 
         self.menufile_save.addSeparator()
         self.menufile_save.addSeparator()
 
 
-        # Save Object PDF
-        self.menufilesave_object_pdf = QtWidgets.QAction(QtGui.QIcon(self.app.resource_location + '/pdf32.png'),
-                                                         _('Save Object as PDF ...'), self)
-        self.menufile_save.addAction(self.menufilesave_object_pdf)
-
         # Separator
         # Separator
         self.menufile.addSeparator()
         self.menufile.addSeparator()
 
 
@@ -1382,17 +1380,21 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
                     </tr>
                     </tr>
                     <tr height="20">
                     <tr height="20">
-                        <td height="20"><strong>CTRL+N</strong></td>
+                        <td height="20"><strong>CTRL+M</strong></td>
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
                     </tr>
                     </tr>
                     <tr height="20">
                     <tr height="20">
-                        <td height="20"><strong>CTRL+M</strong></td>
+                        <td height="20"><strong>CTRL+N</strong></td>
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
-                    </tr>
+                    </tr>                   
                     <tr height="20">
                     <tr height="20">
                         <td height="20"><strong>CTRL+O</strong></td>
                         <td height="20"><strong>CTRL+O</strong></td>
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
                     </tr>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>CTRL+P</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr> 
                     <tr height="20">
                     <tr height="20">
                         <td height="20"><strong>CTRL+Q</strong></td>
                         <td height="20"><strong>CTRL+Q</strong></td>
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
@@ -1579,8 +1581,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
 
                 # CTRL section
                 # CTRL section
                 _("Select All"), _("Copy Obj"), _("Open Tools Database"),
                 _("Select All"), _("Copy Obj"), _("Open Tools Database"),
-                _("Open Excellon File"), _("Open Gerber File"), _("New Project"), _("Distance Tool"),
-                _("Open Project"), _("PDF Import Tool"), _("Save Project As"), _("Toggle Plot Area"),
+                _("Open Excellon File"), _("Open Gerber File"), _("Distance Tool"), _("New Project"),
+                _("Open Project"), _("Print (PDF)"), _("PDF Import Tool"), _("Save Project As"), _("Toggle Plot Area"),
 
 
                 # SHIFT section
                 # SHIFT section
                 _("Copy Obj_Name"),
                 _("Copy Obj_Name"),
@@ -2671,18 +2673,22 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 if key == QtCore.Qt.Key_G:
                 if key == QtCore.Qt.Key_G:
                     self.app.on_fileopengerber()
                     self.app.on_fileopengerber()
 
 
-                # Create New Project
-                if key == QtCore.Qt.Key_N:
-                    self.app.on_file_new_click()
-
                 # Distance Tool
                 # Distance Tool
                 if key == QtCore.Qt.Key_M:
                 if key == QtCore.Qt.Key_M:
                     self.app.distance_tool.run()
                     self.app.distance_tool.run()
 
 
+                # Create New Project
+                if key == QtCore.Qt.Key_N:
+                    self.app.on_file_new_click()
+
                 # Open Project
                 # Open Project
                 if key == QtCore.Qt.Key_O:
                 if key == QtCore.Qt.Key_O:
                     self.app.on_file_openproject()
                     self.app.on_file_openproject()
 
 
+                # Open Project
+                if key == QtCore.Qt.Key_P:
+                    self.app.on_file_save_objects_pdf(use_thread=True)
+
                 # PDF Import
                 # PDF Import
                 if key == QtCore.Qt.Key_Q:
                 if key == QtCore.Qt.Key_Q:
                     self.app.pdf_tool.run()
                     self.app.pdf_tool.run()

+ 8 - 8
flatcamGUI/GUIElements.py

@@ -2239,7 +2239,7 @@ class Dialog_box(QtWidgets.QWidget):
 
 
 
 
 class DialogBoxRadio(QtWidgets.QDialog):
 class DialogBoxRadio(QtWidgets.QDialog):
-    def __init__(self, title=None, label=None, icon=None, initial_text=None):
+    def __init__(self, title=None, label=None, icon=None, initial_text=None, reference='abs'):
         """
         """
 
 
         :param title: string with the window title
         :param title: string with the window title
@@ -2258,11 +2258,6 @@ class DialogBoxRadio(QtWidgets.QDialog):
 
 
         self.form = QtWidgets.QFormLayout(self)
         self.form = QtWidgets.QFormLayout(self)
 
 
-        self.form.addRow(QtWidgets.QLabel(''))
-
-        self.wdg_label = QtWidgets.QLabel('<b>%s</b>' % str(label))
-        self.form.addRow(self.wdg_label)
-
         self.ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
         self.ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
         self.ref_label.setToolTip(
         self.ref_label.setToolTip(
             _("The reference can be:\n"
             _("The reference can be:\n"
@@ -2273,10 +2268,15 @@ class DialogBoxRadio(QtWidgets.QDialog):
             {"label": _("Abs"), "value": "abs"},
             {"label": _("Abs"), "value": "abs"},
             {"label": _("Relative"), "value": "rel"}
             {"label": _("Relative"), "value": "rel"}
         ], orientation='horizontal', stretch=False)
         ], orientation='horizontal', stretch=False)
-        self.ref_radio.set_value('abs')
+        self.ref_radio.set_value(reference)
         self.form.addRow(self.ref_label, self.ref_radio)
         self.form.addRow(self.ref_label, self.ref_radio)
 
 
-        self.loc_label = QtWidgets.QLabel('<b>%s:</b>' % _("Location"))
+        self.form.addRow(QtWidgets.QLabel(''))
+
+        self.wdg_label = QtWidgets.QLabel('<b>%s</b>' % str(label))
+        self.form.addRow(self.wdg_label)
+
+        self.loc_label = QtWidgets.QLabel('%s:' % _("Location"))
         self.loc_label.setToolTip(
         self.loc_label.setToolTip(
             _("The Location value is a tuple (x,y).\n"
             _("The Location value is a tuple (x,y).\n"
               "If the reference is Absolute then the Jump will be at the position (x,y).\n"
               "If the reference is Absolute then the Jump will be at the position (x,y).\n"

+ 128 - 15
flatcamGUI/PreferencesUI.py

@@ -1330,6 +1330,76 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
 
 
         grid0.addWidget(self.machinist_cb, 21, 0, 1, 2)
         grid0.addWidget(self.machinist_cb, 21, 0, 1, 2)
 
 
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.layout.addWidget(separator_line)
+
+        self.layout.addWidget(QtWidgets.QLabel(''))
+
+        grid1 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid1)
+        grid1.setColumnStretch(0, 0)
+        grid1.setColumnStretch(1, 1)
+
+        self.pdf_param_label = QtWidgets.QLabel('<B>%s:</b>' % _("Text to PDF parameters"))
+        self.pdf_param_label.setToolTip(
+            _("Used when saving text in Code Editor or in FlatCAM Document objects.")
+        )
+        grid1.addWidget(self.pdf_param_label, 0, 0, 1, 2)
+
+        # Top Margin value
+        self.tmargin_entry = FCDoubleSpinner()
+        self.tmargin_entry.set_precision(self.decimals)
+        self.tmargin_entry.set_range(0.0000, 9999.9999)
+
+        self.tmargin_label = QtWidgets.QLabel('%s:' % _("Top Margin"))
+        self.tmargin_label.setToolTip(
+            _("Distance between text body and the top of the PDF file.")
+        )
+
+        grid1.addWidget(self.tmargin_label, 1, 0)
+        grid1.addWidget(self.tmargin_entry, 1, 1)
+
+        # Bottom Margin value
+        self.bmargin_entry = FCDoubleSpinner()
+        self.bmargin_entry.set_precision(self.decimals)
+        self.bmargin_entry.set_range(0.0000, 9999.9999)
+
+        self.bmargin_label = QtWidgets.QLabel('%s:' % _("Bottom Margin"))
+        self.bmargin_label.setToolTip(
+            _("Distance between text body and the bottom of the PDF file.")
+        )
+
+        grid1.addWidget(self.bmargin_label, 2, 0)
+        grid1.addWidget(self.bmargin_entry, 2, 1)
+
+        # Left Margin value
+        self.lmargin_entry = FCDoubleSpinner()
+        self.lmargin_entry.set_precision(self.decimals)
+        self.lmargin_entry.set_range(0.0000, 9999.9999)
+
+        self.lmargin_label = QtWidgets.QLabel('%s:' % _("Left Margin"))
+        self.lmargin_label.setToolTip(
+            _("Distance between text body and the left of the PDF file.")
+        )
+
+        grid1.addWidget(self.lmargin_label, 3, 0)
+        grid1.addWidget(self.lmargin_entry, 3, 1)
+
+        # Right Margin value
+        self.rmargin_entry = FCDoubleSpinner()
+        self.rmargin_entry.set_precision(self.decimals)
+        self.rmargin_entry.set_range(0.0000, 9999.9999)
+
+        self.rmargin_label = QtWidgets.QLabel('%s:' % _("Right Margin"))
+        self.rmargin_label.setToolTip(
+            _("Distance between text body and the right of the PDF file.")
+        )
+
+        grid1.addWidget(self.rmargin_label, 4, 0)
+        grid1.addWidget(self.rmargin_entry, 4, 1)
+
         self.layout.addStretch()
         self.layout.addStretch()
 
 
         if sys.platform != 'win32':
         if sys.platform != 'win32':
@@ -1435,6 +1505,29 @@ class GerberGenPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.gerber_zeros_label, 5, 0)
         grid0.addWidget(self.gerber_zeros_label, 5, 0)
         grid0.addWidget(self.gerber_zeros_radio, 5, 1, 1, 2)
         grid0.addWidget(self.gerber_zeros_radio, 5, 1, 1, 2)
 
 
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 6, 0, 1, 3)
+
+        # Apertures Cleaning
+        self.gerber_clean_cb = FCCheckBox(label='%s' % _('Clean Apertures'))
+        self.gerber_clean_cb.setToolTip(
+            _("Will remove apertures that do not have geometry\n"
+              "thus lowering the number of apertures in the Gerber object.")
+        )
+        grid0.addWidget(self.gerber_clean_cb, 7, 0, 1, 3)
+
+        # Apply Extra Buffering
+        self.gerber_extra_buffering = FCCheckBox(label='%s' % _('Polarity change buffer'))
+        self.gerber_extra_buffering.setToolTip(
+            _("Will apply extra buffering for the\n"
+              "solid geometry when we have polarity changes.\n"
+              "May help loading Gerber files that otherwise\n"
+              "do not load correctly.")
+        )
+        grid0.addWidget(self.gerber_extra_buffering, 8, 0, 1, 3)
+
         self.layout.addStretch()
         self.layout.addStretch()
 
 
 
 
@@ -1528,6 +1621,11 @@ class GerberOptPrefGroupUI(OptionsGroupUI):
         )
         )
         grid0.addWidget(self.combine_passes_cb, 5, 0, 1, 2)
         grid0.addWidget(self.combine_passes_cb, 5, 0, 1, 2)
 
 
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 6, 0, 1, 2)
+
         # ## Clear non-copper regions
         # ## Clear non-copper regions
         self.clearcopper_label = QtWidgets.QLabel("<b>%s:</b>" % _("Non-copper regions"))
         self.clearcopper_label = QtWidgets.QLabel("<b>%s:</b>" % _("Non-copper regions"))
         self.clearcopper_label.setToolTip(
         self.clearcopper_label.setToolTip(
@@ -1564,6 +1662,11 @@ class GerberOptPrefGroupUI(OptionsGroupUI):
         )
         )
         grid1.addWidget(self.noncopper_rounded_cb, 1, 0, 1, 2)
         grid1.addWidget(self.noncopper_rounded_cb, 1, 0, 1, 2)
 
 
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid1.addWidget(separator_line, 2, 0, 1, 2)
+
         # ## Bounding box
         # ## Bounding box
         self.boundingbox_label = QtWidgets.QLabel('<b>%s:</b>' % _('Bounding Box'))
         self.boundingbox_label = QtWidgets.QLabel('<b>%s:</b>' % _('Bounding Box'))
         self.layout.addWidget(self.boundingbox_label)
         self.layout.addWidget(self.boundingbox_label)
@@ -1634,6 +1737,11 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
         )
         )
         grid0.addWidget(self.aperture_table_visibility_cb, 1, 0, 1, 2)
         grid0.addWidget(self.aperture_table_visibility_cb, 1, 0, 1, 2)
 
 
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 2, 0, 1, 2)
+
         # Tool Type
         # Tool Type
         self.tool_type_label = QtWidgets.QLabel('<b>%s</b>' % _('Tool Type'))
         self.tool_type_label = QtWidgets.QLabel('<b>%s</b>' % _('Tool Type'))
         self.tool_type_label.setToolTip(
         self.tool_type_label.setToolTip(
@@ -1645,8 +1753,8 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
         self.tool_type_radio = RadioSet([{'label': 'Circular', 'value': 'circular'},
         self.tool_type_radio = RadioSet([{'label': 'Circular', 'value': 'circular'},
                                          {'label': 'V-Shape', 'value': 'v'}])
                                          {'label': 'V-Shape', 'value': 'v'}])
 
 
-        grid0.addWidget(self.tool_type_label, 2, 0)
-        grid0.addWidget(self.tool_type_radio, 2, 1, 1, 2)
+        grid0.addWidget(self.tool_type_label, 3, 0)
+        grid0.addWidget(self.tool_type_radio, 3, 1, 1, 2)
 
 
         # Tip Dia
         # Tip Dia
         self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia'))
         self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia'))
@@ -1658,8 +1766,8 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
         self.tipdia_spinner.set_range(-99.9999, 99.9999)
         self.tipdia_spinner.set_range(-99.9999, 99.9999)
         self.tipdia_spinner.setSingleStep(0.1)
         self.tipdia_spinner.setSingleStep(0.1)
         self.tipdia_spinner.setWrapping(True)
         self.tipdia_spinner.setWrapping(True)
-        grid0.addWidget(self.tipdialabel, 3, 0)
-        grid0.addWidget(self.tipdia_spinner, 3, 1, 1, 2)
+        grid0.addWidget(self.tipdialabel, 4, 0)
+        grid0.addWidget(self.tipdia_spinner, 4, 1, 1, 2)
 
 
         # Tip Angle
         # Tip Angle
         self.tipanglelabel = QtWidgets.QLabel('%s:' % _('V-Tip Angle'))
         self.tipanglelabel = QtWidgets.QLabel('%s:' % _('V-Tip Angle'))
@@ -1671,8 +1779,8 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
         self.tipangle_spinner.set_range(0, 180)
         self.tipangle_spinner.set_range(0, 180)
         self.tipangle_spinner.setSingleStep(5)
         self.tipangle_spinner.setSingleStep(5)
         self.tipangle_spinner.setWrapping(True)
         self.tipangle_spinner.setWrapping(True)
-        grid0.addWidget(self.tipanglelabel, 4, 0)
-        grid0.addWidget(self.tipangle_spinner, 4, 1, 1, 2)
+        grid0.addWidget(self.tipanglelabel, 5, 0)
+        grid0.addWidget(self.tipangle_spinner, 5, 1, 1, 2)
 
 
         # Cut Z
         # Cut Z
         self.cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
         self.cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
@@ -1686,8 +1794,8 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
         self.cutz_spinner.setSingleStep(0.1)
         self.cutz_spinner.setSingleStep(0.1)
         self.cutz_spinner.setWrapping(True)
         self.cutz_spinner.setWrapping(True)
 
 
-        grid0.addWidget(self.cutzlabel, 5, 0)
-        grid0.addWidget(self.cutz_spinner, 5, 1, 1, 2)
+        grid0.addWidget(self.cutzlabel, 6, 0)
+        grid0.addWidget(self.cutz_spinner, 6, 1, 1, 2)
 
 
         # Isolation Type
         # Isolation Type
         self.iso_type_label = QtWidgets.QLabel('%s:' % _('Isolation Type'))
         self.iso_type_label = QtWidgets.QLabel('%s:' % _('Isolation Type'))
@@ -1705,8 +1813,13 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
                                         {'label': _('Exterior'), 'value': 'ext'},
                                         {'label': _('Exterior'), 'value': 'ext'},
                                         {'label': _('Interior'), 'value': 'int'}])
                                         {'label': _('Interior'), 'value': 'int'}])
 
 
-        grid0.addWidget(self.iso_type_label, 6, 0,)
-        grid0.addWidget(self.iso_type_radio, 6, 1, 1, 2)
+        grid0.addWidget(self.iso_type_label, 7, 0,)
+        grid0.addWidget(self.iso_type_radio, 7, 1, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 8, 0, 1, 2)
 
 
         # Buffering Type
         # Buffering Type
         buffering_label = QtWidgets.QLabel('%s:' % _('Buffering'))
         buffering_label = QtWidgets.QLabel('%s:' % _('Buffering'))
@@ -1718,8 +1831,8 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
         )
         )
         self.buffering_radio = RadioSet([{'label': _('None'), 'value': 'no'},
         self.buffering_radio = RadioSet([{'label': _('None'), 'value': 'no'},
                                          {'label': _('Full'), 'value': 'full'}])
                                          {'label': _('Full'), 'value': 'full'}])
-        grid0.addWidget(buffering_label, 7, 0)
-        grid0.addWidget(self.buffering_radio, 7, 1)
+        grid0.addWidget(buffering_label, 9, 0)
+        grid0.addWidget(self.buffering_radio, 9, 1)
 
 
         # Simplification
         # Simplification
         self.simplify_cb = FCCheckBox(label=_('Simplify'))
         self.simplify_cb = FCCheckBox(label=_('Simplify'))
@@ -1728,7 +1841,7 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
               "loaded with simplification having a set tolerance.\n"
               "loaded with simplification having a set tolerance.\n"
               "<<WARNING>>: Don't change this unless you know what you are doing !!!")
               "<<WARNING>>: Don't change this unless you know what you are doing !!!")
                                     )
                                     )
-        grid0.addWidget(self.simplify_cb, 8, 0, 1, 2)
+        grid0.addWidget(self.simplify_cb, 10, 0, 1, 2)
 
 
         # Simplification tolerance
         # Simplification tolerance
         self.simplification_tol_label = QtWidgets.QLabel(_('Tolerance'))
         self.simplification_tol_label = QtWidgets.QLabel(_('Tolerance'))
@@ -1740,8 +1853,8 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
         self.simplification_tol_spinner.setRange(0.00000, 0.01000)
         self.simplification_tol_spinner.setRange(0.00000, 0.01000)
         self.simplification_tol_spinner.setSingleStep(0.0001)
         self.simplification_tol_spinner.setSingleStep(0.0001)
 
 
-        grid0.addWidget(self.simplification_tol_label, 9, 0)
-        grid0.addWidget(self.simplification_tol_spinner, 9, 1)
+        grid0.addWidget(self.simplification_tol_label, 11, 0)
+        grid0.addWidget(self.simplification_tol_spinner, 11, 1)
         self.ois_simplif = OptionalInputSection(
         self.ois_simplif = OptionalInputSection(
             self.simplify_cb,
             self.simplify_cb,
             [
             [

+ 22 - 1
flatcamParsers/ParseGerber.py

@@ -72,6 +72,8 @@ class Gerber(Geometry):
     #     "use_buffer_for_union": True
     #     "use_buffer_for_union": True
     # }
     # }
 
 
+    app = None
+
     def __init__(self, steps_per_circle=None):
     def __init__(self, steps_per_circle=None):
         """
         """
         The constructor takes no parameters. Use ``gerber.parse_files()``
         The constructor takes no parameters. Use ``gerber.parse_files()``
@@ -465,11 +467,12 @@ class Gerber(Geometry):
                             geo_dict['follow'] = geo_f
                             geo_dict['follow'] = geo_f
 
 
                         geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
                         geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
-                        if not geo_s.is_empty:
+                        if not geo_s.is_empty and geo_s.is_valid:
                             if self.app.defaults['gerber_simplification']:
                             if self.app.defaults['gerber_simplification']:
                                 poly_buffer.append(geo_s.simplify(s_tol))
                                 poly_buffer.append(geo_s.simplify(s_tol))
                             else:
                             else:
                                 poly_buffer.append(geo_s)
                                 poly_buffer.append(geo_s)
+
                             if self.is_lpc is True:
                             if self.is_lpc is True:
                                 geo_dict['clear'] = geo_s
                                 geo_dict['clear'] = geo_s
                             else:
                             else:
@@ -1430,6 +1433,18 @@ class Gerber(Geometry):
 
 
                     self.solid_geometry = final_poly
                     self.solid_geometry = final_poly
 
 
+                # FIX for issue #347 - Sprint Layout generate strange Gerber files when the copper pour is enabled
+                # it use a filled bounding box polygon to which add clear polygons (negative) to isolate the copper
+                # features
+                if self.app.defaults['gerber_extra_buffering']:
+                    candidate_geo = list()
+                    try:
+                        for p in self.solid_geometry:
+                            candidate_geo.append(p.buffer(0.0000001))
+                    except TypeError:
+                        candidate_geo.append(self.solid_geometry.buffer(0.0000001))
+                    self.solid_geometry = candidate_geo
+
                 # try:
                 # try:
                 #     self.solid_geometry = self.solid_geometry.union(new_poly)
                 #     self.solid_geometry = self.solid_geometry.union(new_poly)
                 # except Exception as e:
                 # except Exception as e:
@@ -1442,6 +1457,12 @@ class Gerber(Geometry):
             else:
             else:
                 self.solid_geometry = self.solid_geometry.difference(new_poly)
                 self.solid_geometry = self.solid_geometry.difference(new_poly)
 
 
+            if self.app.defaults['gerber_clean_apertures']:
+                # clean the Gerber file of apertures with no geometry
+                for apid, apvalue in list(self.apertures.items()):
+                    if 'geometry' not in apvalue:
+                        self.apertures.pop(apid)
+
             # init this for the following operations
             # init this for the following operations
             self.conversion_done = False
             self.conversion_done = False
         except Exception as err:
         except Exception as err:

+ 154 - 8
flatcamTools/ToolDblSided.py

@@ -2,9 +2,11 @@
 from PyQt5 import QtWidgets, QtCore
 from PyQt5 import QtWidgets, QtCore
 
 
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
-from flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, EvalEntry
+from flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, EvalEntry, FCEntry
 from FlatCAMObj import FlatCAMGerber, FlatCAMExcellon, FlatCAMGeometry
 from FlatCAMObj import FlatCAMGerber, FlatCAMExcellon, FlatCAMGeometry
 
 
+from numpy import Inf
+
 from shapely.geometry import Point
 from shapely.geometry import Point
 from shapely import affinity
 from shapely import affinity
 
 
@@ -219,6 +221,11 @@ class DblSidedTool(FlatCAMTool):
         self.box_combo.hide()
         self.box_combo.hide()
         self.box_combo_type.hide()
         self.box_combo_type.hide()
 
 
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay2.addWidget(separator_line, 12, 0, 1, 2)
+
         # ## Alignment holes
         # ## Alignment holes
         self.ah_label = QtWidgets.QLabel("<b>%s:</b>" % _('Alignment Drill Coordinates'))
         self.ah_label = QtWidgets.QLabel("<b>%s:</b>" % _('Alignment Drill Coordinates'))
         self.ah_label.setToolTip(
         self.ah_label.setToolTip(
@@ -272,14 +279,14 @@ class DblSidedTool(FlatCAMTool):
         # Drill diameter value
         # Drill diameter value
         self.drill_dia = FCDoubleSpinner()
         self.drill_dia = FCDoubleSpinner()
         self.drill_dia.set_precision(self.decimals)
         self.drill_dia.set_precision(self.decimals)
+        self.drill_dia.set_range(0.0000, 9999.9999)
 
 
-        self.dd_label = QtWidgets.QLabel('%s:' % _("Drill dia"))
-        self.dd_label.setToolTip(
+        self.drill_dia.setToolTip(
             _("Diameter of the drill for the "
             _("Diameter of the drill for the "
               "alignment holes.")
               "alignment holes.")
         )
         )
-        grid0.addWidget(self.dd_label, 1, 0)
-        grid0.addWidget(self.drill_dia, 1, 1)
+
+        grid0.addWidget(self.drill_dia, 1, 0, 1, 2)
 
 
         # ## Buttons
         # ## Buttons
         self.create_alignment_hole_button = QtWidgets.QPushButton(_("Create Excellon Object"))
         self.create_alignment_hole_button = QtWidgets.QPushButton(_("Create Excellon Object"))
@@ -296,6 +303,102 @@ class DblSidedTool(FlatCAMTool):
                         """)
                         """)
         self.layout.addWidget(self.create_alignment_hole_button)
         self.layout.addWidget(self.create_alignment_hole_button)
 
 
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.layout.addWidget(separator_line)
+
+        self.layout.addWidget(QtWidgets.QLabel(''))
+
+        grid1 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid1)
+        grid1.setColumnStretch(0, 0)
+        grid1.setColumnStretch(1, 1)
+
+        # Xmin value
+        self.xmin_entry = FCDoubleSpinner()
+        self.xmin_entry.set_precision(self.decimals)
+        self.xmin_entry.set_range(-9999.9999, 9999.9999)
+
+        self.xmin_label = QtWidgets.QLabel('%s:' % _("X min"))
+        self.xmin_label.setToolTip(
+            _("Minimum location.")
+        )
+        self.xmin_entry.setReadOnly(True)
+
+        grid1.addWidget(self.xmin_label, 1, 0)
+        grid1.addWidget(self.xmin_entry, 1, 1)
+
+        # Ymin value
+        self.ymin_entry = FCDoubleSpinner()
+        self.ymin_entry.set_precision(self.decimals)
+        self.ymin_entry.set_range(-9999.9999, 9999.9999)
+
+        self.ymin_label = QtWidgets.QLabel('%s:' % _("Y min"))
+        self.ymin_label.setToolTip(
+            _("Minimum location.")
+        )
+        self.ymin_entry.setReadOnly(True)
+
+        grid1.addWidget(self.ymin_label, 2, 0)
+        grid1.addWidget(self.ymin_entry, 2, 1)
+
+        # Xmax value
+        self.xmax_entry = FCDoubleSpinner()
+        self.xmax_entry.set_precision(self.decimals)
+        self.xmax_entry.set_range(-9999.9999, 9999.9999)
+
+        self.xmax_label = QtWidgets.QLabel('%s:' % _("X max"))
+        self.xmax_label.setToolTip(
+            _("Maximum location.")
+        )
+        self.xmax_entry.setReadOnly(True)
+
+        grid1.addWidget(self.xmax_label, 3, 0)
+        grid1.addWidget(self.xmax_entry, 3, 1)
+
+        # Ymax value
+        self.ymax_entry = FCDoubleSpinner()
+        self.ymax_entry.set_precision(self.decimals)
+        self.ymax_entry.set_range(-9999.9999, 9999.9999)
+
+        self.ymax_label = QtWidgets.QLabel('%s:' % _("Y max"))
+        self.ymax_label.setToolTip(
+            _("Maximum location.")
+        )
+        self.ymax_entry.setReadOnly(True)
+
+        grid1.addWidget(self.ymax_label, 4, 0)
+        grid1.addWidget(self.ymax_entry, 4, 1)
+
+        # Center point value
+        self.center_entry = FCEntry()
+
+        self.center_label = QtWidgets.QLabel('%s:' % _("Centroid"))
+        self.center_label.setToolTip(
+            _("The center point location for the rectangular\n"
+              "bounding shape. Centroid. Format is (x, y).")
+        )
+        self.center_entry.setReadOnly(True)
+
+        grid1.addWidget(self.center_label, 5, 0)
+        grid1.addWidget(self.center_entry, 5, 1)
+
+        # Calculate Bounding box
+        self.calculate_bb_button = QtWidgets.QPushButton(_("Calculate Bounds Values"))
+        self.calculate_bb_button.setToolTip(
+            _("Calculate the enveloping rectangular shape coordinates,\n"
+              "for the selection of objects.\n"
+              "The envelope shape is parallel with the X, Y axis.")
+        )
+        self.calculate_bb_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.calculate_bb_button)
+
         self.layout.addStretch()
         self.layout.addStretch()
 
 
         # ## Reset Tool
         # ## Reset Tool
@@ -312,18 +415,20 @@ class DblSidedTool(FlatCAMTool):
         self.layout.addWidget(self.reset_button)
         self.layout.addWidget(self.reset_button)
 
 
         # ## Signals
         # ## Signals
-        self.create_alignment_hole_button.clicked.connect(self.on_create_alignment_holes)
         self.mirror_gerber_button.clicked.connect(self.on_mirror_gerber)
         self.mirror_gerber_button.clicked.connect(self.on_mirror_gerber)
         self.mirror_exc_button.clicked.connect(self.on_mirror_exc)
         self.mirror_exc_button.clicked.connect(self.on_mirror_exc)
         self.mirror_geo_button.clicked.connect(self.on_mirror_geo)
         self.mirror_geo_button.clicked.connect(self.on_mirror_geo)
         self.add_point_button.clicked.connect(self.on_point_add)
         self.add_point_button.clicked.connect(self.on_point_add)
         self.add_drill_point_button.clicked.connect(self.on_drill_add)
         self.add_drill_point_button.clicked.connect(self.on_drill_add)
-        self.reset_button.clicked.connect(self.reset_fields)
-
         self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
         self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
 
 
         self.axis_location.group_toggle_fn = self.on_toggle_pointbox
         self.axis_location.group_toggle_fn = self.on_toggle_pointbox
 
 
+        self.create_alignment_hole_button.clicked.connect(self.on_create_alignment_holes)
+        self.calculate_bb_button.clicked.connect(self.on_bbox_coordinates)
+
+        self.reset_button.clicked.connect(self.set_tool_ui)
+
         self.drill_values = ""
         self.drill_values = ""
 
 
     def install(self, icon=None, separator=None, **kwargs):
     def install(self, icon=None, separator=None, **kwargs):
@@ -366,6 +471,12 @@ class DblSidedTool(FlatCAMTool):
         self.axis_location.set_value(self.app.defaults["tools_2sided_axis_loc"])
         self.axis_location.set_value(self.app.defaults["tools_2sided_axis_loc"])
         self.drill_dia.set_value(self.app.defaults["tools_2sided_drilldia"])
         self.drill_dia.set_value(self.app.defaults["tools_2sided_drilldia"])
 
 
+        self.xmin_entry.set_value(0.0)
+        self.ymin_entry.set_value(0.0)
+        self.xmax_entry.set_value(0.0)
+        self.ymax_entry.set_value(0.0)
+        self.center_entry.set_value('')
+
     def on_combo_box_type(self):
     def on_combo_box_type(self):
         obj_type = self.box_combo_type.currentIndex()
         obj_type = self.box_combo_type.currentIndex()
         self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
         self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
@@ -589,6 +700,41 @@ class DblSidedTool(FlatCAMTool):
             self.box_combo_type.show()
             self.box_combo_type.show()
             self.add_point_button.setDisabled(True)
             self.add_point_button.setDisabled(True)
 
 
+    def on_bbox_coordinates(self):
+
+        xmin = Inf
+        ymin = Inf
+        xmax = -Inf
+        ymax = -Inf
+
+        obj_list = self.app.collection.get_selected()
+
+        if not obj_list:
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed. No object(s) selected..."))
+            return
+
+        for obj in obj_list:
+            try:
+                gxmin, gymin, gxmax, gymax = obj.bounds()
+                xmin = min([xmin, gxmin])
+                ymin = min([ymin, gymin])
+                xmax = max([xmax, gxmax])
+                ymax = max([ymax, gymax])
+            except Exception as e:
+                log.warning("DEV WARNING: Tried to get bounds of empty geometry in DblSidedTool. %s" % str(e))
+
+        self.xmin_entry.set_value(xmin)
+        self.ymin_entry.set_value(ymin)
+        self.xmax_entry.set_value(xmax)
+        self.ymax_entry.set_value(ymax)
+        cx = '%.*f' % (self.decimals, (((xmax - xmin) / 2.0) + xmin))
+        cy = '%.*f' % (self.decimals, (((ymax - ymin) / 2.0) + ymin))
+        val_txt = '(%s, %s)' % (cx, cy)
+
+        self.center_entry.set_value(val_txt)
+        self.axis_location.set_value('point')
+        self.point_entry.set_value(val_txt)
+
     def reset_fields(self):
     def reset_fields(self):
         self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
         self.exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))

+ 22 - 13
flatcamTools/ToolNonCopperClear.py

@@ -1723,11 +1723,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
 
                 sol_geo = cascaded_union(isolated_geo)
                 sol_geo = cascaded_union(isolated_geo)
                 if has_offset is True:
                 if has_offset is True:
-                    app_obj.inform.emit('[WARNING_NOTCL] %s ...' %
-                                        _("Buffering"))
+                    app_obj.inform.emit('[WARNING_NOTCL] %s ...' % _("Buffering"))
                     sol_geo = sol_geo.buffer(distance=ncc_offset)
                     sol_geo = sol_geo.buffer(distance=ncc_offset)
-                    app_obj.inform.emit('[success] %s ...' %
-                                        _("Buffering finished"))
+                    app_obj.inform.emit('[success] %s ...' % _("Buffering finished"))
                 empty = self.get_ncc_empty_area(target=sol_geo, boundary=bounding_box)
                 empty = self.get_ncc_empty_area(target=sol_geo, boundary=bounding_box)
                 if empty == 'fail':
                 if empty == 'fail':
                     return 'fail'
                     return 'fail'
@@ -1760,6 +1758,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
             log.debug("NCC Tool. Finished calculation of 'empty' area.")
             log.debug("NCC Tool. Finished calculation of 'empty' area.")
             self.app.inform.emit(_("NCC Tool. Finished calculation of 'empty' area."))
             self.app.inform.emit(_("NCC Tool. Finished calculation of 'empty' area."))
 
 
+            # COPPER CLEARING #
             cp = None
             cp = None
             for tool in sorted_tools:
             for tool in sorted_tools:
                 log.debug("Starting geometry processing for tool: %s" % str(tool))
                 log.debug("Starting geometry processing for tool: %s" % str(tool))
@@ -1916,17 +1915,27 @@ class NonCopperClear(FlatCAMTool, Gerber):
             if self.app.defaults["tools_ncc_plotting"] == 'progressive':
             if self.app.defaults["tools_ncc_plotting"] == 'progressive':
                 self.temp_shapes.clear(update=True)
                 self.temp_shapes.clear(update=True)
 
 
+            # # delete tools with empty geometry
+            # keys_to_delete = []
+            # # look for keys in the tools_storage dict that have 'solid_geometry' values empty
+            # for uid in tools_storage:
+            #     # if the solid_geometry (type=list) is empty
+            #     if not tools_storage[uid]['solid_geometry']:
+            #         keys_to_delete.append(uid)
+            #
+            # # actual delete of keys from the tools_storage dict
+            # for k in keys_to_delete:
+            #     tools_storage.pop(k, None)
+
             # delete tools with empty geometry
             # delete tools with empty geometry
-            keys_to_delete = []
             # look for keys in the tools_storage dict that have 'solid_geometry' values empty
             # look for keys in the tools_storage dict that have 'solid_geometry' values empty
-            for uid in tools_storage:
-                # if the solid_geometry (type=list) is empty
-                if not tools_storage[uid]['solid_geometry']:
-                    keys_to_delete.append(uid)
-
-            # actual delete of keys from the tools_storage dict
-            for k in keys_to_delete:
-                tools_storage.pop(k, None)
+            for uid, uid_val in list(tools_storage.items()):
+                try:
+                    # if the solid_geometry (type=list) is empty
+                    if not uid_val['solid_geometry']:
+                        tools_storage.pop(uid, None)
+                except KeyError:
+                    tools_storage.pop(uid, None)
 
 
             geo_obj.options["cnctooldia"] = str(tool)
             geo_obj.options["cnctooldia"] = str(tool)
 
 

BIN
share/printer16.png


BIN
share/printer32.png