Explorar el Código

Merged Beta_8.994 into AutolevellingFeature

Marius Stanciu hace 5 años
padre
commit
2bbab19e0a
Se han modificado 64 ficheros con 3751 adiciones y 2658 borrados
  1. 50 0
      CHANGELOG.md
  2. 50 50
      appEditors/AppGeoEditor.py
  3. 3 3
      appEditors/AppGerberEditor.py
  4. 2 2
      appGUI/MainGUI.py
  5. 129 22
      appGUI/ObjectUI.py
  6. 9 1
      appGUI/preferences/PreferencesUIManager.py
  7. 4 6
      appGUI/preferences/geometry/GeometryAdvOptPrefGroupUI.py
  8. 3 3
      appGUI/preferences/tools/Tools2InvertPrefGroupUI.py
  9. 82 22
      appGUI/preferences/tools/ToolsDrillPrefGroupUI.py
  10. 29 18
      appGUI/preferences/tools/ToolsFilmPrefGroupUI.py
  11. 10 0
      appGUI/preferences/tools/ToolsISOPrefGroupUI.py
  12. 22 2
      appObjects/FlatCAMCNCJob.py
  13. 39 15
      appObjects/FlatCAMExcellon.py
  14. 82 11
      appObjects/FlatCAMGeometry.py
  15. 20 3
      appObjects/FlatCAMGerber.py
  16. 452 3
      appObjects/FlatCAMObj.py
  17. 1 1
      appObjects/ObjectCollection.py
  18. 2 2
      appParsers/ParseGerber.py
  19. 18 9
      appParsers/ParseSVG.py
  20. 7 7
      appTools/ToolCopperThieving.py
  21. 8 5
      appTools/ToolCutOut.py
  22. 3 3
      appTools/ToolDistanceMin.py
  23. 218 46
      appTools/ToolFilm.py
  24. 142 93
      appTools/ToolIsolation.py
  25. 1 1
      appTools/ToolMilling.py
  26. 41 34
      appTools/ToolNCC.py
  27. 16 14
      appTools/ToolPaint.py
  28. 2 2
      appTools/ToolPanelize.py
  29. 7 7
      appTools/ToolProperties.py
  30. 0 1
      appTools/ToolQRCode.py
  31. 2 2
      appTools/ToolSolderPaste.py
  32. 3 3
      appTools/ToolSub.py
  33. 9 60
      app_Main.py
  34. 108 46
      camlib.py
  35. 16 4
      defaults.py
  36. BIN
      locale/de/LC_MESSAGES/strings.mo
  37. 187 182
      locale/de/LC_MESSAGES/strings.po
  38. BIN
      locale/en/LC_MESSAGES/strings.mo
  39. 209 231
      locale/en/LC_MESSAGES/strings.po
  40. BIN
      locale/es/LC_MESSAGES/strings.mo
  41. 194 189
      locale/es/LC_MESSAGES/strings.po
  42. BIN
      locale/fr/LC_MESSAGES/strings.mo
  43. 194 189
      locale/fr/LC_MESSAGES/strings.po
  44. BIN
      locale/hu/LC_MESSAGES/strings.mo
  45. 194 189
      locale/hu/LC_MESSAGES/strings.po
  46. BIN
      locale/it/LC_MESSAGES/strings.mo
  47. 195 190
      locale/it/LC_MESSAGES/strings.po
  48. BIN
      locale/pt_BR/LC_MESSAGES/strings.mo
  49. 194 189
      locale/pt_BR/LC_MESSAGES/strings.po
  50. BIN
      locale/ro/LC_MESSAGES/strings.mo
  51. 187 182
      locale/ro/LC_MESSAGES/strings.po
  52. BIN
      locale/ru/LC_MESSAGES/strings.mo
  53. 193 188
      locale/ru/LC_MESSAGES/strings.po
  54. BIN
      locale/tr/LC_MESSAGES/strings.mo
  55. 197 213
      locale/tr/LC_MESSAGES/strings.po
  56. 202 200
      locale_template/strings.pot
  57. 2 2
      tclCommands/TclCommandBbox.py
  58. 2 2
      tclCommands/TclCommandCutout.py
  59. 3 3
      tclCommands/TclCommandExportSVG.py
  60. 3 3
      tclCommands/TclCommandGeoCutout.py
  61. 2 2
      tclCommands/TclCommandNregions.py
  62. 1 1
      tests/other/test_plotg.py
  63. 1 1
      tests/test_paint.py
  64. 1 1
      tests/test_pathconnect.py

+ 50 - 0
CHANGELOG.md

@@ -7,6 +7,56 @@ CHANGELOG for FlatCAM beta
 
 
 =================================================
 =================================================
 
 
+21.10.2020
+
+- in Geometry Object fixed the issue with not using the End X-Y value and also made some other updates here
+- in NCC and Paint Tool fixed some issues with missing keys in the tool data dictionary
+- In Excellon Object UI fixed the enable/disable for the Milling section according to the Tools Table row that is selected
+- In Excellon Object UI fixed the milling geometry generation
+- updated the translations strings to the changes in the source code
+- some strings changed
+- fixed crash on using shortcut for creating a new Document Object
+- fixed Cutout Tool to work with the endxy parameter
+- added the exclusion parameters for Drilling Tool to the Preferences area
+- cascaded_union() method will be deprecated in Shapely 1.8 in favor of unary_union; replaced the usage of cascaded_union with unary_union in all the app
+- added some strings to the translatable strings and updated the translation strings
+
+20.10.2020
+
+- finished to add the Properties data to the Object Properties (former Selected Tab)
+
+19.10.2020
+
+- added a check (and added to Preferences too) for the verification of tools validity in the Isolation Tool
+- fixed QrCode Tool
+- updated the Turkish translation (by Mehmet Kaya)
+
+18.10.2020
+
+- fixed issue with calling the inform signal in the FlatCAMDefaults.load method
+- fixed macro parsing in Gerber files generated by KiCAD 4.99 (KiCAD 5.0)
+
+17.10.2020
+
+- updated Turkish translation (by Mehmet Kaya)
+
+8.10.2020
+
+- small change in the NCC Tool UI
+- some strings are changed and therefore the translation strings source are updated
+- Isolation Tool - added a check for having a complete isolation
+
+7.10.2020
+
+- working on adding DPI setting for PNG export in Film Tool - update
+- finished updating DPI setting feature for PNG export in Film Tool
+
+5.10.2020
+
+- working on adding DPI setting for PNG export in the Film Tool
+- finished working in adding DPI settings for PNG export in Film Tool although there are some limitations due of Reportlab
+- small change in TclCommandExportSVG
+
 26.09.2020
 26.09.2020
 
 
 - the Selected Tab is now Properties Tab for FlatCAM objects
 - the Selected Tab is now Properties Tab for FlatCAM objects

+ 50 - 50
appEditors/AppGeoEditor.py

@@ -16,12 +16,12 @@ from PyQt5.QtCore import Qt, QSettings
 
 
 from camlib import distance, arc, three_point_circle, Geometry, FlatCAMRTreeStorage
 from camlib import distance, arc, three_point_circle, Geometry, FlatCAMRTreeStorage
 from appTool import AppTool
 from appTool import AppTool
-from appGUI.GUIElements import OptionalInputSection, FCCheckBox, FCEntry, FCComboBox, FCTextAreaRich, \
+from appGUI.GUIElements import OptionalInputSection, FCCheckBox, FCLabel, FCComboBox, FCTextAreaRich, \
     FCDoubleSpinner, FCButton, FCInputDialog, FCTree, NumericalEvalTupleEntry
     FCDoubleSpinner, FCButton, FCInputDialog, FCTree, NumericalEvalTupleEntry
 from appParsers.ParseFont import *
 from appParsers.ParseFont import *
 
 
 from shapely.geometry import LineString, LinearRing, MultiLineString, Polygon, MultiPolygon
 from shapely.geometry import LineString, LinearRing, MultiLineString, Polygon, MultiPolygon
-from shapely.ops import cascaded_union, unary_union, linemerge
+from shapely.ops import unary_union, linemerge
 import shapely.affinity as affinity
 import shapely.affinity as affinity
 from shapely.geometry.polygon import orient
 from shapely.geometry.polygon import orient
 
 
@@ -46,7 +46,7 @@ class BufferSelectionTool(AppTool):
     Simple input for buffer distance.
     Simple input for buffer distance.
     """
     """
 
 
-    toolName = "Buffer Selection"
+    toolName = _("Buffer Selection")
 
 
     def __init__(self, app, draw_app):
     def __init__(self, app, draw_app):
         AppTool.__init__(self, app)
         AppTool.__init__(self, app)
@@ -55,7 +55,7 @@ class BufferSelectionTool(AppTool):
         self.decimals = app.decimals
         self.decimals = app.decimals
 
 
         # Title
         # Title
-        title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName))
+        title_label = FCLabel("%s" % ('Editor ' + self.toolName))
         title_label.setStyleSheet("""
         title_label.setStyleSheet("""
                         QLabel
                         QLabel
                         {
                         {
@@ -82,7 +82,7 @@ class BufferSelectionTool(AppTool):
         self.buffer_distance_entry.set_precision(self.decimals)
         self.buffer_distance_entry.set_precision(self.decimals)
         self.buffer_distance_entry.set_range(0.0000, 999999.9999)
         self.buffer_distance_entry.set_range(0.0000, 999999.9999)
         form_layout.addRow(_("Buffer distance:"), self.buffer_distance_entry)
         form_layout.addRow(_("Buffer distance:"), self.buffer_distance_entry)
-        self.buffer_corner_lbl = QtWidgets.QLabel(_("Buffer corner:"))
+        self.buffer_corner_lbl = FCLabel(_("Buffer corner:"))
         self.buffer_corner_lbl.setToolTip(
         self.buffer_corner_lbl.setToolTip(
             _("There are 3 types of corners:\n"
             _("There are 3 types of corners:\n"
               " - 'Round': the corner is rounded for exterior buffer.\n"
               " - 'Round': the corner is rounded for exterior buffer.\n"
@@ -99,15 +99,15 @@ class BufferSelectionTool(AppTool):
         hlay = QtWidgets.QHBoxLayout()
         hlay = QtWidgets.QHBoxLayout()
         self.buffer_tools_box.addLayout(hlay)
         self.buffer_tools_box.addLayout(hlay)
 
 
-        self.buffer_int_button = QtWidgets.QPushButton(_("Buffer Interior"))
+        self.buffer_int_button = FCButton(_("Buffer Interior"))
         hlay.addWidget(self.buffer_int_button)
         hlay.addWidget(self.buffer_int_button)
-        self.buffer_ext_button = QtWidgets.QPushButton(_("Buffer Exterior"))
+        self.buffer_ext_button = FCButton(_("Buffer Exterior"))
         hlay.addWidget(self.buffer_ext_button)
         hlay.addWidget(self.buffer_ext_button)
 
 
         hlay1 = QtWidgets.QHBoxLayout()
         hlay1 = QtWidgets.QHBoxLayout()
         self.buffer_tools_box.addLayout(hlay1)
         self.buffer_tools_box.addLayout(hlay1)
 
 
-        self.buffer_button = QtWidgets.QPushButton(_("Full Buffer"))
+        self.buffer_button = FCButton(_("Full Buffer"))
         hlay1.addWidget(self.buffer_button)
         hlay1.addWidget(self.buffer_button)
 
 
         self.layout.addStretch()
         self.layout.addStretch()
@@ -191,7 +191,7 @@ class TextInputTool(AppTool):
     Simple input for buffer distance.
     Simple input for buffer distance.
     """
     """
 
 
-    toolName = "Text Input Tool"
+    toolName = _("Text Input Tool")
 
 
     def __init__(self, app):
     def __init__(self, app):
         AppTool.__init__(self, app)
         AppTool.__init__(self, app)
@@ -212,7 +212,7 @@ class TextInputTool(AppTool):
         self.text_tool_frame.setLayout(self.text_tools_box)
         self.text_tool_frame.setLayout(self.text_tools_box)
 
 
         # Title
         # Title
-        title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName))
+        title_label = FCLabel("%s" % self.toolName)
         title_label.setStyleSheet("""
         title_label.setStyleSheet("""
                         QLabel
                         QLabel
                         {
                         {
@@ -238,7 +238,7 @@ class TextInputTool(AppTool):
 
 
         self.font_type_cb = QtWidgets.QFontComboBox(self)
         self.font_type_cb = QtWidgets.QFontComboBox(self)
         self.font_type_cb.setCurrentFont(f_current)
         self.font_type_cb.setCurrentFont(f_current)
-        self.form_layout.addRow(QtWidgets.QLabel('%s:' % _("Font")), self.font_type_cb)
+        self.form_layout.addRow(FCLabel('%s:' % _("Font")), self.font_type_cb)
 
 
         # Flag variables to show if font is bold, italic, both or none (regular)
         # Flag variables to show if font is bold, italic, both or none (regular)
         self.font_bold = False
         self.font_bold = False
@@ -310,7 +310,7 @@ class TextInputTool(AppTool):
         self.font_italic_tb.setIcon(QtGui.QIcon(self.app.resource_location + '/italic32.png'))
         self.font_italic_tb.setIcon(QtGui.QIcon(self.app.resource_location + '/italic32.png'))
         hlay.addWidget(self.font_italic_tb)
         hlay.addWidget(self.font_italic_tb)
 
 
-        self.form_layout.addRow(QtWidgets.QLabel('%s:' % "Size"), hlay)
+        self.form_layout.addRow(FCLabel('%s:' % "Size"), hlay)
 
 
         # Text input
         # Text input
         self.text_input_entry = FCTextAreaRich()
         self.text_input_entry = FCTextAreaRich()
@@ -319,13 +319,13 @@ class TextInputTool(AppTool):
         # self.text_input_entry.setMaximumHeight(150)
         # self.text_input_entry.setMaximumHeight(150)
         self.text_input_entry.setCurrentFont(f_current)
         self.text_input_entry.setCurrentFont(f_current)
         self.text_input_entry.setFontPointSize(10)
         self.text_input_entry.setFontPointSize(10)
-        self.form_layout.addRow(QtWidgets.QLabel('%s:' % _("Text")), self.text_input_entry)
+        self.form_layout.addRow(FCLabel('%s:' % _("Text")), self.text_input_entry)
 
 
         # Buttons
         # Buttons
         hlay1 = QtWidgets.QHBoxLayout()
         hlay1 = QtWidgets.QHBoxLayout()
         self.form_layout.addRow("", hlay1)
         self.form_layout.addRow("", hlay1)
         hlay1.addStretch()
         hlay1.addStretch()
-        self.apply_button = QtWidgets.QPushButton("Apply")
+        self.apply_button = FCButton(_("Apply"))
         hlay1.addWidget(self.apply_button)
         hlay1.addWidget(self.apply_button)
 
 
         # self.layout.addStretch()
         # self.layout.addStretch()
@@ -409,7 +409,7 @@ class PaintOptionsTool(AppTool):
     Inputs to specify how to paint the selected polygons.
     Inputs to specify how to paint the selected polygons.
     """
     """
 
 
-    toolName = "Paint Tool"
+    toolName = _("Paint Tool")
 
 
     def __init__(self, app, fcdraw):
     def __init__(self, app, fcdraw):
         AppTool.__init__(self, app)
         AppTool.__init__(self, app)
@@ -419,7 +419,7 @@ class PaintOptionsTool(AppTool):
         self.decimals = self.app.decimals
         self.decimals = self.app.decimals
 
 
         # Title
         # Title
-        title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName))
+        title_label = FCLabel("%s" % self.toolName)
         title_label.setStyleSheet("""
         title_label.setStyleSheet("""
                         QLabel
                         QLabel
                         {
                         {
@@ -435,7 +435,7 @@ class PaintOptionsTool(AppTool):
         grid.setColumnStretch(1, 1)
         grid.setColumnStretch(1, 1)
 
 
         # Tool dia
         # Tool dia
-        ptdlabel = QtWidgets.QLabel('%s:' % _('Tool dia'))
+        ptdlabel = FCLabel('%s:' % _('Tool dia'))
         ptdlabel.setToolTip(
         ptdlabel.setToolTip(
            _("Diameter of the tool to be used in the operation.")
            _("Diameter of the tool to be used in the operation.")
         )
         )
@@ -447,7 +447,7 @@ class PaintOptionsTool(AppTool):
         grid.addWidget(self.painttooldia_entry, 0, 1)
         grid.addWidget(self.painttooldia_entry, 0, 1)
 
 
         # Overlap
         # Overlap
-        ovlabel = QtWidgets.QLabel('%s:' % _('Overlap'))
+        ovlabel = FCLabel('%s:' % _('Overlap'))
         ovlabel.setToolTip(
         ovlabel.setToolTip(
             _("How much (percentage) of the tool width to overlap each tool pass.\n"
             _("How much (percentage) of the tool width to overlap each tool pass.\n"
               "Adjust the value starting with lower values\n"
               "Adjust the value starting with lower values\n"
@@ -467,7 +467,7 @@ class PaintOptionsTool(AppTool):
         grid.addWidget(self.paintoverlap_entry, 1, 1)
         grid.addWidget(self.paintoverlap_entry, 1, 1)
 
 
         # Margin
         # Margin
-        marginlabel = QtWidgets.QLabel('%s:' % _('Margin'))
+        marginlabel = FCLabel('%s:' % _('Margin'))
         marginlabel.setToolTip(
         marginlabel.setToolTip(
            _("Distance by which to avoid\n"
            _("Distance by which to avoid\n"
              "the edges of the polygon to\n"
              "the edges of the polygon to\n"
@@ -481,7 +481,7 @@ class PaintOptionsTool(AppTool):
         grid.addWidget(self.paintmargin_entry, 2, 1)
         grid.addWidget(self.paintmargin_entry, 2, 1)
 
 
         # Method
         # Method
-        methodlabel = QtWidgets.QLabel('%s:' % _('Method'))
+        methodlabel = FCLabel('%s:' % _('Method'))
         methodlabel.setToolTip(
         methodlabel.setToolTip(
             _("Algorithm to paint the polygons:\n"
             _("Algorithm to paint the polygons:\n"
               "- Standard: Fixed step inwards.\n"
               "- Standard: Fixed step inwards.\n"
@@ -502,7 +502,7 @@ class PaintOptionsTool(AppTool):
         grid.addWidget(self.paintmethod_combo, 3, 1)
         grid.addWidget(self.paintmethod_combo, 3, 1)
 
 
         # Connect lines
         # Connect lines
-        pathconnectlabel = QtWidgets.QLabel(_("Connect:"))
+        pathconnectlabel = FCLabel('%s:' % _("Connect"))
         pathconnectlabel.setToolTip(
         pathconnectlabel.setToolTip(
            _("Draw lines between resulting\n"
            _("Draw lines between resulting\n"
              "segments to minimize tool lifts.")
              "segments to minimize tool lifts.")
@@ -512,7 +512,7 @@ class PaintOptionsTool(AppTool):
         grid.addWidget(pathconnectlabel, 4, 0)
         grid.addWidget(pathconnectlabel, 4, 0)
         grid.addWidget(self.pathconnect_cb, 4, 1)
         grid.addWidget(self.pathconnect_cb, 4, 1)
 
 
-        contourlabel = QtWidgets.QLabel(_("Contour:"))
+        contourlabel = FCLabel('%s:' % _("Contour"))
         contourlabel.setToolTip(
         contourlabel.setToolTip(
             _("Cut around the perimeter of the polygon\n"
             _("Cut around the perimeter of the polygon\n"
               "to trim rough edges.")
               "to trim rough edges.")
@@ -525,7 +525,7 @@ class PaintOptionsTool(AppTool):
         # Buttons
         # Buttons
         hlay = QtWidgets.QHBoxLayout()
         hlay = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay)
         self.layout.addLayout(hlay)
-        self.paint_button = QtWidgets.QPushButton(_("Paint"))
+        self.paint_button = FCButton(_("Paint"))
         hlay.addWidget(self.paint_button)
         hlay.addWidget(self.paint_button)
 
 
         self.layout.addStretch()
         self.layout.addStretch()
@@ -619,7 +619,7 @@ class TransformEditorTool(AppTool):
         self.decimals = self.app.decimals
         self.decimals = self.app.decimals
 
 
         # ## Title
         # ## Title
-        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label = FCLabel("%s" % self.toolName)
         title_label.setStyleSheet("""
         title_label.setStyleSheet("""
                                 QLabel
                                 QLabel
                                 {
                                 {
@@ -628,7 +628,7 @@ class TransformEditorTool(AppTool):
                                 }
                                 }
                                 """)
                                 """)
         self.layout.addWidget(title_label)
         self.layout.addWidget(title_label)
-        self.layout.addWidget(QtWidgets.QLabel(''))
+        self.layout.addWidget(FCLabel(''))
 
 
         # ## Layout
         # ## Layout
         grid0 = QtWidgets.QGridLayout()
         grid0 = QtWidgets.QGridLayout()
@@ -637,10 +637,10 @@ class TransformEditorTool(AppTool):
         grid0.setColumnStretch(1, 1)
         grid0.setColumnStretch(1, 1)
         grid0.setColumnStretch(2, 0)
         grid0.setColumnStretch(2, 0)
 
 
-        grid0.addWidget(QtWidgets.QLabel(''))
+        grid0.addWidget(FCLabel(''))
 
 
         # Reference
         # Reference
-        ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
+        ref_label = FCLabel('%s:' % _("Reference"))
         ref_label.setToolTip(
         ref_label.setToolTip(
             _("The reference point for Rotate, Skew, Scale, Mirror.\n"
             _("The reference point for Rotate, Skew, Scale, Mirror.\n"
               "Can be:\n"
               "Can be:\n"
@@ -656,7 +656,7 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(ref_label, 0, 0)
         grid0.addWidget(ref_label, 0, 0)
         grid0.addWidget(self.ref_combo, 0, 1, 1, 2)
         grid0.addWidget(self.ref_combo, 0, 1, 1, 2)
 
 
-        self.point_label = QtWidgets.QLabel('%s:' % _("Value"))
+        self.point_label = FCLabel('%s:' % _("Value"))
         self.point_label.setToolTip(
         self.point_label.setToolTip(
             _("A point of reference in format X,Y.")
             _("A point of reference in format X,Y.")
         )
         )
@@ -677,10 +677,10 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(separator_line, 5, 0, 1, 3)
         grid0.addWidget(separator_line, 5, 0, 1, 3)
 
 
         # ## Rotate Title
         # ## Rotate Title
-        rotate_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.rotateName)
+        rotate_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.rotateName)
         grid0.addWidget(rotate_title_label, 6, 0, 1, 3)
         grid0.addWidget(rotate_title_label, 6, 0, 1, 3)
 
 
-        self.rotate_label = QtWidgets.QLabel('%s:' % _("Angle"))
+        self.rotate_label = FCLabel('%s:' % _("Angle"))
         self.rotate_label.setToolTip(
         self.rotate_label.setToolTip(
             _("Angle for Rotation action, in degrees.\n"
             _("Angle for Rotation action, in degrees.\n"
               "Float number between -360 and 359.\n"
               "Float number between -360 and 359.\n"
@@ -714,7 +714,7 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(separator_line, 8, 0, 1, 3)
         grid0.addWidget(separator_line, 8, 0, 1, 3)
 
 
         # ## Skew Title
         # ## Skew Title
-        skew_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.skewName)
+        skew_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.skewName)
         grid0.addWidget(skew_title_label, 9, 0, 1, 2)
         grid0.addWidget(skew_title_label, 9, 0, 1, 2)
 
 
         self.skew_link_cb = FCCheckBox()
         self.skew_link_cb = FCCheckBox()
@@ -725,7 +725,7 @@ class TransformEditorTool(AppTool):
 
 
         grid0.addWidget(self.skew_link_cb, 9, 2)
         grid0.addWidget(self.skew_link_cb, 9, 2)
 
 
-        self.skewx_label = QtWidgets.QLabel('%s:' % _("X angle"))
+        self.skewx_label = FCLabel('%s:' % _("X angle"))
         self.skewx_label.setToolTip(
         self.skewx_label.setToolTip(
             _("Angle for Skew action, in degrees.\n"
             _("Angle for Skew action, in degrees.\n"
               "Float number between -360 and 360.")
               "Float number between -360 and 360.")
@@ -746,7 +746,7 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(self.skewx_entry, 10, 1)
         grid0.addWidget(self.skewx_entry, 10, 1)
         grid0.addWidget(self.skewx_button, 10, 2)
         grid0.addWidget(self.skewx_button, 10, 2)
 
 
-        self.skewy_label = QtWidgets.QLabel('%s:' % _("Y angle"))
+        self.skewy_label = FCLabel('%s:' % _("Y angle"))
         self.skewy_label.setToolTip(
         self.skewy_label.setToolTip(
             _("Angle for Skew action, in degrees.\n"
             _("Angle for Skew action, in degrees.\n"
               "Float number between -360 and 360.")
               "Float number between -360 and 360.")
@@ -776,7 +776,7 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(separator_line, 14, 0, 1, 3)
         grid0.addWidget(separator_line, 14, 0, 1, 3)
 
 
         # ## Scale Title
         # ## Scale Title
-        scale_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.scaleName)
+        scale_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.scaleName)
         grid0.addWidget(scale_title_label, 15, 0, 1, 2)
         grid0.addWidget(scale_title_label, 15, 0, 1, 2)
 
 
         self.scale_link_cb = FCCheckBox()
         self.scale_link_cb = FCCheckBox()
@@ -787,7 +787,7 @@ class TransformEditorTool(AppTool):
 
 
         grid0.addWidget(self.scale_link_cb, 15, 2)
         grid0.addWidget(self.scale_link_cb, 15, 2)
 
 
-        self.scalex_label = QtWidgets.QLabel('%s:' % _("X factor"))
+        self.scalex_label = FCLabel('%s:' % _("X factor"))
         self.scalex_label.setToolTip(
         self.scalex_label.setToolTip(
             _("Factor for scaling on X axis.")
             _("Factor for scaling on X axis.")
         )
         )
@@ -807,7 +807,7 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(self.scalex_entry, 17, 1)
         grid0.addWidget(self.scalex_entry, 17, 1)
         grid0.addWidget(self.scalex_button, 17, 2)
         grid0.addWidget(self.scalex_button, 17, 2)
 
 
-        self.scaley_label = QtWidgets.QLabel('%s:' % _("Y factor"))
+        self.scaley_label = FCLabel('%s:' % _("Y factor"))
         self.scaley_label.setToolTip(
         self.scaley_label.setToolTip(
             _("Factor for scaling on Y axis.")
             _("Factor for scaling on Y axis.")
         )
         )
@@ -840,7 +840,7 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(separator_line, 21, 0, 1, 3)
         grid0.addWidget(separator_line, 21, 0, 1, 3)
 
 
         # ## Flip Title
         # ## Flip Title
-        flip_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.flipName)
+        flip_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.flipName)
         grid0.addWidget(flip_title_label, 23, 0, 1, 3)
         grid0.addWidget(flip_title_label, 23, 0, 1, 3)
 
 
         self.flipx_button = FCButton(_("Flip on X"))
         self.flipx_button = FCButton(_("Flip on X"))
@@ -865,10 +865,10 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(separator_line, 27, 0, 1, 3)
         grid0.addWidget(separator_line, 27, 0, 1, 3)
 
 
         # ## Offset Title
         # ## Offset Title
-        offset_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.offsetName)
+        offset_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.offsetName)
         grid0.addWidget(offset_title_label, 29, 0, 1, 3)
         grid0.addWidget(offset_title_label, 29, 0, 1, 3)
 
 
-        self.offx_label = QtWidgets.QLabel('%s:' % _("X val"))
+        self.offx_label = FCLabel('%s:' % _("X val"))
         self.offx_label.setToolTip(
         self.offx_label.setToolTip(
             _("Distance to offset on X axis. In current units.")
             _("Distance to offset on X axis. In current units.")
         )
         )
@@ -888,7 +888,7 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(self.offx_entry, 31, 1)
         grid0.addWidget(self.offx_entry, 31, 1)
         grid0.addWidget(self.offx_button, 31, 2)
         grid0.addWidget(self.offx_button, 31, 2)
 
 
-        self.offy_label = QtWidgets.QLabel('%s:' % _("Y val"))
+        self.offy_label = FCLabel('%s:' % _("Y val"))
         self.offy_label.setToolTip(
         self.offy_label.setToolTip(
             _("Distance to offset on Y axis. In current units.")
             _("Distance to offset on Y axis. In current units.")
         )
         )
@@ -914,7 +914,7 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(separator_line, 34, 0, 1, 3)
         grid0.addWidget(separator_line, 34, 0, 1, 3)
 
 
         # ## Buffer Title
         # ## Buffer Title
-        buffer_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.bufferName)
+        buffer_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.bufferName)
         grid0.addWidget(buffer_title_label, 35, 0, 1, 2)
         grid0.addWidget(buffer_title_label, 35, 0, 1, 2)
 
 
         self.buffer_rounded_cb = FCCheckBox('%s' % _("Rounded"))
         self.buffer_rounded_cb = FCCheckBox('%s' % _("Rounded"))
@@ -927,7 +927,7 @@ class TransformEditorTool(AppTool):
 
 
         grid0.addWidget(self.buffer_rounded_cb, 35, 2)
         grid0.addWidget(self.buffer_rounded_cb, 35, 2)
 
 
-        self.buffer_label = QtWidgets.QLabel('%s:' % _("Distance"))
+        self.buffer_label = FCLabel('%s:' % _("Distance"))
         self.buffer_label.setToolTip(
         self.buffer_label.setToolTip(
             _("A positive value will create the effect of dilation,\n"
             _("A positive value will create the effect of dilation,\n"
               "while a negative value will create the effect of erosion.\n"
               "while a negative value will create the effect of erosion.\n"
@@ -952,7 +952,7 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(self.buffer_entry, 37, 1)
         grid0.addWidget(self.buffer_entry, 37, 1)
         grid0.addWidget(self.buffer_button, 37, 2)
         grid0.addWidget(self.buffer_button, 37, 2)
 
 
-        self.buffer_factor_label = QtWidgets.QLabel('%s:' % _("Value"))
+        self.buffer_factor_label = FCLabel('%s:' % _("Value"))
         self.buffer_factor_label.setToolTip(
         self.buffer_factor_label.setToolTip(
             _("A positive value will create the effect of dilation,\n"
             _("A positive value will create the effect of dilation,\n"
               "while a negative value will create the effect of erosion.\n"
               "while a negative value will create the effect of erosion.\n"
@@ -978,7 +978,7 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(self.buffer_factor_entry, 38, 1)
         grid0.addWidget(self.buffer_factor_entry, 38, 1)
         grid0.addWidget(self.buffer_factor_button, 38, 2)
         grid0.addWidget(self.buffer_factor_button, 38, 2)
 
 
-        grid0.addWidget(QtWidgets.QLabel(''), 42, 0, 1, 3)
+        grid0.addWidget(FCLabel(''), 42, 0, 1, 3)
 
 
         self.layout.addStretch()
         self.layout.addStretch()
 
 
@@ -3147,7 +3147,7 @@ class FCEraser(FCShapeTool):
             temp_shape = eraser_shape.buffer(0.0000001)
             temp_shape = eraser_shape.buffer(0.0000001)
             temp_shape = Polygon(temp_shape.exterior)
             temp_shape = Polygon(temp_shape.exterior)
             eraser_sel_shapes.append(temp_shape)
             eraser_sel_shapes.append(temp_shape)
-        eraser_sel_shapes = cascaded_union(eraser_sel_shapes)
+        eraser_sel_shapes = unary_union(eraser_sel_shapes)
 
 
         for obj_shape in self.storage.get_objects():
         for obj_shape in self.storage.get_objects():
             try:
             try:
@@ -3273,15 +3273,15 @@ class AppGeoEditor(QtCore.QObject):
 
 
         # ## Page Title icon
         # ## Page Title icon
         pixmap = QtGui.QPixmap(self.app.resource_location + '/flatcam_icon32.png')
         pixmap = QtGui.QPixmap(self.app.resource_location + '/flatcam_icon32.png')
-        self.icon = QtWidgets.QLabel()
+        self.icon = FCLabel()
         self.icon.setPixmap(pixmap)
         self.icon.setPixmap(pixmap)
         self.title_box.addWidget(self.icon, stretch=0)
         self.title_box.addWidget(self.icon, stretch=0)
 
 
         # ## Title label
         # ## Title label
-        self.title_label = QtWidgets.QLabel("<font size=5><b>%s</b></font>" % _('Geometry Editor'))
+        self.title_label = FCLabel("<font size=5><b>%s</b></font>" % _('Geometry Editor'))
         self.title_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
         self.title_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
         self.title_box.addWidget(self.title_label, stretch=1)
         self.title_box.addWidget(self.title_label, stretch=1)
-        self.title_box.addWidget(QtWidgets.QLabel(''))
+        self.title_box.addWidget(FCLabel(''))
 
 
         self.tw = FCTree(columns=3, header_hidden=False, protected_column=[0, 1], extended_sel=True)
         self.tw = FCTree(columns=3, header_hidden=False, protected_column=[0, 1], extended_sel=True)
         self.tw.setHeaderLabels(["ID", _("Type"), _("Name")])
         self.tw.setHeaderLabels(["ID", _("Type"), _("Name")])
@@ -3298,7 +3298,7 @@ class AppGeoEditor(QtCore.QObject):
         layout.addStretch()
         layout.addStretch()
 
 
         # Editor
         # Editor
-        self.exit_editor_button = QtWidgets.QPushButton(_('Exit Editor'))
+        self.exit_editor_button = FCButton(_('Exit Editor'))
         self.exit_editor_button.setIcon(QtGui.QIcon(self.app.resource_location + '/power16.png'))
         self.exit_editor_button.setIcon(QtGui.QIcon(self.app.resource_location + '/power16.png'))
         self.exit_editor_button.setToolTip(
         self.exit_editor_button.setToolTip(
             _("Exit from Editor.")
             _("Exit from Editor.")
@@ -5134,7 +5134,7 @@ class AppGeoEditor(QtCore.QObject):
                     return
                     return
 
 
                 # add the result to the results list
                 # add the result to the results list
-                results.append(cascaded_union(local_results))
+                results.append(unary_union(local_results))
 
 
         # This is a dirty patch:
         # This is a dirty patch:
         for r in results:
         for r in results:

+ 3 - 3
appEditors/AppGerberEditor.py

@@ -9,7 +9,7 @@ from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5.QtCore import Qt, QSettings
 from PyQt5.QtCore import Qt, QSettings
 
 
 from shapely.geometry import LineString, LinearRing, MultiLineString, Point, Polygon, MultiPolygon, box
 from shapely.geometry import LineString, LinearRing, MultiLineString, Point, Polygon, MultiPolygon, box
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 import shapely.affinity as affinity
 import shapely.affinity as affinity
 
 
 from vispy.geometry import Rect
 from vispy.geometry import Rect
@@ -2235,7 +2235,7 @@ class FCEraser(FCShapeTool):
             temp_shape = eraser_shape['solid'].buffer(0.0000001)
             temp_shape = eraser_shape['solid'].buffer(0.0000001)
             temp_shape = Polygon(temp_shape.exterior)
             temp_shape = Polygon(temp_shape.exterior)
             eraser_sel_shapes.append(temp_shape)
             eraser_sel_shapes.append(temp_shape)
-        eraser_sel_shapes = cascaded_union(eraser_sel_shapes)
+        eraser_sel_shapes = unary_union(eraser_sel_shapes)
 
 
         for storage in self.draw_app.storage_dict:
         for storage in self.draw_app.storage_dict:
             try:
             try:
@@ -4968,7 +4968,7 @@ class AppGerberEditor(QtCore.QObject):
                 if 'solid' in actual_geo:
                 if 'solid' in actual_geo:
                     edit_geo.append(actual_geo['solid'])
                     edit_geo.append(actual_geo['solid'])
 
 
-        all_geo = cascaded_union(edit_geo)
+        all_geo = unary_union(edit_geo)
 
 
         # calculate the bounds values for the edited Gerber object
         # calculate the bounds values for the edited Gerber object
         xmin, ymin, xmax, ymax = all_geo.bounds
         xmin, ymin, xmax, ymax = all_geo.bounds

+ 2 - 2
appGUI/MainGUI.py

@@ -2684,9 +2684,9 @@ class MainGUI(QtWidgets.QMainWindow):
                 if key == QtCore.Qt.Key_B:
                 if key == QtCore.Qt.Key_B:
                     self.app.app_obj.new_gerber_object()
                     self.app.app_obj.new_gerber_object()
 
 
-                # New Geometry
+                # New Document Object
                 if key == QtCore.Qt.Key_D:
                 if key == QtCore.Qt.Key_D:
-                    self.app.new_document_object()
+                    self.app.app_obj.new_document_object()
 
 
                 # Copy Object Name
                 # Copy Object Name
                 if key == QtCore.Qt.Key_E:
                 if key == QtCore.Qt.Key_E:

+ 129 - 22
appGUI/ObjectUI.py

@@ -251,10 +251,31 @@ class GerberObjectUI(ObjectUI):
                                       """)
                                       """)
         grid0.addWidget(self.editor_button, 4, 0, 1, 3)
         grid0.addWidget(self.editor_button, 4, 0, 1, 3)
 
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 6, 0, 1, 3)
+        # PROPERTIES CB
+        self.properties_button = FCButton('%s' % _("PROPERTIES"), checkable=True)
+        self.properties_button.setIcon(QtGui.QIcon(self.app.resource_location + '/properties32.png'))
+        self.properties_button.setToolTip(_("Show the Properties."))
+        self.properties_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        grid0.addWidget(self.properties_button, 6, 0, 1, 3)
+
+        # PROPERTIES Frame
+        self.properties_frame = QtWidgets.QFrame()
+        self.properties_frame.setContentsMargins(0, 0, 0, 0)
+        grid0.addWidget(self.properties_frame, 7, 0, 1, 3)
+        self.properties_box = QtWidgets.QVBoxLayout()
+        self.properties_box.setContentsMargins(0, 0, 0, 0)
+        self.properties_frame.setLayout(self.properties_box)
+        self.properties_frame.hide()
+
+        self.treeWidget = FCTree(columns=2)
+
+        self.properties_box.addWidget(self.treeWidget)
+        self.properties_box.setStretch(0, 0)
 
 
         # ### Gerber Apertures ####
         # ### Gerber Apertures ####
         self.apertures_table_label = QtWidgets.QLabel('%s:' % _('Apertures'))
         self.apertures_table_label = QtWidgets.QLabel('%s:' % _('Apertures'))
@@ -324,6 +345,11 @@ class GerberObjectUI(ObjectUI):
         )
         )
         grid0.addWidget(self.create_buffer_button, 12, 0, 1, 3)
         grid0.addWidget(self.create_buffer_button, 12, 0, 1, 3)
 
 
+        separator_line1 = QtWidgets.QFrame()
+        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line1, 13, 0, 1, 3)
+
         self.tool_lbl = QtWidgets.QLabel('<b>%s</b>' % _("TOOLS"))
         self.tool_lbl = QtWidgets.QLabel('<b>%s</b>' % _("TOOLS"))
         grid0.addWidget(self.tool_lbl, 14, 0, 1, 3)
         grid0.addWidget(self.tool_lbl, 14, 0, 1, 3)
 
 
@@ -538,6 +564,32 @@ class ExcellonObjectUI(ObjectUI):
                                       """)
                                       """)
         grid0.addWidget(self.editor_button, 4, 0, 1, 3)
         grid0.addWidget(self.editor_button, 4, 0, 1, 3)
 
 
+        # PROPERTIES CB
+        self.properties_button = FCButton('%s' % _("PROPERTIES"), checkable=True)
+        self.properties_button.setIcon(QtGui.QIcon(self.app.resource_location + '/properties32.png'))
+        self.properties_button.setToolTip(_("Show the Properties."))
+        self.properties_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        grid0.addWidget(self.properties_button, 6, 0, 1, 3)
+
+        # PROPERTIES Frame
+        self.properties_frame = QtWidgets.QFrame()
+        self.properties_frame.setContentsMargins(0, 0, 0, 0)
+        grid0.addWidget(self.properties_frame, 7, 0, 1, 3)
+        self.properties_box = QtWidgets.QVBoxLayout()
+        self.properties_box.setContentsMargins(0, 0, 0, 0)
+        self.properties_frame.setLayout(self.properties_box)
+        self.properties_frame.hide()
+
+        self.treeWidget = FCTree(columns=2)
+
+        self.properties_box.addWidget(self.treeWidget)
+        self.properties_box.setStretch(0, 0)
+
         # ### Tools Drills ####
         # ### Tools Drills ####
         self.tools_table_label = QtWidgets.QLabel('<b>%s</b>' % _('Tools Table'))
         self.tools_table_label = QtWidgets.QLabel('<b>%s</b>' % _('Tools Table'))
         self.tools_table_label.setToolTip(
         self.tools_table_label.setToolTip(
@@ -561,9 +613,9 @@ class ExcellonObjectUI(ObjectUI):
         hlay_plot.addStretch()
         hlay_plot.addStretch()
         hlay_plot.addWidget(self.plot_cb)
         hlay_plot.addWidget(self.plot_cb)
 
 
-        grid0.addWidget(self.tools_table_label, 6, 0)
-        grid0.addWidget(self.table_visibility_cb, 6, 1)
-        grid0.addLayout(hlay_plot, 6, 2)
+        grid0.addWidget(self.tools_table_label, 8, 0)
+        grid0.addWidget(self.table_visibility_cb, 8, 1)
+        grid0.addLayout(hlay_plot, 8, 2)
 
 
         # #############################################################################################################
         # #############################################################################################################
         # #############################################################################################################
         # #############################################################################################################
@@ -657,7 +709,7 @@ class ExcellonObjectUI(ObjectUI):
         self.milling_button = QtWidgets.QPushButton(_('Milling Tool'))
         self.milling_button = QtWidgets.QPushButton(_('Milling Tool'))
         self.milling_button.setIcon(QtGui.QIcon(self.app.resource_location + '/milling_tool32.png'))
         self.milling_button.setIcon(QtGui.QIcon(self.app.resource_location + '/milling_tool32.png'))
         self.milling_button.setToolTip(
         self.milling_button.setToolTip(
-            _("Generate GCode out of slot holes in an Excellon object.")
+            _("Generate a Geometry for milling drills or slots in an Excellon object.")
         )
         )
         self.milling_button.setStyleSheet("""
         self.milling_button.setStyleSheet("""
                         QPushButton
                         QPushButton
@@ -666,6 +718,8 @@ class ExcellonObjectUI(ObjectUI):
                         }
                         }
                         """)
                         """)
         grid2.addWidget(self.milling_button, 6, 0, 1, 2)
         grid2.addWidget(self.milling_button, 6, 0, 1, 2)
+        # TODO until the Milling Tool is finished this stays disabled
+        self.milling_button.setDisabled(True)
 
 
         separator_line = QtWidgets.QFrame()
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
@@ -689,7 +743,7 @@ class ExcellonObjectUI(ObjectUI):
         )
         )
         self.grid6.addWidget(self.mill_hole_label, 5, 0, 1, 3)
         self.grid6.addWidget(self.mill_hole_label, 5, 0, 1, 3)
 
 
-        self.tdlabel = QtWidgets.QLabel('%s:' % _('Tool Dia'))
+        self.tdlabel = QtWidgets.QLabel('%s:' % _('Milling Diameter'))
         self.tdlabel.setToolTip(
         self.tdlabel.setToolTip(
             _("Diameter of the cutting tool.")
             _("Diameter of the cutting tool.")
         )
         )
@@ -704,7 +758,7 @@ class ExcellonObjectUI(ObjectUI):
         self.generate_milling_button = QtWidgets.QPushButton(_('Mill Drills'))
         self.generate_milling_button = QtWidgets.QPushButton(_('Mill Drills'))
         self.generate_milling_button.setToolTip(
         self.generate_milling_button.setToolTip(
             _("Create the Geometry Object\n"
             _("Create the Geometry Object\n"
-              "for milling DRILLS toolpaths.")
+              "for milling drills.")
         )
         )
         self.generate_milling_button.setStyleSheet("""
         self.generate_milling_button.setStyleSheet("""
                         QPushButton
                         QPushButton
@@ -724,7 +778,7 @@ class ExcellonObjectUI(ObjectUI):
         self.generate_milling_slots_button = QtWidgets.QPushButton(_('Mill Slots'))
         self.generate_milling_slots_button = QtWidgets.QPushButton(_('Mill Slots'))
         self.generate_milling_slots_button.setToolTip(
         self.generate_milling_slots_button.setToolTip(
             _("Create the Geometry Object\n"
             _("Create the Geometry Object\n"
-              "for milling SLOTS toolpaths.")
+              "for milling slots.")
         )
         )
         self.generate_milling_slots_button.setStyleSheet("""
         self.generate_milling_slots_button.setStyleSheet("""
                         QPushButton
                         QPushButton
@@ -814,6 +868,32 @@ class GeometryObjectUI(ObjectUI):
                                       """)
                                       """)
         grid_header.addWidget(self.editor_button, 4, 0, 1, 3)
         grid_header.addWidget(self.editor_button, 4, 0, 1, 3)
 
 
+        # PROPERTIES CB
+        self.properties_button = FCButton('%s' % _("PROPERTIES"), checkable=True)
+        self.properties_button.setIcon(QtGui.QIcon(self.app.resource_location + '/properties32.png'))
+        self.properties_button.setToolTip(_("Show the Properties."))
+        self.properties_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        grid_header.addWidget(self.properties_button, 6, 0, 1, 3)
+
+        # PROPERTIES Frame
+        self.properties_frame = QtWidgets.QFrame()
+        self.properties_frame.setContentsMargins(0, 0, 0, 0)
+        grid_header.addWidget(self.properties_frame, 7, 0, 1, 3)
+        self.properties_box = QtWidgets.QVBoxLayout()
+        self.properties_box.setContentsMargins(0, 0, 0, 0)
+        self.properties_frame.setLayout(self.properties_box)
+        self.properties_frame.hide()
+
+        self.treeWidget = FCTree(columns=2)
+
+        self.properties_box.addWidget(self.treeWidget)
+        self.properties_box.setStretch(0, 0)
+
         # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Tools widgets
         # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Tools widgets
         # this way I can hide/show the frame
         # this way I can hide/show the frame
         self.geo_tools_frame = QtWidgets.QFrame()
         self.geo_tools_frame = QtWidgets.QFrame()
@@ -980,8 +1060,9 @@ class GeometryObjectUI(ObjectUI):
         self.addtool_from_db_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/search_db32.png'))
         self.addtool_from_db_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/search_db32.png'))
         self.addtool_from_db_btn.setToolTip(
         self.addtool_from_db_btn.setToolTip(
             _("Add a new tool to the Tool Table\n"
             _("Add a new tool to the Tool Table\n"
-              "from the Tool Database.\n"
-              "Tool database administration in Menu: Options -> Tools Database")
+              "from the Tools Database.\n"
+              "Tools database administration in in:\n"
+              "Menu: Options -> Tools Database")
         )
         )
 
 
         bhlay.addWidget(self.addtool_btn)
         bhlay.addWidget(self.addtool_btn)
@@ -1757,6 +1838,32 @@ class CNCObjectUI(ObjectUI):
                                        """)
                                        """)
         f_lay.addWidget(self.editor_button, 4, 0, 1, 3)
         f_lay.addWidget(self.editor_button, 4, 0, 1, 3)
 
 
+        # PROPERTIES CB
+        self.properties_button = FCButton('%s' % _("PROPERTIES"), checkable=True)
+        self.properties_button.setIcon(QtGui.QIcon(self.app.resource_location + '/properties32.png'))
+        self.properties_button.setToolTip(_("Show the Properties."))
+        self.properties_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        f_lay.addWidget(self.properties_button, 6, 0, 1, 3)
+
+        # PROPERTIES Frame
+        self.properties_frame = QtWidgets.QFrame()
+        self.properties_frame.setContentsMargins(0, 0, 0, 0)
+        f_lay.addWidget(self.properties_frame, 7, 0, 1, 3)
+        self.properties_box = QtWidgets.QVBoxLayout()
+        self.properties_box.setContentsMargins(0, 0, 0, 0)
+        self.properties_frame.setLayout(self.properties_box)
+        self.properties_frame.hide()
+
+        self.treeWidget = FCTree(columns=2)
+
+        self.properties_box.addWidget(self.treeWidget)
+        self.properties_box.setStretch(0, 0)
+
         # Annotation
         # Annotation
         self.annotation_cb = FCCheckBox(_("Display Annotation"))
         self.annotation_cb = FCCheckBox(_("Display Annotation"))
         self.annotation_cb.setToolTip(
         self.annotation_cb.setToolTip(
@@ -1764,12 +1871,12 @@ class CNCObjectUI(ObjectUI):
               "When checked it will display numbers in order for each end\n"
               "When checked it will display numbers in order for each end\n"
               "of a travel line.")
               "of a travel line.")
         )
         )
-        f_lay.addWidget(self.annotation_cb, 6, 0, 1, 3)
+        f_lay.addWidget(self.annotation_cb, 8, 0, 1, 3)
 
 
         separator_line = QtWidgets.QFrame()
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        f_lay.addWidget(separator_line, 8, 0, 1, 3)
+        f_lay.addWidget(separator_line, 10, 0, 1, 3)
 
 
         # Travelled Distance
         # Travelled Distance
         self.t_distance_label = QtWidgets.QLabel("<b>%s:</b>" % _("Travelled distance"))
         self.t_distance_label = QtWidgets.QLabel("<b>%s:</b>" % _("Travelled distance"))
@@ -1780,9 +1887,9 @@ class CNCObjectUI(ObjectUI):
         self.t_distance_entry = FCEntry()
         self.t_distance_entry = FCEntry()
         self.units_label = QtWidgets.QLabel()
         self.units_label = QtWidgets.QLabel()
 
 
-        f_lay.addWidget(self.t_distance_label, 10, 0)
-        f_lay.addWidget(self.t_distance_entry, 10, 1)
-        f_lay.addWidget(self.units_label, 10, 2)
+        f_lay.addWidget(self.t_distance_label, 12, 0)
+        f_lay.addWidget(self.t_distance_entry, 12, 1)
+        f_lay.addWidget(self.units_label, 12, 2)
 
 
         # Estimated Time
         # Estimated Time
         self.t_time_label = QtWidgets.QLabel("<b>%s:</b>" % _("Estimated time"))
         self.t_time_label = QtWidgets.QLabel("<b>%s:</b>" % _("Estimated time"))
@@ -1793,9 +1900,9 @@ class CNCObjectUI(ObjectUI):
         self.t_time_entry = FCEntry()
         self.t_time_entry = FCEntry()
         self.units_time_label = QtWidgets.QLabel()
         self.units_time_label = QtWidgets.QLabel()
 
 
-        f_lay.addWidget(self.t_time_label, 12, 0)
-        f_lay.addWidget(self.t_time_entry, 12, 1)
-        f_lay.addWidget(self.units_time_label, 12, 2)
+        f_lay.addWidget(self.t_time_label, 14, 0)
+        f_lay.addWidget(self.t_time_entry, 14, 1)
+        f_lay.addWidget(self.units_time_label, 14, 2)
 
 
         self.t_distance_label.hide()
         self.t_distance_label.hide()
         self.t_distance_entry.setVisible(False)
         self.t_distance_entry.setVisible(False)
@@ -1805,7 +1912,7 @@ class CNCObjectUI(ObjectUI):
         separator_line = QtWidgets.QFrame()
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        f_lay.addWidget(separator_line, 14, 0, 1, 3)
+        f_lay.addWidget(separator_line, 16, 0, 1, 3)
 
 
         hlay = QtWidgets.QHBoxLayout()
         hlay = QtWidgets.QHBoxLayout()
         self.custom_box.addLayout(hlay)
         self.custom_box.addLayout(hlay)

+ 9 - 1
appGUI/preferences/PreferencesUIManager.py

@@ -342,6 +342,7 @@ class PreferencesUIManager:
 
 
             "tools_iso_rest":           self.ui.tools_defaults_form.tools_iso_group.rest_cb,
             "tools_iso_rest":           self.ui.tools_defaults_form.tools_iso_group.rest_cb,
             "tools_iso_combine_passes": self.ui.tools_defaults_form.tools_iso_group.combine_passes_cb,
             "tools_iso_combine_passes": self.ui.tools_defaults_form.tools_iso_group.combine_passes_cb,
+            "tools_iso_check_valid":    self.ui.tools_defaults_form.tools_iso_group.valid_cb,
             "tools_iso_isoexcept":      self.ui.tools_defaults_form.tools_iso_group.except_cb,
             "tools_iso_isoexcept":      self.ui.tools_defaults_form.tools_iso_group.except_cb,
             "tools_iso_selection":      self.ui.tools_defaults_form.tools_iso_group.select_combo,
             "tools_iso_selection":      self.ui.tools_defaults_form.tools_iso_group.select_combo,
             "tools_iso_poly_ints":      self.ui.tools_defaults_form.tools_iso_group.poly_int_cb,
             "tools_iso_poly_ints":      self.ui.tools_defaults_form.tools_iso_group.poly_int_cb,
@@ -381,6 +382,12 @@ class PreferencesUIManager:
             "tools_drill_f_plunge":         self.ui.tools_defaults_form.tools_drill_group.fplunge_cb,
             "tools_drill_f_plunge":         self.ui.tools_defaults_form.tools_drill_group.fplunge_cb,
             "tools_drill_f_retract":        self.ui.tools_defaults_form.tools_drill_group.fretract_cb,
             "tools_drill_f_retract":        self.ui.tools_defaults_form.tools_drill_group.fretract_cb,
 
 
+            # Area Exclusion
+            "tools_drill_area_exclusion":   self.ui.tools_defaults_form.tools_drill_group.exclusion_cb,
+            "tools_drill_area_shape":       self.ui.tools_defaults_form.tools_drill_group.area_shape_radio,
+            "tools_drill_area_strategy":    self.ui.tools_defaults_form.tools_drill_group.strategy_radio,
+            "tools_drill_area_overz":       self.ui.tools_defaults_form.tools_drill_group.over_z_entry,
+
             # NCC Tool
             # NCC Tool
             "tools_ncctools":           self.ui.tools_defaults_form.tools_ncc_group.ncc_tool_dia_entry,
             "tools_ncctools":           self.ui.tools_defaults_form.tools_ncc_group.ncc_tool_dia_entry,
             "tools_nccorder":           self.ui.tools_defaults_form.tools_ncc_group.ncc_order_radio,
             "tools_nccorder":           self.ui.tools_defaults_form.tools_ncc_group.ncc_order_radio,
@@ -461,6 +468,7 @@ class PreferencesUIManager:
             "tools_film_file_type_radio": self.ui.tools_defaults_form.tools_film_group.file_type_radio,
             "tools_film_file_type_radio": self.ui.tools_defaults_form.tools_film_group.file_type_radio,
             "tools_film_orientation": self.ui.tools_defaults_form.tools_film_group.orientation_radio,
             "tools_film_orientation": self.ui.tools_defaults_form.tools_film_group.orientation_radio,
             "tools_film_pagesize": self.ui.tools_defaults_form.tools_film_group.pagesize_combo,
             "tools_film_pagesize": self.ui.tools_defaults_form.tools_film_group.pagesize_combo,
+            "tools_film_png_dpi": self.ui.tools_defaults_form.tools_film_group.png_dpi_spinner,
 
 
             # Panelize Tool
             # Panelize Tool
             "tools_panelize_spacing_columns": self.ui.tools_defaults_form.tools_panelize_group.pspacing_columns,
             "tools_panelize_spacing_columns": self.ui.tools_defaults_form.tools_panelize_group.pspacing_columns,
@@ -949,7 +957,7 @@ class PreferencesUIManager:
 
 
             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
-            self.defaults.load(filename=os.path.join(self.data_path, 'current_defaults.FlatConfig'))
+            self.defaults.load(filename=os.path.join(self.data_path, 'current_defaults.FlatConfig'), inform=self.inform)
 
 
         settgs = QSettings("Open Source", "FlatCAM")
         settgs = QSettings("Open Source", "FlatCAM")
 
 

+ 4 - 6
appGUI/preferences/geometry/GeometryAdvOptPrefGroupUI.py

@@ -197,13 +197,11 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
         # -----------------------------
         # -----------------------------
         # --- Area Exclusion ----------
         # --- Area Exclusion ----------
         # -----------------------------
         # -----------------------------
-        self.adv_label = QtWidgets.QLabel('<b>%s:</b>' % _('Area Exclusion'))
-        self.adv_label.setToolTip(
-            _("Area exclusion parameters.\n"
-              "Those parameters are available only for\n"
-              "Advanced App. Level.")
+        self.area_exc_label = QtWidgets.QLabel('<b>%s:</b>' % _('Area Exclusion'))
+        self.area_exc_label.setToolTip(
+            _("Area exclusion parameters.")
         )
         )
-        grid1.addWidget(self.adv_label, 13, 0, 1, 2)
+        grid1.addWidget(self.area_exc_label, 13, 0, 1, 2)
 
 
         # Exclusion Area CB
         # Exclusion Area CB
         self.exclusion_cb = FCCheckBox('%s' % _("Exclusion areas"))
         self.exclusion_cb = FCCheckBox('%s' % _("Exclusion areas"))

+ 3 - 3
appGUI/preferences/tools/Tools2InvertPrefGroupUI.py

@@ -64,9 +64,9 @@ class Tools2InvertPrefGroupUI(OptionsGroupUI):
               "- bevel -> the lines are joined by a third line")
               "- bevel -> the lines are joined by a third line")
         )
         )
         self.join_radio = RadioSet([
         self.join_radio = RadioSet([
-            {'label': 'Rounded', 'value': 'r'},
-            {'label': 'Square', 'value': 's'},
-            {'label': 'Bevel', 'value': 'b'}
+            {'label': _('Rounded'), 'value': 'r'},
+            {'label': _('Square'), 'value': 's'},
+            {'label': _('Bevel'), 'value': 'b'}
         ], orientation='vertical', stretch=False)
         ], orientation='vertical', stretch=False)
 
 
         grid0.addWidget(self.join_label, 5, 0, 1, 2)
         grid0.addWidget(self.join_label, 5, 0, 1, 2)

+ 82 - 22
appGUI/preferences/tools/ToolsDrillPrefGroupUI.py

@@ -2,7 +2,7 @@ from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings, Qt
 from PyQt5.QtCore import QSettings, Qt
 
 
 from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox, FCSpinner, NumericalEvalTupleEntry, \
 from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox, FCSpinner, NumericalEvalTupleEntry, \
-    OptionalInputSection, NumericalEvalEntry
+    OptionalInputSection, NumericalEvalEntry, FCLabel
 from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
 from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 
 import gettext
 import gettext
@@ -28,7 +28,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         self.decimals = decimals
         self.decimals = decimals
 
 
         # ## Clear non-copper regions
         # ## Clear non-copper regions
-        self.drill_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.drill_label = FCLabel("<b>%s:</b>" % _("Parameters"))
         self.drill_label.setToolTip(
         self.drill_label.setToolTip(
             _("Create CNCJob with toolpaths for drilling or milling holes.")
             _("Create CNCJob with toolpaths for drilling or milling holes.")
         )
         )
@@ -38,7 +38,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         self.layout.addLayout(grid0)
         self.layout.addLayout(grid0)
 
 
         # Tool order Radio Button
         # Tool order Radio Button
-        self.order_label = QtWidgets.QLabel('%s:' % _('Tool order'))
+        self.order_label = FCLabel('%s:' % _('Tool order'))
         self.order_label.setToolTip(_("This set the way that the tools in the tools table are used.\n"
         self.order_label.setToolTip(_("This set the way that the tools in the tools table are used.\n"
                                       "'No' --> means that the used order is the one in the tool table\n"
                                       "'No' --> means that the used order is the one in the tool table\n"
                                       "'Forward' --> means that the tools will be ordered from small to big\n"
                                       "'Forward' --> means that the tools will be ordered from small to big\n"
@@ -54,7 +54,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.order_radio, 1, 1, 1, 2)
         grid0.addWidget(self.order_radio, 1, 1, 1, 2)
 
 
         # Cut Z
         # Cut Z
-        cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
+        cutzlabel = FCLabel('%s:' % _('Cut Z'))
         cutzlabel.setToolTip(
         cutzlabel.setToolTip(
             _("Drill depth (negative)\n"
             _("Drill depth (negative)\n"
               "below the copper surface.")
               "below the copper surface.")
@@ -95,7 +95,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.maxdepth_entry, 4, 1, 1, 2)
         grid0.addWidget(self.maxdepth_entry, 4, 1, 1, 2)
 
 
         # Travel Z
         # Travel Z
-        travelzlabel = QtWidgets.QLabel('%s:' % _('Travel Z'))
+        travelzlabel = FCLabel('%s:' % _('Travel Z'))
         travelzlabel.setToolTip(
         travelzlabel.setToolTip(
             _("Tool height when travelling\n"
             _("Tool height when travelling\n"
               "across the XY plane.")
               "across the XY plane.")
@@ -121,7 +121,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.toolchange_cb, 6, 0, 1, 3)
         grid0.addWidget(self.toolchange_cb, 6, 0, 1, 3)
 
 
         # Tool Change Z
         # Tool Change Z
-        toolchangezlabel = QtWidgets.QLabel('%s:' % _('Toolchange Z'))
+        toolchangezlabel = FCLabel('%s:' % _('Toolchange Z'))
         toolchangezlabel.setToolTip(
         toolchangezlabel.setToolTip(
             _("Z-axis position (height) for\n"
             _("Z-axis position (height) for\n"
               "tool change.")
               "tool change.")
@@ -139,7 +139,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.toolchangez_entry, 7, 1, 1, 2)
         grid0.addWidget(self.toolchangez_entry, 7, 1, 1, 2)
 
 
         # End Move Z
         # End Move Z
-        endz_label = QtWidgets.QLabel('%s:' % _('End move Z'))
+        endz_label = FCLabel('%s:' % _('End move Z'))
         endz_label.setToolTip(
         endz_label.setToolTip(
             _("Height of the tool after\n"
             _("Height of the tool after\n"
               "the last move at the end of the job.")
               "the last move at the end of the job.")
@@ -156,7 +156,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.endz_entry, 8, 1, 1, 2)
         grid0.addWidget(self.endz_entry, 8, 1, 1, 2)
 
 
         # End Move X,Y
         # End Move X,Y
-        endmove_xy_label = QtWidgets.QLabel('%s:' % _('End move X,Y'))
+        endmove_xy_label = FCLabel('%s:' % _('End move X,Y'))
         endmove_xy_label.setToolTip(
         endmove_xy_label.setToolTip(
             _("End move X,Y position. In format (x,y).\n"
             _("End move X,Y position. In format (x,y).\n"
               "If no value is entered then there is no move\n"
               "If no value is entered then there is no move\n"
@@ -168,7 +168,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.endxy_entry, 9, 1, 1, 2)
         grid0.addWidget(self.endxy_entry, 9, 1, 1, 2)
 
 
         # Feedrate Z
         # Feedrate Z
-        frlabel = QtWidgets.QLabel('%s:' % _('Feedrate Z'))
+        frlabel = FCLabel('%s:' % _('Feedrate Z'))
         frlabel.setToolTip(
         frlabel.setToolTip(
             _("Tool speed while drilling\n"
             _("Tool speed while drilling\n"
               "(in units per minute).\n"
               "(in units per minute).\n"
@@ -183,7 +183,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.feedrate_z_entry, 10, 1, 1, 2)
         grid0.addWidget(self.feedrate_z_entry, 10, 1, 1, 2)
 
 
         # Spindle speed
         # Spindle speed
-        spdlabel = QtWidgets.QLabel('%s:' % _('Spindle Speed'))
+        spdlabel = FCLabel('%s:' % _('Spindle Speed'))
         spdlabel.setToolTip(
         spdlabel.setToolTip(
             _("Speed of the spindle\n"
             _("Speed of the spindle\n"
               "in RPM (optional)")
               "in RPM (optional)")
@@ -206,7 +206,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.dwell_cb, 12, 0, 1, 3)
         grid0.addWidget(self.dwell_cb, 12, 0, 1, 3)
 
 
         # Dwell Time
         # Dwell Time
-        dwelltime = QtWidgets.QLabel('%s:' % _('Duration'))
+        dwelltime = FCLabel('%s:' % _('Duration'))
         dwelltime.setToolTip(_("Number of time units for spindle to dwell."))
         dwelltime.setToolTip(_("Number of time units for spindle to dwell."))
         self.dwelltime_entry = FCDoubleSpinner()
         self.dwelltime_entry = FCDoubleSpinner()
         self.dwelltime_entry.set_precision(self.decimals)
         self.dwelltime_entry.set_precision(self.decimals)
@@ -218,7 +218,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         self.ois_dwell_exc = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
         self.ois_dwell_exc = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
 
 
         # preprocessor selection
         # preprocessor selection
-        pp_excellon_label = QtWidgets.QLabel('%s:' % _("Preprocessor"))
+        pp_excellon_label = FCLabel('%s:' % _("Preprocessor"))
         pp_excellon_label.setToolTip(
         pp_excellon_label.setToolTip(
             _("The preprocessor JSON file that dictates\n"
             _("The preprocessor JSON file that dictates\n"
               "Gcode output.")
               "Gcode output.")
@@ -236,7 +236,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(separator_line, 16, 0, 1, 3)
         grid0.addWidget(separator_line, 16, 0, 1, 3)
 
 
         # DRILL SLOTS LABEL
         # DRILL SLOTS LABEL
-        self.dslots_label = QtWidgets.QLabel('<b>%s:</b>' % _('Drilling Slots'))
+        self.dslots_label = FCLabel('<b>%s:</b>' % _('Drilling Slots'))
         grid0.addWidget(self.dslots_label, 18, 0, 1, 3)
         grid0.addWidget(self.dslots_label, 18, 0, 1, 3)
 
 
         # Drill slots
         # Drill slots
@@ -247,7 +247,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.drill_slots_cb, 20, 0, 1, 3)
         grid0.addWidget(self.drill_slots_cb, 20, 0, 1, 3)
 
 
         # Drill Overlap
         # Drill Overlap
-        self.drill_overlap_label = QtWidgets.QLabel('%s:' % _('Overlap'))
+        self.drill_overlap_label = FCLabel('%s:' % _('Overlap'))
         self.drill_overlap_label.setToolTip(
         self.drill_overlap_label.setToolTip(
             _("How much (percentage) of the tool diameter to overlap previous drill hole.")
             _("How much (percentage) of the tool diameter to overlap previous drill hole.")
         )
         )
@@ -273,14 +273,14 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
         grid0.addWidget(separator_line, 26, 0, 1, 3)
         grid0.addWidget(separator_line, 26, 0, 1, 3)
 
 
-        self.exc_label = QtWidgets.QLabel('<b>%s:</b>' % _('Advanced Options'))
+        self.exc_label = FCLabel('<b>%s:</b>' % _('Advanced Options'))
         self.exc_label.setToolTip(
         self.exc_label.setToolTip(
             _("A list of advanced parameters.")
             _("A list of advanced parameters.")
         )
         )
         grid0.addWidget(self.exc_label, 28, 0, 1, 3)
         grid0.addWidget(self.exc_label, 28, 0, 1, 3)
 
 
         # Offset Z
         # Offset Z
-        offsetlabel = QtWidgets.QLabel('%s:' % _('Offset Z'))
+        offsetlabel = FCLabel('%s:' % _('Offset Z'))
         offsetlabel.setToolTip(
         offsetlabel.setToolTip(
             _("Some drill bits (the larger ones) need to drill deeper\n"
             _("Some drill bits (the larger ones) need to drill deeper\n"
               "to create the desired exit hole diameter due of the tip shape.\n"
               "to create the desired exit hole diameter due of the tip shape.\n"
@@ -293,7 +293,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.offset_entry, 29, 1, 1, 2)
         grid0.addWidget(self.offset_entry, 29, 1, 1, 2)
 
 
         # ToolChange X,Y
         # ToolChange X,Y
-        toolchange_xy_label = QtWidgets.QLabel('%s:' % _('Toolchange X,Y'))
+        toolchange_xy_label = FCLabel('%s:' % _('Toolchange X,Y'))
         toolchange_xy_label.setToolTip(
         toolchange_xy_label.setToolTip(
             _("Toolchange X,Y position.")
             _("Toolchange X,Y position.")
         )
         )
@@ -303,7 +303,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.toolchangexy_entry, 31, 1, 1, 2)
         grid0.addWidget(self.toolchangexy_entry, 31, 1, 1, 2)
 
 
         # Start Z
         # Start Z
-        startzlabel = QtWidgets.QLabel('%s:' % _('Start Z'))
+        startzlabel = FCLabel('%s:' % _('Start Z'))
         startzlabel.setToolTip(
         startzlabel.setToolTip(
             _("Height of the tool just after start.\n"
             _("Height of the tool just after start.\n"
               "Delete the value if you don't need this feature.")
               "Delete the value if you don't need this feature.")
@@ -314,7 +314,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.estartz_entry, 33, 1, 1, 2)
         grid0.addWidget(self.estartz_entry, 33, 1, 1, 2)
 
 
         # Feedrate Rapids
         # Feedrate Rapids
-        fr_rapid_label = QtWidgets.QLabel('%s:' % _('Feedrate Rapids'))
+        fr_rapid_label = FCLabel('%s:' % _('Feedrate Rapids'))
         fr_rapid_label.setToolTip(
         fr_rapid_label.setToolTip(
             _("Tool speed while drilling\n"
             _("Tool speed while drilling\n"
               "(in units per minute).\n"
               "(in units per minute).\n"
@@ -330,7 +330,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.feedrate_rapid_entry, 35, 1, 1, 2)
         grid0.addWidget(self.feedrate_rapid_entry, 35, 1, 1, 2)
 
 
         # Probe depth
         # Probe depth
-        self.pdepth_label = QtWidgets.QLabel('%s:' % _("Probe Z depth"))
+        self.pdepth_label = FCLabel('%s:' % _("Probe Z depth"))
         self.pdepth_label.setToolTip(
         self.pdepth_label.setToolTip(
             _("The maximum depth that the probe is allowed\n"
             _("The maximum depth that the probe is allowed\n"
               "to probe. Negative value, in current units.")
               "to probe. Negative value, in current units.")
@@ -343,7 +343,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.pdepth_entry, 37, 1, 1, 2)
         grid0.addWidget(self.pdepth_entry, 37, 1, 1, 2)
 
 
         # Probe feedrate
         # Probe feedrate
-        self.feedrate_probe_label = QtWidgets.QLabel('%s:' % _("Feedrate Probe"))
+        self.feedrate_probe_label = FCLabel('%s:' % _("Feedrate Probe"))
         self.feedrate_probe_label.setToolTip(
         self.feedrate_probe_label.setToolTip(
            _("The feedrate used while the probe is probing.")
            _("The feedrate used while the probe is probing.")
         )
         )
@@ -355,7 +355,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.feedrate_probe_entry, 38, 1, 1, 2)
         grid0.addWidget(self.feedrate_probe_entry, 38, 1, 1, 2)
 
 
         # Spindle direction
         # Spindle direction
-        spindle_dir_label = QtWidgets.QLabel('%s:' % _('Spindle direction'))
+        spindle_dir_label = FCLabel('%s:' % _('Spindle direction'))
         spindle_dir_label.setToolTip(
         spindle_dir_label.setToolTip(
             _("This sets the direction that the spindle is rotating.\n"
             _("This sets the direction that the spindle is rotating.\n"
               "It can be either:\n"
               "It can be either:\n"
@@ -389,4 +389,64 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
 
 
         grid0.addWidget(self.fretract_cb, 45, 0, 1, 3)
         grid0.addWidget(self.fretract_cb, 45, 0, 1, 3)
 
 
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 46, 0, 1, 3)
+
+        # -----------------------------
+        # --- Area Exclusion ----------
+        # -----------------------------
+        self.area_exc_label = FCLabel('<b>%s:</b>' % _('Area Exclusion'))
+        self.area_exc_label.setToolTip(
+            _("Area exclusion parameters.")
+        )
+        grid0.addWidget(self.area_exc_label, 47, 0, 1, 2)
+
+        # Exclusion Area CB
+        self.exclusion_cb = FCCheckBox('%s' % _("Exclusion areas"))
+        self.exclusion_cb.setToolTip(
+            _(
+                "Include exclusion areas.\n"
+                "In those areas the travel of the tools\n"
+                "is forbidden."
+            )
+        )
+        grid0.addWidget(self.exclusion_cb, 49, 0, 1, 2)
+
+        # Area Selection shape
+        self.area_shape_label = FCLabel('%s:' % _("Shape"))
+        self.area_shape_label.setToolTip(
+            _("The kind of selection shape used for area selection.")
+        )
+
+        self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'},
+                                          {'label': _("Polygon"), 'value': 'polygon'}])
+
+        grid0.addWidget(self.area_shape_label, 51, 0)
+        grid0.addWidget(self.area_shape_radio, 51, 1)
+
+        # Chose Strategy
+        self.strategy_label = FCLabel('%s:' % _("Strategy"))
+        self.strategy_label.setToolTip(_("The strategy followed when encountering an exclusion area.\n"
+                                         "Can be:\n"
+                                         "- Over -> when encountering the area, the tool will go to a set height\n"
+                                         "- Around -> will avoid the exclusion area by going around the area"))
+        self.strategy_radio = RadioSet([{'label': _('Over'), 'value': 'over'},
+                                        {'label': _('Around'), 'value': 'around'}])
+
+        grid0.addWidget(self.strategy_label, 53, 0)
+        grid0.addWidget(self.strategy_radio, 53, 1)
+
+        # Over Z
+        self.over_z_label = FCLabel('%s:' % _("Over Z"))
+        self.over_z_label.setToolTip(_("The height Z to which the tool will rise in order to avoid\n"
+                                       "an interdiction area."))
+        self.over_z_entry = FCDoubleSpinner()
+        self.over_z_entry.set_range(0.000, 9999.9999)
+        self.over_z_entry.set_precision(self.decimals)
+
+        grid0.addWidget(self.over_z_label, 55, 0)
+        grid0.addWidget(self.over_z_entry, 55, 1)
+
         self.layout.addStretch()
         self.layout.addStretch()

+ 29 - 18
appGUI/preferences/tools/ToolsFilmPrefGroupUI.py

@@ -1,7 +1,7 @@
-from PyQt5 import QtWidgets, QtCore, QtGui
-from PyQt5.QtCore import Qt, QSettings
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
 
 
-from appGUI.GUIElements import RadioSet, FCEntry, FCDoubleSpinner, FCCheckBox, FCComboBox, FCColorEntry
+from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, FCComboBox, FCColorEntry, FCLabel, FCSpinner
 from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
 from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 
 import gettext
 import gettext
@@ -28,7 +28,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         self.decimals = decimals
         self.decimals = decimals
 
 
         # ## Parameters
         # ## Parameters
-        self.film_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.film_label = FCLabel("<b>%s:</b>" % _("Parameters"))
         self.film_label.setToolTip(
         self.film_label.setToolTip(
             _("Create a PCB film from a Gerber or Geometry object.\n"
             _("Create a PCB film from a Gerber or Geometry object.\n"
               "The file is saved in SVG format.")
               "The file is saved in SVG format.")
@@ -40,7 +40,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
 
 
         self.film_type_radio = RadioSet([{'label': 'Pos', 'value': 'pos'},
         self.film_type_radio = RadioSet([{'label': 'Pos', 'value': 'pos'},
                                          {'label': 'Neg', 'value': 'neg'}])
                                          {'label': 'Neg', 'value': 'neg'}])
-        ftypelbl = QtWidgets.QLabel('%s:' % _('Film Type'))
+        ftypelbl = FCLabel('%s:' % _('Film Type'))
         ftypelbl.setToolTip(
         ftypelbl.setToolTip(
             _("Generate a Positive black film or a Negative film.\n"
             _("Generate a Positive black film or a Negative film.\n"
               "Positive means that it will print the features\n"
               "Positive means that it will print the features\n"
@@ -53,7 +53,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.film_type_radio, 0, 1)
         grid0.addWidget(self.film_type_radio, 0, 1)
 
 
         # Film Color
         # Film Color
-        self.film_color_label = QtWidgets.QLabel('%s:' % _('Film Color'))
+        self.film_color_label = FCLabel('%s:' % _('Film Color'))
         self.film_color_label.setToolTip(
         self.film_color_label.setToolTip(
             _("Set the film color when positive film is selected.")
             _("Set the film color when positive film is selected.")
         )
         )
@@ -68,7 +68,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         self.film_boundary_entry.set_range(0, 9999.9999)
         self.film_boundary_entry.set_range(0, 9999.9999)
         self.film_boundary_entry.setSingleStep(0.1)
         self.film_boundary_entry.setSingleStep(0.1)
 
 
-        self.film_boundary_label = QtWidgets.QLabel('%s:' % _("Border"))
+        self.film_boundary_label = FCLabel('%s:' % _("Border"))
         self.film_boundary_label.setToolTip(
         self.film_boundary_label.setToolTip(
             _("Specify a border around the object.\n"
             _("Specify a border around the object.\n"
               "Only for negative film.\n"
               "Only for negative film.\n"
@@ -87,7 +87,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         self.film_scale_stroke_entry.set_range(0, 9999.9999)
         self.film_scale_stroke_entry.set_range(0, 9999.9999)
         self.film_scale_stroke_entry.setSingleStep(0.1)
         self.film_scale_stroke_entry.setSingleStep(0.1)
 
 
-        self.film_scale_stroke_label = QtWidgets.QLabel('%s:' % _("Scale Stroke"))
+        self.film_scale_stroke_label = FCLabel('%s:' % _("Scale Stroke"))
         self.film_scale_stroke_label.setToolTip(
         self.film_scale_stroke_label.setToolTip(
             _("Scale the line stroke thickness of each feature in the SVG file.\n"
             _("Scale the line stroke thickness of each feature in the SVG file.\n"
               "It means that the line that envelope each SVG feature will be thicker or thinner,\n"
               "It means that the line that envelope each SVG feature will be thicker or thinner,\n"
@@ -96,7 +96,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.film_scale_stroke_label, 3, 0)
         grid0.addWidget(self.film_scale_stroke_label, 3, 0)
         grid0.addWidget(self.film_scale_stroke_entry, 3, 1)
         grid0.addWidget(self.film_scale_stroke_entry, 3, 1)
 
 
-        self.film_adj_label = QtWidgets.QLabel('<b>%s</b>' % _("Film Adjustments"))
+        self.film_adj_label = FCLabel('<b>%s</b>' % _("Film Adjustments"))
         self.film_adj_label.setToolTip(
         self.film_adj_label.setToolTip(
             _("Sometime the printers will distort the print shape, especially the Laser types.\n"
             _("Sometime the printers will distort the print shape, especially the Laser types.\n"
               "This section provide the tools to compensate for the print distortions.")
               "This section provide the tools to compensate for the print distortions.")
@@ -117,7 +117,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         )
         )
         grid0.addWidget(self.film_scale_cb, 5, 0, 1, 2)
         grid0.addWidget(self.film_scale_cb, 5, 0, 1, 2)
 
 
-        self.film_scalex_label = QtWidgets.QLabel('%s:' % _("X factor"))
+        self.film_scalex_label = FCLabel('%s:' % _("X factor"))
         self.film_scalex_entry = FCDoubleSpinner()
         self.film_scalex_entry = FCDoubleSpinner()
         self.film_scalex_entry.set_range(-999.9999, 999.9999)
         self.film_scalex_entry.set_range(-999.9999, 999.9999)
         self.film_scalex_entry.set_precision(self.decimals)
         self.film_scalex_entry.set_precision(self.decimals)
@@ -126,7 +126,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.film_scalex_label, 6, 0)
         grid0.addWidget(self.film_scalex_label, 6, 0)
         grid0.addWidget(self.film_scalex_entry, 6, 1)
         grid0.addWidget(self.film_scalex_entry, 6, 1)
 
 
-        self.film_scaley_label = QtWidgets.QLabel('%s:' % _("Y factor"))
+        self.film_scaley_label = FCLabel('%s:' % _("Y factor"))
         self.film_scaley_entry = FCDoubleSpinner()
         self.film_scaley_entry = FCDoubleSpinner()
         self.film_scaley_entry.set_range(-999.9999, 999.9999)
         self.film_scaley_entry.set_range(-999.9999, 999.9999)
         self.film_scaley_entry.set_precision(self.decimals)
         self.film_scaley_entry.set_precision(self.decimals)
@@ -148,7 +148,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         )
         )
         grid0.addWidget(self.film_skew_cb, 8, 0, 1, 2)
         grid0.addWidget(self.film_skew_cb, 8, 0, 1, 2)
 
 
-        self.film_skewx_label = QtWidgets.QLabel('%s:' % _("X angle"))
+        self.film_skewx_label = FCLabel('%s:' % _("X angle"))
         self.film_skewx_entry = FCDoubleSpinner()
         self.film_skewx_entry = FCDoubleSpinner()
         self.film_skewx_entry.set_range(-999.9999, 999.9999)
         self.film_skewx_entry.set_range(-999.9999, 999.9999)
         self.film_skewx_entry.set_precision(self.decimals)
         self.film_skewx_entry.set_precision(self.decimals)
@@ -157,7 +157,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.film_skewx_label, 9, 0)
         grid0.addWidget(self.film_skewx_label, 9, 0)
         grid0.addWidget(self.film_skewx_entry, 9, 1)
         grid0.addWidget(self.film_skewx_entry, 9, 1)
 
 
-        self.film_skewy_label = QtWidgets.QLabel('%s:' % _("Y angle"))
+        self.film_skewy_label = FCLabel('%s:' % _("Y angle"))
         self.film_skewy_entry = FCDoubleSpinner()
         self.film_skewy_entry = FCDoubleSpinner()
         self.film_skewy_entry.set_range(-999.9999, 999.9999)
         self.film_skewy_entry.set_range(-999.9999, 999.9999)
         self.film_skewy_entry.set_precision(self.decimals)
         self.film_skewy_entry.set_precision(self.decimals)
@@ -166,7 +166,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.film_skewy_label, 10, 0)
         grid0.addWidget(self.film_skewy_label, 10, 0)
         grid0.addWidget(self.film_skewy_entry, 10, 1)
         grid0.addWidget(self.film_skewy_entry, 10, 1)
 
 
-        self.film_skew_ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
+        self.film_skew_ref_label = FCLabel('%s:' % _("Reference"))
         self.film_skew_ref_label.setToolTip(
         self.film_skew_ref_label.setToolTip(
             _("The reference point to be used as origin for the skew.\n"
             _("The reference point to be used as origin for the skew.\n"
               "It can be one of the four points of the geometry bounding box.")
               "It can be one of the four points of the geometry bounding box.")
@@ -198,7 +198,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
                                           {'label': _('Y'), 'value': 'y'},
                                           {'label': _('Y'), 'value': 'y'},
                                           {'label': _('Both'), 'value': 'both'}],
                                           {'label': _('Both'), 'value': 'both'}],
                                          stretch=False)
                                          stretch=False)
-        self.film_mirror_axis_label = QtWidgets.QLabel('%s:' % _("Mirror axis"))
+        self.film_mirror_axis_label = FCLabel('%s:' % _("Mirror axis"))
 
 
         grid0.addWidget(self.film_mirror_axis_label, 13, 0)
         grid0.addWidget(self.film_mirror_axis_label, 13, 0)
         grid0.addWidget(self.film_mirror_axis, 13, 1)
         grid0.addWidget(self.film_mirror_axis, 13, 1)
@@ -213,7 +213,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
                                          {'label': _('PDF'), 'value': 'pdf'}
                                          {'label': _('PDF'), 'value': 'pdf'}
                                          ], stretch=False)
                                          ], stretch=False)
 
 
-        self.file_type_label = QtWidgets.QLabel(_("Film Type:"))
+        self.file_type_label = FCLabel(_("Film Type:"))
         self.file_type_label.setToolTip(
         self.file_type_label.setToolTip(
             _("The file type of the saved film. Can be:\n"
             _("The file type of the saved film. Can be:\n"
               "- 'SVG' -> open-source vectorial format\n"
               "- 'SVG' -> open-source vectorial format\n"
@@ -224,7 +224,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.file_type_radio, 15, 1)
         grid0.addWidget(self.file_type_radio, 15, 1)
 
 
         # Page orientation
         # Page orientation
-        self.orientation_label = QtWidgets.QLabel('%s:' % _("Page Orientation"))
+        self.orientation_label = FCLabel('%s:' % _("Page Orientation"))
         self.orientation_label.setToolTip(_("Can be:\n"
         self.orientation_label.setToolTip(_("Can be:\n"
                                             "- Portrait\n"
                                             "- Portrait\n"
                                             "- Landscape"))
                                             "- Landscape"))
@@ -237,7 +237,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.orientation_radio, 16, 1)
         grid0.addWidget(self.orientation_radio, 16, 1)
 
 
         # Page Size
         # Page Size
-        self.pagesize_label = QtWidgets.QLabel('%s:' % _("Page Size"))
+        self.pagesize_label = FCLabel('%s:' % _("Page Size"))
         self.pagesize_label.setToolTip(_("A selection of standard ISO 216 page sizes."))
         self.pagesize_label.setToolTip(_("A selection of standard ISO 216 page sizes."))
 
 
         self.pagesize_combo = FCComboBox()
         self.pagesize_combo = FCComboBox()
@@ -302,6 +302,17 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.pagesize_label, 17, 0)
         grid0.addWidget(self.pagesize_label, 17, 0)
         grid0.addWidget(self.pagesize_combo, 17, 1)
         grid0.addWidget(self.pagesize_combo, 17, 1)
 
 
+        # PNG DPI
+        self.png_dpi_label = FCLabel('%s:' % "PNG DPI")
+        self.png_dpi_label.setToolTip(
+            _("Default value is 96 DPI. Change this value to scale the PNG file.")
+        )
+        self.png_dpi_spinner = FCSpinner()
+        self.png_dpi_spinner.set_range(0, 100000)
+
+        grid0.addWidget(self.png_dpi_label, 19, 0)
+        grid0.addWidget(self.png_dpi_spinner, 19, 1)
+
         self.layout.addStretch()
         self.layout.addStretch()
 
 
         # Film Tool
         # Film Tool

+ 10 - 0
appGUI/preferences/tools/ToolsISOPrefGroupUI.py

@@ -271,6 +271,16 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
         self.except_cb.setObjectName("i_except")
         self.except_cb.setObjectName("i_except")
         grid0.addWidget(self.except_cb, 17, 2)
         grid0.addWidget(self.except_cb, 17, 2)
 
 
+        # Check Tool validity
+        self.valid_cb = FCCheckBox(label=_('Check validity'))
+        self.valid_cb.setToolTip(
+            _("If checked then the tools diameters are verified\n"
+              "if they will provide a complete isolation.")
+        )
+        self.valid_cb.setObjectName("i_check")
+
+        grid0.addWidget(self.valid_cb, 18, 0, 1, 3)
+
         # Isolation Scope
         # Isolation Scope
         self.select_label = QtWidgets.QLabel('%s:' % _("Selection"))
         self.select_label = QtWidgets.QLabel('%s:' % _("Selection"))
         self.select_label.setToolTip(
         self.select_label.setToolTip(

+ 22 - 2
appObjects/FlatCAMCNCJob.py

@@ -574,8 +574,14 @@ class CNCJobObject(FlatCAMObj, CNCjob):
         self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click)
         self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click)
         self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click)
         self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click)
         self.ui.review_gcode_button.clicked.connect(self.on_edit_code_click)
         self.ui.review_gcode_button.clicked.connect(self.on_edit_code_click)
+
+        # Editor Signal
         self.ui.editor_button.clicked.connect(lambda: self.app.object2editor())
         self.ui.editor_button.clicked.connect(lambda: self.app.object2editor())
 
 
+        # Properties
+        self.ui.properties_button.toggled.connect(self.on_properties)
+        self.calculations_finished.connect(self.update_area_chull)
+
         # autolevelling signals
         # autolevelling signals
         self.ui.sal_cb.stateChanged.connect(self.on_toggle_autolevelling)
         self.ui.sal_cb.stateChanged.connect(self.on_toggle_autolevelling)
         self.ui.al_mode_radio.activated_custom.connect(self.on_mode_radio)
         self.ui.al_mode_radio.activated_custom.connect(self.on_mode_radio)
@@ -699,6 +705,20 @@ class CNCJobObject(FlatCAMObj, CNCjob):
         except (TypeError, AttributeError):
         except (TypeError, AttributeError):
             pass
             pass
 
 
+    def on_properties(self, state):
+        if state:
+            self.ui.properties_frame.show()
+        else:
+            self.ui.properties_frame.hide()
+            return
+
+        self.ui.treeWidget.clear()
+        self.add_properties_items(obj=self, treeWidget=self.ui.treeWidget)
+
+        self.ui.treeWidget.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.MinimumExpanding)
+        # make sure that the FCTree widget columns are resized to content
+        self.ui.treeWidget.resize_sig.emit()
+
     def on_add_al_probepoints(self):
     def on_add_al_probepoints(self):
         # create the solid_geo
         # create the solid_geo
 
 
@@ -1260,7 +1280,7 @@ class CNCJobObject(FlatCAMObj, CNCjob):
                 pass
                 pass
 
 
             answer = self.on_grbl_wake()
             answer = self.on_grbl_wake()
-            answer = ['ok']   # hack for development without a GRBL controller connected
+            answer = ['ok']   # FIXME: hack for development without a GRBL controller connected
             for line in answer:
             for line in answer:
                 if 'ok' in line.lower():
                 if 'ok' in line.lower():
                     self.ui.com_connect_button.setStyleSheet("QPushButton {background-color: seagreen;}")
                     self.ui.com_connect_button.setStyleSheet("QPushButton {background-color: seagreen;}")
@@ -2548,7 +2568,7 @@ class CNCJobObject(FlatCAMObj, CNCjob):
                 #         g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0))
                 #         g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0))
                 #
                 #
                 #     tool_dia_copy['gcode_parsed'] = deepcopy(dia_value)
                 #     tool_dia_copy['gcode_parsed'] = deepcopy(dia_value)
-                #     tool_dia_copy['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_value])
+                #     tool_dia_copy['solid_geometry'] = unary_union([geo['geom'] for geo in dia_value])
 
 
             temp_tools_dict.update({
             temp_tools_dict.update({
                 tooluid_key: deepcopy(tool_dia_copy)
                 tooluid_key: deepcopy(tool_dia_copy)

+ 39 - 15
appObjects/FlatCAMExcellon.py

@@ -176,7 +176,11 @@ class ExcellonObject(FlatCAMObj, Excellon):
 
 
         # Editor
         # Editor
         self.ui.editor_button.clicked.connect(lambda: self.app.object2editor())
         self.ui.editor_button.clicked.connect(lambda: self.app.object2editor())
-        
+
+        # Properties
+        self.ui.properties_button.toggled.connect(self.on_properties)
+        self.calculations_finished.connect(self.update_area_chull)
+
         self.ui.drill_button.clicked.connect(lambda: self.app.drilling_tool.run(toggle=True))
         self.ui.drill_button.clicked.connect(lambda: self.app.drilling_tool.run(toggle=True))
         # self.ui.milling_button.clicked.connect(lambda: self.app.milling_tool.run(toggle=True))
         # self.ui.milling_button.clicked.connect(lambda: self.app.milling_tool.run(toggle=True))
 
 
@@ -438,6 +442,9 @@ class ExcellonObject(FlatCAMObj, Excellon):
             self.ui.slot_tooldia_entry.setDisabled(False)
             self.ui.slot_tooldia_entry.setDisabled(False)
             self.ui.generate_milling_slots_button.setDisabled(False)
             self.ui.generate_milling_slots_button.setDisabled(False)
 
 
+        # update the milling section
+        self.on_row_selection_change()
+
         self.ui_connect()
         self.ui_connect()
 
 
     def ui_connect(self):
     def ui_connect(self):
@@ -510,12 +517,22 @@ class ExcellonObject(FlatCAMObj, Excellon):
             self.ui.slot_tooldia_entry.setDisabled(False)
             self.ui.slot_tooldia_entry.setDisabled(False)
             self.ui.generate_milling_slots_button.setDisabled(False)
             self.ui.generate_milling_slots_button.setDisabled(False)
 
 
-            # find if we have drills:
-            has_drills = None
-            for tt in self.tools:
-                if 'drills' in self.tools[tt] and self.tools[tt]['drills']:
-                    has_drills = True
-                    break
+            has_drills = True
+            has_slots = True
+            for row in sel_rows:
+                row_dia = self.app.dec_format(float(self.ui.tools_table.item(row, 1).text()), self.decimals)
+
+                for tt in self.tools:
+                    tool_dia = self.app.dec_format(float(self.tools[tt]['tooldia']), self.decimals)
+                    if tool_dia == row_dia:
+                        # find if we have drills:
+                        if 'drills' not in self.tools[tt] or not self.tools[tt]['drills']:
+                            has_drills = None
+
+                        # find if we have slots
+                        if 'slots' not in self.tools[tt] or not self.tools[tt]['slots']:
+                            has_slots = None
+
             if has_drills is None:
             if has_drills is None:
                 self.ui.tooldia_entry.setDisabled(True)
                 self.ui.tooldia_entry.setDisabled(True)
                 self.ui.generate_milling_button.setDisabled(True)
                 self.ui.generate_milling_button.setDisabled(True)
@@ -523,12 +540,6 @@ class ExcellonObject(FlatCAMObj, Excellon):
                 self.ui.tooldia_entry.setDisabled(False)
                 self.ui.tooldia_entry.setDisabled(False)
                 self.ui.generate_milling_button.setDisabled(False)
                 self.ui.generate_milling_button.setDisabled(False)
 
 
-            # find if we have slots
-            has_slots = None
-            for tt in self.tools:
-                if 'slots' in self.tools[tt] and self.tools[tt]['slots']:
-                    has_slots = True
-                    break
             if has_slots is None:
             if has_slots is None:
                 self.ui.slot_tooldia_entry.setDisabled(True)
                 self.ui.slot_tooldia_entry.setDisabled(True)
                 self.ui.generate_milling_slots_button.setDisabled(True)
                 self.ui.generate_milling_slots_button.setDisabled(True)
@@ -603,6 +614,19 @@ class ExcellonObject(FlatCAMObj, Excellon):
     def on_table_visibility_toggle(self, state):
     def on_table_visibility_toggle(self, state):
         self.ui.tools_table.show() if state else self.ui.tools_table.hide()
         self.ui.tools_table.show() if state else self.ui.tools_table.hide()
 
 
+    def on_properties(self, state):
+        if state:
+            self.ui.properties_frame.show()
+        else:
+            self.ui.properties_frame.hide()
+            return
+
+        self.ui.treeWidget.clear()
+        self.add_properties_items(obj=self, treeWidget=self.ui.treeWidget)
+
+        # make sure that the FCTree widget columns are resized to content
+        self.ui.treeWidget.resize_sig.emit()
+
     def export_excellon(self, whole, fract, e_zeros=None, form='dec', factor=1, slot_type='routing'):
     def export_excellon(self, whole, fract, e_zeros=None, form='dec', factor=1, slot_type='routing'):
         """
         """
         Returns two values, first is a boolean , if 1 then the file has slots and second contain the Excellon code
         Returns two values, first is a boolean , if 1 then the file has slots and second contain the Excellon code
@@ -878,7 +902,7 @@ class ExcellonObject(FlatCAMObj, Excellon):
             geo_obj.options['Tools_in_use'] = tool_table_items
             geo_obj.options['Tools_in_use'] = tool_table_items
             geo_obj.options['type'] = 'Excellon Geometry'
             geo_obj.options['type'] = 'Excellon Geometry'
             geo_obj.options["cnctooldia"] = str(tooldia)
             geo_obj.options["cnctooldia"] = str(tooldia)
-            geo_obj.options["multidepth"] = self.options["multidepth"]
+            geo_obj.options["multidepth"] = self.app.defaults["geometry_multidepth"]
             geo_obj.solid_geometry = []
             geo_obj.solid_geometry = []
 
 
             # in case that the tool used has the same diameter with the hole, and since the maximum resolution
             # in case that the tool used has the same diameter with the hole, and since the maximum resolution
@@ -978,7 +1002,7 @@ class ExcellonObject(FlatCAMObj, Excellon):
             geo_obj.options['Tools_in_use'] = tool_table_items
             geo_obj.options['Tools_in_use'] = tool_table_items
             geo_obj.options['type'] = 'Excellon Geometry'
             geo_obj.options['type'] = 'Excellon Geometry'
             geo_obj.options["cnctooldia"] = str(tooldia)
             geo_obj.options["cnctooldia"] = str(tooldia)
-            geo_obj.options["multidepth"] = self.options["multidepth"]
+            geo_obj.options["multidepth"] = self.app.defaults["geometry_multidepth"]
             geo_obj.solid_geometry = []
             geo_obj.solid_geometry = []
 
 
             # in case that the tool used has the same diameter with the hole, and since the maximum resolution
             # in case that the tool used has the same diameter with the hole, and since the maximum resolution

+ 82 - 11
appObjects/FlatCAMGeometry.py

@@ -438,6 +438,11 @@ class GeometryObject(FlatCAMObj, Geometry):
             "area_shape": self.ui.area_shape_radio,
             "area_shape": self.ui.area_shape_radio,
             "area_strategy": self.ui.strategy_radio,
             "area_strategy": self.ui.strategy_radio,
             "area_overz": self.ui.over_z_entry,
             "area_overz": self.ui.over_z_entry,
+            "polish": self.ui.polish_cb,
+            "polish_dia": self.ui.polish_dia_entry,
+            "polish_pressure": self.ui.polish_pressure_entry,
+            "polish_overlap": self.ui.polish_over_entry,
+            "polish_method": self.ui.polish_method_combo,
         })
         })
 
 
         self.param_fields.update({
         self.param_fields.update({
@@ -589,8 +594,13 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
         self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
         self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
 
 
+        # Editor Signal
         self.ui.editor_button.clicked.connect(self.app.object2editor)
         self.ui.editor_button.clicked.connect(self.app.object2editor)
 
 
+        # Properties
+        self.ui.properties_button.toggled.connect(self.on_properties)
+        self.calculations_finished.connect(self.update_area_chull)
+
         self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
         self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
         self.ui.paint_tool_button.clicked.connect(lambda: self.app.paint_tool.run(toggle=False))
         self.ui.paint_tool_button.clicked.connect(lambda: self.app.paint_tool.run(toggle=False))
         self.ui.generate_ncc_button.clicked.connect(lambda: self.app.ncclear_tool.run(toggle=False))
         self.ui.generate_ncc_button.clicked.connect(lambda: self.app.ncclear_tool.run(toggle=False))
@@ -614,6 +624,20 @@ class GeometryObject(FlatCAMObj, Geometry):
 
 
         self.ui.geo_tools_table.drag_drop_sig.connect(self.rebuild_ui)
         self.ui.geo_tools_table.drag_drop_sig.connect(self.rebuild_ui)
 
 
+    def on_properties(self, state):
+        if state:
+            self.ui.properties_frame.show()
+        else:
+            self.ui.properties_frame.hide()
+            return
+
+        self.ui.treeWidget.clear()
+        self.add_properties_items(obj=self, treeWidget=self.ui.treeWidget)
+
+        self.ui.treeWidget.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.MinimumExpanding)
+        # make sure that the FCTree widget columns are resized to content
+        self.ui.treeWidget.resize_sig.emit()
+
     def rebuild_ui(self):
     def rebuild_ui(self):
         # read the table tools uid
         # read the table tools uid
         current_uid_list = []
         current_uid_list = []
@@ -722,7 +746,13 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
 
 
         # common parameters update
         # common parameters update
+        self.ui.toolchangeg_cb.stateChanged.connect(self.update_common_param_in_storage)
+        self.ui.toolchangez_entry.editingFinished.connect(self.update_common_param_in_storage)
+        self.ui.endz_entry.editingFinished.connect(self.update_common_param_in_storage)
+        self.ui.endxy_entry.editingFinished.connect(self.update_common_param_in_storage)
         self.ui.pp_geometry_name_cb.currentIndexChanged.connect(self.update_common_param_in_storage)
         self.ui.pp_geometry_name_cb.currentIndexChanged.connect(self.update_common_param_in_storage)
+        self.ui.exclusion_cb.stateChanged.connect(self.update_common_param_in_storage)
+        self.ui.polish_cb.stateChanged.connect(self.update_common_param_in_storage)
 
 
     def ui_disconnect(self):
     def ui_disconnect(self):
 
 
@@ -806,6 +836,36 @@ class GeometryObject(FlatCAMObj, Geometry):
         except (TypeError, AttributeError):
         except (TypeError, AttributeError):
             pass
             pass
 
 
+        # common parameters update
+        try:
+            self.ui.toolchangeg_cb.stateChanged.disconnect(self.update_common_param_in_storage)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.ui.toolchangez_entry.editingFinished.disconnect(self.update_common_param_in_storage)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.ui.endz_entry.editingFinished.disconnect(self.update_common_param_in_storage)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.ui.endxy_entry.editingFinished.disconnect(self.update_common_param_in_storage)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.ui.pp_geometry_name_cb.currentIndexChanged.disconnect(self.update_common_param_in_storage)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.ui.exclusion_cb.stateChanged.disconnect(self.update_common_param_in_storage)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.ui.polish_cb.stateChanged.disconnect(self.update_common_param_in_storage)
+        except (TypeError, AttributeError):
+            pass
+
     def on_toggle_all_rows(self):
     def on_toggle_all_rows(self):
         """
         """
         will toggle the selection of all rows in Tools table
         will toggle the selection of all rows in Tools table
@@ -1555,7 +1615,13 @@ class GeometryObject(FlatCAMObj, Geometry):
 
 
     def update_common_param_in_storage(self):
     def update_common_param_in_storage(self):
         for tooluid_value in self.tools.values():
         for tooluid_value in self.tools.values():
+            tooluid_value['data']['toolchange'] = self.ui.toolchangeg_cb.get_value()
+            tooluid_value['data']['toolchangez'] = self.ui.toolchangez_entry.get_value()
+            tooluid_value['data']['endz'] = self.ui.endz_entry.get_value()
+            tooluid_value['data']['endxy'] = self.ui.endxy_entry.get_value()
             tooluid_value['data']['ppname_g'] = self.ui.pp_geometry_name_cb.get_value()
             tooluid_value['data']['ppname_g'] = self.ui.pp_geometry_name_cb.get_value()
+            tooluid_value['data']['area_exclusion'] = self.ui.exclusion_cb.get_value()
+            tooluid_value['data']['polish'] = self.ui.polish_cb.get_value()
 
 
     def select_tools_table_row(self, row, clearsel=None):
     def select_tools_table_row(self, row, clearsel=None):
         if clearsel:
         if clearsel:
@@ -1854,6 +1920,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             return
             return
 
 
         self.multigeo = True
         self.multigeo = True
+
         # Object initialization function for app.app_obj.new_object()
         # Object initialization function for app.app_obj.new_object()
         # RUNNING ON SEPARATE THREAD!
         # RUNNING ON SEPARATE THREAD!
         def job_init_single_geometry(job_obj, app_obj):
         def job_init_single_geometry(job_obj, app_obj):
@@ -1985,7 +2052,7 @@ class GeometryObject(FlatCAMObj, Geometry):
                 # TODO this serve for bounding box creation only; should be optimized
                 # TODO this serve for bounding box creation only; should be optimized
                 # commented this; there is no need for the actual GCode geometry - the original one will serve as well
                 # commented this; there is no need for the actual GCode geometry - the original one will serve as well
                 # for bounding box values
                 # for bounding box values
-                # dia_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_cnc_dict['gcode_parsed']])
+                # dia_cnc_dict['solid_geometry'] = unary_union([geo['geom'] for geo in dia_cnc_dict['gcode_parsed']])
                 try:
                 try:
                     dia_cnc_dict['solid_geometry'] = tool_solid_geometry
                     dia_cnc_dict['solid_geometry'] = tool_solid_geometry
                     self.app.inform.emit('[success] %s...' % _("Finished G-Code processing"))
                     self.app.inform.emit('[success] %s...' % _("Finished G-Code processing"))
@@ -2115,9 +2182,9 @@ class GeometryObject(FlatCAMObj, Geometry):
                 is_first = True if tooluid_key == tool_lst[0] else False
                 is_first = True if tooluid_key == tool_lst[0] else False
                 is_last = True if tooluid_key == tool_lst[-1] else False
                 is_last = True if tooluid_key == tool_lst[-1] else False
                 res, start_gcode = job_obj.geometry_tool_gcode_gen(tooluid_key, tools_dict, first_pt=(0, 0),
                 res, start_gcode = job_obj.geometry_tool_gcode_gen(tooluid_key, tools_dict, first_pt=(0, 0),
-                                                                   tolerance = tol,
+                                                                   tolerance=tol,
                                                                    is_first=is_first, is_last=is_last,
                                                                    is_first=is_first, is_last=is_last,
-                                                                   toolchange = True)
+                                                                   toolchange=True)
                 if res == 'fail':
                 if res == 'fail':
                     log.debug("GeometryObject.mtool_gen_cncjob() --> generate_from_geometry2() failed")
                     log.debug("GeometryObject.mtool_gen_cncjob() --> generate_from_geometry2() failed")
                     return 'fail'
                     return 'fail'
@@ -2135,7 +2202,7 @@ class GeometryObject(FlatCAMObj, Geometry):
                 # TODO this serve for bounding box creation only; should be optimized
                 # TODO this serve for bounding box creation only; should be optimized
                 # commented this; there is no need for the actual GCode geometry - the original one will serve as well
                 # commented this; there is no need for the actual GCode geometry - the original one will serve as well
                 # for bounding box values
                 # for bounding box values
-                # geo_for_bound_values = cascaded_union([
+                # geo_for_bound_values = unary_union([
                 #     geo['geom'] for geo in dia_cnc_dict['gcode_parsed'] if geo['geom'].is_valid is True
                 #     geo['geom'] for geo in dia_cnc_dict['gcode_parsed'] if geo['geom'].is_valid is True
                 # ])
                 # ])
                 try:
                 try:
@@ -2303,8 +2370,8 @@ class GeometryObject(FlatCAMObj, Geometry):
                                                    toolchangexy=toolchangexy,
                                                    toolchangexy=toolchangexy,
                                                    extracut=extracut, extracut_length=extracut_length,
                                                    extracut=extracut, extracut_length=extracut_length,
                                                    startz=startz, endz=endz, endxy=endxy,
                                                    startz=startz, endz=endz, endxy=endxy,
-                                                   pp_geometry_name=ppname_g
-            )
+                                                   pp_geometry_name=ppname_g)
+
             job_obj.source_file = res
             job_obj.source_file = res
             # tell gcode_parse from which point to start drawing the lines depending on what kind of object is the
             # tell gcode_parse from which point to start drawing the lines depending on what kind of object is the
             # source of gcode
             # source of gcode
@@ -2872,13 +2939,13 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.plot()
         self.plot()
 
 
     @staticmethod
     @staticmethod
-    def merge(geo_list, geo_final, multigeo=None, fuse_tools=None):
+    def merge(geo_list, geo_final, multi_geo=None, fuse_tools=None):
         """
         """
         Merges the geometry of objects in grb_list into the geometry of geo_final.
         Merges the geometry of objects in grb_list into the geometry of geo_final.
 
 
         :param geo_list:    List of GerberObject Objects to join.
         :param geo_list:    List of GerberObject Objects to join.
         :param geo_final:   Destination GerberObject object.
         :param geo_final:   Destination GerberObject object.
-        :param multigeo:    if the merged geometry objects are of type MultiGeo
+        :param multi_geo:   if the merged geometry objects are of type MultiGeo
         :param fuse_tools:  If True will try to fuse tools of the same type for the Geometry objects
         :param fuse_tools:  If True will try to fuse tools of the same type for the Geometry objects
         :return: None
         :return: None
         """
         """
@@ -2908,7 +2975,7 @@ class GeometryObject(FlatCAMObj, Geometry):
                 GeometryObject.merge(geo_list=geo_obj, geo_final=geo_final)
                 GeometryObject.merge(geo_list=geo_obj, geo_final=geo_final)
             # If not list, just append
             # If not list, just append
             else:
             else:
-                if multigeo is None or multigeo is False:
+                if multi_geo is None or multi_geo is False:
                     geo_final.multigeo = False
                     geo_final.multigeo = False
                 else:
                 else:
                     geo_final.multigeo = True
                     geo_final.multigeo = True
@@ -2966,19 +3033,23 @@ class GeometryObject(FlatCAMObj, Geometry):
             new_tool_nr = 1
             new_tool_nr = 1
             for i_lst in intersect_list:
             for i_lst in intersect_list:
                 new_solid_geo = []
                 new_solid_geo = []
+                last_tool = None
                 for old_tool in i_lst:
                 for old_tool in i_lst:
                     new_solid_geo += new_tools[old_tool]['solid_geometry']
                     new_solid_geo += new_tools[old_tool]['solid_geometry']
+                    last_tool = old_tool
 
 
-                if new_solid_geo:
+                if new_solid_geo and last_tool:
                     final_tools[new_tool_nr] = \
                     final_tools[new_tool_nr] = \
                         {
                         {
-                            k: deepcopy(new_tools[old_tool][k]) for k in new_tools[old_tool] if k != 'solid_geometry'
+                            k: deepcopy(new_tools[last_tool][k]) for k in new_tools[last_tool] if k != 'solid_geometry'
                         }
                         }
                     final_tools[new_tool_nr]['solid_geometry'] = deepcopy(new_solid_geo)
                     final_tools[new_tool_nr]['solid_geometry'] = deepcopy(new_solid_geo)
                     new_tool_nr += 1
                     new_tool_nr += 1
         else:
         else:
             final_tools = new_tools
             final_tools = new_tools
 
 
+        # if not final_tools:
+        #     return 'fail'
         geo_final.tools = final_tools
         geo_final.tools = final_tools
 
 
     @staticmethod
     @staticmethod

+ 20 - 3
appObjects/FlatCAMGerber.py

@@ -12,7 +12,7 @@
 
 
 
 
 from shapely.geometry import Point, Polygon, MultiPolygon, MultiLineString, LineString, LinearRing
 from shapely.geometry import Point, Polygon, MultiPolygon, MultiLineString, LineString, LinearRing
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 
 
 from appParsers.ParseGerber import Gerber
 from appParsers.ParseGerber import Gerber
 from appObjects.FlatCAMObj import *
 from appObjects.FlatCAMObj import *
@@ -150,6 +150,10 @@ class GerberObject(FlatCAMObj, Gerber):
         # Editor
         # Editor
         self.ui.editor_button.clicked.connect(lambda: self.app.object2editor())
         self.ui.editor_button.clicked.connect(lambda: self.app.object2editor())
 
 
+        # Properties
+        self.ui.properties_button.toggled.connect(self.on_properties)
+        self.calculations_finished.connect(self.update_area_chull)
+
         # Tools
         # Tools
         self.ui.iso_button.clicked.connect(self.app.isolation_tool.run)
         self.ui.iso_button.clicked.connect(self.app.isolation_tool.run)
         self.ui.generate_ncc_button.clicked.connect(self.app.ncclear_tool.run)
         self.ui.generate_ncc_button.clicked.connect(self.app.ncclear_tool.run)
@@ -343,6 +347,19 @@ class GerberObject(FlatCAMObj, Gerber):
 
 
         return new_geo
         return new_geo
 
 
+    def on_properties(self, state):
+        if state:
+            self.ui.properties_frame.show()
+        else:
+            self.ui.properties_frame.hide()
+            return
+
+        self.ui.treeWidget.clear()
+        self.add_properties_items(obj=self, treeWidget=self.ui.treeWidget)
+
+        # make sure that the FCTree widget columns are resized to content
+        self.ui.treeWidget.resize_sig.emit()
+
     def on_generate_buffer(self):
     def on_generate_buffer(self):
         self.app.inform.emit('[WARNING_NOTCL] %s...' % _("Buffering solid geometry"))
         self.app.inform.emit('[WARNING_NOTCL] %s...' % _("Buffering solid geometry"))
 
 
@@ -369,7 +386,7 @@ class GerberObject(FlatCAMObj, Gerber):
                 try:
                 try:
                     self.solid_geometry = MultiPolygon(self.solid_geometry)
                     self.solid_geometry = MultiPolygon(self.solid_geometry)
                 except Exception:
                 except Exception:
-                    self.solid_geometry = cascaded_union(self.solid_geometry)
+                    self.solid_geometry = unary_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"]:
@@ -395,7 +412,7 @@ class GerberObject(FlatCAMObj, Gerber):
                 try:
                 try:
                     self.solid_geometry = MultiPolygon(self.solid_geometry)
                     self.solid_geometry = MultiPolygon(self.solid_geometry)
                 except Exception:
                 except Exception:
-                    self.solid_geometry = cascaded_union(self.solid_geometry)
+                    self.solid_geometry = unary_union(self.solid_geometry)
 
 
             # Bounding box with rounded corners
             # Bounding box with rounded corners
             bounding_box = self.solid_geometry.envelope.buffer(float(self.options["bboxmargin"]))
             bounding_box = self.solid_geometry.envelope.buffer(float(self.options["bboxmargin"]))

+ 452 - 3
appObjects/FlatCAMObj.py

@@ -18,7 +18,12 @@ from appCommon.Common import LoudDict
 from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy
 from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy
 from appGUI.VisPyVisuals import ShapeCollection
 from appGUI.VisPyVisuals import ShapeCollection
 
 
+from shapely.ops import unary_union
+from shapely.geometry import Polygon, MultiPolygon
+
+from copy import deepcopy
 import sys
 import sys
+import math
 
 
 import gettext
 import gettext
 import appTranslation as fcTranslate
 import appTranslation as fcTranslate
@@ -55,6 +60,9 @@ class FlatCAMObj(QtCore.QObject):
     # signal to plot a single object
     # signal to plot a single object
     plot_single_object = QtCore.pyqtSignal()
     plot_single_object = QtCore.pyqtSignal()
 
 
+    # signal for Properties
+    calculations_finished = QtCore.pyqtSignal(float, float, float, float, float, object)
+
     def __init__(self, name):
     def __init__(self, name):
         """
         """
         Constructor.
         Constructor.
@@ -112,6 +120,9 @@ class FlatCAMObj(QtCore.QObject):
         # self.units = 'IN'
         # self.units = 'IN'
         self.units = self.app.defaults['units']
         self.units = self.app.defaults['units']
 
 
+        # this is the treeWidget from the UI; it is updated when the add_properties_items() method is called
+        self.treeWidget = None
+
         self.plot_single_object.connect(self.single_object_plot)
         self.plot_single_object.connect(self.single_object_plot)
 
 
     def __del__(self):
     def __del__(self):
@@ -456,6 +467,444 @@ class FlatCAMObj(QtCore.QObject):
                 self.app.defaults[filter_string] = ';;'.join(filter_list)
                 self.app.defaults[filter_string] = ';;'.join(filter_list)
                 return
                 return
 
 
+    def add_properties_items(self, obj, treeWidget):
+        self.treeWidget = treeWidget
+        parent = self.treeWidget.invisibleRootItem()
+        apertures = ''
+        tools = ''
+        drills = ''
+        slots = ''
+        others = ''
+
+        font = QtGui.QFont()
+        font.setBold(True)
+
+        p_color = QtGui.QColor("#000000") if self.app.defaults['global_gray_icons'] is False \
+            else QtGui.QColor("#FFFFFF")
+
+        # main Items categories
+        dims = self.treeWidget.addParent(
+            parent, _('Dimensions'), expanded=True, color=p_color, font=font)
+        options = self.treeWidget.addParent(parent, _('Options'), color=p_color, font=font)
+
+        if obj.kind.lower() == 'gerber':
+            apertures = self.treeWidget.addParent(
+                parent, _('Apertures'), expanded=True, color=p_color, font=font)
+        else:
+            tools = self.treeWidget.addParent(
+                parent, _('Tools'), expanded=True, color=p_color, font=font)
+
+        if obj.kind.lower() == 'excellon':
+            drills = self.treeWidget.addParent(
+                parent, _('Drills'), expanded=True, color=p_color, font=font)
+            slots = self.treeWidget.addParent(
+                parent, _('Slots'), expanded=True, color=p_color, font=font)
+
+        if obj.kind.lower() == 'cncjob':
+            others = self.treeWidget.addParent(
+                parent, _('Others'), expanded=True, color=p_color, font=font)
+
+        # separator = self.treeWidget.addParent(parent, '')
+
+        def job_thread(obj_prop):
+            self.app.proc_container.new(_("Calculating dimensions ... Please wait."))
+
+            length = 0.0
+            width = 0.0
+            area = 0.0
+            copper_area = 0.0
+
+            geo = obj_prop.solid_geometry
+            if geo:
+                # calculate physical dimensions
+                try:
+                    xmin, ymin, xmax, ymax = obj_prop.bounds()
+
+                    length = abs(xmax - xmin)
+                    width = abs(ymax - ymin)
+                except Exception as ee:
+                    log.debug("FlatCAMObj.addItems() -> calculate dimensions --> %s" % str(ee))
+
+                # calculate box area
+                if self.app.defaults['units'].lower() == 'mm':
+                    area = (length * width) / 100
+                else:
+                    area = length * width
+
+                if obj_prop.kind.lower() == 'gerber':
+                    # calculate copper area
+                    try:
+                        for geo_el in geo:
+                            copper_area += geo_el.area
+                    except TypeError:
+                        copper_area += geo.area
+                    copper_area /= 100
+            else:
+                xmin = []
+                ymin = []
+                xmax = []
+                ymax = []
+
+                if obj_prop.kind.lower() == 'cncjob':
+                    try:
+                        for tool_k in obj_prop.exc_cnc_tools:
+                            x0, y0, x1, y1 = unary_union(obj_prop.exc_cnc_tools[tool_k]['solid_geometry']).bounds
+                            xmin.append(x0)
+                            ymin.append(y0)
+                            xmax.append(x1)
+                            ymax.append(y1)
+                    except Exception as ee:
+                        log.debug("FlatCAMObj.addItems() --> %s" % str(ee))
+
+                    try:
+                        for tool_k in obj_prop.cnc_tools:
+                            x0, y0, x1, y1 = unary_union(obj_prop.cnc_tools[tool_k]['solid_geometry']).bounds
+                            xmin.append(x0)
+                            ymin.append(y0)
+                            xmax.append(x1)
+                            ymax.append(y1)
+                    except Exception as ee:
+                        log.debug("FlatCAMObj.addItems() --> %s" % str(ee))
+                else:
+                    try:
+                        for tool_k in obj_prop.tools:
+                            x0, y0, x1, y1 = unary_union(obj_prop.tools[tool_k]['solid_geometry']).bounds
+                            xmin.append(x0)
+                            ymin.append(y0)
+                            xmax.append(x1)
+                            ymax.append(y1)
+                    except Exception as ee:
+                        log.debug("FlatCAMObj.addItems() --> %s" % str(ee))
+
+                try:
+                    xmin = min(xmin)
+                    ymin = min(ymin)
+                    xmax = max(xmax)
+                    ymax = max(ymax)
+
+                    length = abs(xmax - xmin)
+                    width = abs(ymax - ymin)
+
+                    # calculate box area
+                    if self.app.defaults['units'].lower() == 'mm':
+                        area = (length * width) / 100
+                    else:
+                        area = length * width
+
+                    if obj_prop.kind.lower() == 'gerber':
+                        # calculate copper area
+
+                        # create a complete solid_geometry from the tools
+                        geo_tools = []
+                        for tool_k in obj_prop.tools:
+                            if 'solid_geometry' in obj_prop.tools[tool_k]:
+                                for geo_el in obj_prop.tools[tool_k]['solid_geometry']:
+                                    geo_tools.append(geo_el)
+
+                        try:
+                            for geo_el in geo_tools:
+                                copper_area += geo_el.area
+                        except TypeError:
+                            copper_area += geo_tools.area
+                        copper_area /= 100
+                except Exception as err:
+                    log.debug("FlatCAMObj.addItems() --> %s" % str(err))
+
+            area_chull = 0.0
+            if obj_prop.kind.lower() != 'cncjob':
+                # calculate and add convex hull area
+                if geo:
+                    if isinstance(geo, list) and geo[0] is not None:
+                        if isinstance(geo, MultiPolygon):
+                            env_obj = geo.convex_hull
+                        elif (isinstance(geo, MultiPolygon) and len(geo) == 1) or \
+                                (isinstance(geo, list) and len(geo) == 1) and isinstance(geo[0], Polygon):
+                            env_obj = unary_union(geo)
+                            env_obj = env_obj.convex_hull
+                        else:
+                            env_obj = unary_union(geo)
+                            env_obj = env_obj.convex_hull
+
+                        area_chull = env_obj.area
+                    else:
+                        area_chull = 0
+                else:
+                    try:
+                        area_chull = []
+                        for tool_k in obj_prop.tools:
+                            area_el = unary_union(obj_prop.tools[tool_k]['solid_geometry']).convex_hull
+                            area_chull.append(area_el.area)
+                        area_chull = max(area_chull)
+                    except Exception as er:
+                        area_chull = None
+                        log.debug("FlatCAMObj.addItems() --> %s" % str(er))
+
+            if self.app.defaults['units'].lower() == 'mm' and area_chull:
+                area_chull = area_chull / 100
+
+            if area_chull is None:
+                area_chull = 0
+
+            self.calculations_finished.emit(area, length, width, area_chull, copper_area, dims)
+
+        self.app.worker_task.emit({'fcn': job_thread, 'params': [obj]})
+
+        # Options items
+        for option in obj.options:
+            if option == 'name':
+                continue
+            self.treeWidget.addChild(options, [str(option), str(obj.options[option])], True)
+
+        # Items that depend on the object type
+        if obj.kind.lower() == 'gerber':
+            temp_ap = {}
+            for ap in obj.apertures:
+                temp_ap.clear()
+                temp_ap = deepcopy(obj.apertures[ap])
+                temp_ap.pop('geometry', None)
+
+                solid_nr = 0
+                follow_nr = 0
+                clear_nr = 0
+
+                if 'geometry' in obj.apertures[ap]:
+                    if obj.apertures[ap]['geometry']:
+                        font.setBold(True)
+                        for el in obj.apertures[ap]['geometry']:
+                            if 'solid' in el:
+                                solid_nr += 1
+                            if 'follow' in el:
+                                follow_nr += 1
+                            if 'clear' in el:
+                                clear_nr += 1
+                else:
+                    font.setBold(False)
+                temp_ap['Solid_Geo'] = '%s Polygons' % str(solid_nr)
+                temp_ap['Follow_Geo'] = '%s LineStrings' % str(follow_nr)
+                temp_ap['Clear_Geo'] = '%s Polygons' % str(clear_nr)
+
+                apid = self.treeWidget.addParent(
+                    apertures, str(ap), expanded=False, color=p_color, font=font)
+                for key in temp_ap:
+                    self.treeWidget.addChild(apid, [str(key), str(temp_ap[key])], True)
+        elif obj.kind.lower() == 'excellon':
+            tot_drill_cnt = 0
+            tot_slot_cnt = 0
+
+            for tool, value in obj.tools.items():
+                toolid = self.treeWidget.addParent(
+                    tools, str(tool), expanded=False, color=p_color, font=font)
+
+                drill_cnt = 0  # variable to store the nr of drills per tool
+                slot_cnt = 0  # variable to store the nr of slots per tool
+
+                # Find no of drills for the current tool
+                if 'drills' in value and value['drills']:
+                    drill_cnt = len(value['drills'])
+
+                tot_drill_cnt += drill_cnt
+
+                # Find no of slots for the current tool
+                if 'slots' in value and value['slots']:
+                    slot_cnt = len(value['slots'])
+
+                tot_slot_cnt += slot_cnt
+
+                self.treeWidget.addChild(
+                    toolid,
+                    [
+                        _('Diameter'),
+                        '%.*f %s' % (self.decimals, value['tooldia'], self.app.defaults['units'].lower())
+                    ],
+                    True
+                )
+                self.treeWidget.addChild(toolid, [_('Drills number'), str(drill_cnt)], True)
+                self.treeWidget.addChild(toolid, [_('Slots number'), str(slot_cnt)], True)
+
+            self.treeWidget.addChild(drills, [_('Drills total number:'), str(tot_drill_cnt)], True)
+            self.treeWidget.addChild(slots, [_('Slots total number:'), str(tot_slot_cnt)], True)
+        elif obj.kind.lower() == 'geometry':
+            for tool, value in obj.tools.items():
+                geo_tool = self.treeWidget.addParent(
+                    tools, str(tool), expanded=False, color=p_color, font=font)
+                for k, v in value.items():
+                    if k == 'solid_geometry':
+                        # printed_value = _('Present') if v else _('None')
+                        try:
+                            printed_value = str(len(v))
+                        except (TypeError, AttributeError):
+                            printed_value = '1'
+                        self.treeWidget.addChild(geo_tool, [str(k), printed_value], True)
+                    elif k == 'data':
+                        tool_data = self.treeWidget.addParent(
+                            geo_tool, str(k).capitalize(), color=p_color, font=font)
+                        for data_k, data_v in v.items():
+                            self.treeWidget.addChild(tool_data, [str(data_k), str(data_v)], True)
+                    else:
+                        self.treeWidget.addChild(geo_tool, [str(k), str(v)], True)
+        elif obj.kind.lower() == 'cncjob':
+            # for cncjob objects made from gerber or geometry
+            for tool, value in obj.cnc_tools.items():
+                geo_tool = self.treeWidget.addParent(
+                    tools, str(tool), expanded=False, color=p_color, font=font)
+                for k, v in value.items():
+                    if k == 'solid_geometry':
+                        printed_value = _('Present') if v else _('None')
+                        self.treeWidget.addChild(geo_tool, [_("Solid Geometry"), printed_value], True)
+                    elif k == 'gcode':
+                        printed_value = _('Present') if v != '' else _('None')
+                        self.treeWidget.addChild(geo_tool, [_("GCode Text"), printed_value], True)
+                    elif k == 'gcode_parsed':
+                        printed_value = _('Present') if v else _('None')
+                        self.treeWidget.addChild(geo_tool, [_("GCode Geometry"), printed_value], True)
+                    elif k == 'data':
+                        pass
+                    else:
+                        self.treeWidget.addChild(geo_tool, [str(k), str(v)], True)
+
+                v = value['data']
+                tool_data = self.treeWidget.addParent(
+                    geo_tool, _("Tool Data"), color=p_color, font=font)
+                for data_k, data_v in v.items():
+                    self.treeWidget.addChild(tool_data, [str(data_k).capitalize(), str(data_v)], True)
+
+            # for cncjob objects made from excellon
+            for tool_dia, value in obj.exc_cnc_tools.items():
+                exc_tool = self.treeWidget.addParent(
+                    tools, str(value['tool']), expanded=False, color=p_color, font=font
+                )
+                self.treeWidget.addChild(
+                    exc_tool,
+                    [
+                        _('Diameter'),
+                        '%.*f %s' % (self.decimals, tool_dia, self.app.defaults['units'].lower())
+                    ],
+                    True
+                )
+                for k, v in value.items():
+                    if k == 'solid_geometry':
+                        printed_value = _('Present') if v else _('None')
+                        self.treeWidget.addChild(exc_tool, [_("Solid Geometry"), printed_value], True)
+                    elif k == 'nr_drills':
+                        self.treeWidget.addChild(exc_tool, [_("Drills number"), str(v)], True)
+                    elif k == 'nr_slots':
+                        self.treeWidget.addChild(exc_tool, [_("Slots number"), str(v)], True)
+                    elif k == 'gcode':
+                        printed_value = _('Present') if v != '' else _('None')
+                        self.treeWidget.addChild(exc_tool, [_("GCode Text"), printed_value], True)
+                    elif k == 'gcode_parsed':
+                        printed_value = _('Present') if v else _('None')
+                        self.treeWidget.addChild(exc_tool, [_("GCode Geometry"), printed_value], True)
+                    else:
+                        pass
+
+                self.treeWidget.addChild(
+                    exc_tool,
+                    [
+                        _("Depth of Cut"),
+                        '%.*f %s' % (
+                            self.decimals,
+                            (obj.z_cut - abs(value['data']['tools_drill_offset'])),
+                            self.app.defaults['units'].lower()
+                        )
+                    ],
+                    True
+                )
+                self.treeWidget.addChild(
+                    exc_tool,
+                    [
+                        _("Clearance Height"),
+                        '%.*f %s' % (
+                            self.decimals,
+                            obj.z_move,
+                            self.app.defaults['units'].lower()
+                        )
+                    ],
+                    True
+                )
+                self.treeWidget.addChild(
+                    exc_tool,
+                    [
+                        _("Feedrate"),
+                        '%.*f %s/min' % (
+                            self.decimals,
+                            obj.feedrate,
+                            self.app.defaults['units'].lower()
+                        )
+                    ],
+                    True
+                )
+
+                v = value['data']
+                tool_data = self.treeWidget.addParent(
+                    exc_tool, _("Tool Data"), color=p_color, font=font)
+                for data_k, data_v in v.items():
+                    self.treeWidget.addChild(tool_data, [str(data_k).capitalize(), str(data_v)], True)
+
+            r_time = obj.routing_time
+            if r_time > 1:
+                units_lbl = 'min'
+            else:
+                r_time *= 60
+                units_lbl = 'sec'
+            r_time = math.ceil(float(r_time))
+            self.treeWidget.addChild(
+                others,
+                [
+                    '%s:' % _('Routing time'),
+                    '%.*f %s' % (self.decimals, r_time, units_lbl)],
+                True
+            )
+            self.treeWidget.addChild(
+                others,
+                [
+                    '%s:' % _('Travelled distance'),
+                    '%.*f %s' % (self.decimals, obj.travel_distance, self.app.defaults['units'].lower())
+                ],
+                True
+            )
+
+        # treeWidget.addChild(separator, [''])
+
+    def update_area_chull(self, area, length, width, chull_area, copper_area, location):
+
+        # add dimensions
+        self.treeWidget.addChild(
+            location,
+            ['%s:' % _('Length'), '%.*f %s' % (self.decimals, length, self.app.defaults['units'].lower())],
+            True
+        )
+        self.treeWidget.addChild(
+            location,
+            ['%s:' % _('Width'), '%.*f %s' % (self.decimals, width, self.app.defaults['units'].lower())],
+            True
+        )
+
+        # add box area
+        if self.app.defaults['units'].lower() == 'mm':
+            self.treeWidget.addChild(location, ['%s:' % _('Box Area'), '%.*f %s' % (self.decimals, area, 'cm2')], True)
+            self.treeWidget.addChild(
+                location,
+                ['%s:' % _('Convex_Hull Area'), '%.*f %s' % (self.decimals, chull_area, 'cm2')],
+                True
+            )
+
+        else:
+            self.treeWidget.addChild(location, ['%s:' % _('Box Area'), '%.*f %s' % (self.decimals, area, 'in2')], True)
+            self.treeWidget.addChild(
+                location,
+                ['%s:' % _('Convex_Hull Area'), '%.*f %s' % (self.decimals, chull_area, 'in2')],
+                True
+            )
+
+        # add copper area
+        if self.app.defaults['units'].lower() == 'mm':
+            self.treeWidget.addChild(
+                location, ['%s:' % _('Copper Area'), '%.*f %s' % (self.decimals, copper_area, 'cm2')], True)
+        else:
+            self.treeWidget.addChild(
+                location, ['%s:' % _('Copper Area'), '%.*f %s' % (self.decimals, copper_area, 'in2')], True)
+
     @staticmethod
     @staticmethod
     def poly2rings(poly):
     def poly2rings(poly):
         return [poly.exterior] + [interior for interior in poly.interiors]
         return [poly.exterior] + [interior for interior in poly.interiors]
@@ -471,8 +920,8 @@ class FlatCAMObj(QtCore.QObject):
         current_visibility = self.shapes.visible
         current_visibility = self.shapes.visible
         # self.shapes.visible = value   # maybe this is slower in VisPy? use enabled property?
         # self.shapes.visible = value   # maybe this is slower in VisPy? use enabled property?
 
 
-        def task(current_visibility):
-            if current_visibility is True:
+        def task(visibility):
+            if visibility is True:
                 if value is False:
                 if value is False:
                     self.shapes.visible = False
                     self.shapes.visible = False
             else:
             else:
@@ -517,4 +966,4 @@ class FlatCAMObj(QtCore.QObject):
         del self.options
         del self.options
 
 
         # Set flag
         # Set flag
-        self.deleted = True
+        self.deleted = True

+ 1 - 1
appObjects/ObjectCollection.py

@@ -971,7 +971,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             except Exception as e:
             except Exception as e:
                 log.debug("Nothing to remove. %s" % str(e))
                 log.debug("Nothing to remove. %s" % str(e))
 
 
-            self.app.setup_component_editor()
+            self.app.setup_default_properties_tab()
             return
             return
 
 
         if obj:
         if obj:

+ 2 - 2
appParsers/ParseGerber.py

@@ -374,7 +374,7 @@ class Gerber(Geometry):
         geo_f = None
         geo_f = None
 
 
         # Polygons are stored here until there is a change in polarity.
         # Polygons are stored here until there is a change in polarity.
-        # Only then they are combined via cascaded_union and added or
+        # Only then they are combined via unary_union and added or
         # subtracted from solid_geometry. This is ~100 times faster than
         # subtracted from solid_geometry. This is ~100 times faster than
         # applying a union for every new polygon.
         # applying a union for every new polygon.
         poly_buffer = []
         poly_buffer = []
@@ -1680,7 +1680,7 @@ class Gerber(Geometry):
         #
         #
         # self.do_flashes()
         # self.do_flashes()
         #
         #
-        # self.solid_geometry = cascaded_union(self.buffered_paths +
+        # self.solid_geometry = unary_union(self.buffered_paths +
         #                                      [poly['polygon'] for poly in self.regions] +
         #                                      [poly['polygon'] for poly in self.regions] +
         #                                      self.flash_geometry)
         #                                      self.flash_geometry)
 
 

+ 18 - 9
appParsers/ParseSVG.py

@@ -228,8 +228,10 @@ def svgrect2shapely(rect, n_points=32, factor=1.0):
     else:
     else:
         y = 0
         y = 0
 
 
-    rxstr = rect.get('rx') * factor
-    rystr = rect.get('ry') * factor
+    rxstr = rect.get('rx')
+    rxstr = rxstr * factor if rxstr else rxstr
+    rystr = rect.get('ry')
+    rystr = rystr * factor if rystr else rystr
 
 
     if rxstr is None and rystr is None:  # Sharp corners
     if rxstr is None and rystr is None:  # Sharp corners
         pts = [
         pts = [
@@ -290,9 +292,12 @@ def svgcircle2shapely(circle, n_points=64, factor=1.0):
     # cx = float(circle.get('cx'))
     # cx = float(circle.get('cx'))
     # cy = float(circle.get('cy'))
     # cy = float(circle.get('cy'))
     # r = float(circle.get('r'))
     # r = float(circle.get('r'))
-    cx = svgparselength(circle.get('cx'))[0] * factor  # TODO: No units support yet
-    cy = svgparselength(circle.get('cy'))[0] * factor  # TODO: No units support yet
-    r = svgparselength(circle.get('r'))[0] * factor  # TODO: No units support yet
+    cx = svgparselength(circle.get('cx'))[0]  # TODO: No units support yet
+    cx = cx * factor if cx else cx
+    cy = svgparselength(circle.get('cy'))[0]  # TODO: No units support yet
+    cy = cy * factor if cy else cy
+    r = svgparselength(circle.get('r'))[0]  # TODO: No units support yet
+    r = r * factor if r else r
 
 
     return Point(cx, cy).buffer(r, resolution=n_points)
     return Point(cx, cy).buffer(r, resolution=n_points)
 
 
@@ -309,11 +314,15 @@ def svgellipse2shapely(ellipse, n_points=64, factor=1.0):
     :rtype:             shapely.geometry.polygon.LinearRing
     :rtype:             shapely.geometry.polygon.LinearRing
     """
     """
 
 
-    cx = svgparselength(ellipse.get('cx'))[0] * factor  # TODO: No units support yet
-    cy = svgparselength(ellipse.get('cy'))[0] * factor  # TODO: No units support yet
+    cx = svgparselength(ellipse.get('cx'))[0]   # TODO: No units support yet
+    cx = cx * factor if cx else cx
+    cy = svgparselength(ellipse.get('cy'))[0]   # TODO: No units support yet
+    cy = cy * factor if cy else cy
 
 
-    rx = svgparselength(ellipse.get('rx'))[0] * factor  # TODO: No units support yet
-    ry = svgparselength(ellipse.get('ry'))[0] * factor  # TODO: No units support yet
+    rx = svgparselength(ellipse.get('rx'))[0]   # TODO: No units support yet
+    rx = rx * factor if rx else rx
+    ry = svgparselength(ellipse.get('ry'))[0]   # TODO: No units support yet
+    ry = ry * factor if ry else ry
 
 
     t = np.arange(n_points, dtype=float) / n_points
     t = np.arange(n_points, dtype=float) / n_points
     x = cx + rx * np.cos(2 * np.pi * t)
     x = cx + rx * np.cos(2 * np.pi * t)

+ 7 - 7
appTools/ToolCopperThieving.py

@@ -12,7 +12,7 @@ from appTool import AppTool
 from appGUI.GUIElements import FCDoubleSpinner, RadioSet, FCEntry, FCComboBox
 from appGUI.GUIElements import FCDoubleSpinner, RadioSet, FCEntry, FCComboBox
 
 
 import shapely.geometry.base as base
 import shapely.geometry.base as base
-from shapely.ops import cascaded_union, unary_union
+from shapely.ops import unary_union
 from shapely.geometry import Polygon, MultiPolygon, Point, LineString
 from shapely.geometry import Polygon, MultiPolygon, Point, LineString
 from shapely.geometry import box as box
 from shapely.geometry import box as box
 import shapely.affinity as affinity
 import shapely.affinity as affinity
@@ -428,7 +428,7 @@ class ToolCopperThieving(AppTool):
             if len(self.sel_rect) == 0:
             if len(self.sel_rect) == 0:
                 return
                 return
 
 
-            self.sel_rect = cascaded_union(self.sel_rect)
+            self.sel_rect = unary_union(self.sel_rect)
 
 
             if not isinstance(self.sel_rect, Iterable):
             if not isinstance(self.sel_rect, Iterable):
                 self.sel_rect = [self.sel_rect]
                 self.sel_rect = [self.sel_rect]
@@ -606,9 +606,9 @@ class ToolCopperThieving(AppTool):
                             env_obj = geo_n.convex_hull
                             env_obj = geo_n.convex_hull
                         elif (isinstance(geo_n, MultiPolygon) and len(geo_n) == 1) or \
                         elif (isinstance(geo_n, MultiPolygon) and len(geo_n) == 1) or \
                                 (isinstance(geo_n, list) and len(geo_n) == 1) and isinstance(geo_n[0], Polygon):
                                 (isinstance(geo_n, list) and len(geo_n) == 1) and isinstance(geo_n[0], Polygon):
-                            env_obj = cascaded_union(geo_n)
+                            env_obj = unary_union(geo_n)
                         else:
                         else:
-                            env_obj = cascaded_union(geo_n)
+                            env_obj = unary_union(geo_n)
                             env_obj = env_obj.convex_hull
                             env_obj = env_obj.convex_hull
                         bounding_box = env_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
                         bounding_box = env_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
                     else:
                     else:
@@ -660,10 +660,10 @@ class ToolCopperThieving(AppTool):
                             raise grace
                             raise grace
                         geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
                         geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
 
 
-                    bounding_box = cascaded_union(geo_buff_list)
+                    bounding_box = unary_union(geo_buff_list)
                 elif working_obj.kind == 'gerber':
                 elif working_obj.kind == 'gerber':
-                    geo_n = cascaded_union(geo_n).convex_hull
-                    bounding_box = cascaded_union(thieving_obj.solid_geometry).convex_hull.intersection(geo_n)
+                    geo_n = unary_union(geo_n).convex_hull
+                    bounding_box = unary_union(thieving_obj.solid_geometry).convex_hull.intersection(geo_n)
                     bounding_box = bounding_box.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
                     bounding_box = bounding_box.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
                 else:
                 else:
                     app_obj.app.inform.emit('[ERROR_NOTCL] %s' % _("The reference object type is not supported."))
                     app_obj.app.inform.emit('[ERROR_NOTCL] %s' % _("The reference object type is not supported."))

+ 8 - 5
appTools/ToolCutOut.py

@@ -11,7 +11,7 @@ from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, RadioSet, FCComboBox
     FCLabel
     FCLabel
 
 
 from shapely.geometry import box, MultiPolygon, Polygon, LineString, LinearRing, MultiLineString
 from shapely.geometry import box, MultiPolygon, Polygon, LineString, LinearRing, MultiLineString
-from shapely.ops import cascaded_union, unary_union, linemerge
+from shapely.ops import unary_union, linemerge
 import shapely.affinity as affinity
 import shapely.affinity as affinity
 
 
 from matplotlib.backend_bases import KeyEvent as mpl_key_event
 from matplotlib.backend_bases import KeyEvent as mpl_key_event
@@ -217,6 +217,7 @@ class CutOut(AppTool):
             "toolchangez": float(self.app.defaults["geometry_toolchangez"]),
             "toolchangez": float(self.app.defaults["geometry_toolchangez"]),
             "startz": self.app.defaults["geometry_startz"],
             "startz": self.app.defaults["geometry_startz"],
             "endz": float(self.app.defaults["geometry_endz"]),
             "endz": float(self.app.defaults["geometry_endz"]),
+            "endxy": self.app.defaults["geometry_endxy"],
             "area_exclusion": self.app.defaults["geometry_area_exclusion"],
             "area_exclusion": self.app.defaults["geometry_area_exclusion"],
             "area_shape": self.app.defaults["geometry_area_shape"],
             "area_shape": self.app.defaults["geometry_area_shape"],
             "area_strategy": self.app.defaults["geometry_area_strategy"],
             "area_strategy": self.app.defaults["geometry_area_strategy"],
@@ -414,6 +415,7 @@ class CutOut(AppTool):
             "toolchangez": float(self.app.defaults["geometry_toolchangez"]),
             "toolchangez": float(self.app.defaults["geometry_toolchangez"]),
             "startz": self.app.defaults["geometry_startz"],
             "startz": self.app.defaults["geometry_startz"],
             "endz": float(self.app.defaults["geometry_endz"]),
             "endz": float(self.app.defaults["geometry_endz"]),
+            "endxy": self.app.defaults["geometry_endxy"],
             "area_exclusion": self.app.defaults["geometry_area_exclusion"],
             "area_exclusion": self.app.defaults["geometry_area_exclusion"],
             "area_shape": self.app.defaults["geometry_area_shape"],
             "area_shape": self.app.defaults["geometry_area_shape"],
             "area_strategy": self.app.defaults["geometry_area_strategy"],
             "area_strategy": self.app.defaults["geometry_area_strategy"],
@@ -1832,7 +1834,7 @@ class CutOut(AppTool):
         log.debug("%d paths" % len(flat_geometry))
         log.debug("%d paths" % len(flat_geometry))
 
 
         polygon = Polygon(points)
         polygon = Polygon(points)
-        toolgeo = cascaded_union(polygon)
+        toolgeo = unary_union(polygon)
         diffs = []
         diffs = []
         for target in flat_geometry:
         for target in flat_geometry:
             if type(target) == LineString or type(target) == LinearRing:
             if type(target) == LineString or type(target) == LinearRing:
@@ -1906,7 +1908,7 @@ class CutOut(AppTool):
 
 
         :param target_geo:      geometry from which to subtract
         :param target_geo:      geometry from which to subtract
         :param subtractor:      a list of Points, a LinearRing or a Polygon that will be subtracted from target_geo
         :param subtractor:      a list of Points, a LinearRing or a Polygon that will be subtracted from target_geo
-        :return:                a cascaded union of the resulting geometry
+        :return:                a unary_union of the resulting geometry
         """
         """
 
 
         if target_geo is None:
         if target_geo is None:
@@ -2082,8 +2084,9 @@ class CutoutUI:
         self.addtool_from_db_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/search_db32.png'))
         self.addtool_from_db_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/search_db32.png'))
         self.addtool_from_db_btn.setToolTip(
         self.addtool_from_db_btn.setToolTip(
             _("Add a new tool to the Tool Table\n"
             _("Add a new tool to the Tool Table\n"
-              "from the Tool Database.\n"
-              "Tool database administration in Menu: Options -> Tools Database")
+              "from the Tools Database.\n"
+              "Tools database administration in in:\n"
+              "Menu: Options -> Tools Database")
         )
         )
         hlay.addWidget(self.addtool_from_db_btn)
         hlay.addWidget(self.addtool_from_db_btn)
 
 

+ 3 - 3
appTools/ToolDistanceMin.py

@@ -11,7 +11,7 @@ from appGUI.GUIElements import FCEntry
 
 
 from shapely.ops import nearest_points
 from shapely.ops import nearest_points
 from shapely.geometry import Point, MultiPolygon
 from shapely.geometry import Point, MultiPolygon
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 
 
 import math
 import math
 import logging
 import logging
@@ -113,12 +113,12 @@ class DistanceMin(AppTool):
                     try:
                     try:
                         selected_objs[0].solid_geometry = MultiPolygon(selected_objs[0].solid_geometry)
                         selected_objs[0].solid_geometry = MultiPolygon(selected_objs[0].solid_geometry)
                     except Exception:
                     except Exception:
-                        selected_objs[0].solid_geometry = cascaded_union(selected_objs[0].solid_geometry)
+                        selected_objs[0].solid_geometry = unary_union(selected_objs[0].solid_geometry)
 
 
                     try:
                     try:
                         selected_objs[1].solid_geometry = MultiPolygon(selected_objs[1].solid_geometry)
                         selected_objs[1].solid_geometry = MultiPolygon(selected_objs[1].solid_geometry)
                     except Exception:
                     except Exception:
-                        selected_objs[1].solid_geometry = cascaded_union(selected_objs[1].solid_geometry)
+                        selected_objs[1].solid_geometry = unary_union(selected_objs[1].solid_geometry)
 
 
                 first_pos, last_pos = nearest_points(selected_objs[0].solid_geometry, selected_objs[1].solid_geometry)
                 first_pos, last_pos = nearest_points(selected_objs[0].solid_geometry, selected_objs[1].solid_geometry)
 
 

+ 218 - 46
appTools/ToolFilm.py

@@ -9,11 +9,13 @@ from PyQt5 import QtCore, QtWidgets, QtGui
 
 
 from appTool import AppTool
 from appTool import AppTool
 from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, \
 from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, \
-    OptionalHideInputSection, FCComboBox, FCFileSaveDialog, FCButton, FCLabel
+    OptionalHideInputSection, FCComboBox, FCFileSaveDialog, FCButton, FCLabel, FCSpinner
 
 
 from copy import deepcopy
 from copy import deepcopy
 import logging
 import logging
 from shapely.geometry import Polygon, MultiPolygon, Point
 from shapely.geometry import Polygon, MultiPolygon, Point
+import shapely.affinity as affinity
+from shapely.ops import unary_union
 
 
 from reportlab.graphics import renderPDF
 from reportlab.graphics import renderPDF
 from reportlab.pdfgen import canvas
 from reportlab.pdfgen import canvas
@@ -138,6 +140,8 @@ class Film(AppTool):
         self.ui.orientation_radio.set_value(self.app.defaults["tools_film_orientation"])
         self.ui.orientation_radio.set_value(self.app.defaults["tools_film_orientation"])
         self.ui.pagesize_combo.set_value(self.app.defaults["tools_film_pagesize"])
         self.ui.pagesize_combo.set_value(self.app.defaults["tools_film_pagesize"])
 
 
+        self.ui.png_dpi_spinner.set_value(self.app.defaults["tools_film_png_dpi"])
+
         self.ui.tf_type_obj_combo.set_value('grb')
         self.ui.tf_type_obj_combo.set_value('grb')
         self.ui.tf_type_box_combo.set_value('grb')
         self.ui.tf_type_box_combo.set_value('grb')
         # run once to update the obj_type attribute in the FCCombobox so the last object is showed in cb
         # run once to update the obj_type attribute in the FCCombobox so the last object is showed in cb
@@ -187,8 +191,8 @@ class Film(AppTool):
     def generate_positive_normal_film(self, name, boxname, factor, ftype='svg'):
     def generate_positive_normal_film(self, name, boxname, factor, ftype='svg'):
         log.debug("ToolFilm.Film.generate_positive_normal_film() started ...")
         log.debug("ToolFilm.Film.generate_positive_normal_film() started ...")
 
 
-        scale_factor_x = None
-        scale_factor_y = None
+        scale_factor_x = 1
+        scale_factor_y = 1
         skew_factor_x = None
         skew_factor_x = None
         skew_factor_y = None
         skew_factor_y = None
         mirror = None
         mirror = None
@@ -328,8 +332,8 @@ class Film(AppTool):
     def generate_negative_film(self, name, boxname, factor, ftype='svg'):
     def generate_negative_film(self, name, boxname, factor, ftype='svg'):
         log.debug("ToolFilm.Film.generate_negative_film() started ...")
         log.debug("ToolFilm.Film.generate_negative_film() started ...")
 
 
-        scale_factor_x = None
-        scale_factor_y = None
+        scale_factor_x = 1
+        scale_factor_y = 1
         skew_factor_x = None
         skew_factor_x = None
         skew_factor_y = None
         skew_factor_y = None
         mirror = None
         mirror = None
@@ -351,7 +355,7 @@ class Film(AppTool):
             if self.ui.film_mirror_axis.get_value() != 'none':
             if self.ui.film_mirror_axis.get_value() != 'none':
                 mirror = self.ui.film_mirror_axis.get_value()
                 mirror = self.ui.film_mirror_axis.get_value()
 
 
-        border = float(self.ui.boundary_entry.get_value())
+        border = self.ui.boundary_entry.get_value()
 
 
         if border is None:
         if border is None:
             border = 0
             border = 0
@@ -390,7 +394,7 @@ class Film(AppTool):
 
 
     def export_negative(self, obj_name, box_name, filename, boundary,
     def export_negative(self, obj_name, box_name, filename, boundary,
                         scale_stroke_factor=0.00,
                         scale_stroke_factor=0.00,
-                        scale_factor_x=None, scale_factor_y=None,
+                        scale_factor_x=1, scale_factor_y=1,
                         skew_factor_x=None, skew_factor_y=None, skew_reference='center',
                         skew_factor_x=None, skew_factor_y=None, skew_reference='center',
                         mirror=None,
                         mirror=None,
                         use_thread=True, ftype='svg'):
                         use_thread=True, ftype='svg'):
@@ -434,17 +438,86 @@ class Film(AppTool):
             self.app.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), obj))
             self.app.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), obj))
             box = obj
             box = obj
 
 
-        def make_negative_film():
+        scale_factor_x = scale_factor_x
+        scale_factor_y = scale_factor_y
+
+        def make_negative_film(scale_factor_x, scale_factor_y):
+            log.debug("FilmTool.export_negative().make_negative_film()")
+
+            scale_reference = 'center'
+
+            default_dpi = 96
+            new_png_dpi = self.ui.png_dpi_spinner.get_value()
+            dpi_rate = new_png_dpi / default_dpi
+            # Determine bounding area for svg export
+            bounds = box.bounds()
+            tr_scale_reference = (bounds[0], bounds[1])
+
+            if dpi_rate != 1 and ftype == 'png':
+                scale_factor_x += dpi_rate
+                scale_factor_y += dpi_rate
+                scale_reference = (bounds[0], bounds[1])
+
+            if box.kind.lower() == 'geometry':
+                flat_geo = []
+                if box.multigeo:
+                    for tool in box.tools:
+                        flat_geo += box.flatten(box.tools[tool]['solid_geometry'])
+                    box_geo = unary_union(flat_geo)
+                else:
+                    box_geo = unary_union(box.flatten())
+            else:
+                box_geo = unary_union(box.flatten())
+
+            skew_ref = 'center'
+            if skew_reference != 'center':
+                xmin, ymin, xmax, ymax = box_geo.bounds
+                if skew_reference == 'topleft':
+                    skew_ref = (xmin, ymax)
+                elif skew_reference == 'bottomleft':
+                    skew_ref = (xmin, ymin)
+                elif skew_reference == 'topright':
+                    skew_ref = (xmax, ymax)
+                elif skew_reference == 'bottomright':
+                    skew_ref = (xmax, ymin)
+
+            transformed_box_geo = box_geo
+
+            if scale_factor_x and not scale_factor_y:
+                transformed_box_geo = affinity.scale(transformed_box_geo, scale_factor_x, 1.0,
+                                                     origin=tr_scale_reference)
+            elif not scale_factor_x and scale_factor_y:
+                transformed_box_geo = affinity.scale(transformed_box_geo, 1.0, scale_factor_y,
+                                                     origin=tr_scale_reference)
+            elif scale_factor_x and scale_factor_y:
+                transformed_box_geo = affinity.scale(transformed_box_geo, scale_factor_x, scale_factor_y,
+                                                     origin=tr_scale_reference)
+
+            if skew_factor_x and not skew_factor_y:
+                transformed_box_geo = affinity.skew(transformed_box_geo, skew_factor_x, 0.0, origin=skew_ref)
+            elif not skew_factor_x and skew_factor_y:
+                transformed_box_geo = affinity.skew(transformed_box_geo, 0.0, skew_factor_y, origin=skew_ref)
+            elif skew_factor_x and skew_factor_y:
+                transformed_box_geo = affinity.skew(transformed_box_geo, skew_factor_x, skew_factor_y, origin=skew_ref)
+
+            if mirror:
+                if mirror == 'x':
+                    transformed_box_geo = affinity.scale(transformed_box_geo, 1.0, -1.0)
+                if mirror == 'y':
+                    transformed_box_geo = affinity.scale(transformed_box_geo, -1.0, 1.0)
+                if mirror == 'both':
+                    transformed_box_geo = affinity.scale(transformed_box_geo, -1.0, -1.0)
+
+            bounds = transformed_box_geo.bounds
+            size = bounds[2] - bounds[0], bounds[3] - bounds[1]
+
             exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor,
             exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor,
                                           scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
                                           scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
                                           skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
                                           skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
-                                          mirror=mirror
+                                          mirror=mirror,
+                                          scale_reference=scale_reference, skew_reference=skew_reference
                                           )
                                           )
 
 
-            # Determine bounding area for svg export
-            bounds = box.bounds()
-            size = box.size()
-
             uom = obj.units.lower()
             uom = obj.units.lower()
 
 
             # Convert everything to strings for use in the xml doc
             # Convert everything to strings for use in the xml doc
@@ -514,6 +587,11 @@ class Film(AppTool):
                     doc_final = StringIO(doc_final)
                     doc_final = StringIO(doc_final)
                     drawing = svg2rlg(doc_final)
                     drawing = svg2rlg(doc_final)
                     renderPM.drawToFile(drawing, filename, 'PNG')
                     renderPM.drawToFile(drawing, filename, 'PNG')
+
+                    # if new_png_dpi == default_dpi:
+                    #     renderPM.drawToFile(drawing, filename, 'PNG')
+                    # else:
+                    #     renderPM.drawToFile(drawing, filename, 'PNG', dpi=new_png_dpi)
                 except Exception as e:
                 except Exception as e:
                     log.debug("FilmTool.export_negative() --> PNG output --> %s" % str(e))
                     log.debug("FilmTool.export_negative() --> PNG output --> %s" % str(e))
                     return 'fail'
                     return 'fail'
@@ -528,8 +606,7 @@ class Film(AppTool):
                     drawing = svg2rlg(doc_final)
                     drawing = svg2rlg(doc_final)
 
 
                     p_size = self.ui.pagesize_combo.get_value()
                     p_size = self.ui.pagesize_combo.get_value()
-                    if p_size == 'Bounds':
-                        renderPDF.drawToFile(drawing, filename)
+                    if p_size == 'Bounds':                        renderPDF.drawToFile(drawing, filename)
                     else:
                     else:
                         if self.ui.orientation_radio.get_value() == 'p':
                         if self.ui.orientation_radio.get_value() == 'p':
                             page_size = portrait(self.ui.pagesize[p_size])
                             page_size = portrait(self.ui.pagesize[p_size])
@@ -550,23 +627,21 @@ class Film(AppTool):
             self.app.inform.emit('[success] %s: %s' % (_("Film file exported to"), filename))
             self.app.inform.emit('[success] %s: %s' % (_("Film file exported to"), filename))
 
 
         if use_thread is True:
         if use_thread is True:
-            proc = self.app.proc_container.new(_("Generating Film ... Please wait."))
-
-            def job_thread_film(app_obj):
-                try:
-                    make_negative_film()
-                except Exception:
-                    proc.done()
-                    return
-                proc.done()
+            def job_thread_film():
+                with self.app.proc_container.new(_("Working...")):
+                    try:
+                        make_negative_film(scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y)
+                    except Exception as e:
+                        log.debug("export_negative() process -> %s" % str(e))
+                        return
 
 
-            self.app.worker_task.emit({'fcn': job_thread_film, 'params': [self]})
+            self.app.worker_task.emit({'fcn': job_thread_film, 'params': []})
         else:
         else:
-            make_negative_film()
+            make_negative_film(scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y)
 
 
     def export_positive(self, obj_name, box_name, filename,
     def export_positive(self, obj_name, box_name, filename,
                         scale_stroke_factor=0.00,
                         scale_stroke_factor=0.00,
-                        scale_factor_x=None, scale_factor_y=None,
+                        scale_factor_x=1, scale_factor_y=1,
                         skew_factor_x=None, skew_factor_y=None, skew_reference='center',
                         skew_factor_x=None, skew_factor_y=None, skew_reference='center',
                         mirror=None,  orientation_val='p', pagesize_val='A4', color_val='black', opacity_val=1.0,
                         mirror=None,  orientation_val='p', pagesize_val='A4', color_val='black', opacity_val=1.0,
                         use_thread=True, ftype='svg'):
                         use_thread=True, ftype='svg'):
@@ -615,18 +690,89 @@ class Film(AppTool):
             self.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), obj))
             self.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), obj))
             box = obj
             box = obj
 
 
+        scale_factor_x = scale_factor_x
+        scale_factor_y = scale_factor_y
+
         p_size = pagesize_val
         p_size = pagesize_val
         orientation = orientation_val
         orientation = orientation_val
         color = color_val
         color = color_val
         transparency_level = opacity_val
         transparency_level = opacity_val
 
 
-        def make_positive_film(p_size, orientation, color, transparency_level):
+        def make_positive_film(p_size, orientation, color, transparency_level, scale_factor_x, scale_factor_y):
             log.debug("FilmTool.export_positive().make_positive_film()")
             log.debug("FilmTool.export_positive().make_positive_film()")
 
 
+            scale_reference = 'center'
+
+            default_dpi = 96
+            new_png_dpi = self.ui.png_dpi_spinner.get_value()
+            dpi_rate = new_png_dpi / default_dpi
+            # Determine bounding area for svg export
+            bounds = box.bounds()
+            tr_scale_reference = (bounds[0], bounds[1])
+
+            if dpi_rate != 1 and ftype == 'png':
+                scale_factor_x += dpi_rate
+                scale_factor_y += dpi_rate
+                scale_reference = (bounds[0], bounds[1])
+
+            if box.kind.lower() == 'geometry':
+                flat_geo = []
+                if box.multigeo:
+                    for tool in box.tools:
+                        flat_geo += box.flatten(box.tools[tool]['solid_geometry'])
+                    box_geo = unary_union(flat_geo)
+                else:
+                    box_geo = unary_union(box.flatten())
+            else:
+                box_geo = unary_union(box.flatten())
+
+            skew_ref = 'center'
+            if skew_reference != 'center':
+                xmin, ymin, xmax, ymax = box_geo.bounds
+                if skew_reference == 'topleft':
+                    skew_ref = (xmin, ymax)
+                elif skew_reference == 'bottomleft':
+                    skew_ref = (xmin, ymin)
+                elif skew_reference == 'topright':
+                    skew_ref = (xmax, ymax)
+                elif skew_reference == 'bottomright':
+                    skew_ref = (xmax, ymin)
+
+            transformed_box_geo = box_geo
+
+            if scale_factor_x and not scale_factor_y:
+                transformed_box_geo = affinity.scale(transformed_box_geo, scale_factor_x, 1.0,
+                                                     origin=tr_scale_reference)
+            elif not scale_factor_x and scale_factor_y:
+                transformed_box_geo = affinity.scale(transformed_box_geo, 1.0, scale_factor_y,
+                                                     origin=tr_scale_reference)
+            elif scale_factor_x and scale_factor_y:
+                transformed_box_geo = affinity.scale(transformed_box_geo, scale_factor_x, scale_factor_y,
+                                                     origin=tr_scale_reference)
+
+            if skew_factor_x and not skew_factor_y:
+                transformed_box_geo = affinity.skew(transformed_box_geo, skew_factor_x, 0.0, origin=skew_ref)
+            elif not skew_factor_x and skew_factor_y:
+                transformed_box_geo = affinity.skew(transformed_box_geo, 0.0, skew_factor_y, origin=skew_ref)
+            elif skew_factor_x and skew_factor_y:
+                transformed_box_geo = affinity.skew(transformed_box_geo, skew_factor_x, skew_factor_y, origin=skew_ref)
+
+            if mirror:
+                if mirror == 'x':
+                    transformed_box_geo = affinity.scale(transformed_box_geo, 1.0, -1.0)
+                if mirror == 'y':
+                    transformed_box_geo = affinity.scale(transformed_box_geo, -1.0, 1.0)
+                if mirror == 'both':
+                    transformed_box_geo = affinity.scale(transformed_box_geo, -1.0, -1.0)
+
+            bounds = transformed_box_geo.bounds
+            size = bounds[2] - bounds[0], bounds[3] - bounds[1]
+
             exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor,
             exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor,
                                           scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
                                           scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
                                           skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
                                           skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
-                                          mirror=mirror
+                                          mirror=mirror,
+                                          scale_reference=scale_reference, skew_reference=skew_reference
                                           )
                                           )
 
 
             # Change the attributes of the exported SVG
             # Change the attributes of the exported SVG
@@ -641,10 +787,6 @@ class Film(AppTool):
 
 
             exported_svg = ET.tostring(root)
             exported_svg = ET.tostring(root)
 
 
-            # Determine bounding area for svg export
-            bounds = box.bounds()
-            size = box.size()
-
             # This contain the measure units
             # This contain the measure units
             uom = obj.units.lower()
             uom = obj.units.lower()
 
 
@@ -693,6 +835,11 @@ class Film(AppTool):
                     doc_final = StringIO(doc_final)
                     doc_final = StringIO(doc_final)
                     drawing = svg2rlg(doc_final)
                     drawing = svg2rlg(doc_final)
                     renderPM.drawToFile(drawing, filename, 'PNG')
                     renderPM.drawToFile(drawing, filename, 'PNG')
+
+                    # if new_png_dpi == default_dpi:
+                    #     renderPM.drawToFile(drawing, filename, 'PNG')
+                    # else:
+                    #     renderPM.drawToFile(drawing, filename, 'PNG', dpi=new_png_dpi)
                 except Exception as e:
                 except Exception as e:
                     log.debug("FilmTool.export_positive() --> PNG output --> %s" % str(e))
                     log.debug("FilmTool.export_positive() --> PNG output --> %s" % str(e))
                     return 'fail'
                     return 'fail'
@@ -710,9 +857,9 @@ class Film(AppTool):
                         renderPDF.drawToFile(drawing, filename)
                         renderPDF.drawToFile(drawing, filename)
                     else:
                     else:
                         if orientation == 'p':
                         if orientation == 'p':
-                            page_size = portrait(self.pagesize[p_size])
+                            page_size = portrait(self.ui.pagesize[p_size])
                         else:
                         else:
-                            page_size = landscape(self.pagesize[p_size])
+                            page_size = landscape(self.ui.pagesize[p_size])
 
 
                         my_canvas = canvas.Canvas(filename, pagesize=page_size)
                         my_canvas = canvas.Canvas(filename, pagesize=page_size)
                         my_canvas.translate(bounds[0] * unit, bounds[1] * unit)
                         my_canvas.translate(bounds[0] * unit, bounds[1] * unit)
@@ -728,21 +875,21 @@ class Film(AppTool):
             self.app.inform.emit('[success] %s: %s' % (_("Film file exported to"), filename))
             self.app.inform.emit('[success] %s: %s' % (_("Film file exported to"), filename))
 
 
         if use_thread is True:
         if use_thread is True:
-            proc = self.app.proc_container.new(_("Generating Film ... Please wait."))
-
             def job_thread_film():
             def job_thread_film():
-                try:
-                    make_positive_film(p_size=p_size, orientation=orientation, color=color,
-                                       transparency_level=transparency_level)
-                except Exception:
-                    proc.done()
-                    return
-                proc.done()
+                with self.app.proc_container.new(_("Working...")):
+                    try:
+                        make_positive_film(p_size=p_size, orientation=orientation, color=color,
+                                           transparency_level=transparency_level,
+                                           scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y)
+                    except Exception as e:
+                        log.debug("export_positive() process -> %s" % str(e))
+                        return
 
 
             self.app.worker_task.emit({'fcn': job_thread_film, 'params': []})
             self.app.worker_task.emit({'fcn': job_thread_film, 'params': []})
         else:
         else:
             make_positive_film(p_size=p_size, orientation=orientation, color=color,
             make_positive_film(p_size=p_size, orientation=orientation, color=color,
-                               transparency_level=transparency_level)
+                               transparency_level=transparency_level,
+                               scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y)
 
 
     def reset_fields(self):
     def reset_fields(self):
         self.ui.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.ui.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
@@ -1199,6 +1346,20 @@ class FilmUI:
 
 
         self.on_film_type(val='hide')
         self.on_film_type(val='hide')
 
 
+        # PNG DPI
+        self.png_dpi_label = FCLabel('%s:' % "PNG DPI")
+        self.png_dpi_label.setToolTip(
+            _("Default value is 96 DPI. Change this value to scale the PNG file.")
+        )
+        self.png_dpi_spinner = FCSpinner(callback=self.confirmation_message_int)
+        self.png_dpi_spinner.set_range(0, 100000)
+
+        grid1.addWidget(self.png_dpi_label, 4, 0)
+        grid1.addWidget(self.png_dpi_spinner, 4, 1)
+
+        self.png_dpi_label.hide()
+        self.png_dpi_spinner.hide()
+
         # Buttons
         # Buttons
         self.film_object_button = FCButton(_("Save Film"))
         self.film_object_button = FCButton(_("Save Film"))
         self.film_object_button.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
         self.film_object_button.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
@@ -1214,7 +1375,7 @@ class FilmUI:
                                    font-weight: bold;
                                    font-weight: bold;
                                }
                                }
                                """)
                                """)
-        grid1.addWidget(self.film_object_button, 4, 0, 1, 2)
+        grid1.addWidget(self.film_object_button, 6, 0, 1, 2)
 
 
         self.layout.addStretch()
         self.layout.addStretch()
 
 
@@ -1254,11 +1415,22 @@ class FilmUI:
             self.orientation_radio.show()
             self.orientation_radio.show()
             self.pagesize_label.show()
             self.pagesize_label.show()
             self.pagesize_combo.show()
             self.pagesize_combo.show()
+            self.png_dpi_label.hide()
+            self.png_dpi_spinner.hide()
+        elif val == 'png':
+            self.png_dpi_label.show()
+            self.png_dpi_spinner.show()
+            self.orientation_label.hide()
+            self.orientation_radio.hide()
+            self.pagesize_label.hide()
+            self.pagesize_combo.hide()
         else:
         else:
             self.orientation_label.hide()
             self.orientation_label.hide()
             self.orientation_radio.hide()
             self.orientation_radio.hide()
             self.pagesize_label.hide()
             self.pagesize_label.hide()
             self.pagesize_combo.hide()
             self.pagesize_combo.hide()
+            self.png_dpi_label.hide()
+            self.png_dpi_spinner.hide()
 
 
     def on_punch_source(self, val):
     def on_punch_source(self, val):
         if val == 'pad' and self.punch_cb.get_value():
         if val == 'pad' and self.punch_cb.get_value():

+ 142 - 93
appTools/ToolIsolation.py

@@ -19,7 +19,7 @@ import numpy as np
 import simplejson as json
 import simplejson as json
 import sys
 import sys
 
 
-from shapely.ops import cascaded_union, nearest_points
+from shapely.ops import unary_union, nearest_points
 from shapely.geometry import MultiPolygon, Polygon, MultiLineString, LineString, LinearRing, Point
 from shapely.geometry import MultiPolygon, Polygon, MultiLineString, LineString, LinearRing, Point
 
 
 from matplotlib.backend_bases import KeyEvent as mpl_key_event
 from matplotlib.backend_bases import KeyEvent as mpl_key_event
@@ -119,6 +119,8 @@ class ToolIsolation(AppTool, Gerber):
         self.grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
         self.grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
 
 
         self.tooldia = None
         self.tooldia = None
+        # store here the tool diameter that is guaranteed to isolate the object
+        self.safe_tooldia = None
 
 
         # multiprocessing
         # multiprocessing
         self.pool = self.app.pool
         self.pool = self.app.pool
@@ -225,6 +227,9 @@ class ToolIsolation(AppTool, Gerber):
     def set_tool_ui(self):
     def set_tool_ui(self):
         self.units = self.app.defaults['units'].upper()
         self.units = self.app.defaults['units'].upper()
 
 
+        # reset the value to prepare for another isolation
+        self.safe_tooldia = None
+
         # try to select in the Gerber combobox the active object
         # try to select in the Gerber combobox the active object
         try:
         try:
             selected_obj = self.app.collection.get_active()
             selected_obj = self.app.collection.get_active()
@@ -313,6 +318,7 @@ class ToolIsolation(AppTool, Gerber):
         self.ui.iso_overlap_entry.set_value(self.app.defaults["tools_iso_overlap"])
         self.ui.iso_overlap_entry.set_value(self.app.defaults["tools_iso_overlap"])
         self.ui.milling_type_radio.set_value(self.app.defaults["tools_iso_milling_type"])
         self.ui.milling_type_radio.set_value(self.app.defaults["tools_iso_milling_type"])
         self.ui.combine_passes_cb.set_value(self.app.defaults["tools_iso_combine_passes"])
         self.ui.combine_passes_cb.set_value(self.app.defaults["tools_iso_combine_passes"])
+        self.ui.valid_cb.set_value(self.app.defaults["tools_iso_check_valid"])
         self.ui.area_shape_radio.set_value(self.app.defaults["tools_iso_area_shape"])
         self.ui.area_shape_radio.set_value(self.app.defaults["tools_iso_area_shape"])
         self.ui.poly_int_cb.set_value(self.app.defaults["tools_iso_poly_ints"])
         self.ui.poly_int_cb.set_value(self.app.defaults["tools_iso_poly_ints"])
         self.ui.forced_rest_iso_cb.set_value(self.app.defaults["tools_iso_force"])
         self.ui.forced_rest_iso_cb.set_value(self.app.defaults["tools_iso_force"])
@@ -888,6 +894,9 @@ class ToolIsolation(AppTool, Gerber):
             })
             })
 
 
     def on_find_optimal_tooldia(self):
     def on_find_optimal_tooldia(self):
+        self.find_safe_tooldia_worker(is_displayed=True)
+
+    def find_safe_tooldia_worker(self, is_displayed):
         self.units = self.app.defaults['units'].upper()
         self.units = self.app.defaults['units'].upper()
 
 
         obj_name = self.ui.object_combo.currentText()
         obj_name = self.ui.object_combo.currentText()
@@ -903,85 +912,111 @@ class ToolIsolation(AppTool, Gerber):
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(obj_name)))
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(obj_name)))
             return
             return
 
 
-        proc = self.app.proc_container.new(_("Working..."))
+        def job_thread(app_obj, is_display):
+            with self.app.proc_container.new(_("Working...")) as proc:
+                try:
+                    old_disp_number = 0
+                    pol_nr = 0
+                    app_obj.proc_container.update_view_text(' %d%%' % 0)
+                    total_geo = []
+
+                    for ap in list(fcobj.apertures.keys()):
+                        if 'geometry' in fcobj.apertures[ap]:
+                            for geo_el in fcobj.apertures[ap]['geometry']:
+                                if self.app.abort_flag:
+                                    # graceful abort requested by the user
+                                    raise grace
 
 
-        def job_thread(app_obj):
-            try:
-                old_disp_number = 0
-                pol_nr = 0
-                app_obj.proc_container.update_view_text(' %d%%' % 0)
-                total_geo = []
-
-                for ap in list(fcobj.apertures.keys()):
-                    if 'geometry' in fcobj.apertures[ap]:
-                        for geo_el in fcobj.apertures[ap]['geometry']:
+                                if 'solid' in geo_el and geo_el['solid'] is not None and geo_el['solid'].is_valid:
+                                    total_geo.append(geo_el['solid'])
+
+                    total_geo = MultiPolygon(total_geo)
+                    total_geo = total_geo.buffer(0)
+
+                    try:
+                        __ = iter(total_geo)
+                        geo_len = len(total_geo)
+                        geo_len = (geo_len * (geo_len - 1)) / 2
+                    except TypeError:
+                        msg = _("The Gerber object has one Polygon as geometry.\n"
+                                "There are no distances between geometry elements to be found.")
+                        app_obj.inform.emit('[ERROR_NOTCL] %s' % msg)
+                        return 'fail'
+
+                    min_dict = {}
+                    idx = 1
+                    for geo in total_geo:
+                        for s_geo in total_geo[idx:]:
                             if self.app.abort_flag:
                             if self.app.abort_flag:
                                 # graceful abort requested by the user
                                 # graceful abort requested by the user
                                 raise grace
                                 raise grace
 
 
-                            if 'solid' in geo_el and geo_el['solid'] is not None and geo_el['solid'].is_valid:
-                                total_geo.append(geo_el['solid'])
+                            # minimize the number of distances by not taking into considerations those
+                            # that are too small
+                            dist = geo.distance(s_geo)
+                            dist = float('%.*f' % (self.decimals, dist))
+                            loc_1, loc_2 = nearest_points(geo, s_geo)
 
 
-                total_geo = MultiPolygon(total_geo)
-                total_geo = total_geo.buffer(0)
+                            proc_loc = (
+                                (float('%.*f' % (self.decimals, loc_1.x)), float('%.*f' % (self.decimals, loc_1.y))),
+                                (float('%.*f' % (self.decimals, loc_2.x)), float('%.*f' % (self.decimals, loc_2.y)))
+                            )
 
 
-                try:
-                    __ = iter(total_geo)
-                    geo_len = len(total_geo)
-                    geo_len = (geo_len * (geo_len - 1)) / 2
-                except TypeError:
-                    app_obj.inform.emit('[ERROR_NOTCL] %s' %
-                                        _("The Gerber object has one Polygon as geometry.\n"
-                                          "There are no distances between geometry elements to be found."))
-                    return 'fail'
-
-                min_dict = {}
-                idx = 1
-                for geo in total_geo:
-                    for s_geo in total_geo[idx:]:
-                        if self.app.abort_flag:
-                            # graceful abort requested by the user
-                            raise grace
-
-                        # minimize the number of distances by not taking into considerations those that are too small
-                        dist = geo.distance(s_geo)
-                        dist = float('%.*f' % (self.decimals, dist))
-                        loc_1, loc_2 = nearest_points(geo, s_geo)
-
-                        proc_loc = (
-                            (float('%.*f' % (self.decimals, loc_1.x)), float('%.*f' % (self.decimals, loc_1.y))),
-                            (float('%.*f' % (self.decimals, loc_2.x)), float('%.*f' % (self.decimals, loc_2.y)))
-                        )
-
-                        if dist in min_dict:
-                            min_dict[dist].append(proc_loc)
-                        else:
-                            min_dict[dist] = [proc_loc]
+                            if dist in min_dict:
+                                min_dict[dist].append(proc_loc)
+                            else:
+                                min_dict[dist] = [proc_loc]
 
 
-                        pol_nr += 1
-                        disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
+                            pol_nr += 1
+                            disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
 
 
-                        if old_disp_number < disp_number <= 100:
-                            app_obj.proc_container.update_view_text(' %d%%' % disp_number)
-                            old_disp_number = disp_number
-                    idx += 1
+                            if old_disp_number < disp_number <= 100:
+                                app_obj.proc_container.update_view_text(' %d%%' % disp_number)
+                                old_disp_number = disp_number
+                        idx += 1
 
 
-                min_list = list(min_dict.keys())
-                min_dist = min(min_list)
+                    min_list = list(min_dict.keys())
+                    min_dist = min(min_list)
 
 
-                min_dist_truncated = self.app.dec_format(float(min_dist), self.decimals)
+                    min_dist_truncated = self.app.dec_format(float(min_dist), self.decimals)
+                    self.safe_tooldia = min_dist_truncated
 
 
-                self.optimal_found_sig.emit(min_dist_truncated)
+                    if is_display:
+                        self.optimal_found_sig.emit(min_dist_truncated)
 
 
-                app_obj.inform.emit('[success] %s: %s %s' %
-                                    (_("Optimal tool diameter found"), str(min_dist_truncated), self.units.lower()))
-            except Exception as ee:
-                proc.done()
-                log.debug(str(ee))
-                return
-            proc.done()
+                        app_obj.inform.emit('[success] %s: %s %s' %
+                                            (_("Optimal tool diameter found"), str(min_dist_truncated),
+                                             self.units.lower()))
+                    else:
+                        if self.safe_tooldia:
+                            # find the selected tool ID's
+                            sorted_tools = []
+                            table_items = self.ui.tools_table.selectedItems()
+                            sel_rows = {t.row() for t in table_items}
+                            for row in sel_rows:
+                                tid = int(self.ui.tools_table.item(row, 3).text())
+                                sorted_tools.append(tid)
+                            if not sorted_tools:
+                                msg = _("There are no tools selected in the Tool Table.")
+                                self.app.inform.emit('[ERROR_NOTCL] %s' % msg)
+                                return 'fail'
+
+                            # check if the tools diameters are less then the safe tool diameter
+                            for tool in sorted_tools:
+                                tool_dia = float(self.iso_tools[tool]['tooldia'])
+                                if tool_dia > self.safe_tooldia:
+                                    msg = _("Incomplete isolation. "
+                                            "At least one tool could not do a complete isolation.")
+                                    self.app.inform.emit('[WARNING] %s' % msg)
+                                    break
+
+                            # reset the value to prepare for another isolation
+                            self.safe_tooldia = None
+                except Exception as ee:
+                    log.debug(str(ee))
+                    return
 
 
-        self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
+        self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app, is_displayed]})
 
 
     def on_tool_add(self, custom_dia=None):
     def on_tool_add(self, custom_dia=None):
         self.blockSignals(True)
         self.blockSignals(True)
@@ -1333,14 +1368,17 @@ class ToolIsolation(AppTool, Gerber):
         # Get source object.
         # Get source object.
         try:
         try:
             self.grb_obj = self.app.collection.get_by_name(self.obj_name)
             self.grb_obj = self.app.collection.get_by_name(self.obj_name)
-        except Exception as e:
+        except Exception:
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(self.obj_name)))
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(self.obj_name)))
-            return "Could not retrieve object: %s with error: %s" % (self.obj_name, str(e))
+            return
 
 
         if self.grb_obj is None:
         if self.grb_obj is None:
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(self.obj_name)))
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(self.obj_name)))
             return
             return
 
 
+        if self.ui.valid_cb.get_value() is True:
+            self.find_safe_tooldia_worker(is_displayed=False)
+
         def worker_task(iso_obj):
         def worker_task(iso_obj):
             with self.app.proc_container.new(_("Isolating...")):
             with self.app.proc_container.new(_("Isolating...")):
                 self.isolate_handler(iso_obj)
                 self.isolate_handler(iso_obj)
@@ -1436,8 +1474,8 @@ class ToolIsolation(AppTool, Gerber):
 
 
         elif selection == _("Reference Object"):
         elif selection == _("Reference Object"):
             ref_obj = self.app.collection.get_by_name(self.ui.reference_combo.get_value())
             ref_obj = self.app.collection.get_by_name(self.ui.reference_combo.get_value())
-            ref_geo = cascaded_union(ref_obj.solid_geometry)
-            use_geo = cascaded_union(isolated_obj.solid_geometry).difference(ref_geo)
+            ref_geo = unary_union(ref_obj.solid_geometry)
+            use_geo = unary_union(isolated_obj.solid_geometry).difference(ref_geo)
             self.isolate(isolated_obj=isolated_obj, geometry=use_geo)
             self.isolate(isolated_obj=isolated_obj, geometry=use_geo)
 
 
     def isolate(self, isolated_obj, geometry=None, limited_area=None, negative_dia=None, plot=True):
     def isolate(self, isolated_obj, geometry=None, limited_area=None, negative_dia=None, plot=True):
@@ -1467,7 +1505,7 @@ class ToolIsolation(AppTool, Gerber):
             tid = int(self.ui.tools_table.item(row, 3).text())
             tid = int(self.ui.tools_table.item(row, 3).text())
             sorted_tools.append(tid)
             sorted_tools.append(tid)
         if not sorted_tools:
         if not sorted_tools:
-            self.app.inform.emit('[ERROR_NOTCL] %s' % _("No selected tools in Tool Table."))
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("There are no tools selected in the Tool Table."))
             return 'fail'
             return 'fail'
 
 
         # update the Common Parameters values in the self.iso_tools
         # update the Common Parameters values in the self.iso_tools
@@ -1669,7 +1707,7 @@ class ToolIsolation(AppTool, Gerber):
             sorted_tools.append(float('%.*f' % (self.decimals, tdia)))
             sorted_tools.append(float('%.*f' % (self.decimals, tdia)))
 
 
         if not sorted_tools:
         if not sorted_tools:
-            self.app.inform.emit('[ERROR_NOTCL] %s' % _("No selected tools in Tool Table."))
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("There are no tools selected in the Tool Table."))
             return 'fail'
             return 'fail'
 
 
         order = self.ui.order_radio.get_value()
         order = self.ui.order_radio.get_value()
@@ -1856,7 +1894,7 @@ class ToolIsolation(AppTool, Gerber):
             tid = int(self.ui.tools_table.item(row, 3).text())
             tid = int(self.ui.tools_table.item(row, 3).text())
             sorted_tools.append(tid)
             sorted_tools.append(tid)
         if not sorted_tools:
         if not sorted_tools:
-            self.app.inform.emit('[ERROR_NOTCL] %s' % _("No selected tools in Tool Table."))
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("There are no tools selected in the Tool Table."))
             return 'fail'
             return 'fail'
 
 
         for tool in sorted_tools:
         for tool in sorted_tools:
@@ -2010,11 +2048,11 @@ class ToolIsolation(AppTool, Gerber):
         target_geo = geo
         target_geo = geo
 
 
         if subtraction_geo:
         if subtraction_geo:
-            sub_union = cascaded_union(subtraction_geo)
+            sub_union = unary_union(subtraction_geo)
         else:
         else:
             name = self.ui.exc_obj_combo.currentText()
             name = self.ui.exc_obj_combo.currentText()
             subtractor_obj = self.app.collection.get_by_name(name)
             subtractor_obj = self.app.collection.get_by_name(name)
-            sub_union = cascaded_union(subtractor_obj.solid_geometry)
+            sub_union = unary_union(subtractor_obj.solid_geometry)
 
 
         try:
         try:
             for geo_elem in target_geo:
             for geo_elem in target_geo:
@@ -2068,7 +2106,7 @@ class ToolIsolation(AppTool, Gerber):
         new_geometry = []
         new_geometry = []
         target_geo = geo
         target_geo = geo
 
 
-        intersect_union = cascaded_union(intersection_geo)
+        intersect_union = unary_union(intersection_geo)
 
 
         try:
         try:
             for geo_elem in target_geo:
             for geo_elem in target_geo:
@@ -2245,7 +2283,7 @@ class ToolIsolation(AppTool, Gerber):
         except TypeError:
         except TypeError:
             if self.solid_geometry not in self.poly_dict.values():
             if self.solid_geometry not in self.poly_dict.values():
                 if sel_type is True:
                 if sel_type is True:
-                    if self.solid_geometry.within(poly_selection):
+                    if poly_selection.contains(self.solid_geometry):
                         shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
                         shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
                                                             shape=self.solid_geometry,
                                                             shape=self.solid_geometry,
                                                             color=self.app.defaults['global_sel_draw_color'] + 'AF',
                                                             color=self.app.defaults['global_sel_draw_color'] + 'AF',
@@ -2389,7 +2427,7 @@ class ToolIsolation(AppTool, Gerber):
             if len(self.sel_rect) == 0:
             if len(self.sel_rect) == 0:
                 return
                 return
 
 
-            self.sel_rect = cascaded_union(self.sel_rect)
+            self.sel_rect = unary_union(self.sel_rect)
             self.isolate(isolated_obj=self.grb_obj, limited_area=self.sel_rect, plot=True)
             self.isolate(isolated_obj=self.grb_obj, limited_area=self.sel_rect, plot=True)
             self.sel_rect = []
             self.sel_rect = []
 
 
@@ -3096,8 +3134,9 @@ class IsoUI:
         self.addtool_from_db_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/search_db32.png'))
         self.addtool_from_db_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/search_db32.png'))
         self.addtool_from_db_btn.setToolTip(
         self.addtool_from_db_btn.setToolTip(
             _("Add a new tool to the Tool Table\n"
             _("Add a new tool to the Tool Table\n"
-              "from the Tool Database.\n"
-              "Tool database administration in Menu: Options -> Tools Database")
+              "from the Tools Database.\n"
+              "Tools database administration in in:\n"
+              "Menu: Options -> Tools Database")
         )
         )
         bhlay.addWidget(self.addtool_from_db_btn)
         bhlay.addWidget(self.addtool_from_db_btn)
 
 
@@ -3277,13 +3316,23 @@ class IsoUI:
 
 
         self.grid3.addWidget(self.combine_passes_cb, 26, 0, 1, 2)
         self.grid3.addWidget(self.combine_passes_cb, 26, 0, 1, 2)
 
 
+        # Check Tool validity
+        self.valid_cb = FCCheckBox(label=_('Check validity'))
+        self.valid_cb.setToolTip(
+            _("If checked then the tools diameters are verified\n"
+              "if they will provide a complete isolation.")
+        )
+        self.valid_cb.setObjectName("i_check")
+
+        self.grid3.addWidget(self.valid_cb, 28, 0, 1, 2)
+
         # Exception Areas
         # Exception Areas
         self.except_cb = FCCheckBox(label=_('Except'))
         self.except_cb = FCCheckBox(label=_('Except'))
         self.except_cb.setToolTip(_("When the isolation geometry is generated,\n"
         self.except_cb.setToolTip(_("When the isolation geometry is generated,\n"
                                     "by checking this, the area of the object below\n"
                                     "by checking this, the area of the object below\n"
                                     "will be subtracted from the isolation geometry."))
                                     "will be subtracted from the isolation geometry."))
         self.except_cb.setObjectName("i_except")
         self.except_cb.setObjectName("i_except")
-        self.grid3.addWidget(self.except_cb, 27, 0)
+        self.grid3.addWidget(self.except_cb, 30, 0)
 
 
         # Type of object to be excepted
         # Type of object to be excepted
         self.type_excobj_radio = RadioSet([{'label': _("Geometry"), 'value': 'geometry'},
         self.type_excobj_radio = RadioSet([{'label': _("Geometry"), 'value': 'geometry'},
@@ -3295,7 +3344,7 @@ class IsoUI:
               "of objects that will populate the 'Object' combobox.")
               "of objects that will populate the 'Object' combobox.")
         )
         )
 
 
-        self.grid3.addWidget(self.type_excobj_radio, 27, 1)
+        self.grid3.addWidget(self.type_excobj_radio, 30, 1)
 
 
         # The object to be excepted
         # The object to be excepted
         self.exc_obj_combo = FCComboBox()
         self.exc_obj_combo = FCComboBox()
@@ -3307,7 +3356,7 @@ class IsoUI:
         self.exc_obj_combo.is_last = True
         self.exc_obj_combo.is_last = True
         self.exc_obj_combo.obj_type = "gerber"
         self.exc_obj_combo.obj_type = "gerber"
 
 
-        self.grid3.addWidget(self.exc_obj_combo, 28, 0, 1, 2)
+        self.grid3.addWidget(self.exc_obj_combo, 32, 0, 1, 2)
 
 
         self.e_ois = OptionalInputSection(self.except_cb,
         self.e_ois = OptionalInputSection(self.except_cb,
                                           [
                                           [
@@ -3330,8 +3379,8 @@ class IsoUI:
         )
         )
         self.select_combo.setObjectName("i_selection")
         self.select_combo.setObjectName("i_selection")
 
 
-        self.grid3.addWidget(self.select_label, 33, 0)
-        self.grid3.addWidget(self.select_combo, 33, 1)
+        self.grid3.addWidget(self.select_label, 34, 0)
+        self.grid3.addWidget(self.select_combo, 34, 1)
 
 
         self.reference_combo_type_label = FCLabel('%s:' % _("Ref. Type"))
         self.reference_combo_type_label = FCLabel('%s:' % _("Ref. Type"))
         self.reference_combo_type_label.setToolTip(
         self.reference_combo_type_label.setToolTip(
@@ -3341,8 +3390,8 @@ class IsoUI:
         self.reference_combo_type = FCComboBox()
         self.reference_combo_type = FCComboBox()
         self.reference_combo_type.addItems([_("Gerber"), _("Excellon"), _("Geometry")])
         self.reference_combo_type.addItems([_("Gerber"), _("Excellon"), _("Geometry")])
 
 
-        self.grid3.addWidget(self.reference_combo_type_label, 34, 0)
-        self.grid3.addWidget(self.reference_combo_type, 34, 1)
+        self.grid3.addWidget(self.reference_combo_type_label, 36, 0)
+        self.grid3.addWidget(self.reference_combo_type, 36, 1)
 
 
         self.reference_combo_label = FCLabel('%s:' % _("Ref. Object"))
         self.reference_combo_label = FCLabel('%s:' % _("Ref. Object"))
         self.reference_combo_label.setToolTip(
         self.reference_combo_label.setToolTip(
@@ -3353,8 +3402,8 @@ class IsoUI:
         self.reference_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.reference_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.reference_combo.is_last = True
         self.reference_combo.is_last = True
 
 
-        self.grid3.addWidget(self.reference_combo_label, 35, 0)
-        self.grid3.addWidget(self.reference_combo, 35, 1)
+        self.grid3.addWidget(self.reference_combo_label, 38, 0)
+        self.grid3.addWidget(self.reference_combo, 38, 1)
 
 
         self.reference_combo.hide()
         self.reference_combo.hide()
         self.reference_combo_label.hide()
         self.reference_combo_label.hide()
@@ -3368,7 +3417,7 @@ class IsoUI:
               "(holes in the polygon).")
               "(holes in the polygon).")
         )
         )
 
 
-        self.grid3.addWidget(self.poly_int_cb, 36, 0)
+        self.grid3.addWidget(self.poly_int_cb, 40, 0)
 
 
         self.poly_int_cb.hide()
         self.poly_int_cb.hide()
 
 
@@ -3381,8 +3430,8 @@ class IsoUI:
         self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'},
         self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'},
                                           {'label': _("Polygon"), 'value': 'polygon'}])
                                           {'label': _("Polygon"), 'value': 'polygon'}])
 
 
-        self.grid3.addWidget(self.area_shape_label, 38, 0)
-        self.grid3.addWidget(self.area_shape_radio, 38, 1)
+        self.grid3.addWidget(self.area_shape_label, 42, 0)
+        self.grid3.addWidget(self.area_shape_radio, 42, 1)
 
 
         self.area_shape_label.hide()
         self.area_shape_label.hide()
         self.area_shape_radio.hide()
         self.area_shape_radio.hide()
@@ -3390,7 +3439,7 @@ class IsoUI:
         separator_line = QtWidgets.QFrame()
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        self.grid3.addWidget(separator_line, 39, 0, 1, 2)
+        self.grid3.addWidget(separator_line, 44, 0, 1, 2)
 
 
         self.generate_iso_button = FCButton("%s" % _("Generate Geometry"))
         self.generate_iso_button = FCButton("%s" % _("Generate Geometry"))
         self.generate_iso_button.setIcon(QtGui.QIcon(self.app.resource_location + '/geometry32.png'))
         self.generate_iso_button.setIcon(QtGui.QIcon(self.app.resource_location + '/geometry32.png'))

+ 1 - 1
appTools/ToolMilling.py

@@ -17,7 +17,7 @@ from copy import deepcopy
 # import numpy as np
 # import numpy as np
 # import math
 # import math
 
 
-# from shapely.ops import cascaded_union
+# from shapely.ops import unary_union
 from shapely.geometry import Point, LineString
 from shapely.geometry import Point, LineString
 
 
 from matplotlib.backend_bases import KeyEvent as mpl_key_event
 from matplotlib.backend_bases import KeyEvent as mpl_key_event

+ 41 - 34
appTools/ToolNCC.py

@@ -18,7 +18,7 @@ from copy import deepcopy
 
 
 import numpy as np
 import numpy as np
 from shapely.geometry import base
 from shapely.geometry import base
-from shapely.ops import cascaded_union, nearest_points
+from shapely.ops import unary_union, nearest_points
 from shapely.geometry import MultiPolygon, Polygon, MultiLineString, LineString, LinearRing
 from shapely.geometry import MultiPolygon, Polygon, MultiLineString, LineString, LinearRing
 
 
 from matplotlib.backend_bases import KeyEvent as mpl_key_event
 from matplotlib.backend_bases import KeyEvent as mpl_key_event
@@ -548,6 +548,7 @@ class NonCopperClear(AppTool, Gerber):
             "area_shape":               self.app.defaults["geometry_area_shape"],
             "area_shape":               self.app.defaults["geometry_area_shape"],
             "area_strategy":            self.app.defaults["geometry_area_strategy"],
             "area_strategy":            self.app.defaults["geometry_area_strategy"],
             "area_overz":               float(self.app.defaults["geometry_area_overz"]),
             "area_overz":               float(self.app.defaults["geometry_area_overz"]),
+            "optimization_type":        self.app.defaults["geometry_optimization_type"],
 
 
             "tools_nccoperation":       self.app.defaults["tools_nccoperation"],
             "tools_nccoperation":       self.app.defaults["tools_nccoperation"],
             "tools_nccmargin":          self.app.defaults["tools_nccmargin"],
             "tools_nccmargin":          self.app.defaults["tools_nccmargin"],
@@ -1292,7 +1293,7 @@ class NonCopperClear(AppTool, Gerber):
                         else:
                         else:
                             self.ncc_dia_list.append(self.tooldia)
                             self.ncc_dia_list.append(self.tooldia)
         else:
         else:
-            self.app.inform.emit('[ERROR_NOTCL] %s' % _("No selected tools in Tool Table."))
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("There are no tools selected in the Tool Table."))
             return
             return
 
 
         self.o_name = '%s_ncc' % self.obj_name
         self.o_name = '%s_ncc' % self.obj_name
@@ -1460,7 +1461,7 @@ class NonCopperClear(AppTool, Gerber):
             if len(self.sel_rect) == 0:
             if len(self.sel_rect) == 0:
                 return
                 return
 
 
-            self.sel_rect = cascaded_union(self.sel_rect)
+            self.sel_rect = unary_union(self.sel_rect)
 
 
             self.clear_copper(ncc_obj=self.ncc_obj, sel_obj=self.bound_obj, ncctooldia=self.ncc_dia_list,
             self.clear_copper(ncc_obj=self.ncc_obj, sel_obj=self.bound_obj, ncctooldia=self.ncc_dia_list,
                               isotooldia=self.iso_dia_list, outname=self.o_name)
                               isotooldia=self.iso_dia_list, outname=self.o_name)
@@ -1622,16 +1623,16 @@ class NonCopperClear(AppTool, Gerber):
                     env_obj = geo_n.convex_hull
                     env_obj = geo_n.convex_hull
                 elif (isinstance(geo_n, MultiPolygon) and len(geo_n) == 1) or \
                 elif (isinstance(geo_n, MultiPolygon) and len(geo_n) == 1) or \
                         (isinstance(geo_n, list) and len(geo_n) == 1) and isinstance(geo_n[0], Polygon):
                         (isinstance(geo_n, list) and len(geo_n) == 1) and isinstance(geo_n[0], Polygon):
-                    env_obj = cascaded_union(geo_n)
+                    env_obj = unary_union(geo_n)
                 else:
                 else:
-                    env_obj = cascaded_union(geo_n)
+                    env_obj = unary_union(geo_n)
                     env_obj = env_obj.convex_hull
                     env_obj = env_obj.convex_hull
             except Exception as e:
             except Exception as e:
                 log.debug("NonCopperClear.calculate_bounding_box() 'itself'  --> %s" % str(e))
                 log.debug("NonCopperClear.calculate_bounding_box() 'itself'  --> %s" % str(e))
                 self.app.inform.emit('[ERROR_NOTCL] %s' % _("No object available."))
                 self.app.inform.emit('[ERROR_NOTCL] %s' % _("No object available."))
                 return None
                 return None
         elif ncc_select == _("Area Selection"):
         elif ncc_select == _("Area Selection"):
-            env_obj = cascaded_union(self.sel_rect)
+            env_obj = unary_union(self.sel_rect)
             try:
             try:
                 __ = iter(env_obj)
                 __ = iter(env_obj)
             except Exception:
             except Exception:
@@ -1649,8 +1650,8 @@ class NonCopperClear(AppTool, Gerber):
                     env_obj = [box_geo]
                     env_obj = [box_geo]
 
 
             elif box_kind == 'gerber':
             elif box_kind == 'gerber':
-                box_geo = cascaded_union(box_obj.solid_geometry).convex_hull
-                ncc_geo = cascaded_union(ncc_obj.solid_geometry).convex_hull
+                box_geo = unary_union(box_obj.solid_geometry).convex_hull
+                ncc_geo = unary_union(ncc_obj.solid_geometry).convex_hull
                 env_obj = ncc_geo.intersection(box_geo)
                 env_obj = ncc_geo.intersection(box_geo)
             else:
             else:
                 self.app.inform.emit('[ERROR_NOTCL] %s' % _("The reference object type is not supported."))
                 self.app.inform.emit('[ERROR_NOTCL] %s' % _("The reference object type is not supported."))
@@ -1692,7 +1693,7 @@ class NonCopperClear(AppTool, Gerber):
                     # graceful abort requested by the user
                     # graceful abort requested by the user
                     raise grace
                     raise grace
                 geo_buff_list.append(poly.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre))
                 geo_buff_list.append(poly.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre))
-            new_bounding_box = cascaded_union(geo_buff_list)
+            new_bounding_box = unary_union(geo_buff_list)
         elif ncc_select == _("Reference Object"):
         elif ncc_select == _("Reference Object"):
             if box_kind == 'geometry':
             if box_kind == 'geometry':
                 geo_buff_list = []
                 geo_buff_list = []
@@ -1702,7 +1703,7 @@ class NonCopperClear(AppTool, Gerber):
                         raise grace
                         raise grace
                     geo_buff_list.append(poly.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre))
                     geo_buff_list.append(poly.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre))
 
 
-                new_bounding_box = cascaded_union(geo_buff_list)
+                new_bounding_box = unary_union(geo_buff_list)
             elif box_kind == 'gerber':
             elif box_kind == 'gerber':
                 new_bounding_box = bbox.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre)
                 new_bounding_box = bbox.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre)
             else:
             else:
@@ -1873,7 +1874,7 @@ class NonCopperClear(AppTool, Gerber):
                         geo_obj.tools[current_uid] = dict(tools_storage[current_uid])
                         geo_obj.tools[current_uid] = dict(tools_storage[current_uid])
                         break
                         break
 
 
-            sol_geo = cascaded_union(isolated_geo)
+            sol_geo = unary_union(isolated_geo)
             if has_offset is True:
             if has_offset is True:
                 self.app.inform.emit('[WARNING_NOTCL] %s ...' % _("Buffering"))
                 self.app.inform.emit('[WARNING_NOTCL] %s ...' % _("Buffering"))
                 sol_geo = sol_geo.buffer(distance=ncc_offset)
                 sol_geo = sol_geo.buffer(distance=ncc_offset)
@@ -1886,7 +1887,7 @@ class NonCopperClear(AppTool, Gerber):
                 return 'fail'
                 return 'fail'
 
 
         elif ncc_obj.kind == 'geometry':
         elif ncc_obj.kind == 'geometry':
-            sol_geo = cascaded_union(ncc_obj.solid_geometry)
+            sol_geo = unary_union(ncc_obj.solid_geometry)
             if has_offset is True:
             if has_offset is True:
                 self.app.inform.emit('[WARNING_NOTCL] %s ...' % _("Buffering"))
                 self.app.inform.emit('[WARNING_NOTCL] %s ...' % _("Buffering"))
                 sol_geo = sol_geo.buffer(distance=ncc_offset)
                 sol_geo = sol_geo.buffer(distance=ncc_offset)
@@ -2430,7 +2431,7 @@ class NonCopperClear(AppTool, Gerber):
                             log.debug("There are no geometries in the cleared polygon.")
                             log.debug("There are no geometries in the cleared polygon.")
 
 
                 # Area to clear next
                 # Area to clear next
-                buffered_cleared = cascaded_union(cleared_geo).buffer(tool / 2.0)
+                buffered_cleared = unary_union(cleared_geo).buffer(tool / 2.0)
                 area = area.difference(buffered_cleared)
                 area = area.difference(buffered_cleared)
 
 
                 if not area or area.is_empty:
                 if not area or area.is_empty:
@@ -2442,7 +2443,7 @@ class NonCopperClear(AppTool, Gerber):
                 #     new_area = [p.buffer(buff_distance) for p in area if not p.is_empty]
                 #     new_area = [p.buffer(buff_distance) for p in area if not p.is_empty]
                 # except TypeError:
                 # except TypeError:
                 #     new_area = [area.buffer(tool * ncc_overlap)]
                 #     new_area = [area.buffer(tool * ncc_overlap)]
-                # area = cascaded_union(area)
+                # area = unary_union(area)
 
 
             geo_obj.multigeo = True
             geo_obj.multigeo = True
             geo_obj.options["cnctooldia"] = '0.0'
             geo_obj.options["cnctooldia"] = '0.0'
@@ -2615,9 +2616,9 @@ class NonCopperClear(AppTool, Gerber):
                     env_obj = geo_n.convex_hull
                     env_obj = geo_n.convex_hull
                 elif (isinstance(geo_n, MultiPolygon) and len(geo_n) == 1) or \
                 elif (isinstance(geo_n, MultiPolygon) and len(geo_n) == 1) or \
                         (isinstance(geo_n, list) and len(geo_n) == 1) and isinstance(geo_n[0], Polygon):
                         (isinstance(geo_n, list) and len(geo_n) == 1) and isinstance(geo_n[0], Polygon):
-                    env_obj = cascaded_union(geo_n)
+                    env_obj = unary_union(geo_n)
                 else:
                 else:
-                    env_obj = cascaded_union(geo_n)
+                    env_obj = unary_union(geo_n)
                     env_obj = env_obj.convex_hull
                     env_obj = env_obj.convex_hull
 
 
                 bounding_box = env_obj.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre)
                 bounding_box = env_obj.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre)
@@ -2627,7 +2628,7 @@ class NonCopperClear(AppTool, Gerber):
                 return 'fail'
                 return 'fail'
 
 
         elif ncc_select == 'area':
         elif ncc_select == 'area':
-            geo_n = cascaded_union(self.sel_rect)
+            geo_n = unary_union(self.sel_rect)
             try:
             try:
                 __ = iter(geo_n)
                 __ = iter(geo_n)
             except Exception as e:
             except Exception as e:
@@ -2641,7 +2642,7 @@ class NonCopperClear(AppTool, Gerber):
                     raise grace
                     raise grace
                 geo_buff_list.append(poly.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre))
                 geo_buff_list.append(poly.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre))
 
 
-            bounding_box = cascaded_union(geo_buff_list)
+            bounding_box = unary_union(geo_buff_list)
 
 
         elif ncc_select == _("Reference Object"):
         elif ncc_select == _("Reference Object"):
             geo_n = ncc_sel_obj.solid_geometry
             geo_n = ncc_sel_obj.solid_geometry
@@ -2659,10 +2660,10 @@ class NonCopperClear(AppTool, Gerber):
                         raise grace
                         raise grace
                     geo_buff_list.append(poly.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre))
                     geo_buff_list.append(poly.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre))
 
 
-                bounding_box = cascaded_union(geo_buff_list)
+                bounding_box = unary_union(geo_buff_list)
             elif ncc_sel_obj.kind == 'gerber':
             elif ncc_sel_obj.kind == 'gerber':
-                geo_n = cascaded_union(geo_n).convex_hull
-                bounding_box = cascaded_union(self.ncc_obj.solid_geometry).convex_hull.intersection(geo_n)
+                geo_n = unary_union(geo_n).convex_hull
+                bounding_box = unary_union(self.ncc_obj.solid_geometry).convex_hull.intersection(geo_n)
                 bounding_box = bounding_box.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre)
                 bounding_box = bounding_box.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre)
             else:
             else:
                 self.app.inform.emit('[ERROR_NOTCL] %s' % _("The reference object type is not supported."))
                 self.app.inform.emit('[ERROR_NOTCL] %s' % _("The reference object type is not supported."))
@@ -2837,7 +2838,7 @@ class NonCopperClear(AppTool, Gerber):
                                 break
                                 break
                         geo_obj.tools[current_uid] = dict(tools_storage[current_uid])
                         geo_obj.tools[current_uid] = dict(tools_storage[current_uid])
 
 
-                sol_geo = cascaded_union(isolated_geo)
+                sol_geo = unary_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)
@@ -2852,7 +2853,7 @@ class NonCopperClear(AppTool, Gerber):
                     return 'fail'
                     return 'fail'
 
 
             elif ncc_obj.kind == 'geometry':
             elif ncc_obj.kind == 'geometry':
-                sol_geo = cascaded_union(ncc_obj.solid_geometry)
+                sol_geo = unary_union(ncc_obj.solid_geometry)
                 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)
@@ -3219,7 +3220,7 @@ class NonCopperClear(AppTool, Gerber):
                                 break
                                 break
                         geo_obj.tools[current_uid] = dict(tools_storage[current_uid])
                         geo_obj.tools[current_uid] = dict(tools_storage[current_uid])
 
 
-                sol_geo = cascaded_union(isolated_geo)
+                sol_geo = unary_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)
@@ -3234,7 +3235,7 @@ class NonCopperClear(AppTool, Gerber):
                     return 'fail'
                     return 'fail'
 
 
             elif ncc_obj.kind == 'geometry':
             elif ncc_obj.kind == 'geometry':
-                sol_geo = cascaded_union(ncc_obj.solid_geometry)
+                sol_geo = unary_union(ncc_obj.solid_geometry)
                 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)
@@ -3857,26 +3858,31 @@ class NccUI:
         # ### Tool Diameter ####
         # ### Tool Diameter ####
         self.new_tooldia_lbl = FCLabel('%s:' % _('Tool Dia'))
         self.new_tooldia_lbl = FCLabel('%s:' % _('Tool Dia'))
         self.new_tooldia_lbl.setToolTip(
         self.new_tooldia_lbl.setToolTip(
-            _("Diameter for the new tool to add in the Tool Table.\n"
-              "If the tool is V-shape type then this value is automatically\n"
-              "calculated from the other parameters.")
+            _("Diameter for the new tool")
         )
         )
+        self.grid3.addWidget(self.new_tooldia_lbl, 2, 0)
+
+        new_tool_lay = QtWidgets.QHBoxLayout()
+
         self.new_tooldia_entry = FCDoubleSpinner(callback=self.confirmation_message)
         self.new_tooldia_entry = FCDoubleSpinner(callback=self.confirmation_message)
         self.new_tooldia_entry.set_precision(self.decimals)
         self.new_tooldia_entry.set_precision(self.decimals)
         self.new_tooldia_entry.set_range(0.000, 9999.9999)
         self.new_tooldia_entry.set_range(0.000, 9999.9999)
         self.new_tooldia_entry.setObjectName(_("Tool Dia"))
         self.new_tooldia_entry.setObjectName(_("Tool Dia"))
 
 
-        self.grid3.addWidget(self.new_tooldia_lbl, 2, 0)
-        self.grid3.addWidget(self.new_tooldia_entry, 2, 1)
+        new_tool_lay.addWidget(self.new_tooldia_entry)
 
 
         # Find Optimal Tooldia
         # Find Optimal Tooldia
-        self.find_optimal_button = FCButton(_('Find Optimal'))
+        self.find_optimal_button = QtWidgets.QToolButton()
+        self.find_optimal_button.setText(_('Optimal'))
         self.find_optimal_button.setIcon(QtGui.QIcon(self.app.resource_location + '/open_excellon32.png'))
         self.find_optimal_button.setIcon(QtGui.QIcon(self.app.resource_location + '/open_excellon32.png'))
+        self.find_optimal_button.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
         self.find_optimal_button.setToolTip(
         self.find_optimal_button.setToolTip(
             _("Find a tool diameter that is guaranteed\n"
             _("Find a tool diameter that is guaranteed\n"
               "to do a complete isolation.")
               "to do a complete isolation.")
         )
         )
-        self.grid3.addWidget(self.find_optimal_button, 4, 0, 1, 2)
+        new_tool_lay.addWidget(self.find_optimal_button)
+
+        self.grid3.addLayout(new_tool_lay, 2, 1)
 
 
         hlay = QtWidgets.QHBoxLayout()
         hlay = QtWidgets.QHBoxLayout()
 
 
@@ -3895,8 +3901,9 @@ class NccUI:
         self.addtool_from_db_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/search_db32.png'))
         self.addtool_from_db_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/search_db32.png'))
         self.addtool_from_db_btn.setToolTip(
         self.addtool_from_db_btn.setToolTip(
             _("Add a new tool to the Tool Table\n"
             _("Add a new tool to the Tool Table\n"
-              "from the Tool Database.\n"
-              "Tool database administration in Menu: Options -> Tools Database")
+              "from the Tools Database.\n"
+              "Tools database administration in in:\n"
+              "Menu: Options -> Tools Database")
         )
         )
         hlay.addWidget(self.addtool_from_db_btn)
         hlay.addWidget(self.addtool_from_db_btn)
 
 

+ 16 - 14
appTools/ToolPaint.py

@@ -17,7 +17,7 @@ from appGUI.GUIElements import FCTable, FCDoubleSpinner, FCCheckBox, FCInputDial
     FCLabel
     FCLabel
 
 
 from shapely.geometry import base, Polygon, MultiPolygon, LinearRing, Point
 from shapely.geometry import base, Polygon, MultiPolygon, LinearRing, Point
-from shapely.ops import cascaded_union, unary_union, linemerge
+from shapely.ops import unary_union, linemerge
 
 
 from matplotlib.backend_bases import KeyEvent as mpl_key_event
 from matplotlib.backend_bases import KeyEvent as mpl_key_event
 
 
@@ -466,6 +466,7 @@ class ToolPaint(AppTool, Gerber):
             "area_shape":           self.app.defaults["geometry_area_shape"],
             "area_shape":           self.app.defaults["geometry_area_shape"],
             "area_strategy":        self.app.defaults["geometry_area_strategy"],
             "area_strategy":        self.app.defaults["geometry_area_strategy"],
             "area_overz":           float(self.app.defaults["geometry_area_overz"]),
             "area_overz":           float(self.app.defaults["geometry_area_overz"]),
+            "optimization_type":    self.app.defaults["geometry_optimization_type"],
 
 
             "tooldia":              self.app.defaults["tools_painttooldia"],
             "tooldia":              self.app.defaults["tools_painttooldia"],
             "tools_paintoffset":   self.app.defaults["tools_paintoffset"],
             "tools_paintoffset":   self.app.defaults["tools_paintoffset"],
@@ -1027,7 +1028,7 @@ class ToolPaint(AppTool, Gerber):
                         continue
                         continue
                 self.tooldia_list.append(self.tooldia)
                 self.tooldia_list.append(self.tooldia)
         else:
         else:
-            self.app.inform.emit('[ERROR_NOTCL] %s' % _("No selected tools in Tool Table."))
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("There are no tools selected in the Tool Table."))
             return
             return
 
 
         self.select_method = self.ui.selectmethod_combo.get_value()
         self.select_method = self.ui.selectmethod_combo.get_value()
@@ -1293,7 +1294,7 @@ class ToolPaint(AppTool, Gerber):
             if len(self.sel_rect) == 0:
             if len(self.sel_rect) == 0:
                 return
                 return
 
 
-            self.sel_rect = cascaded_union(self.sel_rect)
+            self.sel_rect = unary_union(self.sel_rect)
             self.paint_poly_area(obj=self.paint_obj, tooldia=self.tooldia_list, sel_obj=self.sel_rect,
             self.paint_poly_area(obj=self.paint_obj, tooldia=self.tooldia_list, sel_obj=self.sel_rect,
                                  outname=self.o_name)
                                  outname=self.o_name)
 
 
@@ -1740,7 +1741,7 @@ class ToolPaint(AppTool, Gerber):
                         continue
                         continue
                 sorted_tools.append(self.tooldia)
                 sorted_tools.append(self.tooldia)
             if not sorted_tools:
             if not sorted_tools:
-                self.app.inform.emit('[ERROR_NOTCL] %s' % _("No selected tools in Tool Table."))
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("There are no tools selected in the Tool Table."))
                 return 'fail'
                 return 'fail'
 
 
         # Initializes the new geometry object
         # Initializes the new geometry object
@@ -1880,7 +1881,7 @@ class ToolPaint(AppTool, Gerber):
             geo_obj.tools.clear()
             geo_obj.tools.clear()
             geo_obj.tools = dict(tools_storage)
             geo_obj.tools = dict(tools_storage)
 
 
-            geo_obj.solid_geometry = cascaded_union(final_solid_geometry)
+            geo_obj.solid_geometry = unary_union(final_solid_geometry)
 
 
             try:
             try:
                 if isinstance(geo_obj.solid_geometry, list):
                 if isinstance(geo_obj.solid_geometry, list):
@@ -1934,7 +1935,7 @@ class ToolPaint(AppTool, Gerber):
                     except TypeError:
                     except TypeError:
                         poly_buf.append(buffered_pol)
                         poly_buf.append(buffered_pol)
 
 
-            poly_buf = cascaded_union(poly_buf)
+            poly_buf = unary_union(poly_buf)
 
 
             if not poly_buf:
             if not poly_buf:
                 self.app.inform.emit('[WARNING_NOTCL] %s' % _("Margin parameter too big. Tool is not used"))
                 self.app.inform.emit('[WARNING_NOTCL] %s' % _("Margin parameter too big. Tool is not used"))
@@ -2000,7 +2001,7 @@ class ToolPaint(AppTool, Gerber):
                                                                 prog_plot=prog_plot)
                                                                 prog_plot=prog_plot)
                             geo_elems = list(geo_res.get_objects())
                             geo_elems = list(geo_res.get_objects())
                             # See if the polygon was completely cleared
                             # See if the polygon was completely cleared
-                            pp_cleared = cascaded_union(geo_elems).buffer(tool_dia / 2.0)
+                            pp_cleared = unary_union(geo_elems).buffer(tool_dia / 2.0)
                             rest = pp.difference(pp_cleared)
                             rest = pp.difference(pp_cleared)
                             if rest and not rest.is_empty:
                             if rest and not rest.is_empty:
                                 try:
                                 try:
@@ -2040,7 +2041,7 @@ class ToolPaint(AppTool, Gerber):
                         geo_elems = list(geo_res.get_objects())
                         geo_elems = list(geo_res.get_objects())
 
 
                         # See if the polygon was completely cleared
                         # See if the polygon was completely cleared
-                        pp_cleared = cascaded_union(geo_elems).buffer(tool_dia / 2.0)
+                        pp_cleared = unary_union(geo_elems).buffer(tool_dia / 2.0)
                         rest = poly_buf.difference(pp_cleared)
                         rest = poly_buf.difference(pp_cleared)
                         if rest and not rest.is_empty:
                         if rest and not rest.is_empty:
                             try:
                             try:
@@ -2094,7 +2095,7 @@ class ToolPaint(AppTool, Gerber):
 
 
                 poly_buf = MultiPolygon(tmp)
                 poly_buf = MultiPolygon(tmp)
                 if not poly_buf.is_valid:
                 if not poly_buf.is_valid:
-                    poly_buf = cascaded_union(tmp)
+                    poly_buf = unary_union(tmp)
 
 
                 if not poly_buf or poly_buf.is_empty or not poly_buf.is_valid:
                 if not poly_buf or poly_buf.is_empty or not poly_buf.is_valid:
                     log.debug("Rest geometry empty. Breaking.")
                     log.debug("Rest geometry empty. Breaking.")
@@ -2134,7 +2135,7 @@ class ToolPaint(AppTool, Gerber):
                           "Change the painting parameters and try again.")
                           "Change the painting parameters and try again.")
                     )
                     )
                     return "fail"
                     return "fail"
-                geo_obj.solid_geometry = cascaded_union(final_solid_geometry)
+                geo_obj.solid_geometry = unary_union(final_solid_geometry)
             else:
             else:
                 return 'fail'
                 return 'fail'
             try:
             try:
@@ -2446,9 +2447,9 @@ class ToolPaint(AppTool, Gerber):
                 env_obj = geo.convex_hull
                 env_obj = geo.convex_hull
             elif (isinstance(geo, MultiPolygon) and len(geo) == 1) or \
             elif (isinstance(geo, MultiPolygon) and len(geo) == 1) or \
                     (isinstance(geo, list) and len(geo) == 1) and isinstance(geo[0], Polygon):
                     (isinstance(geo, list) and len(geo) == 1) and isinstance(geo[0], Polygon):
-                env_obj = cascaded_union(self.bound_obj.solid_geometry)
+                env_obj = unary_union(self.bound_obj.solid_geometry)
             else:
             else:
-                env_obj = cascaded_union(self.bound_obj.solid_geometry)
+                env_obj = unary_union(self.bound_obj.solid_geometry)
                 env_obj = env_obj.convex_hull
                 env_obj = env_obj.convex_hull
             sel_rect = env_obj.buffer(distance=0.0000001, join_style=base.JOIN_STYLE.mitre)
             sel_rect = env_obj.buffer(distance=0.0000001, join_style=base.JOIN_STYLE.mitre)
         except Exception as e:
         except Exception as e:
@@ -2909,8 +2910,9 @@ class PaintUI:
         self.addtool_from_db_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/search_db32.png'))
         self.addtool_from_db_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/search_db32.png'))
         self.addtool_from_db_btn.setToolTip(
         self.addtool_from_db_btn.setToolTip(
             _("Add a new tool to the Tool Table\n"
             _("Add a new tool to the Tool Table\n"
-              "from the Tool Database.\n"
-              "Tool database administration in Menu: Options -> Tools Database")
+              "from the Tools Database.\n"
+              "Tools database administration in in:\n"
+              "Menu: Options -> Tools Database")
         )
         )
         hlay.addWidget(self.addtool_from_db_btn)
         hlay.addWidget(self.addtool_from_db_btn)
 
 

+ 2 - 2
appTools/ToolPanelize.py

@@ -589,8 +589,8 @@ class Panelize(AppTool):
                         obj_fin.source_file = self.app.export_dxf(obj_name=self.outname, filename=None,
                         obj_fin.source_file = self.app.export_dxf(obj_name=self.outname, filename=None,
                                                                      local_use=obj_fin, use_thread=False)
                                                                      local_use=obj_fin, use_thread=False)
 
 
-                    # obj_fin.solid_geometry = cascaded_union(obj_fin.solid_geometry)
-                    # app_obj.log.debug("Finished creating a cascaded union for the panel.")
+                    # obj_fin.solid_geometry = unary_union(obj_fin.solid_geometry)
+                    # app_obj.log.debug("Finished creating a unary_union for the panel.")
                     app_obj.proc_container.update_view_text('')
                     app_obj.proc_container.update_view_text('')
 
 
                 self.app.inform.emit('%s: %d' % (_("Generating panel... Spawning copies"), (int(rows * columns))))
                 self.app.inform.emit('%s: %d' % (_("Generating panel... Spawning copies"), (int(rows * columns))))

+ 7 - 7
appTools/ToolProperties.py

@@ -10,7 +10,7 @@ from appTool import AppTool
 from appGUI.GUIElements import FCTree
 from appGUI.GUIElements import FCTree
 
 
 from shapely.geometry import MultiPolygon, Polygon
 from shapely.geometry import MultiPolygon, Polygon
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 
 
 from copy import deepcopy
 from copy import deepcopy
 import math
 import math
@@ -237,7 +237,7 @@ class Properties(AppTool):
                 if obj_prop.kind.lower() == 'cncjob':
                 if obj_prop.kind.lower() == 'cncjob':
                     try:
                     try:
                         for tool_k in obj_prop.exc_cnc_tools:
                         for tool_k in obj_prop.exc_cnc_tools:
-                            x0, y0, x1, y1 = cascaded_union(obj_prop.exc_cnc_tools[tool_k]['solid_geometry']).bounds
+                            x0, y0, x1, y1 = unary_union(obj_prop.exc_cnc_tools[tool_k]['solid_geometry']).bounds
                             xmin.append(x0)
                             xmin.append(x0)
                             ymin.append(y0)
                             ymin.append(y0)
                             xmax.append(x1)
                             xmax.append(x1)
@@ -247,7 +247,7 @@ class Properties(AppTool):
 
 
                     try:
                     try:
                         for tool_k in obj_prop.cnc_tools:
                         for tool_k in obj_prop.cnc_tools:
-                            x0, y0, x1, y1 = cascaded_union(obj_prop.cnc_tools[tool_k]['solid_geometry']).bounds
+                            x0, y0, x1, y1 = unary_union(obj_prop.cnc_tools[tool_k]['solid_geometry']).bounds
                             xmin.append(x0)
                             xmin.append(x0)
                             ymin.append(y0)
                             ymin.append(y0)
                             xmax.append(x1)
                             xmax.append(x1)
@@ -257,7 +257,7 @@ class Properties(AppTool):
                 else:
                 else:
                     try:
                     try:
                         for tool_k in obj_prop.tools:
                         for tool_k in obj_prop.tools:
-                            x0, y0, x1, y1 = cascaded_union(obj_prop.tools[tool_k]['solid_geometry']).bounds
+                            x0, y0, x1, y1 = unary_union(obj_prop.tools[tool_k]['solid_geometry']).bounds
                             xmin.append(x0)
                             xmin.append(x0)
                             ymin.append(y0)
                             ymin.append(y0)
                             xmax.append(x1)
                             xmax.append(x1)
@@ -308,10 +308,10 @@ class Properties(AppTool):
                             env_obj = geo.convex_hull
                             env_obj = geo.convex_hull
                         elif (isinstance(geo, MultiPolygon) and len(geo) == 1) or \
                         elif (isinstance(geo, MultiPolygon) and len(geo) == 1) or \
                                 (isinstance(geo, list) and len(geo) == 1) and isinstance(geo[0], Polygon):
                                 (isinstance(geo, list) and len(geo) == 1) and isinstance(geo[0], Polygon):
-                            env_obj = cascaded_union(geo)
+                            env_obj = unary_union(geo)
                             env_obj = env_obj.convex_hull
                             env_obj = env_obj.convex_hull
                         else:
                         else:
-                            env_obj = cascaded_union(geo)
+                            env_obj = unary_union(geo)
                             env_obj = env_obj.convex_hull
                             env_obj = env_obj.convex_hull
 
 
                         area_chull = env_obj.area
                         area_chull = env_obj.area
@@ -321,7 +321,7 @@ class Properties(AppTool):
                     try:
                     try:
                         area_chull = []
                         area_chull = []
                         for tool_k in obj_prop.tools:
                         for tool_k in obj_prop.tools:
-                            area_el = cascaded_union(obj_prop.tools[tool_k]['solid_geometry']).convex_hull
+                            area_el = unary_union(obj_prop.tools[tool_k]['solid_geometry']).convex_hull
                             area_chull.append(area_el.area)
                             area_chull.append(area_el.area)
                         area_chull = max(area_chull)
                         area_chull = max(area_chull)
                     except Exception as er:
                     except Exception as er:

+ 0 - 1
appTools/ToolQRCode.py

@@ -430,7 +430,6 @@ class QRCode(AppTool):
         units = self.app.defaults['units'] if units is None else units
         units = self.app.defaults['units'] if units is None else units
         res = self.app.defaults['geometry_circle_steps']
         res = self.app.defaults['geometry_circle_steps']
         factor = svgparse_viewbox(svg_root)
         factor = svgparse_viewbox(svg_root)
-
         geos = getsvggeo(svg_root, object_type, units=units, res=res, factor=factor)
         geos = getsvggeo(svg_root, object_type, units=units, res=res, factor=factor)
 
 
         if flip:
         if flip:

+ 2 - 2
appTools/ToolSolderPaste.py

@@ -19,7 +19,7 @@ from copy import deepcopy
 from datetime import datetime
 from datetime import datetime
 
 
 from shapely.geometry import Polygon, LineString
 from shapely.geometry import Polygon, LineString
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 
 
 import traceback
 import traceback
 from io import StringIO
 from io import StringIO
@@ -941,7 +941,7 @@ class SolderPaste(AppTool):
                 tool_cnc_dict['gcode_parsed'] = job_obj.gcode_parse()
                 tool_cnc_dict['gcode_parsed'] = job_obj.gcode_parse()
 
 
                 # TODO this serve for bounding box creation only; should be optimized
                 # TODO this serve for bounding box creation only; should be optimized
-                tool_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in tool_cnc_dict['gcode_parsed']])
+                tool_cnc_dict['solid_geometry'] = unary_union([geo['geom'] for geo in tool_cnc_dict['gcode_parsed']])
 
 
                 # tell gcode_parse from which point to start drawing the lines depending on what kind of
                 # tell gcode_parse from which point to start drawing the lines depending on what kind of
                 # object is the source of gcode
                 # object is the source of gcode

+ 3 - 3
appTools/ToolSub.py

@@ -11,7 +11,7 @@ from appTool import AppTool
 from appGUI.GUIElements import FCCheckBox, FCButton, FCComboBox
 from appGUI.GUIElements import FCCheckBox, FCButton, FCComboBox
 
 
 from shapely.geometry import Polygon, MultiPolygon, MultiLineString, LineString
 from shapely.geometry import Polygon, MultiPolygon, MultiLineString, LineString
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 
 
 import traceback
 import traceback
 from copy import deepcopy
 from copy import deepcopy
@@ -396,7 +396,7 @@ class ToolSub(AppTool):
         else:
         else:
             self.promises.append("single")
             self.promises.append("single")
 
 
-        self.sub_union = cascaded_union(self.sub_geo_obj.solid_geometry)
+        self.sub_union = unary_union(self.sub_geo_obj.solid_geometry)
 
 
         # start the QTimer to check for promises with 0.5 second period check
         # start the QTimer to check for promises with 0.5 second period check
         self.periodic_check(500, reset=True)
         self.periodic_check(500, reset=True)
@@ -421,7 +421,7 @@ class ToolSub(AppTool):
         with self.app.proc_container.new(text):
         with self.app.proc_container.new(text):
             # resulting paths are closed resulting into Polygons
             # resulting paths are closed resulting into Polygons
             if self.ui.close_paths_cb.isChecked():
             if self.ui.close_paths_cb.isChecked():
-                new_geo = (cascaded_union(geo)).difference(self.sub_union)
+                new_geo = (unary_union(geo)).difference(self.sub_union)
                 if new_geo:
                 if new_geo:
                     if not new_geo.is_empty:
                     if not new_geo.is_empty:
                         new_geometry.append(new_geo)
                         new_geometry.append(new_geo)

+ 9 - 60
app_Main.py

@@ -427,7 +427,7 @@ class App(QtCore.QObject):
 
 
         current_defaults_path = os.path.join(self.data_path, "current_defaults.FlatConfig")
         current_defaults_path = os.path.join(self.data_path, "current_defaults.FlatConfig")
         if user_defaults:
         if user_defaults:
-            self.defaults.load(filename=current_defaults_path)
+            self.defaults.load(filename=current_defaults_path, inform=self.inform)
 
 
         if self.defaults['units'] == 'MM':
         if self.defaults['units'] == 'MM':
             self.decimals = int(self.defaults['decimals_metric'])
             self.decimals = int(self.defaults['decimals_metric'])
@@ -1092,7 +1092,7 @@ class App(QtCore.QObject):
             self.ui.splitter.setSizes([0, 1])
             self.ui.splitter.setSizes([0, 1])
 
 
         # Sets up FlatCAMObj, FCProcess and FCProcessContainer.
         # Sets up FlatCAMObj, FCProcess and FCProcessContainer.
-        self.setup_component_editor()
+        self.setup_default_properties_tab()
 
 
         # ###########################################################################################################
         # ###########################################################################################################
         # ####################################### Auto-complete KEYWORDS ############################################
         # ####################################### Auto-complete KEYWORDS ############################################
@@ -2576,7 +2576,7 @@ class App(QtCore.QObject):
             return
             return
 
 
         # Load in the defaults from the chosen file
         # Load in the defaults from the chosen file
-        self.defaults.load(filename=filename)
+        self.defaults.load(filename=filename, inform=self.inform)
 
 
         self.preferencesUiManager.on_preferences_edited()
         self.preferencesUiManager.on_preferences_edited()
         self.inform.emit('[success] %s: %s' % (_("Imported Defaults from"), filename))
         self.inform.emit('[success] %s: %s' % (_("Imported Defaults from"), filename))
@@ -4655,7 +4655,7 @@ class App(QtCore.QObject):
         self.collection.delete_active()
         self.collection.delete_active()
 
 
         # Clear form
         # Clear form
-        self.setup_component_editor()
+        self.setup_default_properties_tab()
 
 
         self.inform.emit('%s: %s' % (_("Object deleted"), name))
         self.inform.emit('%s: %s' % (_("Object deleted"), name))
 
 
@@ -6965,13 +6965,13 @@ class App(QtCore.QObject):
         self.collection.delete_all()
         self.collection.delete_all()
 
 
         # add in Selected tab an initial text that describe the flow of work in FlatCAm
         # add in Selected tab an initial text that describe the flow of work in FlatCAm
-        self.setup_component_editor()
+        self.setup_default_properties_tab()
 
 
         # Clear project filename
         # Clear project filename
         self.project_filename = None
         self.project_filename = None
 
 
         # Load the application defaults
         # Load the application defaults
-        self.defaults.load(filename=os.path.join(self.data_path, 'current_defaults.FlatConfig'))
+        self.defaults.load(filename=os.path.join(self.data_path, 'current_defaults.FlatConfig'), inform=self.inform)
 
 
         # Re-fresh project options
         # Re-fresh project options
         self.on_options_app2project()
         self.on_options_app2project()
@@ -9677,9 +9677,9 @@ class App(QtCore.QObject):
 
 
         self.log.debug("Recent items list has been populated.")
         self.log.debug("Recent items list has been populated.")
 
 
-    def setup_component_editor(self):
+    def setup_default_properties_tab(self):
         """
         """
-        Default text for the Selected tab when is not taken by the Object UI.
+        Default text for the Properties tab when is not taken by the Object UI.
 
 
         :return:
         :return:
         """
         """
@@ -9698,58 +9698,7 @@ class App(QtCore.QObject):
 
 
         tsize = fsize + int(fsize / 2)
         tsize = fsize + int(fsize / 2)
 
 
-        selected_text = '''
-        <p><span style="font-size:{tsize}px"><strong>{title}</strong></span></p>
-
-        <p><span style="font-size:{fsize}px"><strong>{subtitle}</strong>:<br />
-        {s1}</span></p>
-
-        <ol>
-            <li><span style="font-size:{fsize}px">{s2}<br />
-            <br />
-            {s3}</span><br />
-            &nbsp;</li>
-            <li><span style="font-size:{fsize}px">{s4}<br />
-            &nbsp;</li>
-            <br />
-            <li><span style="font-size:{fsize}px">{s5}<br />
-            &nbsp;</li>
-            <br />
-            <li><span style="font-size:{fsize}px">{s6}<br />
-            <br />
-            {s7}</span></li>
-        </ol>
-
-        <p><span style="font-size:{fsize}px">{s8}</span></p>
-        '''.format(
-            title=_("Properties Tab - Choose an Item from Project Tab"),
-            subtitle=_("Details"),
-
-            s1=_("The normal flow when working with the application is the following:"),
-            s2=_("Load/Import a Gerber, Excellon, Gcode, DXF, Raster Image or SVG file into the application "
-                 "using either the toolbars, key shortcuts or even dragging and dropping the "
-                 "files on the GUI."),
-            s3=_("You can also load a project by double clicking on the project file, "
-                 "drag and drop of the file into the GUI or through the menu (or toolbar) "
-                 "actions offered within the app."),
-            s4=_("Once an object is available in the Project Tab, by selecting it and then focusing "
-                 "on Properties TAB (more simpler is to double click the object name in the Project Tab, "
-                 "Properties TAB will be updated with the object properties according to its kind: "
-                 "Gerber, Excellon, Geometry or CNCJob object."),
-            s5=_("If the selection of the object is done on the canvas by single click instead, "
-                 "and the Properties TAB is in focus, again the object properties will be displayed into the "
-                 "Properties Tab. Alternatively, double clicking on the object on the canvas will bring "
-                 "the Properties TAB and populate it even if it was out of focus."),
-            s6=_("You can change the parameters in this screen and the flow direction is like this:"),
-            s7=_("Gerber/Excellon Object --> Change Parameter --> Generate Geometry --> Geometry Object --> "
-                 "Add tools (change param in Selected Tab) --> Generate CNCJob --> CNCJob Object --> "
-                 "Verify GCode (through Edit CNC Code) and/or append/prepend to GCode "
-                 "(again, done in SELECTED TAB) --> Save GCode."),
-            s8=_("A list of key shortcuts is available through an menu entry in Help --> Shortcuts List "
-                 "or through its own key shortcut: <b>F3</b>."),
-            tsize=tsize,
-            fsize=fsize
-        )
+        selected_text = ''
 
 
         sel_title.setText(selected_text)
         sel_title.setText(selected_text)
         sel_title.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
         sel_title.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)

+ 108 - 46
camlib.py

@@ -25,7 +25,7 @@ from lxml import etree as ET
 from shapely.geometry import Polygon, Point, LinearRing
 from shapely.geometry import Polygon, Point, LinearRing
 
 
 from shapely.geometry import box as shply_box
 from shapely.geometry import box as shply_box
-from shapely.ops import cascaded_union, unary_union, substring, linemerge
+from shapely.ops import unary_union, substring, linemerge
 import shapely.affinity as affinity
 import shapely.affinity as affinity
 from shapely.wkt import loads as sloads
 from shapely.wkt import loads as sloads
 from shapely.wkt import dumps as sdumps
 from shapely.wkt import dumps as sdumps
@@ -225,13 +225,14 @@ class ApertureMacro:
         Pads the ``mods`` list with zeros resulting in an
         Pads the ``mods`` list with zeros resulting in an
         list of length n.
         list of length n.
 
 
-        :param n: Length of the resulting list.
-        :type n: int
-        :param mods: List to be padded.
-        :type mods: list
-        :return: Zero-padded list.
-        :rtype: list
+        :param n:       Length of the resulting list.
+        :type n:        int
+        :param mods:    List to be padded.
+        :type mods:     list
+        :return:        Zero-padded list.
+        :rtype:         list
         """
         """
+
         x = [0.0] * n
         x = [0.0] * n
         na = len(mods)
         na = len(mods)
         x[0:na] = mods
         x[0:na] = mods
@@ -244,9 +245,12 @@ class ApertureMacro:
         :param mods: (Exposure 0/1, Diameter >=0, X-coord, Y-coord)
         :param mods: (Exposure 0/1, Diameter >=0, X-coord, Y-coord)
         :return:
         :return:
         """
         """
-
-        pol, dia, x, y = ApertureMacro.default2zero(4, mods)
-
+        val = ApertureMacro.default2zero(4, mods)
+        pol = val[0]
+        dia = val[1]
+        x = val[2]
+        y = val[3]
+        # pol, dia, x, y = ApertureMacro.default2zero(4, mods)
         return {"pol": int(pol), "geometry": Point(x, y).buffer(dia / 2)}
         return {"pol": int(pol), "geometry": Point(x, y).buffer(dia / 2)}
 
 
     @staticmethod
     @staticmethod
@@ -257,7 +261,15 @@ class ApertureMacro:
             rotation angle around origin in degrees)
             rotation angle around origin in degrees)
         :return:
         :return:
         """
         """
-        pol, width, xs, ys, xe, ye, angle = ApertureMacro.default2zero(7, mods)
+        val = ApertureMacro.default2zero(7, mods)
+        pol = val[0]
+        width = val[1]
+        xs = val[2]
+        ys = val[3]
+        xe = val[4]
+        ye = val[5]
+        angle = val[6]
+        # pol, width, xs, ys, xe, ye, angle = ApertureMacro.default2zero(7, mods)
 
 
         line = LineString([(xs, ys), (xe, ye)])
         line = LineString([(xs, ys), (xe, ye)])
         box = line.buffer(width / 2, cap_style=2)
         box = line.buffer(width / 2, cap_style=2)
@@ -274,7 +286,14 @@ class ApertureMacro:
         :return:
         :return:
         """
         """
 
 
-        pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
+        # pol, width, height, x, y, angle = ApertureMacro.default2zero(4, mods)
+        val = ApertureMacro.default2zero(4, mods)
+        pol = val[0]
+        width = val[1]
+        height = val[2]
+        x = val[3]
+        y = val[4]
+        angle = val[5]
 
 
         box = shply_box(x - width / 2, y - height / 2, x + width / 2, y + height / 2)
         box = shply_box(x - width / 2, y - height / 2, x + width / 2, y + height / 2)
         box_rotated = affinity.rotate(box, angle, origin=(0, 0))
         box_rotated = affinity.rotate(box, angle, origin=(0, 0))
@@ -290,7 +309,14 @@ class ApertureMacro:
         :return:
         :return:
         """
         """
 
 
-        pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
+        # pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
+        val = ApertureMacro.default2zero(6, mods)
+        pol = val[0]
+        width = val[1]
+        height = val[2]
+        x = val[3]
+        y = val[4]
+        angle = val[5]
 
 
         box = shply_box(x, y, x + width, y + height)
         box = shply_box(x, y, x + width, y + height)
         box_rotated = affinity.rotate(box, angle, origin=(0, 0))
         box_rotated = affinity.rotate(box, angle, origin=(0, 0))
@@ -330,7 +356,15 @@ class ApertureMacro:
         :return:
         :return:
         """
         """
 
 
-        pol, nverts, x, y, dia, angle = ApertureMacro.default2zero(6, mods)
+        # pol, nverts, x, y, dia, angle = ApertureMacro.default2zero(6, mods)
+        val = ApertureMacro.default2zero(6, mods)
+        pol = val[0]
+        nverts = val[1]
+        x = val[2]
+        y = val[3]
+        dia = val[4]
+        angle = val[5]
+
         points = [(0, 0)] * nverts
         points = [(0, 0)] * nverts
 
 
         for i in range(nverts):
         for i in range(nverts):
@@ -354,7 +388,17 @@ class ApertureMacro:
         :return:
         :return:
         """
         """
 
 
-        x, y, dia, thickness, gap, nrings, cross_th, cross_len, angle = ApertureMacro.default2zero(9, mods)
+        # x, y, dia, thickness, gap, nrings, cross_th, cross_len, angle = ApertureMacro.default2zero(9, mods)
+        val = ApertureMacro.default2zero(9, mods)
+        x = val[0]
+        y = val[1]
+        dia = val[2]
+        thickness = val[3]
+        gap = val[4]
+        nrings = val[5]
+        cross_th = val[6]
+        cross_len = val[7]
+        angle = val[8]
 
 
         r = dia / 2 - thickness / 2
         r = dia / 2 - thickness / 2
         result = Point((x, y)).buffer(r).exterior.buffer(thickness / 2.0)
         result = Point((x, y)).buffer(r).exterior.buffer(thickness / 2.0)
@@ -369,13 +413,13 @@ class ApertureMacro:
             if r <= 0:
             if r <= 0:
                 break
                 break
             ring = Point((x, y)).buffer(r).exterior.buffer(thickness / 2.0)
             ring = Point((x, y)).buffer(r).exterior.buffer(thickness / 2.0)
-            result = cascaded_union([result, ring])
+            result = unary_union([result, ring])
             i += 1
             i += 1
 
 
         # ## Crosshair
         # ## Crosshair
         hor = LineString([(x - cross_len, y), (x + cross_len, y)]).buffer(cross_th / 2.0, cap_style=2)
         hor = LineString([(x - cross_len, y), (x + cross_len, y)]).buffer(cross_th / 2.0, cap_style=2)
         ver = LineString([(x, y - cross_len), (x, y + cross_len)]).buffer(cross_th / 2.0, cap_style=2)
         ver = LineString([(x, y - cross_len), (x, y + cross_len)]).buffer(cross_th / 2.0, cap_style=2)
-        result = cascaded_union([result, hor, ver])
+        result = unary_union([result, hor, ver])
 
 
         return {"pol": 1, "geometry": result}
         return {"pol": 1, "geometry": result}
 
 
@@ -390,7 +434,14 @@ class ApertureMacro:
         :return:
         :return:
         """
         """
 
 
-        x, y, dout, din, t, angle = ApertureMacro.default2zero(6, mods)
+        # x, y, dout, din, t, angle = ApertureMacro.default2zero(6, mods)
+        val = ApertureMacro.default2zero(6, mods)
+        x = val[0]
+        y = val[1]
+        dout = val[2]
+        din = val[3]
+        t = val[4]
+        angle = val[5]
 
 
         ring = Point((x, y)).buffer(dout / 2.0).difference(Point((x, y)).buffer(din / 2.0))
         ring = Point((x, y)).buffer(dout / 2.0).difference(Point((x, y)).buffer(din / 2.0))
         hline = LineString([(x - dout / 2.0, y), (x + dout / 2.0, y)]).buffer(t / 2.0, cap_style=3)
         hline = LineString([(x - dout / 2.0, y), (x + dout / 2.0, y)]).buffer(t / 2.0, cap_style=3)
@@ -675,7 +726,7 @@ class Geometry(object):
             polygon = Polygon(points)
             polygon = Polygon(points)
         else:
         else:
             polygon = points
             polygon = points
-        toolgeo = cascaded_union(polygon)
+        toolgeo = unary_union(polygon)
         diffs = []
         diffs = []
         for target in flat_geometry:
         for target in flat_geometry:
             if isinstance(target, LineString) or isinstance(target, LineString) or isinstance(target, MultiLineString):
             if isinstance(target, LineString) or isinstance(target, LineString) or isinstance(target, MultiLineString):
@@ -787,7 +838,7 @@ class Geometry(object):
         #         if len(self.solid_geometry) == 0:
         #         if len(self.solid_geometry) == 0:
         #             log.debug('solid_geometry is empty []')
         #             log.debug('solid_geometry is empty []')
         #             return 0, 0, 0, 0
         #             return 0, 0, 0, 0
-        #         return cascaded_union(flatten(self.solid_geometry)).bounds
+        #         return unary_union(flatten(self.solid_geometry)).bounds
         #     else:
         #     else:
         #         return self.solid_geometry.bounds
         #         return self.solid_geometry.bounds
         # except Exception as e:
         # except Exception as e:
@@ -802,7 +853,7 @@ class Geometry(object):
         #     if len(self.solid_geometry) == 0:
         #     if len(self.solid_geometry) == 0:
         #         log.debug('solid_geometry is empty []')
         #         log.debug('solid_geometry is empty []')
         #         return 0, 0, 0, 0
         #         return 0, 0, 0, 0
-        #     return cascaded_union(self.solid_geometry).bounds
+        #     return unary_union(self.solid_geometry).bounds
         # else:
         # else:
         #     return self.solid_geometry.bounds
         #     return self.solid_geometry.bounds
 
 
@@ -1325,7 +1376,7 @@ class Geometry(object):
             self.solid_geometry = []
             self.solid_geometry = []
 
 
         if type(self.solid_geometry) is list:
         if type(self.solid_geometry) is list:
-            # self.solid_geometry.append(cascaded_union(geos))
+            # self.solid_geometry.append(unary_union(geos))
             if type(geos) is list:
             if type(geos) is list:
                 self.solid_geometry += geos
                 self.solid_geometry += geos
             else:
             else:
@@ -1335,7 +1386,7 @@ class Geometry(object):
 
 
         # flatten the self.solid_geometry list for import_svg() to import SVG as Gerber
         # flatten the self.solid_geometry list for import_svg() to import SVG as Gerber
         self.solid_geometry = list(self.flatten_list(self.solid_geometry))
         self.solid_geometry = list(self.flatten_list(self.solid_geometry))
-        self.solid_geometry = cascaded_union(self.solid_geometry)
+        self.solid_geometry = unary_union(self.solid_geometry)
 
 
         # self.solid_geometry = MultiPolygon(self.solid_geometry)
         # self.solid_geometry = MultiPolygon(self.solid_geometry)
         # self.solid_geometry = self.solid_geometry.buffer(0.00000001)
         # self.solid_geometry = self.solid_geometry.buffer(0.00000001)
@@ -2211,17 +2262,17 @@ class Geometry(object):
 
 
     def union(self):
     def union(self):
         """
         """
-        Runs a cascaded union on the list of objects in
+        Runs a unary_union on the list of objects in
         solid_geometry.
         solid_geometry.
 
 
         :return: None
         :return: None
         """
         """
-        self.solid_geometry = [cascaded_union(self.solid_geometry)]
+        self.solid_geometry = [unary_union(self.solid_geometry)]
 
 
     def export_svg(self, scale_stroke_factor=0.00,
     def export_svg(self, scale_stroke_factor=0.00,
                    scale_factor_x=None, scale_factor_y=None,
                    scale_factor_x=None, scale_factor_y=None,
                    skew_factor_x=None, skew_factor_y=None,
                    skew_factor_x=None, skew_factor_y=None,
-                   skew_reference='center',
+                   skew_reference='center', scale_reference='center',
                    mirror=None):
                    mirror=None):
         """
         """
         Exports the Geometry Object as a SVG Element
         Exports the Geometry Object as a SVG Element
@@ -2235,11 +2286,11 @@ class Geometry(object):
             if self.multigeo:
             if self.multigeo:
                 for tool in self.tools:
                 for tool in self.tools:
                     flat_geo += self.flatten(self.tools[tool]['solid_geometry'])
                     flat_geo += self.flatten(self.tools[tool]['solid_geometry'])
-                geom_svg = cascaded_union(flat_geo)
+                geom_svg = unary_union(flat_geo)
             else:
             else:
-                geom_svg = cascaded_union(self.flatten())
+                geom_svg = unary_union(self.flatten())
         else:
         else:
-            geom_svg = cascaded_union(self.flatten())
+            geom_svg = unary_union(self.flatten())
 
 
         skew_ref = 'center'
         skew_ref = 'center'
         if skew_reference != 'center':
         if skew_reference != 'center':
@@ -2255,14 +2306,20 @@ class Geometry(object):
 
 
         geom = geom_svg
         geom = geom_svg
 
 
-        if scale_factor_x:
-            geom = affinity.scale(geom_svg, scale_factor_x, 1.0)
-        if scale_factor_y:
-            geom = affinity.scale(geom_svg, 1.0, scale_factor_y)
-        if skew_factor_x:
+        if scale_factor_x and not scale_factor_y:
+            geom = affinity.scale(geom_svg, scale_factor_x, 1.0, origin=scale_reference)
+        elif not scale_factor_x and scale_factor_y:
+            geom = affinity.scale(geom_svg, 1.0, scale_factor_y, origin=scale_reference)
+        elif scale_factor_x and scale_factor_y:
+            geom = affinity.scale(geom_svg, scale_factor_x, scale_factor_y, origin=scale_reference)
+
+        if skew_factor_x and not skew_factor_y:
             geom = affinity.skew(geom_svg, skew_factor_x, 0.0, origin=skew_ref)
             geom = affinity.skew(geom_svg, skew_factor_x, 0.0, origin=skew_ref)
-        if skew_factor_y:
+        elif not skew_factor_x and skew_factor_y:
             geom = affinity.skew(geom_svg, 0.0, skew_factor_y, origin=skew_ref)
             geom = affinity.skew(geom_svg, 0.0, skew_factor_y, origin=skew_ref)
+        elif skew_factor_x and skew_factor_y:
+            geom = affinity.skew(geom_svg, skew_factor_x, skew_factor_y, origin=skew_ref)
+
         if mirror:
         if mirror:
             if mirror == 'x':
             if mirror == 'x':
                 geom = affinity.scale(geom_svg, 1.0, -1.0)
                 geom = affinity.scale(geom_svg, 1.0, -1.0)
@@ -4818,7 +4875,7 @@ class CNCjob(Geometry):
         :return:                    GCode - string
         :return:                    GCode - string
         """
         """
 
 
-        log.debug("Generate_from_multitool_geometry()")
+        log.debug("generate_from_multitool_geometry()")
 
 
         temp_solid_geometry = []
         temp_solid_geometry = []
         if offset != 0.0:
         if offset != 0.0:
@@ -5128,7 +5185,7 @@ class CNCjob(Geometry):
         :rtype:             str
         :rtype:             str
         """
         """
 
 
-        log.debug("Generate_from_multitool_geometry()")
+        log.debug("geometry_tool_gcode_gen()")
 
 
         t_gcode = ''
         t_gcode = ''
         temp_solid_geometry = []
         temp_solid_geometry = []
@@ -5244,7 +5301,11 @@ class CNCjob(Geometry):
         self.feedrate_rapid = float(tool_dict['feedrate_rapid'])
         self.feedrate_rapid = float(tool_dict['feedrate_rapid'])
 
 
         self.spindlespeed = float(tool_dict['spindlespeed'])
         self.spindlespeed = float(tool_dict['spindlespeed'])
-        self.spindledir = tool_dict['spindledir']
+        try:
+            self.spindledir = tool_dict['spindledir']
+        except KeyError:
+            self.spindledir = self.app.defaults["geometry_spindledir"]
+
         self.dwell = tool_dict['dwell']
         self.dwell = tool_dict['dwell']
         self.dwelltime = float(tool_dict['dwelltime'])
         self.dwelltime = float(tool_dict['dwelltime'])
 
 
@@ -5253,8 +5314,9 @@ class CNCjob(Geometry):
             self.startz = None
             self.startz = None
 
 
         self.z_end = float(tool_dict['endz'])
         self.z_end = float(tool_dict['endz'])
+        self.xy_end = tool_dict['endxy']
         try:
         try:
-            if self.xy_end == '':
+            if self.xy_end == '' or self.xy_end is None:
                 self.xy_end = None
                 self.xy_end = None
             else:
             else:
                 # either originally it was a string or not, xy_end will be made string
                 # either originally it was a string or not, xy_end will be made string
@@ -6807,7 +6869,7 @@ class CNCjob(Geometry):
         # This takes forever. Too much data?
         # This takes forever. Too much data?
         # self.app.inform.emit('%s: %s' % (_("Unifying Geometry from parsed Geometry segments"),
         # self.app.inform.emit('%s: %s' % (_("Unifying Geometry from parsed Geometry segments"),
         #                                  str(len(self.gcode_parsed))))
         #                                  str(len(self.gcode_parsed))))
-        # self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed])
+        # self.solid_geometry = unary_union([geo['geom'] for geo in self.gcode_parsed])
 
 
         # This is much faster but not so nice to look at as you can see different segments of the geometry
         # This is much faster but not so nice to look at as you can see different segments of the geometry
         self.solid_geometry = [geo['geom'] for geo in self.gcode_parsed]
         self.solid_geometry = [geo['geom'] for geo in self.gcode_parsed]
@@ -7413,18 +7475,18 @@ class CNCjob(Geometry):
                 travels.append(g)
                 travels.append(g)
 
 
         # Used to determine the overall board size
         # Used to determine the overall board size
-        self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed])
+        self.solid_geometry = unary_union([geo['geom'] for geo in self.gcode_parsed])
 
 
         # Convert the cuts and travels into single geometry objects we can render as svg xml
         # Convert the cuts and travels into single geometry objects we can render as svg xml
         if travels:
         if travels:
-            travelsgeom = cascaded_union([geo['geom'] for geo in travels])
+            travelsgeom = unary_union([geo['geom'] for geo in travels])
 
 
         if self.app.abort_flag:
         if self.app.abort_flag:
             # graceful abort requested by the user
             # graceful abort requested by the user
             raise grace
             raise grace
 
 
         if cuts:
         if cuts:
-            cutsgeom = cascaded_union([geo['geom'] for geo in cuts])
+            cutsgeom = unary_union([geo['geom'] for geo in cuts])
 
 
         # Render the SVG Xml
         # Render the SVG Xml
         # The scale factor affects the size of the lines, and the stroke color adds different formatting for each set
         # The scale factor affects the size of the lines, and the stroke color adds different formatting for each set
@@ -7691,7 +7753,7 @@ class CNCjob(Geometry):
                         self.app.proc_container.update_view_text(' %d%%' % disp_number)
                         self.app.proc_container.update_view_text(' %d%%' % disp_number)
                         self.old_disp_number = disp_number
                         self.old_disp_number = disp_number
 
 
-                v['solid_geometry'] = cascaded_union([geo['geom'] for geo in v['gcode_parsed']])
+                v['solid_geometry'] = unary_union([geo['geom'] for geo in v['gcode_parsed']])
         self.create_geometry()
         self.create_geometry()
         self.app.proc_container.new_text = ''
         self.app.proc_container.new_text = ''
 
 
@@ -7800,7 +7862,7 @@ class CNCjob(Geometry):
                         self.old_disp_number = disp_number
                         self.old_disp_number = disp_number
 
 
                 # for the bounding box
                 # for the bounding box
-                v['solid_geometry'] = cascaded_union([geo['geom'] for geo in v['gcode_parsed']])
+                v['solid_geometry'] = unary_union([geo['geom'] for geo in v['gcode_parsed']])
 
 
         self.app.proc_container.new_text = ''
         self.app.proc_container.new_text = ''
 
 
@@ -8172,7 +8234,7 @@ def dict2obj(d):
 #
 #
 #     m = MultiLineString(edge_points)
 #     m = MultiLineString(edge_points)
 #     triangles = list(polygonize(m))
 #     triangles = list(polygonize(m))
-#     return cascaded_union(triangles), edge_points
+#     return unary_union(triangles), edge_points
 
 
 # def voronoi(P):
 # def voronoi(P):
 #     """
 #     """

+ 16 - 4
defaults.py

@@ -339,6 +339,11 @@ class FlatCAMDefaults:
         "geometry_area_shape": "polygon",
         "geometry_area_shape": "polygon",
         "geometry_area_strategy": "over",
         "geometry_area_strategy": "over",
         "geometry_area_overz": 1.0,
         "geometry_area_overz": 1.0,
+        "geometry_polish": False,
+        "geometry_polish_dia": 10.0,
+        "geometry_polish_pressure": -1.0,
+        "geometry_polish_overlap": 1.0,
+        "geometry_polish_method": _("Standard"),
 
 
         # Geometry Editor
         # Geometry Editor
         "geometry_editor_sel_limit": 30,
         "geometry_editor_sel_limit": 30,
@@ -410,6 +415,7 @@ class FlatCAMDefaults:
 
 
         "tools_iso_rest":           False,
         "tools_iso_rest":           False,
         "tools_iso_combine_passes": True,
         "tools_iso_combine_passes": True,
+        "tools_iso_check_valid":    False,
         "tools_iso_isoexcept":      False,
         "tools_iso_isoexcept":      False,
         "tools_iso_selection":      _("All"),
         "tools_iso_selection":      _("All"),
         "tools_iso_poly_ints":      False,
         "tools_iso_poly_ints":      False,
@@ -533,6 +539,7 @@ class FlatCAMDefaults:
         "tools_film_file_type_radio": 'svg',
         "tools_film_file_type_radio": 'svg',
         "tools_film_orientation": 'p',
         "tools_film_orientation": 'p',
         "tools_film_pagesize": 'A4',
         "tools_film_pagesize": 'A4',
+        "tools_film_png_dpi": 96,
 
 
         # Panel Tool
         # Panel Tool
         "tools_panelize_spacing_columns": 0.0,
         "tools_panelize_spacing_columns": 0.0,
@@ -833,8 +840,13 @@ class FlatCAMDefaults:
         with open(filename, "w") as file:
         with open(filename, "w") as file:
             simplejson.dump(self.defaults, file, default=to_dict, indent=2, sort_keys=True)
             simplejson.dump(self.defaults, file, default=to_dict, indent=2, sort_keys=True)
 
 
-    def load(self, filename: str):
-        """Loads the defaults from a file on disk, performing migration if required."""
+    def load(self, filename: str, inform):
+        """
+        Loads the defaults from a file on disk, performing migration if required.
+
+        :param filename:    a path to the file that is to be loaded
+        :param inform:      a pyqtSignal used to display information's in the StatusBar of the GUI
+        """
 
 
         # Read in the file
         # Read in the file
         try:
         try:
@@ -843,7 +855,7 @@ class FlatCAMDefaults:
             f.close()
             f.close()
         except IOError:
         except IOError:
             log.error("Could not load defaults file.")
             log.error("Could not load defaults file.")
-            self.inform.emit('[ERROR] %s' % _("Could not load defaults file."))
+            inform.emit('[ERROR] %s' % _("Could not load defaults file."))
             # in case the defaults file can't be loaded, show all toolbars
             # in case the defaults file can't be loaded, show all toolbars
             self.defaults["global_toolbar_view"] = 511
             self.defaults["global_toolbar_view"] = 511
             return
             return
@@ -856,7 +868,7 @@ class FlatCAMDefaults:
             self.defaults["global_toolbar_view"] = 511
             self.defaults["global_toolbar_view"] = 511
             e = sys.exc_info()[0]
             e = sys.exc_info()[0]
             log.error(str(e))
             log.error(str(e))
-            self.inform.emit('[ERROR] %s' % _("Failed to parse defaults file."))
+            inform.emit('[ERROR] %s' % _("Failed to parse defaults file."))
             return
             return
         if defaults is None:
         if defaults is None:
             return
             return

BIN
locale/de/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 187 - 182
locale/de/LC_MESSAGES/strings.po


BIN
locale/en/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 209 - 231
locale/en/LC_MESSAGES/strings.po


BIN
locale/es/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 194 - 189
locale/es/LC_MESSAGES/strings.po


BIN
locale/fr/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 194 - 189
locale/fr/LC_MESSAGES/strings.po


BIN
locale/hu/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 194 - 189
locale/hu/LC_MESSAGES/strings.po


BIN
locale/it/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 195 - 190
locale/it/LC_MESSAGES/strings.po


BIN
locale/pt_BR/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 194 - 189
locale/pt_BR/LC_MESSAGES/strings.po


BIN
locale/ro/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 187 - 182
locale/ro/LC_MESSAGES/strings.po


BIN
locale/ru/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 193 - 188
locale/ru/LC_MESSAGES/strings.po


BIN
locale/tr/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 197 - 213
locale/tr/LC_MESSAGES/strings.po


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 202 - 200
locale_template/strings.pot


+ 2 - 2
tclCommands/TclCommandBbox.py

@@ -1,7 +1,7 @@
 import collections
 import collections
 from tclCommands.TclCommand import TclCommand
 from tclCommands.TclCommand import TclCommand
 
 
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 
 
 import gettext
 import gettext
 import appTranslation as fcTranslate
 import appTranslation as fcTranslate
@@ -94,7 +94,7 @@ class TclCommandBbox(TclCommand):
                 # assert geo_obj.kind == 'geometry'
                 # assert geo_obj.kind == 'geometry'
 
 
                 # Bounding box with rounded corners
                 # Bounding box with rounded corners
-                geo = cascaded_union(obj.solid_geometry)
+                geo = unary_union(obj.solid_geometry)
                 bounding_box = geo.envelope.buffer(float(margin))
                 bounding_box = geo.envelope.buffer(float(margin))
                 if not rounded:  # Remove rounded corners
                 if not rounded:  # Remove rounded corners
                     bounding_box = bounding_box.envelope
                     bounding_box = bounding_box.envelope

+ 2 - 2
tclCommands/TclCommandCutout.py

@@ -3,7 +3,7 @@ from tclCommands.TclCommand import TclCommand
 import collections
 import collections
 import logging
 import logging
 
 
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 from shapely.geometry import LineString
 from shapely.geometry import LineString
 
 
 log = logging.getLogger('base')
 log = logging.getLogger('base')
@@ -134,7 +134,7 @@ class TclCommandCutout(TclCommand):
                            [pts[6], pts[7], pts[8]],
                            [pts[6], pts[7], pts[8]],
                            [pts[9], pts[10], pts[11]]]}
                            [pts[9], pts[10], pts[11]]]}
             cuts = cases[gaps_par]
             cuts = cases[gaps_par]
-            geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
+            geo_obj.solid_geometry = unary_union([LineString(segment) for segment in cuts])
 
 
         try:
         try:
             self.app.app_obj.new_object("geometry", outname, geo_init_me, plot=False)
             self.app.app_obj.new_object("geometry", outname, geo_init_me, plot=False)

+ 3 - 3
tclCommands/TclCommandExportSVG.py

@@ -21,12 +21,12 @@ class TclCommandExportSVG(TclCommand):
     arg_names = collections.OrderedDict([
     arg_names = collections.OrderedDict([
         ('name', str),
         ('name', str),
         ('filename', str),
         ('filename', str),
-        ('scale_factor', float)
+        ('scale_stroke_factor', float)
     ])
     ])
 
 
     # Dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
     # Dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
     option_types = collections.OrderedDict([
     option_types = collections.OrderedDict([
-        ('scale_factor', float)
+        ('scale_stroke_factor', float)
     ])
     ])
 
 
     # array of mandatory options for current Tcl command: required = {'name','outname'}
     # array of mandatory options for current Tcl command: required = {'name','outname'}
@@ -39,7 +39,7 @@ class TclCommandExportSVG(TclCommand):
             ('name', 'Name of the object export. Required.'),
             ('name', 'Name of the object export. Required.'),
             ('filename', 'Absolute path to file to export.\n'
             ('filename', 'Absolute path to file to export.\n'
                          'WARNING: no spaces are allowed. If unsure enclose the entire path with quotes.'),
                          'WARNING: no spaces are allowed. If unsure enclose the entire path with quotes.'),
-            ('scale_factor', 'Multiplication factor used for scaling line widths during export.')
+            ('scale_stroke_factor', 'Multiplication factor used for scaling line widths during export.')
         ]),
         ]),
         'examples': ['export_svg my_geometry my_file.svg']
         'examples': ['export_svg my_geometry my_file.svg']
     }
     }

+ 3 - 3
tclCommands/TclCommandGeoCutout.py

@@ -3,7 +3,7 @@ from tclCommands.TclCommand import TclCommandSignaled
 import logging
 import logging
 import collections
 import collections
 from copy import deepcopy
 from copy import deepcopy
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 from shapely.geometry import Polygon, LineString, LinearRing
 from shapely.geometry import Polygon, LineString, LinearRing
 
 
 import gettext
 import gettext
@@ -131,14 +131,14 @@ class TclCommandGeoCutout(TclCommandSignaled):
             flat_geometry = flatten(geo, pathonly=True)
             flat_geometry = flatten(geo, pathonly=True)
 
 
             polygon = Polygon(pts)
             polygon = Polygon(pts)
-            toolgeo = cascaded_union(polygon)
+            toolgeo = unary_union(polygon)
             diffs = []
             diffs = []
             for target in flat_geometry:
             for target in flat_geometry:
                 if type(target) == LineString or type(target) == LinearRing:
                 if type(target) == LineString or type(target) == LinearRing:
                     diffs.append(target.difference(toolgeo))
                     diffs.append(target.difference(toolgeo))
                 else:
                 else:
                     log.warning("Not implemented.")
                     log.warning("Not implemented.")
-            return cascaded_union(diffs)
+            return unary_union(diffs)
 
 
         if 'name' in args:
         if 'name' in args:
             name = args['name']
             name = args['name']

+ 2 - 2
tclCommands/TclCommandNregions.py

@@ -1,6 +1,6 @@
 from tclCommands.TclCommand import TclCommand
 from tclCommands.TclCommand import TclCommand
 
 
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 
 
 import collections
 import collections
 
 
@@ -92,7 +92,7 @@ class TclCommandNregions(TclCommand):
             def geo_init(geo_obj, app_obj):
             def geo_init(geo_obj, app_obj):
                 assert geo_obj.kind == 'geometry'
                 assert geo_obj.kind == 'geometry'
 
 
-                geo = cascaded_union(obj.solid_geometry)
+                geo = unary_union(obj.solid_geometry)
                 bounding_box = geo.envelope.buffer(float(margin))
                 bounding_box = geo.envelope.buffer(float(margin))
                 if not rounded:
                 if not rounded:
                     bounding_box = bounding_box.envelope
                     bounding_box = bounding_box.envelope

+ 1 - 1
tests/other/test_plotg.py

@@ -1,5 +1,5 @@
 from shapely.geometry import LineString, Polygon
 from shapely.geometry import LineString, Polygon
-from shapely.ops import cascaded_union, unary_union
+from shapely.ops import unary_union
 from matplotlib.pyplot import plot, subplot, show, axes
 from matplotlib.pyplot import plot, subplot, show, axes
 from matplotlib.axes import *
 from matplotlib.axes import *
 from camlib import *
 from camlib import *

+ 1 - 1
tests/test_paint.py

@@ -1,7 +1,7 @@
 import unittest
 import unittest
 
 
 from shapely.geometry import LineString, Polygon
 from shapely.geometry import LineString, Polygon
-from shapely.ops import cascaded_union, unary_union
+from shapely.ops import unary_union
 from matplotlib.pyplot import plot, subplot, show, cla, clf, xlim, ylim, title
 from matplotlib.pyplot import plot, subplot, show, cla, clf, xlim, ylim, title
 from matplotlib.axes import *
 from matplotlib.axes import *
 from camlib import *
 from camlib import *

+ 1 - 1
tests/test_pathconnect.py

@@ -1,7 +1,7 @@
 import unittest
 import unittest
 
 
 from shapely.geometry import LineString, Polygon
 from shapely.geometry import LineString, Polygon
-from shapely.ops import cascaded_union, unary_union
+from shapely.ops import unary_union
 from matplotlib.pyplot import plot, subplot, show, cla, clf, xlim, ylim, title
 from matplotlib.pyplot import plot, subplot, show, cla, clf, xlim, ylim, title
 from camlib import *
 from camlib import *
 from random import random
 from random import random

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio