From 8165c797a4047bb38fb2b7bd870ea2dffddc833e Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Sun, 24 May 2020 04:22:49 +0300 Subject: [PATCH] - changes some icons - added a new GUI element which is a evaluated LineEdit that accepts only float numbers and /,*,+,-,% chars - finished the Etch Compensation Tool --- AppGUI/GUIElements.py | 13 +- AppGUI/MainGUI.py | 4 +- AppObjects/FlatCAMExcellon.py | 99 +++++++++-- AppObjects/FlatCAMGerber.py | 5 +- AppTools/ToolEtchCompensation.py | 172 +++++++++++--------- AppTools/ToolInvertGerber.py | 3 - App_Main.py | 30 ++-- CHANGELOG.md | 6 + Common.py | 61 ++++++- assets/resources/dark_resources/etch_32.png | Bin 811 -> 18526 bytes assets/resources/etch_32.png | Bin 637 -> 9517 bytes camlib.py | 2 + 12 files changed, 277 insertions(+), 118 deletions(-) diff --git a/AppGUI/GUIElements.py b/AppGUI/GUIElements.py index 97fe2b6a..0264cf64 100644 --- a/AppGUI/GUIElements.py +++ b/AppGUI/GUIElements.py @@ -573,6 +573,7 @@ class EvalEntry(QtWidgets.QLineEdit): def __init__(self, parent=None): super(EvalEntry, self).__init__(parent) self.readyToEdit = True + self.editingFinished.connect(self.on_edit_finished) def on_edit_finished(self): @@ -599,7 +600,6 @@ class EvalEntry(QtWidgets.QLineEdit): def get_value(self): raw = str(self.text()).strip(' ') - evaled = 0.0 try: evaled = eval(raw) except Exception as e: @@ -656,6 +656,17 @@ class EvalEntry2(QtWidgets.QLineEdit): return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height()) +class NumericalEvalEntry(EvalEntry): + """ + Will evaluate the input and return a value. Accepts only float numbers and formulas using the operators: /,*,+,-,% + """ + def __init__(self): + super().__init__() + + regex = QtCore.QRegExp("[0-9\/\*\+\-\%\.\s]*") + validator = QtGui.QRegExpValidator(regex, self) + self.setValidator(validator) + class FCSpinner(QtWidgets.QSpinBox): returnPressed = QtCore.pyqtSignal() diff --git a/AppGUI/MainGUI.py b/AppGUI/MainGUI.py index cec8ede0..eec4fd42 100644 --- a/AppGUI/MainGUI.py +++ b/AppGUI/MainGUI.py @@ -855,7 +855,7 @@ class MainGUI(QtWidgets.QMainWindow): self.copy_btn = self.toolbaredit.addAction( QtGui.QIcon(self.app.resource_location + '/copy_file32.png'), _("Copy")) self.delete_btn = self.toolbaredit.addAction( - QtGui.QIcon(self.app.resource_location + '/delete_file32.png'), _("&Delete")) + QtGui.QIcon(self.app.resource_location + '/trash32.png'), _("&Delete")) self.toolbaredit.addSeparator() self.distance_btn = self.toolbaredit.addAction( QtGui.QIcon(self.app.resource_location + '/distance32.png'), _("Distance Tool")) @@ -1851,7 +1851,7 @@ class MainGUI(QtWidgets.QMainWindow): self.copy_btn = self.toolbaredit.addAction( QtGui.QIcon(self.app.resource_location + '/copy_file32.png'), _("Copy")) self.delete_btn = self.toolbaredit.addAction( - QtGui.QIcon(self.app.resource_location + '/delete_file32.png'), _("&Delete")) + QtGui.QIcon(self.app.resource_location + '/trash32.png'), _("&Delete")) self.toolbaredit.addSeparator() self.distance_btn = self.toolbaredit.addAction( QtGui.QIcon(self.app.resource_location + '/distance32.png'), _("Distance Tool")) diff --git a/AppObjects/FlatCAMExcellon.py b/AppObjects/FlatCAMExcellon.py index c0250871..262ba293 100644 --- a/AppObjects/FlatCAMExcellon.py +++ b/AppObjects/FlatCAMExcellon.py @@ -31,7 +31,7 @@ if '_' not in builtins.__dict__: class ExcellonObject(FlatCAMObj, Excellon): """ - Represents Excellon/Drill code. + Represents Excellon/Drill code. An object stored in the FlatCAM objects collection (a dict) """ ui_type = ExcellonObjectUI @@ -146,9 +146,11 @@ class ExcellonObject(FlatCAMObj, Excellon): If only one object is in exc_list parameter then this function will copy that object in the exc_final - :param exc_list: List or one object of ExcellonObject Objects to join. - :param exc_final: Destination ExcellonObject object. - :return: None + :param exc_list: List or one object of ExcellonObject Objects to join. + :type exc_list: list + :param exc_final: Destination ExcellonObject object. + :type exc_final: class + :return: None """ if decimals is None: @@ -316,6 +318,12 @@ class ExcellonObject(FlatCAMObj, Excellon): exc_final.create_geometry() def build_ui(self): + """ + Will (re)build the Excellon UI updating it (the tool table) + + :return: None + :rtype: + """ FlatCAMObj.build_ui(self) # Area Exception - exclusion shape added signal @@ -586,9 +594,9 @@ class ExcellonObject(FlatCAMObj, Excellon): Configures the user interface for this object. Connects options to form fields. - :param ui: User interface object. - :type ui: ExcellonObjectUI - :return: None + :param ui: User interface object. + :type ui: ExcellonObjectUI + :return: None """ FlatCAMObj.set_ui(self, ui) @@ -729,6 +737,12 @@ class ExcellonObject(FlatCAMObj, Excellon): self.ui.operation_radio.setEnabled(False) def ui_connect(self): + """ + Will connect all signals in the Excellon UI that needs to be connected + + :return: None + :rtype: + """ # selective plotting for row in range(self.ui.tools_table.rowCount() - 2): @@ -751,6 +765,12 @@ class ExcellonObject(FlatCAMObj, Excellon): current_widget.returnPressed.connect(self.form_to_storage) def ui_disconnect(self): + """ + Will disconnect all signals in the Excellon UI that needs to be disconnected + + :return: None + :rtype: + """ # selective plotting for row in range(self.ui.tools_table.rowCount()): try: @@ -793,6 +813,12 @@ class ExcellonObject(FlatCAMObj, Excellon): pass def on_row_selection_change(self): + """ + Called when the user clicks on a row in Tools Table + + :return: None + :rtype: + """ self.ui_disconnect() sel_rows = [] @@ -843,6 +869,14 @@ class ExcellonObject(FlatCAMObj, Excellon): self.ui_connect() def storage_to_form(self, dict_storage): + """ + Will update the GUI with data from the "storage" in this case the dict self.tools + + :param dict_storage: A dictionary holding the data relevant for gnerating Gcode from Excellon + :type dict_storage: dict + :return: None + :rtype: + """ for form_key in self.form_fields: for storage_key in dict_storage: if form_key == storage_key and form_key not in \ @@ -854,6 +888,12 @@ class ExcellonObject(FlatCAMObj, Excellon): pass def form_to_storage(self): + """ + Will update the 'storage' attribute which is the dict self.tools with data collected from GUI + + :return: None + :rtype: + """ if self.ui.tools_table.rowCount() == 0: # there is no tool in tool table so we can't save the GUI elements values to storage return @@ -882,6 +922,14 @@ class ExcellonObject(FlatCAMObj, Excellon): self.ui_connect() def on_operation_type(self, val): + """ + Called by a RadioSet activated_custom signal + + :param val: Parameter passes by the signal that called this method + :type val: str + :return: None + :rtype: + """ if val == 'mill': self.ui.mill_type_label.show() self.ui.milling_type_radio.show() @@ -912,8 +960,8 @@ class ExcellonObject(FlatCAMObj, Excellon): Returns the keys to the self.tools dictionary corresponding to the selections on the tool list in the AppGUI. - :return: List of tools. - :rtype: list + :return: List of tools. + :rtype: list """ return [str(x.text()) for x in self.ui.tools_table.selectedItems()] @@ -922,8 +970,8 @@ class ExcellonObject(FlatCAMObj, Excellon): """ Returns a list of lists, each list in the list is made out of row elements - :return: List of table_tools items. - :rtype: list + :return: List of table_tools items. + :rtype: list """ table_tools_items = [] for x in self.ui.tools_table.selectedItems(): @@ -951,7 +999,21 @@ class ExcellonObject(FlatCAMObj, Excellon): def export_excellon(self, whole, fract, e_zeros=None, form='dec', factor=1, slot_type='routing'): """ Returns two values, first is a boolean , if 1 then the file has slots and second contain the Excellon code - :return: has_slots and Excellon_code + + :param whole: Integer part digits + :type whole: int + :param fract: Fractional part digits + :type fract: int + :param e_zeros: Excellon zeros suppression: LZ or TZ + :type e_zeros: str + :param form: Excellon format: 'dec', + :type form: str + :param factor: Conversion factor + :type factor: float + :param slot_type: How to treat slots: "routing" or "drilling" + :type slot_type: str + :return: A tuple: (has_slots, Excellon_code) -> (bool, str) + :rtype: tuple """ excellon_code = '' @@ -1123,8 +1185,8 @@ class ExcellonObject(FlatCAMObj, Excellon): object's options and returns a (success, msg) tuple as feedback for shell operations. - :return: Success/failure condition tuple (bool, str). - :rtype: tuple + :return: Success/failure condition tuple (bool, str). + :rtype: tuple """ # Get the tools from the list. These are keys @@ -1167,6 +1229,15 @@ class ExcellonObject(FlatCAMObj, Excellon): return False, "Error: Milling tool is larger than hole." def geo_init(geo_obj, app_obj): + """ + + :param geo_obj: New object + :type geo_obj: GeometryObject + :param app_obj: App + :type app_obj: FlatCAMApp.App + :return: + :rtype: + """ assert geo_obj.kind == 'geometry', "Initializer expected a GeometryObject, got %s" % type(geo_obj) # ## Add properties to the object diff --git a/AppObjects/FlatCAMGerber.py b/AppObjects/FlatCAMGerber.py index 8758d6dc..0b9552e7 100644 --- a/AppObjects/FlatCAMGerber.py +++ b/AppObjects/FlatCAMGerber.py @@ -896,7 +896,7 @@ class GerberObject(FlatCAMObj, Gerber): }) for nr_pass in range(passes): - iso_offset = dia * ((2 * nr_pass + 1) / 2.0) - (nr_pass * overlap * dia) + iso_offset = dia * ((2 * nr_pass + 1) / 2.0000001) - (nr_pass * overlap * dia) # if milling type is climb then the move is counter-clockwise around features mill_dir = 1 if milling_type == 'cl' else 0 @@ -945,8 +945,7 @@ class GerberObject(FlatCAMObj, Gerber): self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot) else: for i in range(passes): - - offset = dia * ((2 * i + 1) / 2.0) - (i * overlap * dia) + offset = dia * ((2 * i + 1) / 2.0000001) - (i * overlap * dia) if passes > 1: if outname is None: if self.iso_type == 0: diff --git a/AppTools/ToolEtchCompensation.py b/AppTools/ToolEtchCompensation.py index c4c15565..57f6bfb6 100644 --- a/AppTools/ToolEtchCompensation.py +++ b/AppTools/ToolEtchCompensation.py @@ -8,9 +8,9 @@ from PyQt5 import QtWidgets, QtCore from AppTool import AppTool -from AppGUI.GUIElements import FCButton, FCDoubleSpinner, RadioSet, FCComboBox +from AppGUI.GUIElements import FCButton, FCDoubleSpinner, RadioSet, FCComboBox, NumericalEvalEntry -from shapely.geometry import box +from shapely.ops import unary_union from copy import deepcopy @@ -95,8 +95,8 @@ class ToolEtchCompensation(AppTool): self.thick_entry.set_range(0.0000, 9999.9999) self.thick_entry.setObjectName(_("Thickness")) - grid0.addWidget(self.thick_label, 5, 0, 1, 2) - grid0.addWidget(self.thick_entry, 6, 0, 1, 2) + grid0.addWidget(self.thick_label, 5, 0) + grid0.addWidget(self.thick_entry, 5, 1) self.ratio_label = QtWidgets.QLabel('%s:' % _("Ratio")) self.ratio_label.setToolTip( @@ -106,17 +106,48 @@ class ToolEtchCompensation(AppTool): "- preselection -> value which depends on a selection of etchants") ) self.ratio_radio = RadioSet([ - {'label': _('PreSelection'), 'value': 'p'}, - {'label': _('Custom'), 'value': 'c'} - ]) + {'label': _('Custom'), 'value': 'c'}, + {'label': _('PreSelection'), 'value': 'p'} + ], orientation='vertical', stretch=False) grid0.addWidget(self.ratio_label, 7, 0, 1, 2) grid0.addWidget(self.ratio_radio, 8, 0, 1, 2) + # Etchants + self.etchants_label = QtWidgets.QLabel('%s:' % _('Etchants')) + self.etchants_label.setToolTip( + _("A list of etchants.") + ) + self.etchants_combo = FCComboBox(callback=self.confirmation_message) + self.etchants_combo.setObjectName(_("Etchants")) + self.etchants_combo.addItems(["CuCl2", "FeCl3"]) + + grid0.addWidget(self.etchants_label, 9, 0) + grid0.addWidget(self.etchants_combo, 9, 1) + + # Etch Factor + self.factor_label = QtWidgets.QLabel('%s:' % _('Etch factor')) + self.factor_label.setToolTip( + _("The ratio between depth etch and lateral etch .\n" + "Accepts real numbers and formulas using the operators: /,*,+,-,%") + ) + self.factor_entry = NumericalEvalEntry() + self.factor_entry.setPlaceholderText(_("Real number or formula")) + self.factor_entry.setObjectName(_("Etch_factor")) + + # Hide the Etchants and Etch factor + self.etchants_label.hide() + self.etchants_combo.hide() + self.factor_label.hide() + self.factor_entry.hide() + + grid0.addWidget(self.factor_label, 10, 0) + grid0.addWidget(self.factor_entry, 10, 1) + separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) - grid0.addWidget(separator_line, 9, 0, 1, 2) + grid0.addWidget(separator_line, 13, 0, 1, 2) self.compensate_btn = FCButton(_('Compensate')) self.compensate_btn.setToolTip( @@ -128,7 +159,7 @@ class ToolEtchCompensation(AppTool): font-weight: bold; } """) - grid0.addWidget(self.compensate_btn, 10, 0, 1, 2) + grid0.addWidget(self.compensate_btn, 14, 0, 1, 2) self.tools_box.addStretch() @@ -145,7 +176,7 @@ class ToolEtchCompensation(AppTool): """) self.tools_box.addWidget(self.reset_button) - self.compensate_btn.clicked.connect(self.on_grb_invert) + self.compensate_btn.clicked.connect(self.on_compensate) self.reset_button.clicked.connect(self.set_tool_ui) self.ratio_radio.activated_custom.connect(self.on_ratio_change) @@ -153,8 +184,8 @@ class ToolEtchCompensation(AppTool): AppTool.install(self, icon, separator, shortcut='', **kwargs) def run(self, toggle=True): - self.app.defaults.report_usage("ToolInvertGerber()") - log.debug("ToolInvertGerber() is running ...") + self.app.defaults.report_usage("ToolEtchCompensation()") + log.debug("ToolEtchCompensation() is running ...") if toggle: # if the splitter is hidden, display it, else hide it but only if the current widget is the same @@ -178,28 +209,40 @@ class ToolEtchCompensation(AppTool): AppTool.run(self) self.set_tool_ui() - self.app.ui.notebook.setTabText(2, _("Invert Tool")) + self.app.ui.notebook.setTabText(2, _("Etch Compensation Tool")) def set_tool_ui(self): - self.thick_entry.set_value(18) - self.ratio_radio.set_value('p') + self.thick_entry.set_value(18.0) + self.ratio_radio.set_value('c') def on_ratio_change(self, val): - pass + """ + Called on activated_custom signal of the RadioSet GUI element self.radio_ratio - def on_grb_invert(self): - margin = self.margin_entry.get_value() - if round(margin, self.decimals) == 0.0: - margin = 1E-10 + :param val: 'c' or 'p': 'c' means custom factor and 'p' means preselected etchants + :type val: str + :return: None + :rtype: + """ + if val == 'c': + self.etchants_label.hide() + self.etchants_combo.hide() + self.factor_label.show() + self.factor_entry.show() + else: + self.etchants_label.show() + self.etchants_combo.show() + self.factor_label.hide() + self.factor_entry.hide() - join_style = {'r': 1, 'b': 3, 's': 2}[self.join_radio.get_value()] - if join_style is None: - join_style = 'r' + def on_compensate(self): + ratio_type = self.ratio_radio.get_value() + thickness = self.thick_entry.get_value() / 1000 # in microns grb_circle_steps = int(self.app.defaults["gerber_circle_steps"]) obj_name = self.gerber_combo.currentText() - outname = obj_name + "_inverted" + outname = obj_name + "_comp" # Get source object. try: @@ -214,74 +257,51 @@ class ToolEtchCompensation(AppTool): self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(obj_name))) return - xmin, ymin, xmax, ymax = grb_obj.bounds() - - grb_box = box(xmin, ymin, xmax, ymax).buffer(margin, resolution=grb_circle_steps, join_style=join_style) + if ratio_type == 'c': + etch_factor = 1 / self.factor_entry.get_value() + else: + etchant = self.etchants_combo.get_value() + if etchant == "CuCl2": + etch_factor = 0.33 + else: + etch_factor = 0.25 + offset = thickness / etch_factor try: __ = iter(grb_obj.solid_geometry) except TypeError: grb_obj.solid_geometry = list(grb_obj.solid_geometry) - new_solid_geometry = deepcopy(grb_box) + new_solid_geometry = [] for poly in grb_obj.solid_geometry: - new_solid_geometry = new_solid_geometry.difference(poly) + new_solid_geometry.append(poly.buffer(offset, int(grb_circle_steps))) + new_solid_geometry = unary_union(new_solid_geometry) new_options = {} for opt in grb_obj.options: new_options[opt] = deepcopy(grb_obj.options[opt]) - new_apertures = {} + new_apertures = deepcopy(grb_obj.apertures) - # for apid, val in grb_obj.apertures.items(): - # new_apertures[apid] = {} - # for key in val: - # if key == 'geometry': - # new_apertures[apid]['geometry'] = [] - # for elem in val['geometry']: - # geo_elem = {} - # if 'follow' in elem: - # try: - # geo_elem['clear'] = elem['follow'].buffer(val['size'] / 2.0).exterior - # except AttributeError: - # # TODO should test if width or height is bigger - # geo_elem['clear'] = elem['follow'].buffer(val['width'] / 2.0).exterior - # if 'clear' in elem: - # if isinstance(elem['clear'], Polygon): - # try: - # geo_elem['solid'] = elem['clear'].buffer(val['size'] / 2.0, grb_circle_steps) - # except AttributeError: - # # TODO should test if width or height is bigger - # geo_elem['solid'] = elem['clear'].buffer(val['width'] / 2.0, grb_circle_steps) - # else: - # geo_elem['follow'] = elem['clear'] - # new_apertures[apid]['geometry'].append(deepcopy(geo_elem)) - # else: - # new_apertures[apid][key] = deepcopy(val[key]) - - if '0' not in new_apertures: - new_apertures['0'] = {} - new_apertures['0']['type'] = 'C' - new_apertures['0']['size'] = 0.0 - new_apertures['0']['geometry'] = [] - - try: - for poly in new_solid_geometry: - new_el = {} - new_el['solid'] = poly - new_el['follow'] = poly.exterior - new_apertures['0']['geometry'].append(new_el) - except TypeError: - new_el = {} - new_el['solid'] = new_solid_geometry - new_el['follow'] = new_solid_geometry.exterior - new_apertures['0']['geometry'].append(new_el) - - for td in new_apertures: - print(td, new_apertures[td]) + for ap in new_apertures: + for k in ap: + if k == 'geometry': + for geo_el in new_apertures[ap]['geometry']: + if 'solid' in geo_el: + geo_el['solid'] = geo_el['solid'].buffer(offset, int(grb_circle_steps)) def init_func(new_obj, app_obj): + """ + Init a new object in FlatCAM Object collection + + :param new_obj: New object + :type new_obj: ObjectCollection + :param app_obj: App + :type app_obj: App_Main.App + :return: None + :rtype: + """ new_obj.options.update(new_options) new_obj.options['name'] = outname new_obj.fill_color = deepcopy(grb_obj.fill_color) diff --git a/AppTools/ToolInvertGerber.py b/AppTools/ToolInvertGerber.py index 6acc013b..50579a93 100644 --- a/AppTools/ToolInvertGerber.py +++ b/AppTools/ToolInvertGerber.py @@ -278,9 +278,6 @@ class ToolInvertGerber(AppTool): new_el['follow'] = new_solid_geometry.exterior new_apertures['0']['geometry'].append(new_el) - for td in new_apertures: - print(td, new_apertures[td]) - def init_func(new_obj, app_obj): new_obj.options.update(new_options) new_obj.options['name'] = outname diff --git a/App_Main.py b/App_Main.py index dc72594a..f5202d02 100644 --- a/App_Main.py +++ b/App_Main.py @@ -42,7 +42,7 @@ import socket # ################################### Imports part of FlatCAM ############################################# # #################################################################################################################### -# Diverse +# Various from Common import LoudDict from Common import color_variant from Common import ExclusionAreas @@ -53,8 +53,10 @@ from AppDatabase import ToolsDB2 from vispy.gloo.util import _screenshot from vispy.io import write_png -# FlatCAM Objects +# FlatCAM defaults (preferences) from defaults import FlatCAMDefaults + +# FlatCAM Objects from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI from AppGUI.preferences.PreferencesUIManager import PreferencesUIManager from AppObjects.ObjectCollection import * @@ -105,7 +107,7 @@ if '_' not in builtins.__dict__: class App(QtCore.QObject): """ - The main application class. The constructor starts the AppGUI. + The main application class. The constructor starts the GUI and all other classes used by the program. """ # ############################################################################################################### @@ -289,7 +291,7 @@ class App(QtCore.QObject): self.new_launch.start.emit() # ############################################################################################################ - # # ######################################## OS-specific ##################################################### + # ########################################## OS-specific ##################################################### # ############################################################################################################ portable = False @@ -401,13 +403,12 @@ class App(QtCore.QObject): json.dump([], fp) fp.close() - # Application directory. CHDIR to it. Otherwise, trying to load - # GUI icons will fail as their path is relative. + # Application directory. CHDIR to it. Otherwise, trying to load GUI icons will fail as their path is relative. # This will fail under cx_freeze ... self.app_home = os.path.dirname(os.path.realpath(__file__)) - App.log.debug("Application path is " + self.app_home) - App.log.debug("Started in " + os.getcwd()) + log.debug("Application path is " + self.app_home) + log.debug("Started in " + os.getcwd()) # cx_freeze workaround if os.path.isfile(self.app_home): @@ -451,7 +452,6 @@ class App(QtCore.QObject): # ########################################################################################################### # ###################################### Setting the Splash Screen ########################################## # ########################################################################################################### - splash_settings = QSettings("Open Source", "FlatCAM") if splash_settings.contains("splash_screen"): show_splash = splash_settings.value("splash_screen") @@ -1923,7 +1923,7 @@ class App(QtCore.QObject): self.corners_tool.install(icon=QtGui.QIcon(self.resource_location + '/corners_32.png'), pos=self.ui.menutool) self.etch_tool = ToolEtchCompensation(self) - self.etch_tool.install(icon=QtGui.QIcon(self.resource_location + '/etcg_32.png'), pos=self.ui.menutool) + self.etch_tool.install(icon=QtGui.QIcon(self.resource_location + '/etch_32.png'), pos=self.ui.menutool) self.transform_tool = ToolTransform(self) self.transform_tool.install(icon=QtGui.QIcon(self.resource_location + '/transform.png'), @@ -4811,6 +4811,16 @@ class App(QtCore.QObject): self.defaults.report_usage("on_copy_command()") def initialize(obj_init, app): + """ + + :param obj_init: the new object + :type obj_init: class + :param app: An instance of the App class + :type app: App + :return: None + :rtype: + """ + obj_init.solid_geometry = deepcopy(obj.solid_geometry) try: obj_init.follow_geometry = deepcopy(obj.follow_geometry) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21ccc48c..26af1127 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ CHANGELOG for FlatCAM beta ================================================= +24.05.2020 + +- changes some icons +- added a new GUI element which is a evaluated LineEdit that accepts only float numbers and /,*,+,-,% chars +- finished the Etch Compensation Tool + 23.05.2020 - fixed a issue when testing for Exclusion areas overlap over the Geometry object solid_geometry diff --git a/Common.py b/Common.py index 7887954b..9d89b66b 100644 --- a/Common.py +++ b/Common.py @@ -12,7 +12,7 @@ # ########################################################## from PyQt5 import QtCore -from shapely.geometry import Polygon, MultiPolygon, Point, LineString +from shapely.geometry import Polygon, Point, LineString from shapely.ops import unary_union from AppGUI.VisPyVisuals import ShapeCollection @@ -32,7 +32,9 @@ if '_' not in builtins.__dict__: class GracefulException(Exception): - # Graceful Exception raised when the user is requesting to cancel the current threaded task + """ + Graceful Exception raised when the user is requesting to cancel the current threaded task + """ def __init__(self): super().__init__() @@ -107,8 +109,11 @@ def color_variant(hex_color, bright_factor=1): Takes a color in HEX format #FF00FF and produces a lighter or darker variant :param hex_color: color to change - :param bright_factor: factor to change the color brightness [0 ... 1] - :return: modified color + :type hex_color: str + :param bright_factor: factor to change the color brightness [0 ... 1] + :type bright_factor: float + :return: Modified color + :rtype: str """ if len(hex_color) != 7: @@ -133,7 +138,9 @@ def color_variant(hex_color, bright_factor=1): class ExclusionAreas(QtCore.QObject): - + """ + Functionality for adding Exclusion Areas for the Excellon and Geometry FlatCAM Objects + """ e_shape_modified = QtCore.pyqtSignal() def __init__(self, app): @@ -230,6 +237,14 @@ class ExclusionAreas(QtCore.QObject): # To be called after clicking on the plot. def on_mouse_release(self, event): + """ + Called on mouse click release. + + :param event: Mouse event + :type event: + :return: None + :rtype: + """ if self.app.is_legacy is False: event_pos = event.pos # event_is_dragging = event.is_dragging @@ -417,6 +432,13 @@ class ExclusionAreas(QtCore.QObject): self.e_shape_modified.emit() def area_disconnect(self): + """ + Will do the cleanup. Will disconnect the mouse events for the custom handlers in this class and initialize + certain class attributes. + + :return: None + :rtype: + """ 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) @@ -441,8 +463,15 @@ class ExclusionAreas(QtCore.QObject): self.app.call_source = "app" self.app.inform.emit("[WARNING_NOTCL] %s" % _("Cancelled. Area exclusion drawing was interrupted.")) - # called on mouse move def on_mouse_move(self, event): + """ + Called on mouse move + + :param event: mouse event + :type event: + :return: None + :rtype: + """ shape_type = self.shape_type_button.get_value() if self.app.is_legacy is False: @@ -513,6 +542,12 @@ class ExclusionAreas(QtCore.QObject): data=(curr_pos[0], curr_pos[1])) def on_clear_area_click(self): + """ + Slot for clicking the button for Deleting all the Exclusion areas. + + :return: None + :rtype: + """ self.clear_shapes() # restore the default StyleSheet @@ -527,6 +562,12 @@ class ExclusionAreas(QtCore.QObject): self.cnc_button.setToolTip('%s' % _("Generate the CNC Job object.")) def clear_shapes(self): + """ + Will delete all the Exclusion areas; will delete on canvas any possible selection box for the Exclusion areas. + + :return: None + :rtype: + """ self.exclusion_areas_storage.clear() AppTool.delete_moving_selection_shape(self) self.app.delete_selection_shape() @@ -536,8 +577,9 @@ class ExclusionAreas(QtCore.QObject): def delete_sel_shapes(self, idxs): """ - :param idxs: list of indexes in self.exclusion_areas_storage list to be deleted - :return: + :param idxs: list of indexes in self.exclusion_areas_storage list to be deleted + :type idxs: list + :return: None """ # delete all plotted shapes @@ -583,7 +625,8 @@ class ExclusionAreas(QtCore.QObject): def travel_coordinates(self, start_point, end_point, tooldia): """ - WIll create a path the go around the exclusion areas on the shortest path + WIll create a path the go around the exclusion areas on the shortest path when travelling (at a Z above the + material). :param start_point: X,Y coordinates for the start point of the travel line :type start_point: tuple diff --git a/assets/resources/dark_resources/etch_32.png b/assets/resources/dark_resources/etch_32.png index ec8687ab79f6e8328a7700550d0de20b3ec1af1f..2f06671bd616263a5b85f9569daf975caefc2a19 100644 GIT binary patch literal 18526 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 WvrF1g1e^>(x7F{c72L3V@qYm6wh|ox delta 642 zcmcaNfpImPVrPJ#J1>_M7Xt$WucwDg5Rg^?VGd>>>9VBj*hEG9`bz;mA+A9Flb3Iv zynOfg#hb@3-#&iv?*IS)k6(h>K<>-8SFiqm_~8Rk@C*=x!DAo*seJq7#oI?O-#!5; zdj~`hU%!6z`ZZ7rB=`!X_sI*OIEVofd<8NSD1PtdOCaOn%lZ$GK#JZxe*PM07DUng z=kK2a^}Kllbj>5MW}xGqy?OKC*_+#M-vUj)`|cgsN+9{}$;p+P!o-U3d7N^f%y3W^RAky&g@e$q?9U5Ja7QE}MKKU+$Y_g2G$Ig|a|0r;h>jTwOLb6?co!qDToZ7ph zo-+BaefatFhrPS_deZ{hxaUm!&F853Kau5@+I~K^*jSDqlTC_3{2yOGe!!`D|Bo{Q z2@+q^HYPmTUy#hD7R;2EaAv01h9wS>dk@a4-Se06$)$fXPXwHVa<=(c-%u`TD3JA? zz1x0fx>d|NbG;q*^BDBguQb=E`0z%ls` PHkrVbA6q8tdKdx#n$=;_ diff --git a/assets/resources/etch_32.png b/assets/resources/etch_32.png index a0c23ce2a9b685045ef3410bcb8069a6027feb04..dd70dd742a63ccac0c336e32a701e8b87c44e9eb 100644 GIT binary patch literal 9517 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 ho7eB?S6_2@`d`aH2POaj delta 451 zcmZ4M^_NAlGr-TCmrII^fq{Y7)59eQNGpIa2MdtAS-h!!qGDFPAfLLB)3QY~85kJ7 zJzX3_JdWR;V(86mDA6{*A~dSxWd?iuq6>}%1qI@)?Cyn^eOc5O^6mP;%u@J?|AoE4 z_C;5gF6O(ZB$IShyKKWHzO1hXd>_w=e7scQbkO{8txDy5{qIkenQo+a1*mQ_$ZUV9 zv995Ov)kgNEr(mT-4{K;P~UtpX>z&pg82(I@>(6cc>Kg~I$NfAycE9e5vIfN+M}y$ z$(K{2=~s5M?RgluY#HDE^oc)bvrNBZe445I+CAnzMwP4QrEc^oO?dO;Kg)vh%fh+Z z@4J6_%+q`rS(bF1^U@gxgF{QEe)lU~cSQYs+$1}W9nU^)cYX8M^k1et!Dasf*oXYhDU3T* z7YIZ%&lj`2bb$Hnl9#5r7ZOe;{nz5X_N9~4Phu-WPvI$x1MIV#8%|3J&z?DXmWqt1 mp?Qd*iIuUXm7#^Ufq|8QLBDUnn#rfJ$@~*|Au#!miXi~e)Vq5C diff --git a/camlib.py b/camlib.py index d8701b87..a718910a 100644 --- a/camlib.py +++ b/camlib.py @@ -964,6 +964,8 @@ class Geometry(object): corner_type = 1 if corner is None else corner geo_iso.append(pol.buffer(offset, int(self.geo_steps_per_circle), join_style=corner_type)) pol_nr += 1 + + # activity view update disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100])) if old_disp_number < disp_number <= 100: