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

- added to Paint and NCC Tool a feature that allow polygon area selection when the reference is selected as Area Selection
- in Paint Tool and NCC Tool added ability to use Escape Tool to cancel Area Selection and for Paint Tool to cancel Polygon Selection

Marius Stanciu 5 лет назад
Родитель
Сommit
22f74edfab
6 измененных файлов с 524 добавлено и 81 удалено
  1. 4 0
      FlatCAMApp.py
  2. 93 1
      FlatCAMTool.py
  3. 2 0
      README.md
  4. 37 13
      flatcamGUI/PreferencesUI.py
  5. 188 33
      flatcamTools/ToolNCC.py
  6. 200 34
      flatcamTools/ToolPaint.py

+ 4 - 0
FlatCAMApp.py

@@ -786,6 +786,7 @@ class App(QtCore.QObject):
             "tools_ncc_offset_choice": False,
             "tools_ncc_offset_choice": False,
             "tools_ncc_offset_value": 0.0000,
             "tools_ncc_offset_value": 0.0000,
             "tools_nccref": _('Itself'),
             "tools_nccref": _('Itself'),
+            "tools_ncc_area_shape": "square",
             "tools_ncc_plotting": 'normal',
             "tools_ncc_plotting": 'normal',
             "tools_nccmilling_type": 'cl',
             "tools_nccmilling_type": 'cl',
             "tools_ncctool_type": 'C1',
             "tools_ncctool_type": 'C1',
@@ -812,6 +813,7 @@ class App(QtCore.QObject):
             "tools_paintmargin": 0.0,
             "tools_paintmargin": 0.0,
             "tools_paintmethod": _("Seed"),
             "tools_paintmethod": _("Seed"),
             "tools_selectmethod": _("All Polygons"),
             "tools_selectmethod": _("All Polygons"),
+            "tools_paint_area_shape": "square",
             "tools_pathconnect": True,
             "tools_pathconnect": True,
             "tools_paintcontour": True,
             "tools_paintcontour": True,
             "tools_paint_plotting": 'normal',
             "tools_paint_plotting": 'normal',
@@ -1468,6 +1470,7 @@ class App(QtCore.QObject):
             "tools_ncc_offset_choice": self.ui.tools_defaults_form.tools_ncc_group.ncc_choice_offset_cb,
             "tools_ncc_offset_choice": self.ui.tools_defaults_form.tools_ncc_group.ncc_choice_offset_cb,
             "tools_ncc_offset_value": self.ui.tools_defaults_form.tools_ncc_group.ncc_offset_spinner,
             "tools_ncc_offset_value": self.ui.tools_defaults_form.tools_ncc_group.ncc_offset_spinner,
             "tools_nccref": self.ui.tools_defaults_form.tools_ncc_group.select_combo,
             "tools_nccref": self.ui.tools_defaults_form.tools_ncc_group.select_combo,
+            "tools_ncc_area_shape": self.ui.tools_defaults_form.tools_ncc_group.area_shape_radio,
             "tools_ncc_plotting": self.ui.tools_defaults_form.tools_ncc_group.ncc_plotting_radio,
             "tools_ncc_plotting": self.ui.tools_defaults_form.tools_ncc_group.ncc_plotting_radio,
             "tools_nccmilling_type": self.ui.tools_defaults_form.tools_ncc_group.milling_type_radio,
             "tools_nccmilling_type": self.ui.tools_defaults_form.tools_ncc_group.milling_type_radio,
             "tools_ncctool_type": self.ui.tools_defaults_form.tools_ncc_group.tool_type_radio,
             "tools_ncctool_type": self.ui.tools_defaults_form.tools_ncc_group.tool_type_radio,
@@ -1494,6 +1497,7 @@ class App(QtCore.QObject):
             "tools_paintmargin": self.ui.tools_defaults_form.tools_paint_group.paintmargin_entry,
             "tools_paintmargin": self.ui.tools_defaults_form.tools_paint_group.paintmargin_entry,
             "tools_paintmethod": self.ui.tools_defaults_form.tools_paint_group.paintmethod_combo,
             "tools_paintmethod": self.ui.tools_defaults_form.tools_paint_group.paintmethod_combo,
             "tools_selectmethod": self.ui.tools_defaults_form.tools_paint_group.selectmethod_combo,
             "tools_selectmethod": self.ui.tools_defaults_form.tools_paint_group.selectmethod_combo,
+            "tools_paint_area_shape": self.ui.tools_defaults_form.tools_paint_group.area_shape_radio,
             "tools_pathconnect": self.ui.tools_defaults_form.tools_paint_group.pathconnect_cb,
             "tools_pathconnect": self.ui.tools_defaults_form.tools_paint_group.pathconnect_cb,
             "tools_paintcontour": self.ui.tools_defaults_form.tools_paint_group.contour_cb,
             "tools_paintcontour": self.ui.tools_defaults_form.tools_paint_group.contour_cb,
             "tools_paint_plotting": self.ui.tools_defaults_form.tools_paint_group.paint_plotting_radio,
             "tools_paint_plotting": self.ui.tools_defaults_form.tools_paint_group.paint_plotting_radio,

+ 93 - 1
FlatCAMTool.py

@@ -9,7 +9,7 @@
 from PyQt5 import QtGui, QtCore, QtWidgets, QtWidgets
 from PyQt5 import QtGui, QtCore, QtWidgets, QtWidgets
 from PyQt5.QtCore import Qt
 from PyQt5.QtCore import Qt
 
 
-from shapely.geometry import Polygon
+from shapely.geometry import Polygon, LineString
 
 
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
@@ -106,6 +106,7 @@ class FlatCAMTool(QtWidgets.QWidget):
 
 
         :param old_coords: old coordinates
         :param old_coords: old coordinates
         :param coords: new coordinates
         :param coords: new coordinates
+        :param kwargs:
         :return:
         :return:
         """
         """
 
 
@@ -143,10 +144,101 @@ class FlatCAMTool(QtWidgets.QWidget):
         if self.app.is_legacy is True:
         if self.app.is_legacy is True:
             self.app.tool_shapes.redraw()
             self.app.tool_shapes.redraw()
 
 
+    def draw_selection_shape_polygon(self, points, **kwargs):
+        """
+
+        :param points: a list of points from which to create a Polygon
+        :param kwargs:
+        :return:
+        """
+        if 'color' in kwargs:
+            color = kwargs['color']
+        else:
+            color = self.app.defaults['global_sel_line']
+
+        if 'face_color' in kwargs:
+            face_color = kwargs['face_color']
+        else:
+            face_color = self.app.defaults['global_sel_fill']
+
+        if 'face_alpha' in kwargs:
+            face_alpha = kwargs['face_alpha']
+        else:
+            face_alpha = 0.3
+        if len(points) < 3:
+            sel_rect = LineString(points)
+        else:
+            sel_rect = Polygon(points)
+
+        # color_t = Color(face_color)
+        # color_t.alpha = face_alpha
+
+        color_t = face_color[:-2] + str(hex(int(face_alpha * 255)))[2:]
+
+        self.app.tool_shapes.add(sel_rect, color=color, face_color=color_t, update=True,
+                                 layer=0, tolerance=None)
+        if self.app.is_legacy is True:
+            self.app.tool_shapes.redraw()
+
     def delete_tool_selection_shape(self):
     def delete_tool_selection_shape(self):
         self.app.tool_shapes.clear()
         self.app.tool_shapes.clear()
         self.app.tool_shapes.redraw()
         self.app.tool_shapes.redraw()
 
 
+    def draw_moving_selection_shape_poly(self, points, data, **kwargs):
+        """
+
+        :param points:
+        :param data:
+        :param kwargs:
+        :return:
+        """
+        if 'color' in kwargs:
+            color = kwargs['color']
+        else:
+            color = self.app.defaults['global_sel_line']
+
+        if 'face_color' in kwargs:
+            face_color = kwargs['face_color']
+        else:
+            face_color = self.app.defaults['global_sel_fill']
+
+        if 'face_alpha' in kwargs:
+            face_alpha = kwargs['face_alpha']
+        else:
+            face_alpha = 0.3
+
+        temp_points = [x for x in points]
+        try:
+            if data != temp_points[-1]:
+                temp_points.append(data)
+        except IndexError:
+            return
+
+        l_points = len(temp_points)
+        if l_points == 2:
+            geo = LineString(temp_points)
+        elif l_points > 2:
+            geo = Polygon(temp_points)
+        else:
+            return
+
+        color_t = face_color[:-2] + str(hex(int(face_alpha * 255)))[2:]
+        color_t_error = "#00000000"
+
+        if geo.is_valid and not geo.is_empty:
+            self.app.move_tool.sel_shapes.add(geo, color=color, face_color=color_t, update=True,
+                                              layer=0, tolerance=None)
+        elif not geo.is_valid:
+            self.app.move_tool.sel_shapes.add(geo, color="red", face_color=color_t_error, update=True,
+                                              layer=0, tolerance=None)
+
+        if self.app.is_legacy is True:
+            self.app.move_tool.sel_shapes.redraw()
+
+    def delete_moving_selection_shape(self):
+        self.app.move_tool.sel_shapes.clear()
+        self.app.move_tool.sel_shapes.redraw()
+
     def confirmation_message(self, accepted, minval, maxval):
     def confirmation_message(self, accepted, minval, maxval):
         if accepted is False:
         if accepted is False:
             self.app.inform.emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' %
             self.app.inform.emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' %

+ 2 - 0
README.md

@@ -12,6 +12,8 @@ CAD program, and create G-Code for Isolation routing.
 20.03.2020
 20.03.2020
 
 
 - updated the "re-cut" feature in Geometry object; now if the re-cut parameter is non zero it will cut half of the entered distance before the isolation end and half of it after the isolation end
 - updated the "re-cut" feature in Geometry object; now if the re-cut parameter is non zero it will cut half of the entered distance before the isolation end and half of it after the isolation end
+- added to Paint and NCC Tool a feature that allow polygon area selection when the reference is selected as Area Selection
+- in Paint Tool and NCC Tool added ability to use Escape Tool to cancel Area Selection and for Paint Tool to cancel Polygon Selection
 
 
 13.03.2020
 13.03.2020
 
 

+ 37 - 13
flatcamGUI/PreferencesUI.py

@@ -2521,7 +2521,7 @@ class GerberEditorPrefGroupUI(OptionsGroupUI):
 
 
         self.adddim_label = QtWidgets.QLabel('%s:' % _('Aperture Dimensions'))
         self.adddim_label = QtWidgets.QLabel('%s:' % _('Aperture Dimensions'))
         self.adddim_label.setToolTip(
         self.adddim_label.setToolTip(
-            _("Diameters of the cutting tools, separated by comma.\n"
+            _("Diameters of the tools, separated by comma.\n"
               "The value of the diameter has to use the dot decimals separator.\n"
               "The value of the diameter has to use the dot decimals separator.\n"
               "Valid values: 0.3, 1.0")
               "Valid values: 0.3, 1.0")
         )
         )
@@ -3970,9 +3970,9 @@ class GeometryGenPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.tools_label, 2, 0, 1, 2)
         grid0.addWidget(self.tools_label, 2, 0, 1, 2)
 
 
         # Tooldia
         # Tooldia
-        tdlabel = QtWidgets.QLabel('%s:' % _('Tool dia'))
+        tdlabel = QtWidgets.QLabel('<b><font color="green">%s:</font></b>' % _('Tools Dia'))
         tdlabel.setToolTip(
         tdlabel.setToolTip(
-            _("Diameters of the cutting tools, separated by comma.\n"
+            _("Diameters of the tools, separated by comma.\n"
               "The value of the diameter has to use the dot decimals separator.\n"
               "The value of the diameter has to use the dot decimals separator.\n"
               "Valid values: 0.3, 1.0")
               "Valid values: 0.3, 1.0")
         )
         )
@@ -5139,7 +5139,7 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI):
 
 
         ncctdlabel = QtWidgets.QLabel('<b><font color="green">%s:</font></b>' % _('Tools Dia'))
         ncctdlabel = QtWidgets.QLabel('<b><font color="green">%s:</font></b>' % _('Tools Dia'))
         ncctdlabel.setToolTip(
         ncctdlabel.setToolTip(
-            _("Diameters of the cutting tools, separated by comma.\n"
+            _("Diameters of the tools, separated by comma.\n"
               "The value of the diameter has to use the dot decimals separator.\n"
               "The value of the diameter has to use the dot decimals separator.\n"
               "Valid values: 0.3, 1.0")
               "Valid values: 0.3, 1.0")
         )
         )
@@ -5418,10 +5418,21 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(select_label, 18, 0)
         grid0.addWidget(select_label, 18, 0)
         grid0.addWidget(self.select_combo, 18, 1)
         grid0.addWidget(self.select_combo, 18, 1)
 
 
+        self.area_shape_label = QtWidgets.QLabel('%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, 19, 0)
+        grid0.addWidget(self.area_shape_radio, 19, 1)
+
         separator_line = QtWidgets.QFrame()
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 19, 0, 1, 2)
+        grid0.addWidget(separator_line, 20, 0, 1, 2)
 
 
         # ## Plotting type
         # ## Plotting type
         self.ncc_plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'},
         self.ncc_plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'},
@@ -5431,8 +5442,8 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI):
             _("- 'Normal' -  normal plotting, done at the end of the NCC job\n"
             _("- 'Normal' -  normal plotting, done at the end of the NCC job\n"
               "- 'Progressive' - after each shape is generated it will be plotted.")
               "- 'Progressive' - after each shape is generated it will be plotted.")
         )
         )
-        grid0.addWidget(plotting_label, 20, 0)
-        grid0.addWidget(self.ncc_plotting_radio, 20, 1)
+        grid0.addWidget(plotting_label, 21, 0)
+        grid0.addWidget(self.ncc_plotting_radio, 21, 1)
 
 
         self.layout.addStretch()
         self.layout.addStretch()
 
 
@@ -5695,7 +5706,7 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
         # Tool dia
         # Tool dia
         ptdlabel = QtWidgets.QLabel('<b><font color="green">%s:</font></b>' % _('Tools Dia'))
         ptdlabel = QtWidgets.QLabel('<b><font color="green">%s:</font></b>' % _('Tools Dia'))
         ptdlabel.setToolTip(
         ptdlabel.setToolTip(
-            _("Diameters of the cutting tools, separated by comma.\n"
+            _("Diameters of the tools, separated by comma.\n"
               "The value of the diameter has to use the dot decimals separator.\n"
               "The value of the diameter has to use the dot decimals separator.\n"
               "Valid values: 0.3, 1.0")
               "Valid values: 0.3, 1.0")
         )
         )
@@ -5931,10 +5942,21 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(selectlabel, 15, 0)
         grid0.addWidget(selectlabel, 15, 0)
         grid0.addWidget(self.selectmethod_combo, 15, 1)
         grid0.addWidget(self.selectmethod_combo, 15, 1)
 
 
+        self.area_shape_label = QtWidgets.QLabel('%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, 18, 0)
+        grid0.addWidget(self.area_shape_radio, 18, 1)
+
         separator_line = QtWidgets.QFrame()
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 16, 0, 1, 2)
+        grid0.addWidget(separator_line, 19, 0, 1, 2)
 
 
         # ## Plotting type
         # ## Plotting type
         self.paint_plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'},
         self.paint_plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'},
@@ -5944,8 +5966,8 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
             _("- 'Normal' -  normal plotting, done at the end of the Paint job\n"
             _("- 'Normal' -  normal plotting, done at the end of the Paint job\n"
               "- 'Progressive' - after each shape is generated it will be plotted.")
               "- 'Progressive' - after each shape is generated it will be plotted.")
         )
         )
-        grid0.addWidget(plotting_label, 17, 0)
-        grid0.addWidget(self.paint_plotting_radio, 17, 1)
+        grid0.addWidget(plotting_label, 20, 0)
+        grid0.addWidget(self.paint_plotting_radio, 20, 1)
 
 
         self.layout.addStretch()
         self.layout.addStretch()
 
 
@@ -6748,9 +6770,11 @@ class ToolsSolderpastePrefGroupUI(OptionsGroupUI):
         self.layout.addLayout(grid0)
         self.layout.addLayout(grid0)
 
 
         # Nozzle Tool Diameters
         # Nozzle Tool Diameters
-        nozzletdlabel = QtWidgets.QLabel('%s:' % _('Tools dia'))
+        nozzletdlabel = QtWidgets.QLabel('<b><font color="green">%s:</font></b>' % _('Tools Dia'))
         nozzletdlabel.setToolTip(
         nozzletdlabel.setToolTip(
-            _("Diameters of nozzle tools, separated by ','")
+            _("Diameters of the tools, separated by comma.\n"
+              "The value of the diameter has to use the dot decimals separator.\n"
+              "Valid values: 0.3, 1.0")
         )
         )
         self.nozzle_tool_dia_entry = FCEntry()
         self.nozzle_tool_dia_entry = FCEntry()
 
 

+ 188 - 33
flatcamTools/ToolNCC.py

@@ -22,6 +22,8 @@ from shapely.geometry import base
 from shapely.ops import cascaded_union
 from shapely.ops import cascaded_union
 from shapely.geometry import MultiPolygon, Polygon, MultiLineString, LineString, LinearRing
 from shapely.geometry import MultiPolygon, Polygon, MultiLineString, LineString, LinearRing
 
 
+from matplotlib.backend_bases import KeyEvent as mpl_key_event
+
 import logging
 import logging
 import traceback
 import traceback
 import gettext
 import gettext
@@ -571,10 +573,25 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.reference_combo_type.hide()
         self.reference_combo_type.hide()
         self.reference_combo_type_label.hide()
         self.reference_combo_type_label.hide()
 
 
+        # Area Selection shape
+        self.area_shape_label = QtWidgets.QLabel('%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'}])
+
+        self.grid3.addWidget(self.area_shape_label, 29, 0)
+        self.grid3.addWidget(self.area_shape_radio, 29, 1)
+
+        self.area_shape_label.hide()
+        self.area_shape_radio.hide()
+
         separator_line = QtWidgets.QFrame()
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        self.grid3.addWidget(separator_line, 29, 0, 1, 2)
+        self.grid3.addWidget(separator_line, 30, 0, 1, 2)
 
 
         self.generate_ncc_button = QtWidgets.QPushButton(_('Generate Geometry'))
         self.generate_ncc_button = QtWidgets.QPushButton(_('Generate Geometry'))
         self.generate_ncc_button.setToolTip(
         self.generate_ncc_button.setToolTip(
@@ -652,9 +669,17 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.cursor_pos = None
         self.cursor_pos = None
         self.mouse_is_dragging = False
         self.mouse_is_dragging = False
 
 
+        # store here the points for the "Polygon" area selection shape
+        self.points = []
+        # set this as True when in middle of drawing a "Polygon" area selection shape
+        # it is made False by first click to signify that the shape is complete
+        self.poly_drawn = False
+
         self.mm = None
         self.mm = None
         self.mr = None
         self.mr = None
 
 
+        self.kp = None
+
         # store here solid_geometry when there are tool with isolation job
         # store here solid_geometry when there are tool with isolation job
         self.solid_geometry = []
         self.solid_geometry = []
 
 
@@ -666,7 +691,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.tooldia = None
         self.tooldia = None
 
 
         self.form_fields = {
         self.form_fields = {
-            "nccoperation":self.op_radio,
+            "nccoperation": self.op_radio,
             "nccoverlap": self.ncc_overlap_entry,
             "nccoverlap": self.ncc_overlap_entry,
             "nccmargin": self.ncc_margin_entry,
             "nccmargin": self.ncc_margin_entry,
             "nccmethod": self.ncc_method_combo,
             "nccmethod": self.ncc_method_combo,
@@ -970,6 +995,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.ncc_offset_spinner.set_value(self.app.defaults["tools_ncc_offset_value"])
         self.ncc_offset_spinner.set_value(self.app.defaults["tools_ncc_offset_value"])
 
 
         self.select_combo.set_value(self.app.defaults["tools_nccref"])
         self.select_combo.set_value(self.app.defaults["tools_nccref"])
+        self.area_shape_radio.set_value(self.app.defaults["tools_ncc_area_shape"])
+
         self.milling_type_radio.set_value(self.app.defaults["tools_nccmilling_type"])
         self.milling_type_radio.set_value(self.app.defaults["tools_nccmilling_type"])
         self.cutz_entry.set_value(self.app.defaults["tools_ncccutz"])
         self.cutz_entry.set_value(self.app.defaults["tools_ncccutz"])
         self.tool_type_radio.set_value(self.app.defaults["tools_ncctool_type"])
         self.tool_type_radio.set_value(self.app.defaults["tools_ncctool_type"])
@@ -1271,16 +1298,39 @@ class NonCopperClear(FlatCAMTool, Gerber):
         }[self.reference_combo_type.get_value()]
         }[self.reference_combo_type.get_value()]
 
 
     def on_toggle_reference(self):
     def on_toggle_reference(self):
-        if self.select_combo.get_value() == _("Itself") or self.select_combo.get_value() == _("Area Selection"):
+        sel_combo = self.select_combo.get_value()
+
+        if sel_combo == _("Itself"):
+            self.reference_combo.hide()
+            self.reference_combo_label.hide()
+            self.reference_combo_type.hide()
+            self.reference_combo_type_label.hide()
+            self.area_shape_label.hide()
+            self.area_shape_radio.hide()
+
+            # disable rest-machining for area painting
+            self.ncc_rest_cb.setDisabled(False)
+        elif sel_combo == _("Area Selection"):
             self.reference_combo.hide()
             self.reference_combo.hide()
             self.reference_combo_label.hide()
             self.reference_combo_label.hide()
             self.reference_combo_type.hide()
             self.reference_combo_type.hide()
             self.reference_combo_type_label.hide()
             self.reference_combo_type_label.hide()
+            self.area_shape_label.show()
+            self.area_shape_radio.show()
+
+            # disable rest-machining for area painting
+            self.ncc_rest_cb.set_value(False)
+            self.ncc_rest_cb.setDisabled(True)
         else:
         else:
             self.reference_combo.show()
             self.reference_combo.show()
             self.reference_combo_label.show()
             self.reference_combo_label.show()
             self.reference_combo_type.show()
             self.reference_combo_type.show()
             self.reference_combo_type_label.show()
             self.reference_combo_type_label.show()
+            self.area_shape_label.hide()
+            self.area_shape_radio.hide()
+
+            # disable rest-machining for area painting
+            self.ncc_rest_cb.setDisabled(False)
 
 
     def on_order_changed(self, order):
     def on_order_changed(self, order):
         if order != 'no':
         if order != 'no':
@@ -1616,6 +1666,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
 
             self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
             self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
             self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
             self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
+            self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
+
         elif self.select_method == 'box':
         elif self.select_method == 'box':
             self.bound_obj_name = self.reference_combo.currentText()
             self.bound_obj_name = self.reference_combo.currentText()
             # Get source object.
             # Get source object.
@@ -1643,52 +1695,94 @@ class NonCopperClear(FlatCAMTool, Gerber):
             right_button = 3
             right_button = 3
 
 
         event_pos = self.app.plotcanvas.translate_coords(event_pos)
         event_pos = self.app.plotcanvas.translate_coords(event_pos)
+        if self.app.grid_status():
+            curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
+        else:
+            curr_pos = (event_pos[0], event_pos[1])
+
+        x1, y1 = curr_pos[0], curr_pos[1]
+
+        shape_type = self.area_shape_radio.get_value()
 
 
         # do clear area only for left mouse clicks
         # do clear area only for left mouse clicks
         if event.button == 1:
         if event.button == 1:
-            if self.first_click is False:
-                self.first_click = True
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the paint area."))
+            if shape_type == "square":
+                if self.first_click is False:
+                    self.first_click = True
+                    self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the paint area."))
+
+                    self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos)
+                    if self.app.grid_status():
+                        self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
+                else:
+                    self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish."))
+                    self.app.delete_selection_shape()
 
 
-                self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos)
-                if self.app.grid_status():
-                    self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
-            else:
-                self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish."))
-                self.app.delete_selection_shape()
+                    x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
 
 
-                if self.app.grid_status():
-                    curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
-                else:
-                    curr_pos = (event_pos[0], event_pos[1])
+                    pt1 = (x0, y0)
+                    pt2 = (x1, y0)
+                    pt3 = (x1, y1)
+                    pt4 = (x0, y1)
 
 
-                x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
-                x1, y1 = curr_pos[0], curr_pos[1]
-                pt1 = (x0, y0)
-                pt2 = (x1, y0)
-                pt3 = (x1, y1)
-                pt4 = (x0, y1)
+                    new_rectangle = Polygon([pt1, pt2, pt3, pt4])
+                    self.sel_rect.append(new_rectangle)
 
 
-                new_rectangle = Polygon([pt1, pt2, pt3, pt4])
-                self.sel_rect.append(new_rectangle)
+                    # add a temporary shape on canvas
+                    self.draw_tool_selection_shape(old_coords=(x0, y0), coords=(x1, y1))
 
 
-                # add a temporary shape on canvas
-                self.draw_tool_selection_shape(old_coords=(x0, y0), coords=(x1, y1))
+                    self.first_click = False
+                    return
+            else:
+                self.points.append((x1, y1))
 
 
-                self.first_click = False
-                return
+                if len(self.points) > 1:
+                    self.poly_drawn = True
+                    self.app.inform.emit(_("Click on next Point or click right mouse button to complete ..."))
 
 
+                return ""
         elif event.button == right_button and self.mouse_is_dragging is False:
         elif event.button == right_button and self.mouse_is_dragging is False:
-            self.first_click = False
+
+            shape_type = self.area_shape_radio.get_value()
+
+            if shape_type == "square":
+                self.first_click = False
+            else:
+                # if we finish to add a polygon
+                if self.poly_drawn is True:
+                    try:
+                        # try to add the point where we last clicked if it is not already in the self.points
+                        last_pt = (x1, y1)
+                        if last_pt != self.points[-1]:
+                            self.points.append(last_pt)
+                    except IndexError:
+                        pass
+
+                    # we need to add a Polygon and a Polygon can be made only from at least 3 points
+                    if len(self.points) > 2:
+                        self.delete_moving_selection_shape()
+                        pol = Polygon(self.points)
+                        # do not add invalid polygons even if they are drawn by utility geometry
+                        if pol.is_valid:
+                            self.sel_rect.append(pol)
+                            self.draw_selection_shape_polygon(points=self.points)
+                            self.app.inform.emit(
+                                _("Zone added. Click to start adding next zone or right click to finish."))
+
+                    self.points = []
+                    self.poly_drawn = False
+                    return
 
 
             self.delete_tool_selection_shape()
             self.delete_tool_selection_shape()
 
 
             if self.app.is_legacy is False:
             if self.app.is_legacy is False:
                 self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
                 self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
                 self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
                 self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
+                self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
             else:
             else:
                 self.app.plotcanvas.graph_event_disconnect(self.mr)
                 self.app.plotcanvas.graph_event_disconnect(self.mr)
                 self.app.plotcanvas.graph_event_disconnect(self.mm)
                 self.app.plotcanvas.graph_event_disconnect(self.mm)
+                self.app.plotcanvas.graph_event_disconnect(self.kp)
 
 
             self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
             self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
                                                                   self.app.on_mouse_click_over_plot)
                                                                   self.app.on_mouse_click_over_plot)
@@ -1710,6 +1804,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
 
     # called on mouse move
     # called on mouse move
     def on_mouse_move(self, event):
     def on_mouse_move(self, event):
+        shape_type = self.area_shape_radio.get_value()
+
         if self.app.is_legacy is False:
         if self.app.is_legacy is False:
             event_pos = event.pos
             event_pos = event.pos
             event_is_dragging = event.is_dragging
             event_is_dragging = event.is_dragging
@@ -1749,10 +1845,69 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                                "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
                                                "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
 
 
         # draw the utility geometry
         # draw the utility geometry
-        if self.first_click:
-            self.app.delete_selection_shape()
-            self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
-                                                 coords=(curr_pos[0], curr_pos[1]))
+        if shape_type == "square":
+            if self.first_click:
+                self.app.delete_selection_shape()
+                self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
+                                                     coords=(curr_pos[0], curr_pos[1]))
+        else:
+            self.delete_moving_selection_shape()
+            self.draw_moving_selection_shape_poly(points=self.points, data=(curr_pos[0], curr_pos[1]))
+
+    def on_key_press(self, event):
+        modifiers = QtWidgets.QApplication.keyboardModifiers()
+        matplotlib_key_flag = False
+
+        # events out of the self.app.collection view (it's about Project Tab) are of type int
+        if type(event) is int:
+            key = event
+        # events from the GUI are of type QKeyEvent
+        elif type(event) == QtGui.QKeyEvent:
+            key = event.key()
+        elif isinstance(event, mpl_key_event):  # MatPlotLib key events are trickier to interpret than the rest
+            matplotlib_key_flag = True
+
+            key = event.key
+            key = QtGui.QKeySequence(key)
+
+            # check for modifiers
+            key_string = key.toString().lower()
+            if '+' in key_string:
+                mod, __, key_text = key_string.rpartition('+')
+                if mod.lower() == 'ctrl':
+                    modifiers = QtCore.Qt.ControlModifier
+                elif mod.lower() == 'alt':
+                    modifiers = QtCore.Qt.AltModifier
+                elif mod.lower() == 'shift':
+                    modifiers = QtCore.Qt.ShiftModifier
+                else:
+                    modifiers = QtCore.Qt.NoModifier
+                key = QtGui.QKeySequence(key_text)
+
+        # events from Vispy are of type KeyEvent
+        else:
+            key = event.key
+
+        if key == QtCore.Qt.Key_Escape or key == 'Escape':
+            if self.app.is_legacy is False:
+                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
+                self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
+                self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
+            else:
+                self.app.plotcanvas.graph_event_disconnect(self.mr)
+                self.app.plotcanvas.graph_event_disconnect(self.mm)
+                self.app.plotcanvas.graph_event_disconnect(self.kp)
+
+            self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
+                                                                  self.app.on_mouse_click_over_plot)
+            self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
+                                                                  self.app.on_mouse_move_over_plot)
+            self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
+                                                                  self.app.on_mouse_click_release_over_plot)
+            self.points = []
+            self.poly_drawn = False
+            self.delete_moving_selection_shape()
+            self.delete_tool_selection_shape()
 
 
     def envelope_object(self, ncc_obj, ncc_select, box_obj=None):
     def envelope_object(self, ncc_obj, ncc_select, box_obj=None):
         """
         """

+ 200 - 34
flatcamTools/ToolPaint.py

@@ -20,6 +20,8 @@ import FlatCAMApp
 from shapely.geometry import base, Polygon, MultiPolygon, LinearRing, Point, MultiLineString
 from shapely.geometry import base, Polygon, MultiPolygon, LinearRing, Point, MultiLineString
 from shapely.ops import cascaded_union, unary_union, linemerge
 from shapely.ops import cascaded_union, unary_union, linemerge
 
 
+from matplotlib.backend_bases import KeyEvent as mpl_key_event
+
 import numpy as np
 import numpy as np
 import math
 import math
 from numpy import Inf
 from numpy import Inf
@@ -516,6 +518,21 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.reference_type_combo.hide()
         self.reference_type_combo.hide()
         self.reference_type_label.hide()
         self.reference_type_label.hide()
 
 
+        # Area Selection shape
+        self.area_shape_label = QtWidgets.QLabel('%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'}])
+
+        grid4.addWidget(self.area_shape_label, 21, 0)
+        grid4.addWidget(self.area_shape_radio, 21, 1)
+
+        self.area_shape_label.hide()
+        self.area_shape_radio.hide()
+
         # GO Button
         # GO Button
         self.generate_paint_button = QtWidgets.QPushButton(_('Generate Geometry'))
         self.generate_paint_button = QtWidgets.QPushButton(_('Generate Geometry'))
         self.generate_paint_button.setToolTip(
         self.generate_paint_button.setToolTip(
@@ -573,6 +590,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.units = ''
         self.units = ''
         self.paint_tools = {}
         self.paint_tools = {}
         self.tooluid = 0
         self.tooluid = 0
+
         self.first_click = False
         self.first_click = False
         self.cursor_pos = None
         self.cursor_pos = None
         self.mouse_is_dragging = False
         self.mouse_is_dragging = False
@@ -580,6 +598,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.mm = None
         self.mm = None
         self.mp = None
         self.mp = None
         self.mr = None
         self.mr = None
+        self.kp = None
 
 
         self.sel_rect = []
         self.sel_rect = []
 
 
@@ -612,6 +631,12 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
         self.old_tool_dia = None
         self.old_tool_dia = None
 
 
+        # store here the points for the "Polygon" area selection shape
+        self.points = []
+        # set this as True when in middle of drawing a "Polygon" area selection shape
+        # it is made False by first click to signify that the shape is complete
+        self.poly_drawn = False
+
         # #############################################################################
         # #############################################################################
         # ################################# Signals ###################################
         # ################################# Signals ###################################
         # #############################################################################
         # #############################################################################
@@ -895,7 +920,9 @@ class ToolPaint(FlatCAMTool, Gerber):
             return float(self.addtool_entry.get_value())
             return float(self.addtool_entry.get_value())
 
 
     def on_selection(self):
     def on_selection(self):
-        if self.selectmethod_combo.get_value() == _("Reference Object"):
+        sel_combo = self.selectmethod_combo.get_value()
+
+        if sel_combo == _("Reference Object"):
             self.reference_combo.show()
             self.reference_combo.show()
             self.reference_combo_label.show()
             self.reference_combo_label.show()
             self.reference_type_combo.show()
             self.reference_type_combo.show()
@@ -906,14 +933,17 @@ class ToolPaint(FlatCAMTool, Gerber):
             self.reference_type_combo.hide()
             self.reference_type_combo.hide()
             self.reference_type_label.hide()
             self.reference_type_label.hide()
 
 
-        if self.selectmethod_combo.get_value() == _("Polygon Selection"):
+        if sel_combo == _("Polygon Selection"):
             # disable rest-machining for single polygon painting
             # disable rest-machining for single polygon painting
             self.rest_cb.set_value(False)
             self.rest_cb.set_value(False)
             self.rest_cb.setDisabled(True)
             self.rest_cb.setDisabled(True)
-        if self.selectmethod_combo.get_value() == _("Area Selection"):
-            # disable rest-machining for single polygon painting
+        if sel_combo == _("Area Selection"):
+            # disable rest-machining for area painting
             self.rest_cb.set_value(False)
             self.rest_cb.set_value(False)
             self.rest_cb.setDisabled(True)
             self.rest_cb.setDisabled(True)
+
+            self.area_shape_label.show()
+            self.area_shape_radio.show()
         else:
         else:
             self.rest_cb.setDisabled(False)
             self.rest_cb.setDisabled(False)
             self.addtool_entry.setDisabled(False)
             self.addtool_entry.setDisabled(False)
@@ -921,6 +951,9 @@ class ToolPaint(FlatCAMTool, Gerber):
             self.deltool_btn.setDisabled(False)
             self.deltool_btn.setDisabled(False)
             self.tools_table.setContextMenuPolicy(Qt.ActionsContextMenu)
             self.tools_table.setContextMenuPolicy(Qt.ActionsContextMenu)
 
 
+            self.area_shape_label.hide()
+            self.area_shape_radio.hide()
+
     def on_order_changed(self, order):
     def on_order_changed(self, order):
         if order != 'no':
         if order != 'no':
             self.build_ui()
             self.build_ui()
@@ -989,6 +1022,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.paintmargin_entry.set_value(self.app.defaults["tools_paintmargin"])
         self.paintmargin_entry.set_value(self.app.defaults["tools_paintmargin"])
         self.paintmethod_combo.set_value(self.app.defaults["tools_paintmethod"])
         self.paintmethod_combo.set_value(self.app.defaults["tools_paintmethod"])
         self.selectmethod_combo.set_value(self.app.defaults["tools_selectmethod"])
         self.selectmethod_combo.set_value(self.app.defaults["tools_selectmethod"])
+        self.area_shape_radio.set_value(self.app.defaults["tools_paint_area_shape"])
         self.pathconnect_cb.set_value(self.app.defaults["tools_pathconnect"])
         self.pathconnect_cb.set_value(self.app.defaults["tools_pathconnect"])
         self.paintcontour_cb.set_value(self.app.defaults["tools_paintcontour"])
         self.paintcontour_cb.set_value(self.app.defaults["tools_paintcontour"])
         self.paintoverlap_entry.set_value(self.app.defaults["tools_paintoverlap"])
         self.paintoverlap_entry.set_value(self.app.defaults["tools_paintoverlap"])
@@ -1396,6 +1430,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                 self.grid_status_memory = False
                 self.grid_status_memory = False
 
 
             self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_single_poly_mouse_release)
             self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_single_poly_mouse_release)
+            self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
 
 
             if self.app.is_legacy is False:
             if self.app.is_legacy is False:
                 self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
                 self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
@@ -1418,6 +1453,8 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
             self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
             self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
             self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
             self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
+            self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
+
         elif self.select_method == _("Reference Object"):
         elif self.select_method == _("Reference Object"):
             self.bound_obj_name = self.reference_combo.currentText()
             self.bound_obj_name = self.reference_combo.currentText()
             # Get source object.
             # Get source object.
@@ -1498,8 +1535,10 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
             if self.app.is_legacy is False:
             if self.app.is_legacy is False:
                 self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_single_poly_mouse_release)
                 self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_single_poly_mouse_release)
+                self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
             else:
             else:
                 self.app.plotcanvas.graph_event_disconnect(self.mr)
                 self.app.plotcanvas.graph_event_disconnect(self.mr)
+                self.app.plotcanvas.graph_event_disconnect(self.kp)
 
 
             self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
             self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
                                                                   self.app.on_mouse_click_over_plot)
                                                                   self.app.on_mouse_click_over_plot)
@@ -1540,51 +1579,93 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
         event_pos = (x, y)
         event_pos = (x, y)
 
 
+        shape_type = self.area_shape_radio.get_value()
+
+        curr_pos = self.app.plotcanvas.translate_coords(event_pos)
+        if self.app.grid_status():
+            curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
+
+        x1, y1 = curr_pos[0], curr_pos[1]
+
         # do paint single only for left mouse clicks
         # do paint single only for left mouse clicks
         if event.button == 1:
         if event.button == 1:
-            if not self.first_click:
-                self.first_click = True
-                self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                     _("Click the end point of the paint area."))
+            if shape_type == "square":
+                if not self.first_click:
+                    self.first_click = True
+                    self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                         _("Click the end point of the paint area."))
+
+                    self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos)
+                    if self.app.grid_status():
+                        self.cursor_pos = self.app.geo_editor.snap(self.cursor_pos[0], self.cursor_pos[1])
+                else:
+                    self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish."))
+                    self.app.delete_selection_shape()
 
 
-                self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos)
-                if self.app.grid_status():
-                    self.cursor_pos = self.app.geo_editor.snap(self.cursor_pos[0], self.cursor_pos[1])
-            else:
-                self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish."))
-                self.app.delete_selection_shape()
+                    x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
+                    pt1 = (x0, y0)
+                    pt2 = (x1, y0)
+                    pt3 = (x1, y1)
+                    pt4 = (x0, y1)
 
 
-                curr_pos = self.app.plotcanvas.translate_coords(event_pos)
-                if self.app.grid_status():
-                    curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
+                    new_rectangle = Polygon([pt1, pt2, pt3, pt4])
+                    self.sel_rect.append(new_rectangle)
 
 
-                x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
-                x1, y1 = curr_pos[0], curr_pos[1]
-                pt1 = (x0, y0)
-                pt2 = (x1, y0)
-                pt3 = (x1, y1)
-                pt4 = (x0, y1)
+                    # add a temporary shape on canvas
+                    self.draw_tool_selection_shape(old_coords=(x0, y0), coords=(x1, y1))
 
 
-                new_rectangle = Polygon([pt1, pt2, pt3, pt4])
-                self.sel_rect.append(new_rectangle)
+                    self.first_click = False
+                    return
+            else:
+                self.points.append((x1, y1))
+
+                if len(self.points) > 1:
+                    self.poly_drawn = True
+                    self.app.inform.emit(_("Click on next Point or click right mouse button to complete ..."))
+
+                return ""
+        elif event.button == right_button and self.mouse_is_dragging is False:
 
 
-                # add a temporary shape on canvas
-                self.draw_tool_selection_shape(old_coords=(x0, y0), coords=(x1, y1))
+            shape_type = self.area_shape_radio.get_value()
 
 
+            if shape_type == "square":
                 self.first_click = False
                 self.first_click = False
-                return
+            else:
+                # if we finish to add a polygon
+                if self.poly_drawn is True:
+                    try:
+                        # try to add the point where we last clicked if it is not already in the self.points
+                        last_pt = (x1, y1)
+                        if last_pt != self.points[-1]:
+                            self.points.append(last_pt)
+                    except IndexError:
+                        pass
 
 
-        elif event.button == right_button and self.mouse_is_dragging is False:
-            self.first_click = False
+                    # we need to add a Polygon and a Polygon can be made only from at least 3 points
+                    if len(self.points) > 2:
+                        self.delete_moving_selection_shape()
+                        pol = Polygon(self.points)
+                        # do not add invalid polygons even if they are drawn by utility geometry
+                        if pol.is_valid:
+                            self.sel_rect.append(pol)
+                            self.draw_selection_shape_polygon(points=self.points)
+                            self.app.inform.emit(
+                                _("Zone added. Click to start adding next zone or right click to finish."))
+
+                    self.points = []
+                    self.poly_drawn = False
+                    return
 
 
             self.delete_tool_selection_shape()
             self.delete_tool_selection_shape()
 
 
             if self.app.is_legacy is False:
             if self.app.is_legacy is False:
                 self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
                 self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
                 self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
                 self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
+                self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
             else:
             else:
                 self.app.plotcanvas.graph_event_disconnect(self.mr)
                 self.app.plotcanvas.graph_event_disconnect(self.mr)
                 self.app.plotcanvas.graph_event_disconnect(self.mm)
                 self.app.plotcanvas.graph_event_disconnect(self.mm)
+                self.app.plotcanvas.graph_event_disconnect(self.kp)
 
 
             self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
             self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
                                                                   self.app.on_mouse_click_over_plot)
                                                                   self.app.on_mouse_click_over_plot)
@@ -1607,6 +1688,8 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
     # called on mouse move
     # called on mouse move
     def on_mouse_move(self, event):
     def on_mouse_move(self, event):
+        shape_type = self.area_shape_radio.get_value()
+
         if self.app.is_legacy is False:
         if self.app.is_legacy is False:
             event_pos = event.pos
             event_pos = event.pos
             event_is_dragging = event.is_dragging
             event_is_dragging = event.is_dragging
@@ -1652,10 +1735,93 @@ class ToolPaint(FlatCAMTool, Gerber):
                                                "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
                                                "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
 
 
         # draw the utility geometry
         # draw the utility geometry
-        if self.first_click:
-            self.app.delete_selection_shape()
-            self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
-                                                 coords=(curr_pos[0], curr_pos[1]))
+        if shape_type == "square":
+            if self.first_click:
+                self.app.delete_selection_shape()
+                self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
+                                                     coords=(curr_pos[0], curr_pos[1]))
+        else:
+            self.delete_moving_selection_shape()
+            self.draw_moving_selection_shape_poly(points=self.points, data=(curr_pos[0], curr_pos[1]))
+
+    def on_key_press(self, event):
+        modifiers = QtWidgets.QApplication.keyboardModifiers()
+        matplotlib_key_flag = False
+
+        # events out of the self.app.collection view (it's about Project Tab) are of type int
+        if type(event) is int:
+            key = event
+        # events from the GUI are of type QKeyEvent
+        elif type(event) == QtGui.QKeyEvent:
+            key = event.key()
+        elif isinstance(event, mpl_key_event):  # MatPlotLib key events are trickier to interpret than the rest
+            matplotlib_key_flag = True
+
+            key = event.key
+            key = QtGui.QKeySequence(key)
+
+            # check for modifiers
+            key_string = key.toString().lower()
+            if '+' in key_string:
+                mod, __, key_text = key_string.rpartition('+')
+                if mod.lower() == 'ctrl':
+                    modifiers = QtCore.Qt.ControlModifier
+                elif mod.lower() == 'alt':
+                    modifiers = QtCore.Qt.AltModifier
+                elif mod.lower() == 'shift':
+                    modifiers = QtCore.Qt.ShiftModifier
+                else:
+                    modifiers = QtCore.Qt.NoModifier
+                key = QtGui.QKeySequence(key_text)
+
+        # events from Vispy are of type KeyEvent
+        else:
+            key = event.key
+
+        print(key)
+        if key == QtCore.Qt.Key_Escape or key == 'Escape':
+            try:
+                if self.app.is_legacy is False:
+                    self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
+                    self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
+                    self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
+                else:
+                    self.app.plotcanvas.graph_event_disconnect(self.mr)
+                    self.app.plotcanvas.graph_event_disconnect(self.mm)
+                    self.app.plotcanvas.graph_event_disconnect(self.kp)
+            except Exception as e:
+                log.debug("ToolPaint.on_key_press() _1 --> %s" % str(e))
+
+            try:
+                # restore the Grid snapping if it was active before
+                if self.grid_status_memory is True:
+                    self.app.ui.grid_snap_btn.trigger()
+
+                if self.app.is_legacy is False:
+                    self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_single_poly_mouse_release)
+                    self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
+                else:
+                    self.app.plotcanvas.graph_event_disconnect(self.mr)
+                    self.app.plotcanvas.graph_event_disconnect(self.kp)
+
+                self.app.tool_shapes.clear(update=True)
+            except Exception as e:
+                log.debug("ToolPaint.on_key_press() _2 --> %s" % str(e))
+
+            self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
+                                                                  self.app.on_mouse_click_over_plot)
+            self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
+                                                                  self.app.on_mouse_move_over_plot)
+            self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
+                                                                  self.app.on_mouse_click_release_over_plot)
+
+            self.points = []
+            self.poly_drawn = False
+
+            self.poly_dict.clear()
+
+            self.delete_moving_selection_shape()
+            self.delete_tool_selection_shape()
 
 
     def paint_poly(self, obj, inside_pt=None, poly_list=None, tooldia=None, overlap=None, order=None,
     def paint_poly(self, obj, inside_pt=None, poly_list=None, tooldia=None, overlap=None, order=None,
                    margin=None, method=None, outname=None, connect=None, contour=None, tools_storage=None,
                    margin=None, method=None, outname=None, connect=None, contour=None, tools_storage=None,