diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 7245a1e0..0564f774 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -786,6 +786,7 @@ class App(QtCore.QObject): "tools_ncc_offset_choice": False, "tools_ncc_offset_value": 0.0000, "tools_nccref": _('Itself'), + "tools_ncc_area_shape": "square", "tools_ncc_plotting": 'normal', "tools_nccmilling_type": 'cl', "tools_ncctool_type": 'C1', @@ -812,6 +813,7 @@ class App(QtCore.QObject): "tools_paintmargin": 0.0, "tools_paintmethod": _("Seed"), "tools_selectmethod": _("All Polygons"), + "tools_paint_area_shape": "square", "tools_pathconnect": True, "tools_paintcontour": True, "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_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_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_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, @@ -1494,6 +1497,7 @@ class App(QtCore.QObject): "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_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_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, diff --git a/FlatCAMTool.py b/FlatCAMTool.py index 02bd08bd..3343c06c 100644 --- a/FlatCAMTool.py +++ b/FlatCAMTool.py @@ -9,7 +9,7 @@ from PyQt5 import QtGui, QtCore, QtWidgets, QtWidgets from PyQt5.QtCore import Qt -from shapely.geometry import Polygon +from shapely.geometry import Polygon, LineString import gettext import FlatCAMTranslation as fcTranslate @@ -106,6 +106,7 @@ class FlatCAMTool(QtWidgets.QWidget): :param old_coords: old coordinates :param coords: new coordinates + :param kwargs: :return: """ @@ -143,10 +144,101 @@ class FlatCAMTool(QtWidgets.QWidget): if self.app.is_legacy is True: 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): self.app.tool_shapes.clear() 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): if accepted is False: self.app.inform.emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % diff --git a/README.md b/README.md index dd05995f..012292c8 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ CAD program, and create G-Code for Isolation routing. 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 +- 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 diff --git a/flatcamGUI/PreferencesUI.py b/flatcamGUI/PreferencesUI.py index 1cd46b4d..239b6fbb 100644 --- a/flatcamGUI/PreferencesUI.py +++ b/flatcamGUI/PreferencesUI.py @@ -2521,7 +2521,7 @@ class GerberEditorPrefGroupUI(OptionsGroupUI): self.adddim_label = QtWidgets.QLabel('%s:' % _('Aperture Dimensions')) 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" "Valid values: 0.3, 1.0") ) @@ -3970,9 +3970,9 @@ class GeometryGenPrefGroupUI(OptionsGroupUI): grid0.addWidget(self.tools_label, 2, 0, 1, 2) # Tooldia - tdlabel = QtWidgets.QLabel('%s:' % _('Tool dia')) + tdlabel = QtWidgets.QLabel('%s:' % _('Tools Dia')) 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" "Valid values: 0.3, 1.0") ) @@ -5139,7 +5139,7 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI): ncctdlabel = QtWidgets.QLabel('%s:' % _('Tools Dia')) 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" "Valid values: 0.3, 1.0") ) @@ -5418,10 +5418,21 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI): grid0.addWidget(select_label, 18, 0) 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.setFrameShape(QtWidgets.QFrame.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) - grid0.addWidget(separator_line, 19, 0, 1, 2) + grid0.addWidget(separator_line, 20, 0, 1, 2) # ## Plotting type 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" "- '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() @@ -5695,7 +5706,7 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI): # Tool dia ptdlabel = QtWidgets.QLabel('%s:' % _('Tools Dia')) 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" "Valid values: 0.3, 1.0") ) @@ -5931,10 +5942,21 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI): grid0.addWidget(selectlabel, 15, 0) 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.setFrameShape(QtWidgets.QFrame.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) - grid0.addWidget(separator_line, 16, 0, 1, 2) + grid0.addWidget(separator_line, 19, 0, 1, 2) # ## Plotting type 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" "- '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() @@ -6748,9 +6770,11 @@ class ToolsSolderpastePrefGroupUI(OptionsGroupUI): self.layout.addLayout(grid0) # Nozzle Tool Diameters - nozzletdlabel = QtWidgets.QLabel('%s:' % _('Tools dia')) + nozzletdlabel = QtWidgets.QLabel('%s:' % _('Tools Dia')) 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() diff --git a/flatcamTools/ToolNCC.py b/flatcamTools/ToolNCC.py index 2873be89..630e87b5 100644 --- a/flatcamTools/ToolNCC.py +++ b/flatcamTools/ToolNCC.py @@ -22,6 +22,8 @@ from shapely.geometry import base from shapely.ops import cascaded_union from shapely.geometry import MultiPolygon, Polygon, MultiLineString, LineString, LinearRing +from matplotlib.backend_bases import KeyEvent as mpl_key_event + import logging import traceback import gettext @@ -571,10 +573,25 @@ class NonCopperClear(FlatCAMTool, Gerber): self.reference_combo_type.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.setFrameShape(QtWidgets.QFrame.HLine) 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.setToolTip( @@ -652,9 +669,17 @@ class NonCopperClear(FlatCAMTool, Gerber): self.cursor_pos = None 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.mr = None + self.kp = None + # store here solid_geometry when there are tool with isolation job self.solid_geometry = [] @@ -666,7 +691,7 @@ class NonCopperClear(FlatCAMTool, Gerber): self.tooldia = None self.form_fields = { - "nccoperation":self.op_radio, + "nccoperation": self.op_radio, "nccoverlap": self.ncc_overlap_entry, "nccmargin": self.ncc_margin_entry, "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.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.cutz_entry.set_value(self.app.defaults["tools_ncccutz"]) 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()] 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_label.hide() + self.reference_combo_type.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: self.reference_combo.show() self.reference_combo_label.show() self.reference_combo_type.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): 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.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': self.bound_obj_name = self.reference_combo.currentText() # Get source object. @@ -1643,52 +1695,94 @@ class NonCopperClear(FlatCAMTool, Gerber): right_button = 3 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 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() - - if self.app.grid_status(): - curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1]) + 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: - curr_pos = (event_pos[0], event_pos[1]) + 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] - x1, y1 = curr_pos[0], curr_pos[1] - pt1 = (x0, y0) - pt2 = (x1, y0) - pt3 = (x1, y1) - pt4 = (x0, y1) + x0, y0 = self.cursor_pos[0], self.cursor_pos[1] - new_rectangle = Polygon([pt1, pt2, pt3, pt4]) - self.sel_rect.append(new_rectangle) + 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 + # 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)) + + 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: - 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() 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) @@ -1710,6 +1804,8 @@ class NonCopperClear(FlatCAMTool, Gerber): # called on mouse move def on_mouse_move(self, event): + shape_type = self.area_shape_radio.get_value() + if self.app.is_legacy is False: event_pos = event.pos event_is_dragging = event.is_dragging @@ -1749,10 +1845,69 @@ class NonCopperClear(FlatCAMTool, Gerber): "%.4f    " % (dx, dy)) # 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): """ diff --git a/flatcamTools/ToolPaint.py b/flatcamTools/ToolPaint.py index 3951a5cc..8837651e 100644 --- a/flatcamTools/ToolPaint.py +++ b/flatcamTools/ToolPaint.py @@ -20,6 +20,8 @@ import FlatCAMApp from shapely.geometry import base, Polygon, MultiPolygon, LinearRing, Point, MultiLineString from shapely.ops import cascaded_union, unary_union, linemerge +from matplotlib.backend_bases import KeyEvent as mpl_key_event + import numpy as np import math from numpy import Inf @@ -516,6 +518,21 @@ class ToolPaint(FlatCAMTool, Gerber): self.reference_type_combo.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 self.generate_paint_button = QtWidgets.QPushButton(_('Generate Geometry')) self.generate_paint_button.setToolTip( @@ -573,6 +590,7 @@ class ToolPaint(FlatCAMTool, Gerber): self.units = '' self.paint_tools = {} self.tooluid = 0 + self.first_click = False self.cursor_pos = None self.mouse_is_dragging = False @@ -580,6 +598,7 @@ class ToolPaint(FlatCAMTool, Gerber): self.mm = None self.mp = None self.mr = None + self.kp = None self.sel_rect = [] @@ -612,6 +631,12 @@ class ToolPaint(FlatCAMTool, Gerber): 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 ################################### # ############################################################################# @@ -895,7 +920,9 @@ class ToolPaint(FlatCAMTool, Gerber): return float(self.addtool_entry.get_value()) 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_label.show() self.reference_type_combo.show() @@ -906,14 +933,17 @@ class ToolPaint(FlatCAMTool, Gerber): self.reference_type_combo.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 self.rest_cb.set_value(False) 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.setDisabled(True) + + self.area_shape_label.show() + self.area_shape_radio.show() else: self.rest_cb.setDisabled(False) self.addtool_entry.setDisabled(False) @@ -921,6 +951,9 @@ class ToolPaint(FlatCAMTool, Gerber): self.deltool_btn.setDisabled(False) self.tools_table.setContextMenuPolicy(Qt.ActionsContextMenu) + self.area_shape_label.hide() + self.area_shape_radio.hide() + def on_order_changed(self, order): if order != 'no': self.build_ui() @@ -989,6 +1022,7 @@ class ToolPaint(FlatCAMTool, Gerber): self.paintmargin_entry.set_value(self.app.defaults["tools_paintmargin"]) self.paintmethod_combo.set_value(self.app.defaults["tools_paintmethod"]) 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.paintcontour_cb.set_value(self.app.defaults["tools_paintcontour"]) self.paintoverlap_entry.set_value(self.app.defaults["tools_paintoverlap"]) @@ -1396,6 +1430,7 @@ class ToolPaint(FlatCAMTool, Gerber): self.grid_status_memory = False 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: 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.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"): self.bound_obj_name = self.reference_combo.currentText() # Get source object. @@ -1498,8 +1535,10 @@ class ToolPaint(FlatCAMTool, Gerber): 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.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot) @@ -1540,51 +1579,93 @@ class ToolPaint(FlatCAMTool, Gerber): 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 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]) + 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) + + 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)) + + self.first_click = False + return else: - self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish.")) - self.app.delete_selection_shape() + self.points.append((x1, 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]) - - 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) - - # add a temporary shape on canvas - self.draw_tool_selection_shape(old_coords=(x0, y0), coords=(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: - 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() 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) @@ -1607,6 +1688,8 @@ class ToolPaint(FlatCAMTool, Gerber): # called on mouse move def on_mouse_move(self, event): + shape_type = self.area_shape_radio.get_value() + if self.app.is_legacy is False: event_pos = event.pos event_is_dragging = event.is_dragging @@ -1652,10 +1735,93 @@ class ToolPaint(FlatCAMTool, Gerber): "%.4f    " % (dx, dy)) # 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, margin=None, method=None, outname=None, connect=None, contour=None, tools_storage=None,