diff --git a/CHANGELOG.md b/CHANGELOG.md index 0546f4e3..3abc6ad5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ CHANGELOG for FlatCAM Evo beta ================================================= +5.06.2023 + +- updated the FCButton and FCLabel custom widgets +- Paint Plugin: fixed issues caused by the latest changes in the Shapely module +- NCC Plugin: some changes in the method used by the Tcl Command + 31.05.2023 - Pdf Import plugin: fixed an issue with assigning a wrong property to a Qtimer diff --git a/appGUI/GUIElements.py b/appGUI/GUIElements.py index 518033d8..d660af0e 100644 --- a/appGUI/GUIElements.py +++ b/appGUI/GUIElements.py @@ -3005,16 +3005,37 @@ class FCInputDialogSpinnerButton(QtWidgets.QDialog): class FCButton(QtWidgets.QPushButton): - def __init__(self, text=None, checkable=None, click_callback=None, bold=False, color=None, parent=None): + def __init__(self, text=None, checkable=None, click_callback=None, bold=False, font_size=None, + color=None, back_color=None, border_color=None, parent=None): super(FCButton, self).__init__(text, parent) - self._bold = False - self._color = None + self._bold = bold + self._color = color + self._background_color = back_color + self._border_color = border_color self.bold = True if bold else False + font = QtGui.QFont() + if font_size: + font.setPointSize(font_size) + font.setBold(True) if bold else font.setBold(False) + self.setFont(font) if color: self.color = self.patching_text_color(color) + self.setStyleSheet(".CustomButton{color: %s;}" % color) + + if back_color: + stylesheet_content = "background-color:%s;" % back_color + self.setStyleSheet(".FCButton{%s}" % stylesheet_content) + + if border_color: + if self.styleSheet() != "": + self.setStyleSheet( + self.styleSheet().replace('}', "border-radius:3px; border:1px solid %s;}" % border_color)) + else: + stylesheet_content = "border-radius:3px; border:1px solid %s;" % border_color + self.setStyleSheet(".FCButton{%s}" % stylesheet_content) if checkable is not None: self.setCheckable(checkable) @@ -3022,6 +3043,26 @@ class FCButton(QtWidgets.QPushButton): if click_callback is not None: self.clicked.connect(click_callback) + self.default_stylesheet = self.styleSheet() + + def restore_stylesheet(self): + self.setStyleSheet(self.default_stylesheet) + + def set_stylesheet(self): + self.default_stylesheet = self.styleSheet() + + def setDisabled(self, arg__1): + if not self.isEnabled() and arg__1 is True: + return + if self.isEnabled() and arg__1 is False: + return + if arg__1 is False: + self.restore_stylesheet() + else: + self.set_stylesheet() + self.setStyleSheet("") + super().setDisabled(arg__1) + @property def bold(self): return self._bold @@ -3040,7 +3081,38 @@ class FCButton(QtWidgets.QPushButton): @color.setter def color(self, color): self._color = color - self.setStyleSheet(".FCButton{color: %s;}" % color) + if self.styleSheet() != "": + self.setStyleSheet(self.styleSheet().replace('}', "color: %s;}" % color)) + else: + self.setStyleSheet('.FCButton{"color: %s;}' % color) + self.set_stylesheet() + + @property + def b_color(self): + return self._background_color + + @b_color.setter + def b_color(self, color): + self._background_color = color + if self.styleSheet() != "": + self.setStyleSheet(self.styleSheet().replace('}', "background-color:%s;}" % color)) + else: + self.setStyleSheet('.FCButton{"background-color:%s;}' % color) + self.set_stylesheet() + + @property + def border_color(self): + return self._border_color + + @border_color.setter + def border_color(self, color): + self._border_color = color + if self.styleSheet() != "": + self.setStyleSheet(self.styleSheet().replace('}', "border-radius:3px; border:1px solid %s;}" % color)) + else: + stylesheet_content = "border-radius:3px; border:1px solid %s;" % color + self.setStyleSheet(".FCButton{%s}" % stylesheet_content) + self.set_stylesheet() def get_value(self): return self.isChecked() @@ -3053,17 +3125,19 @@ class FCButton(QtWidgets.QPushButton): class FCLabel(QtWidgets.QLabel): - clicked = QtCore.pyqtSignal(bool) - right_clicked = QtCore.pyqtSignal(bool) - middle_clicked = QtCore.pyqtSignal(bool) + clicked = QtCore.Signal(bool) + right_clicked = QtCore.Signal(bool) + middle_clicked = QtCore.Signal(bool) - def __init__(self, title=None, color=None, bold=None, size=None, parent=None): + def __init__(self, title=None, color=None, b_color=None, bold=None, size=None, parent=None): """ :param title: the label's text :type title: str :param color: text color :type color: str + :param b_color: Label color + :type b_color: str :param bold: the text weight :type bold: bool :param size: Font Size in points @@ -3074,23 +3148,18 @@ class FCLabel(QtWidgets.QLabel): super(FCLabel, self).__init__(parent) - self._color = None - self._bold = False + self._color = color + self._background_color = b_color + self._bold = bold self._title = title - self._font_size = None + self._font_size = size + + self.original_color = self.palette().color(QtGui.QPalette.ColorRole.WindowText) if color: color = self.patching_text_color(color) if isinstance(title, str): - # if color and not bold: - # self.setText('%s' % (str(color), title)) - # elif not color and bold: - # self.setText('%s' % title) - # elif color and bold: - # self.setText('%s' % (str(color), title)) - # else: - # self.setText(title) if color: self.setText('%s' % (str(color), title)) else: @@ -3100,14 +3169,22 @@ class FCLabel(QtWidgets.QLabel): font.setBold(True) if bold else font.setBold(False) if size: font.setPointSize(size) - # "%s" self.setFont(font) + if b_color: + stylesheet_content = "background-color:%s;" % b_color + self.setStyleSheet(".CustomLabel{%s}" % stylesheet_content) + # for the usage of this label as a clickable label, to know that current state self.clicked_state = False self.middle_clicked_state = False self.right_clicked_state = False + self.default_stylesheet = self.styleSheet() + + def restore_stylesheet(self): + self.setStyleSheet(self.default_stylesheet) + @property def color(self): return self._color @@ -3118,11 +3195,20 @@ class FCLabel(QtWidgets.QLabel): self.setText('%s' % (str(color), self._title)) @property - def bold(self): + def b_color(self): + return self._background_color + + @b_color.setter + def b_color(self, color): + self._background_color = color + self.setStyleSheet(self.styleSheet().replace('}', "background-color:%s;}" % color)) + + @property + def bold(self) -> bool: return self._bold @bold.setter - def bold(self, bold): + def bold(self, bold: bool): self._bold = bold font = QtGui.QFont() font.setBold(True) if bold else font.setBold(False) diff --git a/appPlugins/ToolNCC.py b/appPlugins/ToolNCC.py index 9ffb3c01..5419fbb5 100644 --- a/appPlugins/ToolNCC.py +++ b/appPlugins/ToolNCC.py @@ -2935,18 +2935,14 @@ class NonCopperClear(AppTool, Gerber): bounding_box = None if ncc_select == 0: # itself - geo_n = ncc_sel_obj.solid_geometry + geo_n = flatten_shapely_geometry(ncc_sel_obj.solid_geometry) try: - if isinstance(geo_n, MultiPolygon): - env_obj = geo_n.convex_hull - elif (isinstance(geo_n, MultiPolygon) and len(geo_n.geoms) == 1) or \ - (isinstance(geo_n, list) and len(geo_n) == 1) and isinstance(geo_n[0], Polygon): + if len(geo_n) == 1: env_obj = unary_union(geo_n) else: env_obj = unary_union(geo_n) env_obj = env_obj.convex_hull - bounding_box = env_obj.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre) except Exception as e: self.app.log.error("NonCopperClear.clear_copper() 'itself' --> %s" % str(e)) @@ -3224,128 +3220,93 @@ class NonCopperClear(AppTool, Gerber): except Exception: continue - # Transform area to MultiPolygon - if type(area) is Polygon: - area = MultiPolygon([area]) + area = flatten_shapely_geometry(area) # variables to display the percentage of work done - geo_len = len(area.geoms) + geo_len = len(area) old_disp_number = 0 self.app.log.warning("Total number of polygons to be cleared. %s" % str(geo_len)) if area.geoms: - if len(area.geoms) > 0: - pol_nr = 0 - for p in area.geoms: - # provide the app with a way to process the GUI events when in a blocking loop - QtWidgets.QApplication.processEvents() + pol_nr = 0 + for p in area: + # provide the app with a way to process the GUI events when in a blocking loop + QtWidgets.QApplication.processEvents() - if self.app.abort_flag: - # graceful abort requested by the user - raise grace + if self.app.abort_flag: + # graceful abort requested by the user + raise grace - # clean the polygon - p = p.buffer(0) + # clean the polygon + p = p.buffer(0) - if p is not None and p.is_valid: - poly_processed = [] - try: - for pol in p: - if pol is not None and isinstance(pol, Polygon): - if ncc_method == 0: # standard - cp = self.clear_polygon(pol, tool, - self.circle_steps, - overlap=overlap, contour=contour, - connect=connect, - prog_plot=False) - elif ncc_method == 1: # seed - cp = self.clear_polygon2(pol, tool, - self.circle_steps, - overlap=overlap, contour=contour, - connect=connect, - prog_plot=False) - else: - cp = self.clear_polygon3(pol, tool, - self.circle_steps, - overlap=overlap, contour=contour, - connect=connect, - prog_plot=False) - if cp: - cleared_geo += list(cp.get_objects()) - poly_processed.append(True) - else: - poly_processed.append(False) - self.app.log.warning("Polygon in MultiPolygon can not be cleared.") - else: - self.app.log.warning( - "Geo in Iterable can not be cleared because it is not Polygon. " - "It is: %s" % str(type(pol))) - except TypeError: - if isinstance(p, Polygon): - if ncc_method == 0: # standard - cp = self.clear_polygon(p, tool, self.circle_steps, - overlap=overlap, contour=contour, connect=connect, - prog_plot=False) - elif ncc_method == 1: # seed - cp = self.clear_polygon2(p, tool, self.circle_steps, - overlap=overlap, contour=contour, connect=connect, - prog_plot=False) - else: - cp = self.clear_polygon3(p, tool, self.circle_steps, - overlap=overlap, contour=contour, connect=connect, - prog_plot=False) - if cp: - cleared_geo += list(cp.get_objects()) - poly_processed.append(True) - else: - poly_processed.append(False) - self.app.log.warning("Polygon can not be cleared.") - else: - self.app.log.warning("Geo can not be cleared because it is: %s" % str(type(p))) + if p and p.is_valid: + poly_processed = [] + if isinstance(p, Polygon): + if ncc_method == 0: # standard + cp = self.clear_polygon(p, tool, self.circle_steps, + overlap=overlap, contour=contour, connect=connect, + prog_plot=False) + elif ncc_method == 1: # seed + cp = self.clear_polygon2(p, tool, self.circle_steps, + overlap=overlap, contour=contour, connect=connect, + prog_plot=False) + else: + cp = self.clear_polygon3(p, tool, self.circle_steps, + overlap=overlap, contour=contour, connect=connect, + prog_plot=False) + if cp: + cleared_geo += list(cp.get_objects()) + poly_processed.append(True) + else: + poly_processed.append(False) + self.app.log.warning("Polygon can not be cleared.") + else: + self.app.log.warning("Geo can not be cleared because it is: %s" % str(type(p))) - p_cleared = poly_processed.count(True) - p_not_cleared = poly_processed.count(False) + p_cleared = poly_processed.count(True) + p_not_cleared = poly_processed.count(False) - if p_not_cleared: - app_obj.poly_not_cleared = True + if p_not_cleared: + app_obj.poly_not_cleared = True - if p_cleared == 0: - continue + if p_cleared == 0: + continue - pol_nr += 1 - disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100])) - # log.debug("Polygons cleared: %d" % pol_nr) + pol_nr += 1 + disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100])) + # log.debug("Polygons cleared: %d" % pol_nr) - if old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - old_disp_number = disp_number - # log.debug("Polygons cleared: %d. Percentage done: %d%%" % (pol_nr, disp_number)) + if old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + old_disp_number = disp_number + # log.debug("Polygons cleared: %d. Percentage done: %d%%" % (pol_nr, disp_number)) - # check if there is a geometry at all in the cleared geometry - if cleared_geo: - # Overall cleared area - cleared = empty.buffer(-offset_a * (1 + overlap)).buffer(-tool / 1.999999).buffer( - tool / 1.999999) + # check if there is a geometry at all in the cleared geometry + if cleared_geo: + # Overall cleared area + cleared = empty.buffer(-offset_a * (1 + overlap)).buffer(-tool / 1.999999).buffer( + tool / 1.999999) - # clean-up cleared geo - cleared = cleared.buffer(0) + # clean-up cleared geo + cleared = cleared.buffer(0) - # find the tooluid associated with the current tool_dia so we know where to add the tool - # solid_geometry - for k, v in tools_storage.items(): - if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals, - tool)): - current_uid = int(k) + # find the tooluid associated with the current tool_dia so we know where to add the tool + # solid_geometry + for k, v in tools_storage.items(): + if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals, + tool)): + current_uid = int(k) - # add the solid_geometry to the current too in self.paint_tools dictionary - # and then reset the temporary list that stored that solid_geometry - v['solid_geometry'] = deepcopy(cleared_geo) - v['data']['name'] = name - break - geo_obj.tools[current_uid] = dict(tools_storage[current_uid]) - else: - app_obj.log.debug("There are no geometries in the cleared polygon.") + # add the solid_geometry to the current too in self.paint_tools dictionary + # and then reset the temporary list that stored that solid_geometry + v['solid_geometry'] = flatten_shapely_geometry(cleared_geo) + v['data']['name'] = name + break + geo_obj.tools[current_uid] = dict(tools_storage[current_uid]) + else: + app_obj.log.debug("There are no geometries in the cleared polygon.") # delete tools with empty geometry # look for keys in the tools_storage dict that have 'solid_geometry' values empty @@ -3747,7 +3708,7 @@ class NonCopperClear(AppTool, Gerber): # add the solid_geometry to the current too in self.paint_tools dictionary # and then reset the temporary list that stored that solid_geometry - v['solid_geometry'] = deepcopy(cleared_area) + v['solid_geometry'] = flatten_shapely_geometry(cleared_area) v['data']['name'] = name cleared_area[:] = [] break diff --git a/appPlugins/ToolPaint.py b/appPlugins/ToolPaint.py index bb214bfc..b9689f55 100644 --- a/appPlugins/ToolPaint.py +++ b/appPlugins/ToolPaint.py @@ -31,7 +31,7 @@ import appTranslation as fcTranslate import builtins from appParsers.ParseGerber import Gerber -from camlib import Geometry, AppRTreeStorage, grace +from camlib import Geometry, AppRTreeStorage, grace, flatten_shapely_geometry fcTranslate.apply_language('strings') if '_' not in builtins.__dict__: @@ -1938,7 +1938,7 @@ class ToolPaint(AppTool, Gerber): paint_offset = float(tools_storage[current_uid]['data']['tools_paint_offset']) poly_buf = [] - for pol in geometry: + for pol in flatten_shapely_geometry(geometry): buffered_pol = pol.buffer(-paint_offset) if buffered_pol and not buffered_pol.is_empty: poly_buf.append(buffered_pol) @@ -1960,37 +1960,24 @@ class ToolPaint(AppTool, Gerber): # ----------------------------- try: cp = [] - try: - for pp in poly_buf: - # provide the app with a way to process the GUI events when in a blocking loop - QtWidgets.QApplication.processEvents() - if self.app.abort_flag: - # graceful abort requested by the user - raise grace - geo_res = self.paint_polygon_worker(pp, tooldiameter=tool_dia, over=over, conn=conn, - cont=cont, paint_method=paint_method, obj=obj, - prog_plot=prog_plot) - if geo_res: - cp.append(geo_res) - pol_nr += 1 - disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100])) - # log.debug("Polygons cleared: %d" % pol_nr) - - if old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - old_disp_number = disp_number - except TypeError: + for pp in poly_buf: # provide the app with a way to process the GUI events when in a blocking loop QtWidgets.QApplication.processEvents() if self.app.abort_flag: # graceful abort requested by the user raise grace - - geo_res = self.paint_polygon_worker(poly_buf, tooldiameter=tool_dia, over=over, conn=conn, + geo_res = self.paint_polygon_worker(pp, tooldiameter=tool_dia, over=over, conn=conn, cont=cont, paint_method=paint_method, obj=obj, prog_plot=prog_plot) if geo_res: cp.append(geo_res) + pol_nr += 1 + disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100])) + # log.debug("Polygons cleared: %d" % pol_nr) + + if old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + old_disp_number = disp_number total_geometry = [] if cp: @@ -2035,7 +2022,7 @@ class ToolPaint(AppTool, Gerber): geo_obj.tools.clear() geo_obj.tools = dict(tools_storage) - geo_obj.solid_geometry = unary_union(final_solid_geometry) + geo_obj.solid_geometry = flatten_shapely_geometry(unary_union(final_solid_geometry)) try: if isinstance(geo_obj.solid_geometry, list): @@ -2090,6 +2077,7 @@ class ToolPaint(AppTool, Gerber): poly_buf.append(buffered_pol) poly_buf = unary_union(poly_buf) + poly_buf = flatten_shapely_geometry(poly_buf) if not poly_buf: self.app.inform.emit( @@ -2136,47 +2124,7 @@ class ToolPaint(AppTool, Gerber): # ----------------------------- try: cleared_geo = [] - try: - for pp in poly_buf: - # provide the app with a way to process the GUI events when in a blocking loop - QtWidgets.QApplication.processEvents() - if self.app.abort_flag: - # graceful abort requested by the user - raise grace - - # speedup the clearing by not trying to clear polygons that is clear they can't be - # cleared with the current tool. this tremendously reduce the clearing time - check_dist = -tool_dia / 2.0 - check_buff = pp.buffer(check_dist) - if not check_buff or check_buff.is_empty: - continue - - geo_res = self.paint_polygon_worker(pp, tooldiameter=tool_dia, over=over, conn=conn, - cont=cont, paint_method=paint_method, obj=obj, - prog_plot=prog_plot) - geo_elems = list(geo_res.get_objects()) - # See if the polygon was completely cleared - pp_cleared = unary_union(geo_elems).buffer(tool_dia / 2.0) - rest_geo = pp.difference(pp_cleared) - if rest_geo and not rest_geo.is_empty: - try: - for r in rest_geo: - if r.is_valid: - rest_list.append(r) - except TypeError: - if rest_geo.is_valid: - rest_list.append(rest_geo) - - if geo_res: - cleared_geo += geo_elems - pol_nr += 1 - disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100])) - # log.debug("Polygons cleared: %d" % pol_nr) - - if old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - old_disp_number = disp_number - except TypeError: + for pp in poly_buf: # provide the app with a way to process the GUI events when in a blocking loop QtWidgets.QApplication.processEvents() if self.app.abort_flag: @@ -2186,30 +2134,32 @@ class ToolPaint(AppTool, Gerber): # speedup the clearing by not trying to clear polygons that is clear they can't be # cleared with the current tool. this tremendously reduce the clearing time check_dist = -tool_dia / 2.0 - check_buff = poly_buf.buffer(check_dist) + check_buff = pp.buffer(check_dist) if not check_buff or check_buff.is_empty: continue - geo_res = self.paint_polygon_worker(poly_buf, tooldiameter=tool_dia, over=over, conn=conn, + geo_res = self.paint_polygon_worker(pp, tooldiameter=tool_dia, over=over, conn=conn, cont=cont, paint_method=paint_method, obj=obj, prog_plot=prog_plot) geo_elems = list(geo_res.get_objects()) - # See if the polygon was completely cleared pp_cleared = unary_union(geo_elems).buffer(tool_dia / 2.0) - rest_geo = poly_buf.difference(pp_cleared) - if rest_geo and not rest_geo.is_empty: - try: - for r in rest_geo: - if r.is_valid: - rest_list.append(r) - except TypeError: - if rest_geo.is_valid: - rest_list.append(rest_geo) + rest_geo = pp.difference(pp_cleared) + if rest_geo: + rest_geo = flatten_shapely_geometry(rest_geo) + for r in rest_geo: + if r.is_valid and not r.is_empty: + rest_list.append(r) if geo_res: cleared_geo += geo_elems + pol_nr += 1 + disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100])) + # log.debug("Polygons cleared: %d" % pol_nr) + if old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + old_disp_number = disp_number except grace: return "fail" except Exception as e: @@ -2235,26 +2185,23 @@ class ToolPaint(AppTool, Gerber): buffered_cleared = unary_union(cleared_geo) buffered_cleared = buffered_cleared.buffer(tool_dia / 2.0) - poly_buf = poly_buf.difference(buffered_cleared) + poly_buf = MultiPolygon(poly_buf).difference(buffered_cleared) + poly_buf = flatten_shapely_geometry(poly_buf) tmp = [] - try: - for p in poly_buf: - if p.is_valid: - tmp.append(p) - except TypeError: - if poly_buf.is_valid: - tmp.append(poly_buf) - + for p in poly_buf: + if p.is_valid: + tmp.append(p) tmp += rest_list + print(tmp) poly_buf = MultiPolygon(tmp) if not poly_buf.is_valid: poly_buf = unary_union(tmp) - if not poly_buf or poly_buf.is_empty or not poly_buf.is_valid: app_obj.log.debug("Rest geometry empty. Breaking.") break + poly_buf = flatten_shapely_geometry(poly_buf) geo_obj.multigeo = True geo_obj.obj_options["tools_mill_tooldia"] = '0.0' @@ -2290,7 +2237,7 @@ class ToolPaint(AppTool, Gerber): "Change the painting parameters and try again.") ) return "fail" - geo_obj.solid_geometry = unary_union(final_solid_geometry) + geo_obj.solid_geometry = flatten_shapely_geometry(unary_union(final_solid_geometry)) else: return 'fail' try: