Sfoglia il codice sorgente

Merged Beta_8.994 into AutolevellingFeature

Marius Stanciu 5 anni fa
parent
commit
2bbab19e0a
64 ha cambiato i file con 3751 aggiunte e 2658 eliminazioni
  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
 
 - 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 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
 from appParsers.ParseFont import *
 
 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
 from shapely.geometry.polygon import orient
 
@@ -46,7 +46,7 @@ class BufferSelectionTool(AppTool):
     Simple input for buffer distance.
     """
 
-    toolName = "Buffer Selection"
+    toolName = _("Buffer Selection")
 
     def __init__(self, app, draw_app):
         AppTool.__init__(self, app)
@@ -55,7 +55,7 @@ class BufferSelectionTool(AppTool):
         self.decimals = app.decimals
 
         # Title
-        title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName))
+        title_label = FCLabel("%s" % ('Editor ' + self.toolName))
         title_label.setStyleSheet("""
                         QLabel
                         {
@@ -82,7 +82,7 @@ class BufferSelectionTool(AppTool):
         self.buffer_distance_entry.set_precision(self.decimals)
         self.buffer_distance_entry.set_range(0.0000, 999999.9999)
         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(
             _("There are 3 types of corners:\n"
               " - 'Round': the corner is rounded for exterior buffer.\n"
@@ -99,15 +99,15 @@ class BufferSelectionTool(AppTool):
         hlay = QtWidgets.QHBoxLayout()
         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)
-        self.buffer_ext_button = QtWidgets.QPushButton(_("Buffer Exterior"))
+        self.buffer_ext_button = FCButton(_("Buffer Exterior"))
         hlay.addWidget(self.buffer_ext_button)
 
         hlay1 = QtWidgets.QHBoxLayout()
         self.buffer_tools_box.addLayout(hlay1)
 
-        self.buffer_button = QtWidgets.QPushButton(_("Full Buffer"))
+        self.buffer_button = FCButton(_("Full Buffer"))
         hlay1.addWidget(self.buffer_button)
 
         self.layout.addStretch()
@@ -191,7 +191,7 @@ class TextInputTool(AppTool):
     Simple input for buffer distance.
     """
 
-    toolName = "Text Input Tool"
+    toolName = _("Text Input Tool")
 
     def __init__(self, app):
         AppTool.__init__(self, app)
@@ -212,7 +212,7 @@ class TextInputTool(AppTool):
         self.text_tool_frame.setLayout(self.text_tools_box)
 
         # Title
-        title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName))
+        title_label = FCLabel("%s" % self.toolName)
         title_label.setStyleSheet("""
                         QLabel
                         {
@@ -238,7 +238,7 @@ class TextInputTool(AppTool):
 
         self.font_type_cb = QtWidgets.QFontComboBox(self)
         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)
         self.font_bold = False
@@ -310,7 +310,7 @@ class TextInputTool(AppTool):
         self.font_italic_tb.setIcon(QtGui.QIcon(self.app.resource_location + '/italic32.png'))
         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
         self.text_input_entry = FCTextAreaRich()
@@ -319,13 +319,13 @@ class TextInputTool(AppTool):
         # self.text_input_entry.setMaximumHeight(150)
         self.text_input_entry.setCurrentFont(f_current)
         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
         hlay1 = QtWidgets.QHBoxLayout()
         self.form_layout.addRow("", hlay1)
         hlay1.addStretch()
-        self.apply_button = QtWidgets.QPushButton("Apply")
+        self.apply_button = FCButton(_("Apply"))
         hlay1.addWidget(self.apply_button)
 
         # self.layout.addStretch()
@@ -409,7 +409,7 @@ class PaintOptionsTool(AppTool):
     Inputs to specify how to paint the selected polygons.
     """
 
-    toolName = "Paint Tool"
+    toolName = _("Paint Tool")
 
     def __init__(self, app, fcdraw):
         AppTool.__init__(self, app)
@@ -419,7 +419,7 @@ class PaintOptionsTool(AppTool):
         self.decimals = self.app.decimals
 
         # Title
-        title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName))
+        title_label = FCLabel("%s" % self.toolName)
         title_label.setStyleSheet("""
                         QLabel
                         {
@@ -435,7 +435,7 @@ class PaintOptionsTool(AppTool):
         grid.setColumnStretch(1, 1)
 
         # Tool dia
-        ptdlabel = QtWidgets.QLabel('%s:' % _('Tool dia'))
+        ptdlabel = FCLabel('%s:' % _('Tool dia'))
         ptdlabel.setToolTip(
            _("Diameter of the tool to be used in the operation.")
         )
@@ -447,7 +447,7 @@ class PaintOptionsTool(AppTool):
         grid.addWidget(self.painttooldia_entry, 0, 1)
 
         # Overlap
-        ovlabel = QtWidgets.QLabel('%s:' % _('Overlap'))
+        ovlabel = FCLabel('%s:' % _('Overlap'))
         ovlabel.setToolTip(
             _("How much (percentage) of the tool width to overlap each tool pass.\n"
               "Adjust the value starting with lower values\n"
@@ -467,7 +467,7 @@ class PaintOptionsTool(AppTool):
         grid.addWidget(self.paintoverlap_entry, 1, 1)
 
         # Margin
-        marginlabel = QtWidgets.QLabel('%s:' % _('Margin'))
+        marginlabel = FCLabel('%s:' % _('Margin'))
         marginlabel.setToolTip(
            _("Distance by which to avoid\n"
              "the edges of the polygon to\n"
@@ -481,7 +481,7 @@ class PaintOptionsTool(AppTool):
         grid.addWidget(self.paintmargin_entry, 2, 1)
 
         # Method
-        methodlabel = QtWidgets.QLabel('%s:' % _('Method'))
+        methodlabel = FCLabel('%s:' % _('Method'))
         methodlabel.setToolTip(
             _("Algorithm to paint the polygons:\n"
               "- Standard: Fixed step inwards.\n"
@@ -502,7 +502,7 @@ class PaintOptionsTool(AppTool):
         grid.addWidget(self.paintmethod_combo, 3, 1)
 
         # Connect lines
-        pathconnectlabel = QtWidgets.QLabel(_("Connect:"))
+        pathconnectlabel = FCLabel('%s:' % _("Connect"))
         pathconnectlabel.setToolTip(
            _("Draw lines between resulting\n"
              "segments to minimize tool lifts.")
@@ -512,7 +512,7 @@ class PaintOptionsTool(AppTool):
         grid.addWidget(pathconnectlabel, 4, 0)
         grid.addWidget(self.pathconnect_cb, 4, 1)
 
-        contourlabel = QtWidgets.QLabel(_("Contour:"))
+        contourlabel = FCLabel('%s:' % _("Contour"))
         contourlabel.setToolTip(
             _("Cut around the perimeter of the polygon\n"
               "to trim rough edges.")
@@ -525,7 +525,7 @@ class PaintOptionsTool(AppTool):
         # Buttons
         hlay = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay)
-        self.paint_button = QtWidgets.QPushButton(_("Paint"))
+        self.paint_button = FCButton(_("Paint"))
         hlay.addWidget(self.paint_button)
 
         self.layout.addStretch()
@@ -619,7 +619,7 @@ class TransformEditorTool(AppTool):
         self.decimals = self.app.decimals
 
         # ## Title
-        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label = FCLabel("%s" % self.toolName)
         title_label.setStyleSheet("""
                                 QLabel
                                 {
@@ -628,7 +628,7 @@ class TransformEditorTool(AppTool):
                                 }
                                 """)
         self.layout.addWidget(title_label)
-        self.layout.addWidget(QtWidgets.QLabel(''))
+        self.layout.addWidget(FCLabel(''))
 
         # ## Layout
         grid0 = QtWidgets.QGridLayout()
@@ -637,10 +637,10 @@ class TransformEditorTool(AppTool):
         grid0.setColumnStretch(1, 1)
         grid0.setColumnStretch(2, 0)
 
-        grid0.addWidget(QtWidgets.QLabel(''))
+        grid0.addWidget(FCLabel(''))
 
         # Reference
-        ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
+        ref_label = FCLabel('%s:' % _("Reference"))
         ref_label.setToolTip(
             _("The reference point for Rotate, Skew, Scale, Mirror.\n"
               "Can be:\n"
@@ -656,7 +656,7 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(ref_label, 0, 0)
         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(
             _("A point of reference in format X,Y.")
         )
@@ -677,10 +677,10 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(separator_line, 5, 0, 1, 3)
 
         # ## 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)
 
-        self.rotate_label = QtWidgets.QLabel('%s:' % _("Angle"))
+        self.rotate_label = FCLabel('%s:' % _("Angle"))
         self.rotate_label.setToolTip(
             _("Angle for Rotation action, in degrees.\n"
               "Float number between -360 and 359.\n"
@@ -714,7 +714,7 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(separator_line, 8, 0, 1, 3)
 
         # ## 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)
 
         self.skew_link_cb = FCCheckBox()
@@ -725,7 +725,7 @@ class TransformEditorTool(AppTool):
 
         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(
             _("Angle for Skew action, in degrees.\n"
               "Float number between -360 and 360.")
@@ -746,7 +746,7 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(self.skewx_entry, 10, 1)
         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(
             _("Angle for Skew action, in degrees.\n"
               "Float number between -360 and 360.")
@@ -776,7 +776,7 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(separator_line, 14, 0, 1, 3)
 
         # ## 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)
 
         self.scale_link_cb = FCCheckBox()
@@ -787,7 +787,7 @@ class TransformEditorTool(AppTool):
 
         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(
             _("Factor for scaling on X axis.")
         )
@@ -807,7 +807,7 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(self.scalex_entry, 17, 1)
         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(
             _("Factor for scaling on Y axis.")
         )
@@ -840,7 +840,7 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(separator_line, 21, 0, 1, 3)
 
         # ## 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)
 
         self.flipx_button = FCButton(_("Flip on X"))
@@ -865,10 +865,10 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(separator_line, 27, 0, 1, 3)
 
         # ## 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)
 
-        self.offx_label = QtWidgets.QLabel('%s:' % _("X val"))
+        self.offx_label = FCLabel('%s:' % _("X val"))
         self.offx_label.setToolTip(
             _("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_button, 31, 2)
 
-        self.offy_label = QtWidgets.QLabel('%s:' % _("Y val"))
+        self.offy_label = FCLabel('%s:' % _("Y val"))
         self.offy_label.setToolTip(
             _("Distance to offset on Y axis. In current units.")
         )
@@ -914,7 +914,7 @@ class TransformEditorTool(AppTool):
         grid0.addWidget(separator_line, 34, 0, 1, 3)
 
         # ## 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)
 
         self.buffer_rounded_cb = FCCheckBox('%s' % _("Rounded"))
@@ -927,7 +927,7 @@ class TransformEditorTool(AppTool):
 
         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(
             _("A positive value will create the effect of dilation,\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_button, 37, 2)
 
-        self.buffer_factor_label = QtWidgets.QLabel('%s:' % _("Value"))
+        self.buffer_factor_label = FCLabel('%s:' % _("Value"))
         self.buffer_factor_label.setToolTip(
             _("A positive value will create the effect of dilation,\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_button, 38, 2)
 
-        grid0.addWidget(QtWidgets.QLabel(''), 42, 0, 1, 3)
+        grid0.addWidget(FCLabel(''), 42, 0, 1, 3)
 
         self.layout.addStretch()
 
@@ -3147,7 +3147,7 @@ class FCEraser(FCShapeTool):
             temp_shape = eraser_shape.buffer(0.0000001)
             temp_shape = Polygon(temp_shape.exterior)
             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():
             try:
@@ -3273,15 +3273,15 @@ class AppGeoEditor(QtCore.QObject):
 
         # ## Page Title icon
         pixmap = QtGui.QPixmap(self.app.resource_location + '/flatcam_icon32.png')
-        self.icon = QtWidgets.QLabel()
+        self.icon = FCLabel()
         self.icon.setPixmap(pixmap)
         self.title_box.addWidget(self.icon, stretch=0)
 
         # ## 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_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.setHeaderLabels(["ID", _("Type"), _("Name")])
@@ -3298,7 +3298,7 @@ class AppGeoEditor(QtCore.QObject):
         layout.addStretch()
 
         # 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.setToolTip(
             _("Exit from Editor.")
@@ -5134,7 +5134,7 @@ class AppGeoEditor(QtCore.QObject):
                     return
 
                 # add the result to the results list
-                results.append(cascaded_union(local_results))
+                results.append(unary_union(local_results))
 
         # This is a dirty patch:
         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 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
 
 from vispy.geometry import Rect
@@ -2235,7 +2235,7 @@ class FCEraser(FCShapeTool):
             temp_shape = eraser_shape['solid'].buffer(0.0000001)
             temp_shape = Polygon(temp_shape.exterior)
             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:
             try:
@@ -4968,7 +4968,7 @@ class AppGerberEditor(QtCore.QObject):
                 if 'solid' in actual_geo:
                     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
         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:
                     self.app.app_obj.new_gerber_object()
 
-                # New Geometry
+                # New Document Object
                 if key == QtCore.Qt.Key_D:
-                    self.app.new_document_object()
+                    self.app.app_obj.new_document_object()
 
                 # Copy Object Name
                 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)
 
-        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 ####
         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)
 
+        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"))
         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)
 
+        # 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 ####
         self.tools_table_label = QtWidgets.QLabel('<b>%s</b>' % _('Tools Table'))
         self.tools_table_label.setToolTip(
@@ -561,9 +613,9 @@ class ExcellonObjectUI(ObjectUI):
         hlay_plot.addStretch()
         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.setIcon(QtGui.QIcon(self.app.resource_location + '/milling_tool32.png'))
         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("""
                         QPushButton
@@ -666,6 +718,8 @@ class ExcellonObjectUI(ObjectUI):
                         }
                         """)
         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.setFrameShape(QtWidgets.QFrame.HLine)
@@ -689,7 +743,7 @@ class ExcellonObjectUI(ObjectUI):
         )
         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(
             _("Diameter of the cutting tool.")
         )
@@ -704,7 +758,7 @@ class ExcellonObjectUI(ObjectUI):
         self.generate_milling_button = QtWidgets.QPushButton(_('Mill Drills'))
         self.generate_milling_button.setToolTip(
             _("Create the Geometry Object\n"
-              "for milling DRILLS toolpaths.")
+              "for milling drills.")
         )
         self.generate_milling_button.setStyleSheet("""
                         QPushButton
@@ -724,7 +778,7 @@ class ExcellonObjectUI(ObjectUI):
         self.generate_milling_slots_button = QtWidgets.QPushButton(_('Mill Slots'))
         self.generate_milling_slots_button.setToolTip(
             _("Create the Geometry Object\n"
-              "for milling SLOTS toolpaths.")
+              "for milling slots.")
         )
         self.generate_milling_slots_button.setStyleSheet("""
                         QPushButton
@@ -814,6 +868,32 @@ class GeometryObjectUI(ObjectUI):
                                       """)
         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
         # this way I can hide/show the frame
         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.setToolTip(
             _("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)
@@ -1757,6 +1838,32 @@ class CNCObjectUI(ObjectUI):
                                        """)
         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
         self.annotation_cb = FCCheckBox(_("Display Annotation"))
         self.annotation_cb.setToolTip(
@@ -1764,12 +1871,12 @@ class CNCObjectUI(ObjectUI):
               "When checked it will display numbers in order for each end\n"
               "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.setFrameShape(QtWidgets.QFrame.HLine)
         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
         self.t_distance_label = QtWidgets.QLabel("<b>%s:</b>" % _("Travelled distance"))
@@ -1780,9 +1887,9 @@ class CNCObjectUI(ObjectUI):
         self.t_distance_entry = FCEntry()
         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
         self.t_time_label = QtWidgets.QLabel("<b>%s:</b>" % _("Estimated time"))
@@ -1793,9 +1900,9 @@ class CNCObjectUI(ObjectUI):
         self.t_time_entry = FCEntry()
         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_entry.setVisible(False)
@@ -1805,7 +1912,7 @@ class CNCObjectUI(ObjectUI):
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         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()
         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_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_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,
@@ -381,6 +382,12 @@ class PreferencesUIManager:
             "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,
 
+            # 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
             "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,
@@ -461,6 +468,7 @@ class PreferencesUIManager:
             "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_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
             "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)
             # 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")
 

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

@@ -197,13 +197,11 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
         # -----------------------------
         # --- 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
         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")
         )
         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)
 
         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 appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox, FCSpinner, NumericalEvalTupleEntry, \
-    OptionalInputSection, NumericalEvalEntry
+    OptionalInputSection, NumericalEvalEntry, FCLabel
 from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
@@ -28,7 +28,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         self.decimals = decimals
 
         # ## 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(
             _("Create CNCJob with toolpaths for drilling or milling holes.")
         )
@@ -38,7 +38,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         self.layout.addLayout(grid0)
 
         # 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"
                                       "'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"
@@ -54,7 +54,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.order_radio, 1, 1, 1, 2)
 
         # Cut Z
-        cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
+        cutzlabel = FCLabel('%s:' % _('Cut Z'))
         cutzlabel.setToolTip(
             _("Drill depth (negative)\n"
               "below the copper surface.")
@@ -95,7 +95,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.maxdepth_entry, 4, 1, 1, 2)
 
         # Travel Z
-        travelzlabel = QtWidgets.QLabel('%s:' % _('Travel Z'))
+        travelzlabel = FCLabel('%s:' % _('Travel Z'))
         travelzlabel.setToolTip(
             _("Tool height when travelling\n"
               "across the XY plane.")
@@ -121,7 +121,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.toolchange_cb, 6, 0, 1, 3)
 
         # Tool Change Z
-        toolchangezlabel = QtWidgets.QLabel('%s:' % _('Toolchange Z'))
+        toolchangezlabel = FCLabel('%s:' % _('Toolchange Z'))
         toolchangezlabel.setToolTip(
             _("Z-axis position (height) for\n"
               "tool change.")
@@ -139,7 +139,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.toolchangez_entry, 7, 1, 1, 2)
 
         # End Move Z
-        endz_label = QtWidgets.QLabel('%s:' % _('End move Z'))
+        endz_label = FCLabel('%s:' % _('End move Z'))
         endz_label.setToolTip(
             _("Height of the tool after\n"
               "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)
 
         # 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(
             _("End move X,Y position. In format (x,y).\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)
 
         # Feedrate Z
-        frlabel = QtWidgets.QLabel('%s:' % _('Feedrate Z'))
+        frlabel = FCLabel('%s:' % _('Feedrate Z'))
         frlabel.setToolTip(
             _("Tool speed while drilling\n"
               "(in units per minute).\n"
@@ -183,7 +183,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.feedrate_z_entry, 10, 1, 1, 2)
 
         # Spindle speed
-        spdlabel = QtWidgets.QLabel('%s:' % _('Spindle Speed'))
+        spdlabel = FCLabel('%s:' % _('Spindle Speed'))
         spdlabel.setToolTip(
             _("Speed of the spindle\n"
               "in RPM (optional)")
@@ -206,7 +206,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.dwell_cb, 12, 0, 1, 3)
 
         # Dwell Time
-        dwelltime = QtWidgets.QLabel('%s:' % _('Duration'))
+        dwelltime = FCLabel('%s:' % _('Duration'))
         dwelltime.setToolTip(_("Number of time units for spindle to dwell."))
         self.dwelltime_entry = FCDoubleSpinner()
         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])
 
         # preprocessor selection
-        pp_excellon_label = QtWidgets.QLabel('%s:' % _("Preprocessor"))
+        pp_excellon_label = FCLabel('%s:' % _("Preprocessor"))
         pp_excellon_label.setToolTip(
             _("The preprocessor JSON file that dictates\n"
               "Gcode output.")
@@ -236,7 +236,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(separator_line, 16, 0, 1, 3)
 
         # 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)
 
         # Drill slots
@@ -247,7 +247,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.drill_slots_cb, 20, 0, 1, 3)
 
         # Drill Overlap
-        self.drill_overlap_label = QtWidgets.QLabel('%s:' % _('Overlap'))
+        self.drill_overlap_label = FCLabel('%s:' % _('Overlap'))
         self.drill_overlap_label.setToolTip(
             _("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)
         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(
             _("A list of advanced parameters.")
         )
         grid0.addWidget(self.exc_label, 28, 0, 1, 3)
 
         # Offset Z
-        offsetlabel = QtWidgets.QLabel('%s:' % _('Offset Z'))
+        offsetlabel = FCLabel('%s:' % _('Offset Z'))
         offsetlabel.setToolTip(
             _("Some drill bits (the larger ones) need to drill deeper\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)
 
         # 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 X,Y position.")
         )
@@ -303,7 +303,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.toolchangexy_entry, 31, 1, 1, 2)
 
         # Start Z
-        startzlabel = QtWidgets.QLabel('%s:' % _('Start Z'))
+        startzlabel = FCLabel('%s:' % _('Start Z'))
         startzlabel.setToolTip(
             _("Height of the tool just after start.\n"
               "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)
 
         # Feedrate Rapids
-        fr_rapid_label = QtWidgets.QLabel('%s:' % _('Feedrate Rapids'))
+        fr_rapid_label = FCLabel('%s:' % _('Feedrate Rapids'))
         fr_rapid_label.setToolTip(
             _("Tool speed while drilling\n"
               "(in units per minute).\n"
@@ -330,7 +330,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.feedrate_rapid_entry, 35, 1, 1, 2)
 
         # Probe depth
-        self.pdepth_label = QtWidgets.QLabel('%s:' % _("Probe Z depth"))
+        self.pdepth_label = FCLabel('%s:' % _("Probe Z depth"))
         self.pdepth_label.setToolTip(
             _("The maximum depth that the probe is allowed\n"
               "to probe. Negative value, in current units.")
@@ -343,7 +343,7 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.pdepth_entry, 37, 1, 1, 2)
 
         # Probe feedrate
-        self.feedrate_probe_label = QtWidgets.QLabel('%s:' % _("Feedrate Probe"))
+        self.feedrate_probe_label = FCLabel('%s:' % _("Feedrate Probe"))
         self.feedrate_probe_label.setToolTip(
            _("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)
 
         # Spindle direction
-        spindle_dir_label = QtWidgets.QLabel('%s:' % _('Spindle direction'))
+        spindle_dir_label = FCLabel('%s:' % _('Spindle direction'))
         spindle_dir_label.setToolTip(
             _("This sets the direction that the spindle is rotating.\n"
               "It can be either:\n"
@@ -389,4 +389,64 @@ class ToolsDrillPrefGroupUI(OptionsGroupUI):
 
         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()

+ 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
 
 import gettext
@@ -28,7 +28,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         self.decimals = decimals
 
         # ## Parameters
-        self.film_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.film_label = FCLabel("<b>%s:</b>" % _("Parameters"))
         self.film_label.setToolTip(
             _("Create a PCB film from a Gerber or Geometry object.\n"
               "The file is saved in SVG format.")
@@ -40,7 +40,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
 
         self.film_type_radio = RadioSet([{'label': 'Pos', 'value': 'pos'},
                                          {'label': 'Neg', 'value': 'neg'}])
-        ftypelbl = QtWidgets.QLabel('%s:' % _('Film Type'))
+        ftypelbl = FCLabel('%s:' % _('Film Type'))
         ftypelbl.setToolTip(
             _("Generate a Positive black film or a Negative film.\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)
 
         # Film Color
-        self.film_color_label = QtWidgets.QLabel('%s:' % _('Film Color'))
+        self.film_color_label = FCLabel('%s:' % _('Film Color'))
         self.film_color_label.setToolTip(
             _("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.setSingleStep(0.1)
 
-        self.film_boundary_label = QtWidgets.QLabel('%s:' % _("Border"))
+        self.film_boundary_label = FCLabel('%s:' % _("Border"))
         self.film_boundary_label.setToolTip(
             _("Specify a border around the object.\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.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(
             _("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"
@@ -96,7 +96,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.film_scale_stroke_label, 3, 0)
         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(
             _("Sometime the printers will distort the print shape, especially the Laser types.\n"
               "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)
 
-        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.set_range(-999.9999, 999.9999)
         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_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.set_range(-999.9999, 999.9999)
         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)
 
-        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.set_range(-999.9999, 999.9999)
         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_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.set_range(-999.9999, 999.9999)
         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_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(
             _("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.")
@@ -198,7 +198,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
                                           {'label': _('Y'), 'value': 'y'},
                                           {'label': _('Both'), 'value': 'both'}],
                                          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, 13, 1)
@@ -213,7 +213,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
                                          {'label': _('PDF'), 'value': 'pdf'}
                                          ], stretch=False)
 
-        self.file_type_label = QtWidgets.QLabel(_("Film Type:"))
+        self.file_type_label = FCLabel(_("Film Type:"))
         self.file_type_label.setToolTip(
             _("The file type of the saved film. Can be:\n"
               "- 'SVG' -> open-source vectorial format\n"
@@ -224,7 +224,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.file_type_radio, 15, 1)
 
         # Page orientation
-        self.orientation_label = QtWidgets.QLabel('%s:' % _("Page Orientation"))
+        self.orientation_label = FCLabel('%s:' % _("Page Orientation"))
         self.orientation_label.setToolTip(_("Can be:\n"
                                             "- Portrait\n"
                                             "- Landscape"))
@@ -237,7 +237,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.orientation_radio, 16, 1)
 
         # 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_combo = FCComboBox()
@@ -302,6 +302,17 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.pagesize_label, 17, 0)
         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()
 
         # Film Tool

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

@@ -271,6 +271,16 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
         self.except_cb.setObjectName("i_except")
         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
         self.select_label = QtWidgets.QLabel('%s:' % _("Selection"))
         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.export_gcode_button.clicked.connect(self.on_exportgcode_button_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())
 
+        # Properties
+        self.ui.properties_button.toggled.connect(self.on_properties)
+        self.calculations_finished.connect(self.update_area_chull)
+
         # autolevelling signals
         self.ui.sal_cb.stateChanged.connect(self.on_toggle_autolevelling)
         self.ui.al_mode_radio.activated_custom.connect(self.on_mode_radio)
@@ -699,6 +705,20 @@ class CNCJobObject(FlatCAMObj, CNCjob):
         except (TypeError, AttributeError):
             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):
         # create the solid_geo
 
@@ -1260,7 +1280,7 @@ class CNCJobObject(FlatCAMObj, CNCjob):
                 pass
 
             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:
                 if 'ok' in line.lower():
                     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))
                 #
                 #     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({
                 tooluid_key: deepcopy(tool_dia_copy)

+ 39 - 15
appObjects/FlatCAMExcellon.py

@@ -176,7 +176,11 @@ class ExcellonObject(FlatCAMObj, Excellon):
 
         # Editor
         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.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.generate_milling_slots_button.setDisabled(False)
 
+        # update the milling section
+        self.on_row_selection_change()
+
         self.ui_connect()
 
     def ui_connect(self):
@@ -510,12 +517,22 @@ class ExcellonObject(FlatCAMObj, Excellon):
             self.ui.slot_tooldia_entry.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:
                 self.ui.tooldia_entry.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.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:
                 self.ui.slot_tooldia_entry.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):
         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'):
         """
         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['type'] = 'Excellon Geometry'
             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 = []
 
             # 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['type'] = 'Excellon Geometry'
             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 = []
 
             # 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_strategy": self.ui.strategy_radio,
             "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({
@@ -589,8 +594,13 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
         self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
 
+        # Editor Signal
         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.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))
@@ -614,6 +624,20 @@ class GeometryObject(FlatCAMObj, Geometry):
 
         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):
         # read the table tools uid
         current_uid_list = []
@@ -722,7 +746,13 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
 
         # 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.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):
 
@@ -806,6 +836,36 @@ class GeometryObject(FlatCAMObj, Geometry):
         except (TypeError, AttributeError):
             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):
         """
         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):
         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']['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):
         if clearsel:
@@ -1854,6 +1920,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             return
 
         self.multigeo = True
+
         # Object initialization function for app.app_obj.new_object()
         # RUNNING ON SEPARATE THREAD!
         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
                 # commented this; there is no need for the actual GCode geometry - the original one will serve as well
                 # 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:
                     dia_cnc_dict['solid_geometry'] = tool_solid_geometry
                     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_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),
-                                                                   tolerance = tol,
+                                                                   tolerance=tol,
                                                                    is_first=is_first, is_last=is_last,
-                                                                   toolchange = True)
+                                                                   toolchange=True)
                 if res == 'fail':
                     log.debug("GeometryObject.mtool_gen_cncjob() --> generate_from_geometry2() failed")
                     return 'fail'
@@ -2135,7 +2202,7 @@ class GeometryObject(FlatCAMObj, Geometry):
                 # 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
                 # 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
                 # ])
                 try:
@@ -2303,8 +2370,8 @@ class GeometryObject(FlatCAMObj, Geometry):
                                                    toolchangexy=toolchangexy,
                                                    extracut=extracut, extracut_length=extracut_length,
                                                    startz=startz, endz=endz, endxy=endxy,
-                                                   pp_geometry_name=ppname_g
-            )
+                                                   pp_geometry_name=ppname_g)
+
             job_obj.source_file = res
             # tell gcode_parse from which point to start drawing the lines depending on what kind of object is the
             # source of gcode
@@ -2872,13 +2939,13 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.plot()
 
     @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.
 
         :param geo_list:    List of GerberObject Objects to join.
         :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
         :return: None
         """
@@ -2908,7 +2975,7 @@ class GeometryObject(FlatCAMObj, Geometry):
                 GeometryObject.merge(geo_list=geo_obj, geo_final=geo_final)
             # If not list, just append
             else:
-                if multigeo is None or multigeo is False:
+                if multi_geo is None or multi_geo is False:
                     geo_final.multigeo = False
                 else:
                     geo_final.multigeo = True
@@ -2966,19 +3033,23 @@ class GeometryObject(FlatCAMObj, Geometry):
             new_tool_nr = 1
             for i_lst in intersect_list:
                 new_solid_geo = []
+                last_tool = None
                 for old_tool in i_lst:
                     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] = \
                         {
-                            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)
                     new_tool_nr += 1
         else:
             final_tools = new_tools
 
+        # if not final_tools:
+        #     return 'fail'
         geo_final.tools = final_tools
 
     @staticmethod

+ 20 - 3
appObjects/FlatCAMGerber.py

@@ -12,7 +12,7 @@
 
 
 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 appObjects.FlatCAMObj import *
@@ -150,6 +150,10 @@ class GerberObject(FlatCAMObj, Gerber):
         # Editor
         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
         self.ui.iso_button.clicked.connect(self.app.isolation_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
 
+    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):
         self.app.inform.emit('[WARNING_NOTCL] %s...' % _("Buffering solid geometry"))
 
@@ -369,7 +386,7 @@ class GerberObject(FlatCAMObj, Gerber):
                 try:
                     self.solid_geometry = MultiPolygon(self.solid_geometry)
                 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"]))
             if not self.options["noncopperrounded"]:
@@ -395,7 +412,7 @@ class GerberObject(FlatCAMObj, Gerber):
                 try:
                     self.solid_geometry = MultiPolygon(self.solid_geometry)
                 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 = 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.VisPyVisuals import ShapeCollection
 
+from shapely.ops import unary_union
+from shapely.geometry import Polygon, MultiPolygon
+
+from copy import deepcopy
 import sys
+import math
 
 import gettext
 import appTranslation as fcTranslate
@@ -55,6 +60,9 @@ class FlatCAMObj(QtCore.QObject):
     # signal to plot a single object
     plot_single_object = QtCore.pyqtSignal()
 
+    # signal for Properties
+    calculations_finished = QtCore.pyqtSignal(float, float, float, float, float, object)
+
     def __init__(self, name):
         """
         Constructor.
@@ -112,6 +120,9 @@ class FlatCAMObj(QtCore.QObject):
         # self.units = 'IN'
         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)
 
     def __del__(self):
@@ -456,6 +467,444 @@ class FlatCAMObj(QtCore.QObject):
                 self.app.defaults[filter_string] = ';;'.join(filter_list)
                 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
     def poly2rings(poly):
         return [poly.exterior] + [interior for interior in poly.interiors]
@@ -471,8 +920,8 @@ class FlatCAMObj(QtCore.QObject):
         current_visibility = self.shapes.visible
         # 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:
                     self.shapes.visible = False
             else:
@@ -517,4 +966,4 @@ class FlatCAMObj(QtCore.QObject):
         del self.options
 
         # 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:
                 log.debug("Nothing to remove. %s" % str(e))
 
-            self.app.setup_component_editor()
+            self.app.setup_default_properties_tab()
             return
 
         if obj:

+ 2 - 2
appParsers/ParseGerber.py

@@ -374,7 +374,7 @@ class Gerber(Geometry):
         geo_f = None
 
         # 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
         # applying a union for every new polygon.
         poly_buffer = []
@@ -1680,7 +1680,7 @@ class Gerber(Geometry):
         #
         # 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] +
         #                                      self.flash_geometry)
 

+ 18 - 9
appParsers/ParseSVG.py

@@ -228,8 +228,10 @@ def svgrect2shapely(rect, n_points=32, factor=1.0):
     else:
         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
         pts = [
@@ -290,9 +292,12 @@ def svgcircle2shapely(circle, n_points=64, factor=1.0):
     # cx = float(circle.get('cx'))
     # cy = float(circle.get('cy'))
     # 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)
 
@@ -309,11 +314,15 @@ def svgellipse2shapely(ellipse, n_points=64, factor=1.0):
     :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
     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
 
 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 box as box
 import shapely.affinity as affinity
@@ -428,7 +428,7 @@ class ToolCopperThieving(AppTool):
             if len(self.sel_rect) == 0:
                 return
 
-            self.sel_rect = cascaded_union(self.sel_rect)
+            self.sel_rect = unary_union(self.sel_rect)
 
             if not isinstance(self.sel_rect, Iterable):
                 self.sel_rect = [self.sel_rect]
@@ -606,9 +606,9 @@ class ToolCopperThieving(AppTool):
                             env_obj = geo_n.convex_hull
                         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):
-                            env_obj = cascaded_union(geo_n)
+                            env_obj = unary_union(geo_n)
                         else:
-                            env_obj = cascaded_union(geo_n)
+                            env_obj = unary_union(geo_n)
                             env_obj = env_obj.convex_hull
                         bounding_box = env_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
                     else:
@@ -660,10 +660,10 @@ class ToolCopperThieving(AppTool):
                             raise grace
                         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':
-                    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)
                 else:
                     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
 
 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
 
 from matplotlib.backend_bases import KeyEvent as mpl_key_event
@@ -217,6 +217,7 @@ class CutOut(AppTool):
             "toolchangez": float(self.app.defaults["geometry_toolchangez"]),
             "startz": self.app.defaults["geometry_startz"],
             "endz": float(self.app.defaults["geometry_endz"]),
+            "endxy": self.app.defaults["geometry_endxy"],
             "area_exclusion": self.app.defaults["geometry_area_exclusion"],
             "area_shape": self.app.defaults["geometry_area_shape"],
             "area_strategy": self.app.defaults["geometry_area_strategy"],
@@ -414,6 +415,7 @@ class CutOut(AppTool):
             "toolchangez": float(self.app.defaults["geometry_toolchangez"]),
             "startz": self.app.defaults["geometry_startz"],
             "endz": float(self.app.defaults["geometry_endz"]),
+            "endxy": self.app.defaults["geometry_endxy"],
             "area_exclusion": self.app.defaults["geometry_area_exclusion"],
             "area_shape": self.app.defaults["geometry_area_shape"],
             "area_strategy": self.app.defaults["geometry_area_strategy"],
@@ -1832,7 +1834,7 @@ class CutOut(AppTool):
         log.debug("%d paths" % len(flat_geometry))
 
         polygon = Polygon(points)
-        toolgeo = cascaded_union(polygon)
+        toolgeo = unary_union(polygon)
         diffs = []
         for target in flat_geometry:
             if type(target) == LineString or type(target) == LinearRing:
@@ -1906,7 +1908,7 @@ class CutOut(AppTool):
 
         :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
-        :return:                a cascaded union of the resulting geometry
+        :return:                a unary_union of the resulting geometry
         """
 
         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.setToolTip(
             _("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)
 

+ 3 - 3
appTools/ToolDistanceMin.py

@@ -11,7 +11,7 @@ from appGUI.GUIElements import FCEntry
 
 from shapely.ops import nearest_points
 from shapely.geometry import Point, MultiPolygon
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 
 import math
 import logging
@@ -113,12 +113,12 @@ class DistanceMin(AppTool):
                     try:
                         selected_objs[0].solid_geometry = MultiPolygon(selected_objs[0].solid_geometry)
                     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:
                         selected_objs[1].solid_geometry = MultiPolygon(selected_objs[1].solid_geometry)
                     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)
 

+ 218 - 46
appTools/ToolFilm.py

@@ -9,11 +9,13 @@ from PyQt5 import QtCore, QtWidgets, QtGui
 
 from appTool import AppTool
 from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, \
-    OptionalHideInputSection, FCComboBox, FCFileSaveDialog, FCButton, FCLabel
+    OptionalHideInputSection, FCComboBox, FCFileSaveDialog, FCButton, FCLabel, FCSpinner
 
 from copy import deepcopy
 import logging
 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.pdfgen import canvas
@@ -138,6 +140,8 @@ class Film(AppTool):
         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.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_box_combo.set_value('grb')
         # 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'):
         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_y = None
         mirror = None
@@ -328,8 +332,8 @@ class Film(AppTool):
     def generate_negative_film(self, name, boxname, factor, ftype='svg'):
         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_y = None
         mirror = None
@@ -351,7 +355,7 @@ class Film(AppTool):
             if self.ui.film_mirror_axis.get_value() != 'none':
                 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:
             border = 0
@@ -390,7 +394,7 @@ class Film(AppTool):
 
     def export_negative(self, obj_name, box_name, filename, boundary,
                         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',
                         mirror=None,
                         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))
             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,
                                           scale_factor_x=scale_factor_x, scale_factor_y=scale_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()
 
             # Convert everything to strings for use in the xml doc
@@ -514,6 +587,11 @@ class Film(AppTool):
                     doc_final = StringIO(doc_final)
                     drawing = svg2rlg(doc_final)
                     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:
                     log.debug("FilmTool.export_negative() --> PNG output --> %s" % str(e))
                     return 'fail'
@@ -528,8 +606,7 @@ class Film(AppTool):
                     drawing = svg2rlg(doc_final)
 
                     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:
                         if self.ui.orientation_radio.get_value() == 'p':
                             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))
 
         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:
-            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,
                         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',
                         mirror=None,  orientation_val='p', pagesize_val='A4', color_val='black', opacity_val=1.0,
                         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))
             box = obj
 
+        scale_factor_x = scale_factor_x
+        scale_factor_y = scale_factor_y
+
         p_size = pagesize_val
         orientation = orientation_val
         color = color_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()")
 
+            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,
                                           scale_factor_x=scale_factor_x, scale_factor_y=scale_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
@@ -641,10 +787,6 @@ class Film(AppTool):
 
             exported_svg = ET.tostring(root)
 
-            # Determine bounding area for svg export
-            bounds = box.bounds()
-            size = box.size()
-
             # This contain the measure units
             uom = obj.units.lower()
 
@@ -693,6 +835,11 @@ class Film(AppTool):
                     doc_final = StringIO(doc_final)
                     drawing = svg2rlg(doc_final)
                     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:
                     log.debug("FilmTool.export_positive() --> PNG output --> %s" % str(e))
                     return 'fail'
@@ -710,9 +857,9 @@ class Film(AppTool):
                         renderPDF.drawToFile(drawing, filename)
                     else:
                         if orientation == 'p':
-                            page_size = portrait(self.pagesize[p_size])
+                            page_size = portrait(self.ui.pagesize[p_size])
                         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.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))
 
         if use_thread is True:
-            proc = self.app.proc_container.new(_("Generating Film ... Please wait."))
-
             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': []})
         else:
             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):
         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')
 
+        # 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
         self.film_object_button = FCButton(_("Save Film"))
         self.film_object_button.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
@@ -1214,7 +1375,7 @@ class FilmUI:
                                    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()
 
@@ -1254,11 +1415,22 @@ class FilmUI:
             self.orientation_radio.show()
             self.pagesize_label.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:
             self.orientation_label.hide()
             self.orientation_radio.hide()
             self.pagesize_label.hide()
             self.pagesize_combo.hide()
+            self.png_dpi_label.hide()
+            self.png_dpi_spinner.hide()
 
     def on_punch_source(self, val):
         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 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 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.tooldia = None
+        # store here the tool diameter that is guaranteed to isolate the object
+        self.safe_tooldia = None
 
         # multiprocessing
         self.pool = self.app.pool
@@ -225,6 +227,9 @@ class ToolIsolation(AppTool, Gerber):
     def set_tool_ui(self):
         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:
             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.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.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.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"])
@@ -888,6 +894,9 @@ class ToolIsolation(AppTool, Gerber):
             })
 
     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()
 
         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)))
             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:
                                 # graceful abort requested by the user
                                 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):
         self.blockSignals(True)
@@ -1333,14 +1368,17 @@ class ToolIsolation(AppTool, Gerber):
         # Get source object.
         try:
             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)))
-            return "Could not retrieve object: %s with error: %s" % (self.obj_name, str(e))
+            return
 
         if self.grb_obj is None:
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(self.obj_name)))
             return
 
+        if self.ui.valid_cb.get_value() is True:
+            self.find_safe_tooldia_worker(is_displayed=False)
+
         def worker_task(iso_obj):
             with self.app.proc_container.new(_("Isolating...")):
                 self.isolate_handler(iso_obj)
@@ -1436,8 +1474,8 @@ class ToolIsolation(AppTool, Gerber):
 
         elif selection == _("Reference Object"):
             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)
 
     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())
             sorted_tools.append(tid)
         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'
 
         # 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)))
 
         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'
 
         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())
             sorted_tools.append(tid)
         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'
 
         for tool in sorted_tools:
@@ -2010,11 +2048,11 @@ class ToolIsolation(AppTool, Gerber):
         target_geo = geo
 
         if subtraction_geo:
-            sub_union = cascaded_union(subtraction_geo)
+            sub_union = unary_union(subtraction_geo)
         else:
             name = self.ui.exc_obj_combo.currentText()
             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:
             for geo_elem in target_geo:
@@ -2068,7 +2106,7 @@ class ToolIsolation(AppTool, Gerber):
         new_geometry = []
         target_geo = geo
 
-        intersect_union = cascaded_union(intersection_geo)
+        intersect_union = unary_union(intersection_geo)
 
         try:
             for geo_elem in target_geo:
@@ -2245,7 +2283,7 @@ class ToolIsolation(AppTool, Gerber):
         except TypeError:
             if self.solid_geometry not in self.poly_dict.values():
                 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=self.solid_geometry,
                                                             color=self.app.defaults['global_sel_draw_color'] + 'AF',
@@ -2389,7 +2427,7 @@ class ToolIsolation(AppTool, Gerber):
             if len(self.sel_rect) == 0:
                 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.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.setToolTip(
             _("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)
 
@@ -3277,13 +3316,23 @@ class IsoUI:
 
         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
         self.except_cb = FCCheckBox(label=_('Except'))
         self.except_cb.setToolTip(_("When the isolation geometry is generated,\n"
                                     "by checking this, the area of the object below\n"
                                     "will be subtracted from the isolation geometry."))
         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
         self.type_excobj_radio = RadioSet([{'label': _("Geometry"), 'value': 'geometry'},
@@ -3295,7 +3344,7 @@ class IsoUI:
               "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
         self.exc_obj_combo = FCComboBox()
@@ -3307,7 +3356,7 @@ class IsoUI:
         self.exc_obj_combo.is_last = True
         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,
                                           [
@@ -3330,8 +3379,8 @@ class IsoUI:
         )
         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.setToolTip(
@@ -3341,8 +3390,8 @@ class IsoUI:
         self.reference_combo_type = FCComboBox()
         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.setToolTip(
@@ -3353,8 +3402,8 @@ class IsoUI:
         self.reference_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         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_label.hide()
@@ -3368,7 +3417,7 @@ class IsoUI:
               "(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()
 
@@ -3381,8 +3430,8 @@ class IsoUI:
         self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'},
                                           {'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_radio.hide()
@@ -3390,7 +3439,7 @@ class IsoUI:
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         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.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 math
 
-# from shapely.ops import cascaded_union
+# from shapely.ops import unary_union
 from shapely.geometry import Point, LineString
 
 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
 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 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_strategy":            self.app.defaults["geometry_area_strategy"],
             "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_nccmargin":          self.app.defaults["tools_nccmargin"],
@@ -1292,7 +1293,7 @@ class NonCopperClear(AppTool, Gerber):
                         else:
                             self.ncc_dia_list.append(self.tooldia)
         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
 
         self.o_name = '%s_ncc' % self.obj_name
@@ -1460,7 +1461,7 @@ class NonCopperClear(AppTool, Gerber):
             if len(self.sel_rect) == 0:
                 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,
                               isotooldia=self.iso_dia_list, outname=self.o_name)
@@ -1622,16 +1623,16 @@ class NonCopperClear(AppTool, Gerber):
                     env_obj = geo_n.convex_hull
                 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):
-                    env_obj = cascaded_union(geo_n)
+                    env_obj = unary_union(geo_n)
                 else:
-                    env_obj = cascaded_union(geo_n)
+                    env_obj = unary_union(geo_n)
                     env_obj = env_obj.convex_hull
             except Exception as e:
                 log.debug("NonCopperClear.calculate_bounding_box() 'itself'  --> %s" % str(e))
                 self.app.inform.emit('[ERROR_NOTCL] %s' % _("No object available."))
                 return None
         elif ncc_select == _("Area Selection"):
-            env_obj = cascaded_union(self.sel_rect)
+            env_obj = unary_union(self.sel_rect)
             try:
                 __ = iter(env_obj)
             except Exception:
@@ -1649,8 +1650,8 @@ class NonCopperClear(AppTool, Gerber):
                     env_obj = [box_geo]
 
             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)
             else:
                 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
                     raise grace
                 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"):
             if box_kind == 'geometry':
                 geo_buff_list = []
@@ -1702,7 +1703,7 @@ class NonCopperClear(AppTool, Gerber):
                         raise grace
                     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':
                 new_bounding_box = bbox.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre)
             else:
@@ -1873,7 +1874,7 @@ class NonCopperClear(AppTool, Gerber):
                         geo_obj.tools[current_uid] = dict(tools_storage[current_uid])
                         break
 
-            sol_geo = cascaded_union(isolated_geo)
+            sol_geo = unary_union(isolated_geo)
             if has_offset is True:
                 self.app.inform.emit('[WARNING_NOTCL] %s ...' % _("Buffering"))
                 sol_geo = sol_geo.buffer(distance=ncc_offset)
@@ -1886,7 +1887,7 @@ class NonCopperClear(AppTool, Gerber):
                 return 'fail'
 
         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:
                 self.app.inform.emit('[WARNING_NOTCL] %s ...' % _("Buffering"))
                 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.")
 
                 # 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)
 
                 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]
                 # except TypeError:
                 #     new_area = [area.buffer(tool * ncc_overlap)]
-                # area = cascaded_union(area)
+                # area = unary_union(area)
 
             geo_obj.multigeo = True
             geo_obj.options["cnctooldia"] = '0.0'
@@ -2615,9 +2616,9 @@ class NonCopperClear(AppTool, Gerber):
                     env_obj = geo_n.convex_hull
                 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):
-                    env_obj = cascaded_union(geo_n)
+                    env_obj = unary_union(geo_n)
                 else:
-                    env_obj = cascaded_union(geo_n)
+                    env_obj = unary_union(geo_n)
                     env_obj = env_obj.convex_hull
 
                 bounding_box = env_obj.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre)
@@ -2627,7 +2628,7 @@ class NonCopperClear(AppTool, Gerber):
                 return 'fail'
 
         elif ncc_select == 'area':
-            geo_n = cascaded_union(self.sel_rect)
+            geo_n = unary_union(self.sel_rect)
             try:
                 __ = iter(geo_n)
             except Exception as e:
@@ -2641,7 +2642,7 @@ class NonCopperClear(AppTool, Gerber):
                     raise grace
                 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"):
             geo_n = ncc_sel_obj.solid_geometry
@@ -2659,10 +2660,10 @@ class NonCopperClear(AppTool, Gerber):
                         raise grace
                     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':
-                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)
             else:
                 self.app.inform.emit('[ERROR_NOTCL] %s' % _("The reference object type is not supported."))
@@ -2837,7 +2838,7 @@ class NonCopperClear(AppTool, Gerber):
                                 break
                         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:
                     app_obj.inform.emit('[WARNING_NOTCL] %s ...' % _("Buffering"))
                     sol_geo = sol_geo.buffer(distance=ncc_offset)
@@ -2852,7 +2853,7 @@ class NonCopperClear(AppTool, Gerber):
                     return 'fail'
 
             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:
                     app_obj.inform.emit('[WARNING_NOTCL] %s ...' % _("Buffering"))
                     sol_geo = sol_geo.buffer(distance=ncc_offset)
@@ -3219,7 +3220,7 @@ class NonCopperClear(AppTool, Gerber):
                                 break
                         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:
                     app_obj.inform.emit('[WARNING_NOTCL] %s ...' % _("Buffering"))
                     sol_geo = sol_geo.buffer(distance=ncc_offset)
@@ -3234,7 +3235,7 @@ class NonCopperClear(AppTool, Gerber):
                     return 'fail'
 
             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:
                     app_obj.inform.emit('[WARNING_NOTCL] %s ...' % _("Buffering"))
                     sol_geo = sol_geo.buffer(distance=ncc_offset)
@@ -3857,26 +3858,31 @@ class NccUI:
         # ### Tool Diameter ####
         self.new_tooldia_lbl = FCLabel('%s:' % _('Tool Dia'))
         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.set_precision(self.decimals)
         self.new_tooldia_entry.set_range(0.000, 9999.9999)
         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
-        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.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
         self.find_optimal_button.setToolTip(
             _("Find a tool diameter that is guaranteed\n"
               "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()
 
@@ -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.setToolTip(
             _("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)
 

+ 16 - 14
appTools/ToolPaint.py

@@ -17,7 +17,7 @@ from appGUI.GUIElements import FCTable, FCDoubleSpinner, FCCheckBox, FCInputDial
     FCLabel
 
 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
 
@@ -466,6 +466,7 @@ class ToolPaint(AppTool, Gerber):
             "area_shape":           self.app.defaults["geometry_area_shape"],
             "area_strategy":        self.app.defaults["geometry_area_strategy"],
             "area_overz":           float(self.app.defaults["geometry_area_overz"]),
+            "optimization_type":    self.app.defaults["geometry_optimization_type"],
 
             "tooldia":              self.app.defaults["tools_painttooldia"],
             "tools_paintoffset":   self.app.defaults["tools_paintoffset"],
@@ -1027,7 +1028,7 @@ class ToolPaint(AppTool, Gerber):
                         continue
                 self.tooldia_list.append(self.tooldia)
         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
 
         self.select_method = self.ui.selectmethod_combo.get_value()
@@ -1293,7 +1294,7 @@ class ToolPaint(AppTool, Gerber):
             if len(self.sel_rect) == 0:
                 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,
                                  outname=self.o_name)
 
@@ -1740,7 +1741,7 @@ class ToolPaint(AppTool, Gerber):
                         continue
                 sorted_tools.append(self.tooldia)
             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'
 
         # Initializes the new geometry object
@@ -1880,7 +1881,7 @@ class ToolPaint(AppTool, Gerber):
             geo_obj.tools.clear()
             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:
                 if isinstance(geo_obj.solid_geometry, list):
@@ -1934,7 +1935,7 @@ class ToolPaint(AppTool, Gerber):
                     except TypeError:
                         poly_buf.append(buffered_pol)
 
-            poly_buf = cascaded_union(poly_buf)
+            poly_buf = unary_union(poly_buf)
 
             if not poly_buf:
                 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)
                             geo_elems = list(geo_res.get_objects())
                             # 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)
                             if rest and not rest.is_empty:
                                 try:
@@ -2040,7 +2041,7 @@ class ToolPaint(AppTool, Gerber):
                         geo_elems = list(geo_res.get_objects())
 
                         # 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)
                         if rest and not rest.is_empty:
                             try:
@@ -2094,7 +2095,7 @@ class ToolPaint(AppTool, Gerber):
 
                 poly_buf = MultiPolygon(tmp)
                 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:
                     log.debug("Rest geometry empty. Breaking.")
@@ -2134,7 +2135,7 @@ class ToolPaint(AppTool, Gerber):
                           "Change the painting parameters and try again.")
                     )
                     return "fail"
-                geo_obj.solid_geometry = cascaded_union(final_solid_geometry)
+                geo_obj.solid_geometry = unary_union(final_solid_geometry)
             else:
                 return 'fail'
             try:
@@ -2446,9 +2447,9 @@ class ToolPaint(AppTool, Gerber):
                 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 = cascaded_union(self.bound_obj.solid_geometry)
+                env_obj = unary_union(self.bound_obj.solid_geometry)
             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
             sel_rect = env_obj.buffer(distance=0.0000001, join_style=base.JOIN_STYLE.mitre)
         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.setToolTip(
             _("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)
 

+ 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,
                                                                      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('')
 
                 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 shapely.geometry import MultiPolygon, Polygon
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 
 from copy import deepcopy
 import math
@@ -237,7 +237,7 @@ class Properties(AppTool):
                 if obj_prop.kind.lower() == 'cncjob':
                     try:
                         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)
                             ymin.append(y0)
                             xmax.append(x1)
@@ -247,7 +247,7 @@ class Properties(AppTool):
 
                     try:
                         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)
                             ymin.append(y0)
                             xmax.append(x1)
@@ -257,7 +257,7 @@ class Properties(AppTool):
                 else:
                     try:
                         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)
                             ymin.append(y0)
                             xmax.append(x1)
@@ -308,10 +308,10 @@ class Properties(AppTool):
                             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 = cascaded_union(geo)
+                            env_obj = unary_union(geo)
                             env_obj = env_obj.convex_hull
                         else:
-                            env_obj = cascaded_union(geo)
+                            env_obj = unary_union(geo)
                             env_obj = env_obj.convex_hull
 
                         area_chull = env_obj.area
@@ -321,7 +321,7 @@ class Properties(AppTool):
                     try:
                         area_chull = []
                         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 = max(area_chull)
                     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
         res = self.app.defaults['geometry_circle_steps']
         factor = svgparse_viewbox(svg_root)
-
         geos = getsvggeo(svg_root, object_type, units=units, res=res, factor=factor)
 
         if flip:

+ 2 - 2
appTools/ToolSolderPaste.py

@@ -19,7 +19,7 @@ from copy import deepcopy
 from datetime import datetime
 
 from shapely.geometry import Polygon, LineString
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 
 import traceback
 from io import StringIO
@@ -941,7 +941,7 @@ class SolderPaste(AppTool):
                 tool_cnc_dict['gcode_parsed'] = job_obj.gcode_parse()
 
                 # 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
                 # 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 shapely.geometry import Polygon, MultiPolygon, MultiLineString, LineString
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 
 import traceback
 from copy import deepcopy
@@ -396,7 +396,7 @@ class ToolSub(AppTool):
         else:
             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
         self.periodic_check(500, reset=True)
@@ -421,7 +421,7 @@ class ToolSub(AppTool):
         with self.app.proc_container.new(text):
             # resulting paths are closed resulting into Polygons
             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 not new_geo.is_empty:
                         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")
         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':
             self.decimals = int(self.defaults['decimals_metric'])
@@ -1092,7 +1092,7 @@ class App(QtCore.QObject):
             self.ui.splitter.setSizes([0, 1])
 
         # Sets up FlatCAMObj, FCProcess and FCProcessContainer.
-        self.setup_component_editor()
+        self.setup_default_properties_tab()
 
         # ###########################################################################################################
         # ####################################### Auto-complete KEYWORDS ############################################
@@ -2576,7 +2576,7 @@ class App(QtCore.QObject):
             return
 
         # 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.inform.emit('[success] %s: %s' % (_("Imported Defaults from"), filename))
@@ -4655,7 +4655,7 @@ class App(QtCore.QObject):
         self.collection.delete_active()
 
         # Clear form
-        self.setup_component_editor()
+        self.setup_default_properties_tab()
 
         self.inform.emit('%s: %s' % (_("Object deleted"), name))
 
@@ -6965,13 +6965,13 @@ class App(QtCore.QObject):
         self.collection.delete_all()
 
         # 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
         self.project_filename = None
 
         # 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
         self.on_options_app2project()
@@ -9677,9 +9677,9 @@ class App(QtCore.QObject):
 
         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:
         """
@@ -9698,58 +9698,7 @@ class App(QtCore.QObject):
 
         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.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 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
 from shapely.wkt import loads as sloads
 from shapely.wkt import dumps as sdumps
@@ -225,13 +225,14 @@ class ApertureMacro:
         Pads the ``mods`` list with zeros resulting in an
         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
         na = len(mods)
         x[0:na] = mods
@@ -244,9 +245,12 @@ class ApertureMacro:
         :param mods: (Exposure 0/1, Diameter >=0, X-coord, Y-coord)
         :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)}
 
     @staticmethod
@@ -257,7 +261,15 @@ class ApertureMacro:
             rotation angle around origin in degrees)
         :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)])
         box = line.buffer(width / 2, cap_style=2)
@@ -274,7 +286,14 @@ class ApertureMacro:
         :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_rotated = affinity.rotate(box, angle, origin=(0, 0))
@@ -290,7 +309,14 @@ class ApertureMacro:
         :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_rotated = affinity.rotate(box, angle, origin=(0, 0))
@@ -330,7 +356,15 @@ class ApertureMacro:
         :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
 
         for i in range(nverts):
@@ -354,7 +388,17 @@ class ApertureMacro:
         :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
         result = Point((x, y)).buffer(r).exterior.buffer(thickness / 2.0)
@@ -369,13 +413,13 @@ class ApertureMacro:
             if r <= 0:
                 break
             ring = Point((x, y)).buffer(r).exterior.buffer(thickness / 2.0)
-            result = cascaded_union([result, ring])
+            result = unary_union([result, ring])
             i += 1
 
         # ## Crosshair
         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)
-        result = cascaded_union([result, hor, ver])
+        result = unary_union([result, hor, ver])
 
         return {"pol": 1, "geometry": result}
 
@@ -390,7 +434,14 @@ class ApertureMacro:
         :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))
         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)
         else:
             polygon = points
-        toolgeo = cascaded_union(polygon)
+        toolgeo = unary_union(polygon)
         diffs = []
         for target in flat_geometry:
             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:
         #             log.debug('solid_geometry is empty []')
         #             return 0, 0, 0, 0
-        #         return cascaded_union(flatten(self.solid_geometry)).bounds
+        #         return unary_union(flatten(self.solid_geometry)).bounds
         #     else:
         #         return self.solid_geometry.bounds
         # except Exception as e:
@@ -802,7 +853,7 @@ class Geometry(object):
         #     if len(self.solid_geometry) == 0:
         #         log.debug('solid_geometry is empty []')
         #         return 0, 0, 0, 0
-        #     return cascaded_union(self.solid_geometry).bounds
+        #     return unary_union(self.solid_geometry).bounds
         # else:
         #     return self.solid_geometry.bounds
 
@@ -1325,7 +1376,7 @@ class Geometry(object):
             self.solid_geometry = []
 
         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:
                 self.solid_geometry += geos
             else:
@@ -1335,7 +1386,7 @@ class Geometry(object):
 
         # 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 = cascaded_union(self.solid_geometry)
+        self.solid_geometry = unary_union(self.solid_geometry)
 
         # self.solid_geometry = MultiPolygon(self.solid_geometry)
         # self.solid_geometry = self.solid_geometry.buffer(0.00000001)
@@ -2211,17 +2262,17 @@ class Geometry(object):
 
     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.
 
         :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,
                    scale_factor_x=None, scale_factor_y=None,
                    skew_factor_x=None, skew_factor_y=None,
-                   skew_reference='center',
+                   skew_reference='center', scale_reference='center',
                    mirror=None):
         """
         Exports the Geometry Object as a SVG Element
@@ -2235,11 +2286,11 @@ class Geometry(object):
             if self.multigeo:
                 for tool in self.tools:
                     flat_geo += self.flatten(self.tools[tool]['solid_geometry'])
-                geom_svg = cascaded_union(flat_geo)
+                geom_svg = unary_union(flat_geo)
             else:
-                geom_svg = cascaded_union(self.flatten())
+                geom_svg = unary_union(self.flatten())
         else:
-            geom_svg = cascaded_union(self.flatten())
+            geom_svg = unary_union(self.flatten())
 
         skew_ref = 'center'
         if skew_reference != 'center':
@@ -2255,14 +2306,20 @@ class Geometry(object):
 
         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)
-        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)
+        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 == 'x':
                 geom = affinity.scale(geom_svg, 1.0, -1.0)
@@ -4818,7 +4875,7 @@ class CNCjob(Geometry):
         :return:                    GCode - string
         """
 
-        log.debug("Generate_from_multitool_geometry()")
+        log.debug("generate_from_multitool_geometry()")
 
         temp_solid_geometry = []
         if offset != 0.0:
@@ -5128,7 +5185,7 @@ class CNCjob(Geometry):
         :rtype:             str
         """
 
-        log.debug("Generate_from_multitool_geometry()")
+        log.debug("geometry_tool_gcode_gen()")
 
         t_gcode = ''
         temp_solid_geometry = []
@@ -5244,7 +5301,11 @@ class CNCjob(Geometry):
         self.feedrate_rapid = float(tool_dict['feedrate_rapid'])
 
         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.dwelltime = float(tool_dict['dwelltime'])
 
@@ -5253,8 +5314,9 @@ class CNCjob(Geometry):
             self.startz = None
 
         self.z_end = float(tool_dict['endz'])
+        self.xy_end = tool_dict['endxy']
         try:
-            if self.xy_end == '':
+            if self.xy_end == '' or self.xy_end is None:
                 self.xy_end = None
             else:
                 # 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?
         # self.app.inform.emit('%s: %s' % (_("Unifying Geometry from parsed Geometry segments"),
         #                                  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
         self.solid_geometry = [geo['geom'] for geo in self.gcode_parsed]
@@ -7413,18 +7475,18 @@ class CNCjob(Geometry):
                 travels.append(g)
 
         # 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
         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:
             # graceful abort requested by the user
             raise grace
 
         if cuts:
-            cutsgeom = cascaded_union([geo['geom'] for geo in cuts])
+            cutsgeom = unary_union([geo['geom'] for geo in cuts])
 
         # Render the SVG Xml
         # 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.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.app.proc_container.new_text = ''
 
@@ -7800,7 +7862,7 @@ class CNCjob(Geometry):
                         self.old_disp_number = disp_number
 
                 # 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 = ''
 
@@ -8172,7 +8234,7 @@ def dict2obj(d):
 #
 #     m = MultiLineString(edge_points)
 #     triangles = list(polygonize(m))
-#     return cascaded_union(triangles), edge_points
+#     return unary_union(triangles), edge_points
 
 # def voronoi(P):
 #     """

+ 16 - 4
defaults.py

@@ -339,6 +339,11 @@ class FlatCAMDefaults:
         "geometry_area_shape": "polygon",
         "geometry_area_strategy": "over",
         "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_sel_limit": 30,
@@ -410,6 +415,7 @@ class FlatCAMDefaults:
 
         "tools_iso_rest":           False,
         "tools_iso_combine_passes": True,
+        "tools_iso_check_valid":    False,
         "tools_iso_isoexcept":      False,
         "tools_iso_selection":      _("All"),
         "tools_iso_poly_ints":      False,
@@ -533,6 +539,7 @@ class FlatCAMDefaults:
         "tools_film_file_type_radio": 'svg',
         "tools_film_orientation": 'p',
         "tools_film_pagesize": 'A4',
+        "tools_film_png_dpi": 96,
 
         # Panel Tool
         "tools_panelize_spacing_columns": 0.0,
@@ -833,8 +840,13 @@ class FlatCAMDefaults:
         with open(filename, "w") as file:
             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
         try:
@@ -843,7 +855,7 @@ class FlatCAMDefaults:
             f.close()
         except IOError:
             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
             self.defaults["global_toolbar_view"] = 511
             return
@@ -856,7 +868,7 @@ class FlatCAMDefaults:
             self.defaults["global_toolbar_view"] = 511
             e = sys.exc_info()[0]
             log.error(str(e))
-            self.inform.emit('[ERROR] %s' % _("Failed to parse defaults file."))
+            inform.emit('[ERROR] %s' % _("Failed to parse defaults file."))
             return
         if defaults is None:
             return

BIN
locale/de/LC_MESSAGES/strings.mo


File diff suppressed because it is too large
+ 187 - 182
locale/de/LC_MESSAGES/strings.po


BIN
locale/en/LC_MESSAGES/strings.mo


File diff suppressed because it is too large
+ 209 - 231
locale/en/LC_MESSAGES/strings.po


BIN
locale/es/LC_MESSAGES/strings.mo


File diff suppressed because it is too large
+ 194 - 189
locale/es/LC_MESSAGES/strings.po


BIN
locale/fr/LC_MESSAGES/strings.mo


File diff suppressed because it is too large
+ 194 - 189
locale/fr/LC_MESSAGES/strings.po


BIN
locale/hu/LC_MESSAGES/strings.mo


File diff suppressed because it is too large
+ 194 - 189
locale/hu/LC_MESSAGES/strings.po


BIN
locale/it/LC_MESSAGES/strings.mo


File diff suppressed because it is too large
+ 195 - 190
locale/it/LC_MESSAGES/strings.po


BIN
locale/pt_BR/LC_MESSAGES/strings.mo


File diff suppressed because it is too large
+ 194 - 189
locale/pt_BR/LC_MESSAGES/strings.po


BIN
locale/ro/LC_MESSAGES/strings.mo


File diff suppressed because it is too large
+ 187 - 182
locale/ro/LC_MESSAGES/strings.po


BIN
locale/ru/LC_MESSAGES/strings.mo


File diff suppressed because it is too large
+ 193 - 188
locale/ru/LC_MESSAGES/strings.po


BIN
locale/tr/LC_MESSAGES/strings.mo


File diff suppressed because it is too large
+ 197 - 213
locale/tr/LC_MESSAGES/strings.po


File diff suppressed because it is too large
+ 202 - 200
locale_template/strings.pot


+ 2 - 2
tclCommands/TclCommandBbox.py

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

+ 2 - 2
tclCommands/TclCommandCutout.py

@@ -3,7 +3,7 @@ from tclCommands.TclCommand import TclCommand
 import collections
 import logging
 
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 from shapely.geometry import LineString
 
 log = logging.getLogger('base')
@@ -134,7 +134,7 @@ class TclCommandCutout(TclCommand):
                            [pts[6], pts[7], pts[8]],
                            [pts[9], pts[10], pts[11]]]}
             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:
             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([
         ('name', 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
     option_types = collections.OrderedDict([
-        ('scale_factor', float)
+        ('scale_stroke_factor', float)
     ])
 
     # 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.'),
             ('filename', 'Absolute path to file to export.\n'
                          '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']
     }

+ 3 - 3
tclCommands/TclCommandGeoCutout.py

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

+ 2 - 2
tclCommands/TclCommandNregions.py

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

+ 1 - 1
tests/other/test_plotg.py

@@ -1,5 +1,5 @@
 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.axes import *
 from camlib import *

+ 1 - 1
tests/test_paint.py

@@ -1,7 +1,7 @@
 import unittest
 
 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.axes import *
 from camlib import *

+ 1 - 1
tests/test_pathconnect.py

@@ -1,7 +1,7 @@
 import unittest
 
 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 camlib import *
 from random import random

Some files were not shown because too many files changed in this diff