From c27a2d29e71e4de598c3a396d37cb5b8d3f77cb4 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Wed, 27 May 2020 00:27:10 +0300 Subject: [PATCH] - all tuple entries in the Preferences UI are now protected against letter entry - all entries in the Preferences UI that have numerical entry are protected now against letters - cleaned the Preferences UI in the Gerber area --- AppGUI/GUIElements.py | 15 +- AppGUI/ObjectUI.py | 289 +------ AppGUI/preferences/PreferencesUIManager.py | 9 +- .../excellon/ExcellonAdvOptPrefGroupUI.py | 6 +- .../excellon/ExcellonOptPrefGroupUI.py | 4 +- .../geometry/GeometryAdvOptPrefGroupUI.py | 28 +- .../geometry/GeometryOptPrefGroupUI.py | 5 +- .../gerber/GerberAdvOptPrefGroupUI.py | 79 -- .../gerber/GerberEditorPrefGroupUI.py | 5 +- .../gerber/GerberOptPrefGroupUI.py | 90 --- .../preferences/gerber/GerberPreferencesUI.py | 1 + .../preferences/tools/Tools2CalPrefGroupUI.py | 4 +- .../preferences/tools/ToolsISOPrefGroupUI.py | 58 +- .../preferences/tools/ToolsNCCPrefGroupUI.py | 14 +- .../tools/ToolsPaintPrefGroupUI.py | 10 +- .../tools/ToolsSolderpastePrefGroupUI.py | 6 +- .../tools/ToolsTransformPrefGroupUI.py | 4 +- AppObjects/FlatCAMExcellon.py | 42 +- AppObjects/FlatCAMGerber.py | 691 +---------------- AppTools/ToolEtchCompensation.py | 6 +- AppTools/ToolIsolation.py | 727 +++++++++++++++--- AppTools/ToolNCC.py | 2 +- AppTools/ToolPaint.py | 2 +- CHANGELOG.md | 4 + camlib.py | 2 +- defaults.py | 11 +- 26 files changed, 796 insertions(+), 1318 deletions(-) diff --git a/AppGUI/GUIElements.py b/AppGUI/GUIElements.py index 2c346f3c..9594fb6d 100644 --- a/AppGUI/GUIElements.py +++ b/AppGUI/GUIElements.py @@ -570,10 +570,13 @@ class FCEntry3(FCEntry): class EvalEntry(QtWidgets.QLineEdit): - def __init__(self, parent=None): + def __init__(self, border_color=None, parent=None): super(EvalEntry, self).__init__(parent) self.readyToEdit = True + if border_color: + self.setStyleSheet("QLineEdit {border: 1px solid %s;}" % border_color) + self.editingFinished.connect(self.on_edit_finished) def on_edit_finished(self): @@ -639,7 +642,7 @@ class EvalEntry2(QtWidgets.QLineEdit): def get_value(self): raw = str(self.text()).strip(' ') - evaled = 0.0 + try: evaled = eval(raw) except Exception as e: @@ -660,8 +663,8 @@ 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__() + def __init__(self, border_color=None): + super().__init__(border_color=border_color) regex = QtCore.QRegExp("[0-9\/\*\+\-\%\.\s]*") validator = QtGui.QRegExpValidator(regex, self) @@ -672,8 +675,8 @@ class NumericalEvalTupleEntry(EvalEntry): """ Will evaluate the input and return a value. Accepts only float numbers and formulas using the operators: /,*,+,-,% """ - def __init__(self): - super().__init__() + def __init__(self, border_color=None): + super().__init__(border_color=border_color) regex = QtCore.QRegExp("[0-9\/\*\+\-\%\.\s\,]*") validator = QtGui.QRegExpValidator(regex, self) diff --git a/AppGUI/ObjectUI.py b/AppGUI/ObjectUI.py index 947236f7..834587a4 100644 --- a/AppGUI/ObjectUI.py +++ b/AppGUI/ObjectUI.py @@ -114,7 +114,7 @@ class ObjectUI(QtWidgets.QWidget): self.common_grid.addWidget(self.transform_label, 2, 0, 1, 2) # ### Scale #### - self.scale_entry = NumericalEvalEntry() + self.scale_entry = NumericalEvalEntry(border_color='#0069A9') self.scale_entry.set_value(1.0) self.scale_entry.setToolTip( _("Factor by which to multiply\n" @@ -132,7 +132,7 @@ class ObjectUI(QtWidgets.QWidget): self.common_grid.addWidget(self.scale_button, 3, 1) # ### Offset #### - self.offsetvector_entry = NumericalEvalTupleEntry() + self.offsetvector_entry = NumericalEvalTupleEntry(border_color='#0069A9') self.offsetvector_entry.setText("(0.0, 0.0)") self.offsetvector_entry.setToolTip( _("Amount by which to move the object\n" @@ -206,15 +206,17 @@ class GerberObjectUI(ObjectUI): grid0.addWidget(self.multicolored_cb, 0, 2) # Plot CB - self.plot_cb = FCCheckBox('%s' % _("Plot")) - # self.plot_cb.setLayoutDirection(QtCore.Qt.RightToLeft) - self.plot_cb.setToolTip(_("Plot (show) this object.")) + self.plot_lbl = FCLabel('%s' % _("Plot")) + self.plot_lbl.setToolTip(_("Plot (show) this object.")) + self.plot_cb = FCCheckBox() - grid0.addWidget(self.plot_cb, 1, 0, 1, 2) + grid0.addWidget(self.plot_lbl, 1, 0) + grid0.addWidget(self.plot_cb, 1, 1) # ## Object name self.name_hlay = QtWidgets.QHBoxLayout() self.custom_box.addLayout(self.name_hlay) + name_label = QtWidgets.QLabel("%s:" % _("Name")) self.name_entry = FCEntry() self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus) @@ -225,7 +227,7 @@ class GerberObjectUI(ObjectUI): self.custom_box.addLayout(hlay_plot) # ### Gerber Apertures #### - self.apertures_table_label = QtWidgets.QLabel('%s:' % _('Apertures')) + self.apertures_table_label = QtWidgets.QLabel('%s:' % _('Apertures')) self.apertures_table_label.setToolTip( _("Apertures Table for the Gerber Object.") ) @@ -282,253 +284,21 @@ class GerberObjectUI(ObjectUI): # start with apertures table hidden self.apertures_table.setVisible(False) - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) - self.custom_box.addWidget(separator_line) - - # Isolation Routing - self.isolation_routing_label = QtWidgets.QLabel("%s" % _("Isolation Routing")) - self.isolation_routing_label.setToolTip( - _("Create a Geometry object with\n" - "toolpaths to cut outside polygons.") - ) - self.custom_box.addWidget(self.isolation_routing_label) - - # ########################################### - # ########## NEW GRID ####################### - # ########################################### - - grid1 = QtWidgets.QGridLayout() - self.custom_box.addLayout(grid1) - grid1.setColumnStretch(0, 0) - grid1.setColumnStretch(1, 1) - grid1.setColumnStretch(2, 1) - - # Tool Type - self.tool_type_label = QtWidgets.QLabel('%s:' % _('Tool Type')) - self.tool_type_label.setToolTip( - _("Choose which tool to use for Gerber isolation:\n" - "'Circular' or 'V-shape'.\n" - "When the 'V-shape' is selected then the tool\n" - "diameter will depend on the chosen cut depth.") - ) - self.tool_type_radio = RadioSet([{'label': _('Circular'), 'value': 'circular'}, - {'label': _('V-Shape'), 'value': 'v'}]) - - grid1.addWidget(self.tool_type_label, 0, 0) - grid1.addWidget(self.tool_type_radio, 0, 1, 1, 2) - - # Tip Dia - self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia')) - self.tipdialabel.setToolTip( - _("The tip diameter for V-Shape Tool") - ) - self.tipdia_spinner = FCDoubleSpinner(callback=self.confirmation_message) - self.tipdia_spinner.set_range(-99.9999, 99.9999) - self.tipdia_spinner.set_precision(self.decimals) - self.tipdia_spinner.setSingleStep(0.1) - self.tipdia_spinner.setWrapping(True) - grid1.addWidget(self.tipdialabel, 1, 0) - grid1.addWidget(self.tipdia_spinner, 1, 1, 1, 2) - - # Tip Angle - self.tipanglelabel = QtWidgets.QLabel('%s:' % _('V-Tip Angle')) - self.tipanglelabel.setToolTip( - _("The tip angle for V-Shape Tool.\n" - "In degree.") - ) - self.tipangle_spinner = FCDoubleSpinner(callback=self.confirmation_message) - self.tipangle_spinner.set_range(1, 180) - self.tipangle_spinner.set_precision(self.decimals) - self.tipangle_spinner.setSingleStep(5) - self.tipangle_spinner.setWrapping(True) - grid1.addWidget(self.tipanglelabel, 2, 0) - grid1.addWidget(self.tipangle_spinner, 2, 1, 1, 2) - - # Cut Z - self.cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z')) - self.cutzlabel.setToolTip( - _("Cutting depth (negative)\n" - "below the copper surface.") - ) - self.cutz_spinner = FCDoubleSpinner(callback=self.confirmation_message) - self.cutz_spinner.set_range(-9999.9999, 0.0000) - self.cutz_spinner.set_precision(self.decimals) - self.cutz_spinner.setSingleStep(0.1) - self.cutz_spinner.setWrapping(True) - grid1.addWidget(self.cutzlabel, 3, 0) - grid1.addWidget(self.cutz_spinner, 3, 1, 1, 2) - - # Tool diameter - tdlabel = QtWidgets.QLabel('%s:' % _('Tool dia')) - tdlabel.setToolTip( - _("Diameter of the cutting tool.\n" - "If you want to have an isolation path\n" - "inside the actual shape of the Gerber\n" - "feature, use a negative value for\n" - "this parameter.") - ) - tdlabel.setMinimumWidth(90) - self.iso_tool_dia_entry = FCDoubleSpinner(callback=self.confirmation_message) - self.iso_tool_dia_entry.set_range(-9999.9999, 9999.9999) - self.iso_tool_dia_entry.set_precision(self.decimals) - self.iso_tool_dia_entry.setSingleStep(0.1) - - grid1.addWidget(tdlabel, 4, 0) - grid1.addWidget(self.iso_tool_dia_entry, 4, 1, 1, 2) - - # Number of Passes - passlabel = QtWidgets.QLabel('%s:' % _('# Passes')) - passlabel.setToolTip( - _("Width of the isolation gap in\n" - "number (integer) of tool widths.") - ) - passlabel.setMinimumWidth(90) - self.iso_width_entry = FCSpinner(callback=self.confirmation_message_int) - self.iso_width_entry.set_range(1, 999) - - grid1.addWidget(passlabel, 5, 0) - grid1.addWidget(self.iso_width_entry, 5, 1, 1, 2) - - # Pass overlap - overlabel = QtWidgets.QLabel('%s:' % _('Pass overlap')) - overlabel.setToolTip( - _("How much (percentage) of the tool width to overlap each tool pass.") - ) - overlabel.setMinimumWidth(90) - self.iso_overlap_entry = FCDoubleSpinner(suffix='%', callback=self.confirmation_message) - self.iso_overlap_entry.set_precision(self.decimals) - self.iso_overlap_entry.setWrapping(True) - self.iso_overlap_entry.set_range(0.0000, 99.9999) - self.iso_overlap_entry.setSingleStep(0.1) - grid1.addWidget(overlabel, 6, 0) - grid1.addWidget(self.iso_overlap_entry, 6, 1, 1, 2) - - # Milling Type Radio Button - self.milling_type_label = QtWidgets.QLabel('%s:' % _('Milling Type')) - self.milling_type_label.setToolTip( - _("Milling type:\n" - "- climb / best for precision milling and to reduce tool usage\n" - "- conventional / useful when there is no backlash compensation") - ) - self.milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'}, - {'label': _('Conventional'), 'value': 'cv'}]) - grid1.addWidget(self.milling_type_label, 7, 0) - grid1.addWidget(self.milling_type_radio, 7, 1, 1, 2) - - # combine all passes CB - self.combine_passes_cb = FCCheckBox(label=_('Combine')) - self.combine_passes_cb.setToolTip( - _("Combine all passes into one object") - ) - # generate follow - self.follow_cb = FCCheckBox(label=_('"Follow"')) - self.follow_cb.setToolTip(_("Generate a 'Follow' geometry.\n" - "This means that it will cut through\n" - "the middle of the trace.")) + self.follow_lbl = FCLabel('%s:' % _("Follow")) + self.follow_lbl.setToolTip(_("Generate a 'Follow' geometry.\n" + "This means that it will cut through\n" + "the middle of the trace.")) + self.follow_lbl.setMinimumWidth(90) + self.follow_cb = FCCheckBox() - # avoid an area from isolation - self.except_cb = FCCheckBox(label=_('Except')) - self.except_cb.setToolTip(_("When the isolation geometry is generated,\n" - "by checking this, the area of the object below\n" - "will be subtracted from the isolation geometry.")) - grid1.addWidget(self.combine_passes_cb, 8, 0) - grid1.addWidget(self.follow_cb, 8, 1) - grid1.addWidget(self.except_cb, 8, 2) - - # ## Form Layout - form_layout = QtWidgets.QFormLayout() - grid1.addLayout(form_layout, 9, 0, 1, 3) - - # ################################################ - # ##### Type of object to be excepted ############ - # ################################################ - self.type_obj_combo = FCComboBox() - self.type_obj_combo.addItems([_("Gerber"), _("Geometry")]) - - # we get rid of item1 ("Excellon") as it is not suitable - # self.type_obj_combo.view().setRowHidden(1, True) - self.type_obj_combo.setItemIcon(0, QtGui.QIcon(self.resource_loc + "/flatcam_icon16.png")) - self.type_obj_combo.setItemIcon(1, QtGui.QIcon(self.resource_loc + "/geometry16.png")) - - self.type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Obj Type")) - self.type_obj_combo_label.setToolTip( - _("Specify the type of object to be excepted from isolation.\n" - "It can be of type: Gerber or Geometry.\n" - "What is selected here will dictate the kind\n" - "of objects that will populate the 'Object' combobox.") - ) - # self.type_obj_combo_label.setMinimumWidth(60) - form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo) - - # ################################################ - # ##### The object to be excepted ################ - # ################################################ - self.obj_combo = FCComboBox() - - self.obj_label = QtWidgets.QLabel('%s:' % _("Object")) - self.obj_label.setToolTip(_("Object whose area will be removed from isolation geometry.")) - - form_layout.addRow(self.obj_label, self.obj_combo) - - # ---------------------------------------------- # - # --------- Isolation scope -------------------- # - # ---------------------------------------------- # - self.iso_scope_label = QtWidgets.QLabel('%s:' % _('Scope')) - self.iso_scope_label.setToolTip( - _("Isolation scope. Choose what to isolate:\n" - "- 'All' -> Isolate all the polygons in the object\n" - "- 'Selection' -> Isolate a selection of polygons.") - ) - self.iso_scope_radio = RadioSet([{'label': _('All'), 'value': 'all'}, - {'label': _('Selection'), 'value': 'single'}]) - - grid1.addWidget(self.iso_scope_label, 10, 0) - grid1.addWidget(self.iso_scope_radio, 10, 1, 1, 2) - - # ---------------------------------------------- # - # --------- Isolation type -------------------- # - # ---------------------------------------------- # - self.iso_type_label = QtWidgets.QLabel('%s:' % _('Isolation Type')) - self.iso_type_label.setToolTip( - _("Choose how the isolation will be executed:\n" - "- 'Full' -> complete isolation of polygons\n" - "- 'Ext' -> will isolate only on the outside\n" - "- 'Int' -> will isolate only on the inside\n" - "'Exterior' isolation is almost always possible\n" - "(with the right tool) but 'Interior'\n" - "isolation can be done only when there is an opening\n" - "inside of the polygon (e.g polygon is a 'doughnut' shape).") - ) - self.iso_type_radio = RadioSet([{'label': _('Full'), 'value': 'full'}, - {'label': _('Ext'), 'value': 'ext'}, - {'label': _('Int'), 'value': 'int'}]) - - grid1.addWidget(self.iso_type_label, 11, 0) - grid1.addWidget(self.iso_type_radio, 11, 1, 1, 2) - - self.generate_iso_button = QtWidgets.QPushButton("%s" % _("Generate Isolation Geometry")) - self.generate_iso_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) - self.generate_iso_button.setToolTip( - _("Create a Geometry object with toolpaths to cut \n" - "isolation outside, inside or on both sides of the\n" - "object. For a Gerber object outside means outside\n" - "of the Gerber feature and inside means inside of\n" - "the Gerber feature, if possible at all. This means\n" - "that only if the Gerber feature has openings inside, they\n" - "will be isolated. If what is wanted is to cut isolation\n" - "inside the actual Gerber feature, use a negative tool\n" - "diameter above.") - ) - grid1.addWidget(self.generate_iso_button, 12, 0, 1, 3) + hf_lay = QtWidgets.QHBoxLayout() + self.custom_box.addLayout(hf_lay) + hf_lay.addWidget(self.follow_lbl) + hf_lay.addWidget(self.follow_cb) + hf_lay.addStretch() + # Buffer Geometry self.create_buffer_button = QtWidgets.QPushButton(_('Buffer Solid Geometry')) self.create_buffer_button.setToolTip( _("This button is shown only when the Gerber file\n" @@ -536,19 +306,12 @@ class GerberObjectUI(ObjectUI): "Clicking this will create the buffered geometry\n" "required for isolation.") ) - grid1.addWidget(self.create_buffer_button, 13, 0, 1, 3) + self.custom_box.addWidget(self.create_buffer_button) - self.ohis_iso = OptionalHideInputSection( - self.except_cb, - [self.type_obj_combo, self.type_obj_combo_label, self.obj_combo, self.obj_label], - logic=True - ) - - separator_line2 = QtWidgets.QFrame() - separator_line2.setFrameShape(QtWidgets.QFrame.HLine) - separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken) - grid1.addWidget(separator_line2, 14, 0, 1, 3) - # grid1.addWidget(QtWidgets.QLabel(''), 15, 0) + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) + self.custom_box.addWidget(separator_line) # ########################################### # ########## NEW GRID ####################### diff --git a/AppGUI/preferences/PreferencesUIManager.py b/AppGUI/preferences/PreferencesUIManager.py index 38c61e70..3d537a2a 100644 --- a/AppGUI/preferences/PreferencesUIManager.py +++ b/AppGUI/preferences/PreferencesUIManager.py @@ -330,9 +330,9 @@ class PreferencesUIManager: "tools_iso_tooldia": self.ui.tools_defaults_form.tools_iso_group.tool_dia_entry, "tools_iso_order": self.ui.tools_defaults_form.tools_iso_group.order_radio, "tools_iso_tool_type": self.ui.tools_defaults_form.tools_iso_group.tool_type_radio, - "tools_iso_tool_vtipdia": self.ui.tools_defaults_form.tools_iso_group.tipdia_spinner, - "tools_iso_tool_vtipangle": self.ui.tools_defaults_form.tools_iso_group.tipangle_spinner, - "tools_iso_tool_cutz": self.ui.tools_defaults_form.tools_iso_group.cutz_spinner, + "tools_iso_tool_vtipdia": self.ui.tools_defaults_form.tools_iso_group.tipdia_entry, + "tools_iso_tool_vtipangle": self.ui.tools_defaults_form.tools_iso_group.tipangle_entry, + "tools_iso_tool_cutz": self.ui.tools_defaults_form.tools_iso_group.cutz_entry, "tools_iso_newdia": self.ui.tools_defaults_form.tools_iso_group.newdia_entry, "tools_iso_passes": self.ui.tools_defaults_form.tools_iso_group.passes_entry, @@ -346,6 +346,7 @@ class PreferencesUIManager: "tools_iso_isoexcept": self.ui.tools_defaults_form.tools_iso_group.except_cb, "tools_iso_selection": self.ui.tools_defaults_form.tools_iso_group.select_combo, "tools_iso_area_shape": self.ui.tools_defaults_form.tools_iso_group.area_shape_radio, + "tools_iso_plotting": self.ui.tools_defaults_form.tools_iso_group.plotting_radio, # NCC Tool "tools_ncctools": self.ui.tools_defaults_form.tools_ncc_group.ncc_tool_dia_entry, @@ -360,13 +361,13 @@ class PreferencesUIManager: "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, "tools_ncccutz": self.ui.tools_defaults_form.tools_ncc_group.cutz_entry, "tools_ncctipdia": self.ui.tools_defaults_form.tools_ncc_group.tipdia_entry, "tools_ncctipangle": self.ui.tools_defaults_form.tools_ncc_group.tipangle_entry, "tools_nccnewdia": self.ui.tools_defaults_form.tools_ncc_group.newdia_entry, + "tools_ncc_plotting": self.ui.tools_defaults_form.tools_ncc_group.plotting_radio, # CutOut Tool "tools_cutouttooldia": self.ui.tools_defaults_form.tools_cutout_group.cutout_tooldia_entry, diff --git a/AppGUI/preferences/excellon/ExcellonAdvOptPrefGroupUI.py b/AppGUI/preferences/excellon/ExcellonAdvOptPrefGroupUI.py index 085ccbf1..dd267816 100644 --- a/AppGUI/preferences/excellon/ExcellonAdvOptPrefGroupUI.py +++ b/AppGUI/preferences/excellon/ExcellonAdvOptPrefGroupUI.py @@ -1,7 +1,7 @@ from PyQt5 import QtWidgets from PyQt5.QtCore import QSettings -from AppGUI.GUIElements import FCDoubleSpinner, FCEntry, FloatEntry, RadioSet, FCCheckBox +from AppGUI.GUIElements import FCDoubleSpinner, RadioSet, FCCheckBox, NumericalEvalTupleEntry, NumericalEvalEntry from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI import gettext import AppTranslation as fcTranslate @@ -60,7 +60,7 @@ class ExcellonAdvOptPrefGroupUI(OptionsGroupUI): toolchange_xy_label.setToolTip( _("Toolchange X,Y position.") ) - self.toolchangexy_entry = FCEntry() + self.toolchangexy_entry = NumericalEvalTupleEntry(border_color='#0069A9') grid1.addWidget(toolchange_xy_label, 1, 0) grid1.addWidget(self.toolchangexy_entry, 1, 1) @@ -71,7 +71,7 @@ class ExcellonAdvOptPrefGroupUI(OptionsGroupUI): _("Height of the tool just after start.\n" "Delete the value if you don't need this feature.") ) - self.estartz_entry = FloatEntry() + self.estartz_entry = NumericalEvalEntry(border_color='#0069A9') grid1.addWidget(startzlabel, 2, 0) grid1.addWidget(self.estartz_entry, 2, 1) diff --git a/AppGUI/preferences/excellon/ExcellonOptPrefGroupUI.py b/AppGUI/preferences/excellon/ExcellonOptPrefGroupUI.py index 757c9195..93aec0b3 100644 --- a/AppGUI/preferences/excellon/ExcellonOptPrefGroupUI.py +++ b/AppGUI/preferences/excellon/ExcellonOptPrefGroupUI.py @@ -2,7 +2,7 @@ from PyQt5 import QtWidgets from PyQt5.QtCore import Qt, QSettings from AppGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, FCEntry, FCSpinner, OptionalInputSection, \ - FCComboBox + FCComboBox, NumericalEvalTupleEntry from AppGUI.preferences import machinist_setting from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI import gettext @@ -198,7 +198,7 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI): "If no value is entered then there is no move\n" "on X,Y plane at the end of the job.") ) - self.endxy_entry = FCEntry() + self.endxy_entry = NumericalEvalTupleEntry(border_color='#0069A9') grid2.addWidget(endmove_xy_label, 9, 0) grid2.addWidget(self.endxy_entry, 9, 1) diff --git a/AppGUI/preferences/geometry/GeometryAdvOptPrefGroupUI.py b/AppGUI/preferences/geometry/GeometryAdvOptPrefGroupUI.py index 08326ee1..f122186c 100644 --- a/AppGUI/preferences/geometry/GeometryAdvOptPrefGroupUI.py +++ b/AppGUI/preferences/geometry/GeometryAdvOptPrefGroupUI.py @@ -1,7 +1,8 @@ from PyQt5 import QtWidgets from PyQt5.QtCore import QSettings -from AppGUI.GUIElements import FCEntry, FloatEntry, FCDoubleSpinner, FCCheckBox, RadioSet, FCLabel +from AppGUI.GUIElements import FCDoubleSpinner, FCCheckBox, RadioSet, FCLabel, NumericalEvalTupleEntry, \ + NumericalEvalEntry from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI import gettext @@ -46,8 +47,9 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI): toolchange_xy_label.setToolTip( _("Toolchange X,Y position.") ) + self.toolchangexy_entry = NumericalEvalTupleEntry(border_color='#0069A9') + grid1.addWidget(toolchange_xy_label, 1, 0) - self.toolchangexy_entry = FCEntry() grid1.addWidget(self.toolchangexy_entry, 1, 1) # Start move Z @@ -56,8 +58,9 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI): _("Height of the tool just after starting the work.\n" "Delete the value if you don't need this feature.") ) + self.gstartz_entry = NumericalEvalEntry(border_color='#0069A9') + grid1.addWidget(startzlabel, 2, 0) - self.gstartz_entry = FloatEntry() grid1.addWidget(self.gstartz_entry, 2, 1) # Feedrate rapids @@ -186,6 +189,11 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI): grid1.addWidget(segy_label, 11, 0) grid1.addWidget(self.segy_entry, 11, 1) + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) + grid1.addWidget(separator_line, 12, 0, 1, 2) + # ----------------------------- # --- Area Exclusion ---------- # ----------------------------- @@ -195,10 +203,10 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI): "Those parameters are available only for\n" "Advanced App. Level.") ) - grid1.addWidget(self.adv_label, 12, 0, 1, 2) + grid1.addWidget(self.adv_label, 13, 0, 1, 2) # Exclusion Area CB - self.exclusion_cb = FCCheckBox('%s:' % _("Exclusion areas")) + self.exclusion_cb = FCCheckBox('%s' % _("Exclusion areas")) self.exclusion_cb.setToolTip( _( "Include exclusion areas.\n" @@ -206,7 +214,7 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI): "is forbidden." ) ) - grid1.addWidget(self.exclusion_cb, 13, 0, 1, 2) + grid1.addWidget(self.exclusion_cb, 14, 0, 1, 2) # Area Selection shape self.area_shape_label = QtWidgets.QLabel('%s:' % _("Shape")) @@ -217,8 +225,8 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI): self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'}, {'label': _("Polygon"), 'value': 'polygon'}]) - grid1.addWidget(self.area_shape_label, 14, 0) - grid1.addWidget(self.area_shape_radio, 14, 1) + grid1.addWidget(self.area_shape_label, 15, 0) + grid1.addWidget(self.area_shape_radio, 15, 1) # Chose Strategy self.strategy_label = FCLabel('%s:' % _("Strategy")) @@ -229,8 +237,8 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI): self.strategy_radio = RadioSet([{'label': _('Over'), 'value': 'over'}, {'label': _('Around'), 'value': 'around'}]) - grid1.addWidget(self.strategy_label, 15, 0) - grid1.addWidget(self.strategy_radio, 15, 1) + grid1.addWidget(self.strategy_label, 16, 0) + grid1.addWidget(self.strategy_radio, 16, 1) # Over Z self.over_z_label = FCLabel('%s:' % _("Over Z")) diff --git a/AppGUI/preferences/geometry/GeometryOptPrefGroupUI.py b/AppGUI/preferences/geometry/GeometryOptPrefGroupUI.py index f1e02bda..ebbe0c34 100644 --- a/AppGUI/preferences/geometry/GeometryOptPrefGroupUI.py +++ b/AppGUI/preferences/geometry/GeometryOptPrefGroupUI.py @@ -1,7 +1,8 @@ from PyQt5 import QtWidgets from PyQt5.QtCore import Qt, QSettings -from AppGUI.GUIElements import FCDoubleSpinner, FCCheckBox, OptionalInputSection, FCEntry, FCSpinner, FCComboBox +from AppGUI.GUIElements import FCDoubleSpinner, FCCheckBox, OptionalInputSection, FCSpinner, FCComboBox, \ + NumericalEvalTupleEntry from AppGUI.preferences import machinist_setting from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI @@ -176,7 +177,7 @@ class GeometryOptPrefGroupUI(OptionsGroupUI): "If no value is entered then there is no move\n" "on X,Y plane at the end of the job.") ) - self.endxy_entry = FCEntry() + self.endxy_entry = NumericalEvalTupleEntry(border_color='#0069A9') grid1.addWidget(endmove_xy_label, 7, 0) grid1.addWidget(self.endxy_entry, 7, 1) diff --git a/AppGUI/preferences/gerber/GerberAdvOptPrefGroupUI.py b/AppGUI/preferences/gerber/GerberAdvOptPrefGroupUI.py index f32604c9..7080b81d 100644 --- a/AppGUI/preferences/gerber/GerberAdvOptPrefGroupUI.py +++ b/AppGUI/preferences/gerber/GerberAdvOptPrefGroupUI.py @@ -63,85 +63,6 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI): separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) grid0.addWidget(separator_line, 2, 0, 1, 2) - # Tool Type - self.tool_type_label = QtWidgets.QLabel('%s' % _('Tool Type')) - self.tool_type_label.setToolTip( - _("Choose which tool to use for Gerber isolation:\n" - "'Circular' or 'V-shape'.\n" - "When the 'V-shape' is selected then the tool\n" - "diameter will depend on the chosen cut depth.") - ) - self.tool_type_radio = RadioSet([{'label': 'Circular', 'value': 'circular'}, - {'label': 'V-Shape', 'value': 'v'}]) - - grid0.addWidget(self.tool_type_label, 3, 0) - grid0.addWidget(self.tool_type_radio, 3, 1, 1, 2) - - # Tip Dia - self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia')) - self.tipdialabel.setToolTip( - _("The tip diameter for V-Shape Tool") - ) - self.tipdia_spinner = FCDoubleSpinner() - self.tipdia_spinner.set_precision(self.decimals) - self.tipdia_spinner.set_range(-99.9999, 99.9999) - self.tipdia_spinner.setSingleStep(0.1) - self.tipdia_spinner.setWrapping(True) - grid0.addWidget(self.tipdialabel, 4, 0) - grid0.addWidget(self.tipdia_spinner, 4, 1, 1, 2) - - # Tip Angle - self.tipanglelabel = QtWidgets.QLabel('%s:' % _('V-Tip Angle')) - self.tipanglelabel.setToolTip( - _("The tip angle for V-Shape Tool.\n" - "In degree.") - ) - self.tipangle_spinner = FCSpinner() - self.tipangle_spinner.set_range(1, 180) - self.tipangle_spinner.set_step(5) - self.tipangle_spinner.setWrapping(True) - grid0.addWidget(self.tipanglelabel, 5, 0) - grid0.addWidget(self.tipangle_spinner, 5, 1, 1, 2) - - # Cut Z - self.cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z')) - self.cutzlabel.setToolTip( - _("Cutting depth (negative)\n" - "below the copper surface.") - ) - self.cutz_spinner = FCDoubleSpinner() - self.cutz_spinner.set_precision(self.decimals) - self.cutz_spinner.set_range(-99.9999, 0.0000) - self.cutz_spinner.setSingleStep(0.1) - self.cutz_spinner.setWrapping(True) - - grid0.addWidget(self.cutzlabel, 6, 0) - grid0.addWidget(self.cutz_spinner, 6, 1, 1, 2) - - # Isolation Type - self.iso_type_label = QtWidgets.QLabel('%s:' % _('Isolation Type')) - self.iso_type_label.setToolTip( - _("Choose how the isolation will be executed:\n" - "- 'Full' -> complete isolation of polygons\n" - "- 'Ext' -> will isolate only on the outside\n" - "- 'Int' -> will isolate only on the inside\n" - "'Exterior' isolation is almost always possible\n" - "(with the right tool) but 'Interior'\n" - "isolation can be done only when there is an opening\n" - "inside of the polygon (e.g polygon is a 'doughnut' shape).") - ) - self.iso_type_radio = RadioSet([{'label': _('Full'), 'value': 'full'}, - {'label': _('Exterior'), 'value': 'ext'}, - {'label': _('Interior'), 'value': 'int'}]) - - grid0.addWidget(self.iso_type_label, 7, 0,) - grid0.addWidget(self.iso_type_radio, 7, 1, 1, 2) - - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) - grid0.addWidget(separator_line, 8, 0, 1, 2) - # Buffering Type buffering_label = QtWidgets.QLabel('%s:' % _('Buffering')) buffering_label.setToolTip( diff --git a/AppGUI/preferences/gerber/GerberEditorPrefGroupUI.py b/AppGUI/preferences/gerber/GerberEditorPrefGroupUI.py index dd711a15..aee22e96 100644 --- a/AppGUI/preferences/gerber/GerberEditorPrefGroupUI.py +++ b/AppGUI/preferences/gerber/GerberEditorPrefGroupUI.py @@ -1,7 +1,7 @@ from PyQt5 import QtWidgets from PyQt5.QtCore import QSettings -from AppGUI.GUIElements import FCSpinner, FCDoubleSpinner, FCComboBox, FCEntry, RadioSet +from AppGUI.GUIElements import FCSpinner, FCDoubleSpinner, FCComboBox, FCEntry, RadioSet, NumericalEvalTupleEntry from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI import gettext @@ -109,8 +109,9 @@ class GerberEditorPrefGroupUI(OptionsGroupUI): "The value of the diameter has to use the dot decimals separator.\n" "Valid values: 0.3, 1.0") ) + self.adddim_entry = NumericalEvalTupleEntry(border_color='#0069A9') + grid0.addWidget(self.adddim_label, 5, 0) - self.adddim_entry = FCEntry() grid0.addWidget(self.adddim_entry, 5, 1) self.grb_array_linear_label = QtWidgets.QLabel('%s:' % _('Linear Pad Array')) diff --git a/AppGUI/preferences/gerber/GerberOptPrefGroupUI.py b/AppGUI/preferences/gerber/GerberOptPrefGroupUI.py index 2a57a640..c619a3c1 100644 --- a/AppGUI/preferences/gerber/GerberOptPrefGroupUI.py +++ b/AppGUI/preferences/gerber/GerberOptPrefGroupUI.py @@ -28,96 +28,6 @@ class GerberOptPrefGroupUI(OptionsGroupUI): self.setTitle(str(_("Gerber Options"))) - # ## Isolation Routing - self.isolation_routing_label = QtWidgets.QLabel("%s:" % _("Isolation Routing")) - self.isolation_routing_label.setToolTip( - _("Create a Geometry object with\n" - "toolpaths to cut outside polygons.") - ) - self.layout.addWidget(self.isolation_routing_label) - - # Cutting Tool Diameter - grid0 = QtWidgets.QGridLayout() - self.layout.addLayout(grid0) - - tdlabel = QtWidgets.QLabel('%s:' % _('Tool dia')) - tdlabel.setToolTip( - _("Diameter of the cutting tool.") - ) - grid0.addWidget(tdlabel, 0, 0) - self.iso_tool_dia_entry = FCDoubleSpinner() - self.iso_tool_dia_entry.set_precision(self.decimals) - self.iso_tool_dia_entry.setSingleStep(0.1) - self.iso_tool_dia_entry.set_range(-9999, 9999) - - grid0.addWidget(self.iso_tool_dia_entry, 0, 1) - - # Nr of passes - passlabel = QtWidgets.QLabel('%s:' % _('# Passes')) - passlabel.setToolTip( - _("Width of the isolation gap in\n" - "number (integer) of tool widths.") - ) - self.iso_width_entry = FCSpinner() - self.iso_width_entry.set_range(1, 999) - - grid0.addWidget(passlabel, 1, 0) - grid0.addWidget(self.iso_width_entry, 1, 1) - - # Pass overlap - overlabel = QtWidgets.QLabel('%s:' % _('Pass overlap')) - overlabel.setToolTip( - _("How much (percentage) of the tool width to overlap each tool pass.") - ) - self.iso_overlap_entry = FCDoubleSpinner(suffix='%') - self.iso_overlap_entry.set_precision(self.decimals) - self.iso_overlap_entry.setWrapping(True) - self.iso_overlap_entry.setRange(0.0000, 99.9999) - self.iso_overlap_entry.setSingleStep(0.1) - - grid0.addWidget(overlabel, 2, 0) - grid0.addWidget(self.iso_overlap_entry, 2, 1) - - # Isolation Scope - self.select_label = QtWidgets.QLabel('%s:' % _('Selection')) - self.select_label.setToolTip( - _("Isolation scope. Choose what to isolate:\n" - "- 'All' -> Isolate all the polygons in the object\n" - "- 'Selection' -> Isolate a selection of polygons.\n" - "- 'Reference Object' - will process the area specified by another object.") - ) - self.select_combo = FCComboBox() - self.select_combo.addItems( - [_("All"), _("Area Selection"), _("Reference Object")] - ) - - grid0.addWidget(self.select_label, 3, 0) - grid0.addWidget(self.select_combo, 3, 1, 1, 2) - - # Milling Type - milling_type_label = QtWidgets.QLabel('%s:' % _('Milling Type')) - milling_type_label.setToolTip( - _("Milling type:\n" - "- climb / best for precision milling and to reduce tool usage\n" - "- conventional / useful when there is no backlash compensation") - ) - grid0.addWidget(milling_type_label, 4, 0) - self.milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'}, - {'label': _('Conventional'), 'value': 'cv'}]) - grid0.addWidget(self.milling_type_radio, 4, 1) - - # Combine passes - self.combine_passes_cb = FCCheckBox(label=_('Combine Passes')) - self.combine_passes_cb.setToolTip( - _("Combine all passes into one object") - ) - grid0.addWidget(self.combine_passes_cb, 5, 0, 1, 2) - - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) - grid0.addWidget(separator_line, 6, 0, 1, 2) - # ## Clear non-copper regions self.clearcopper_label = QtWidgets.QLabel("%s:" % _("Non-copper regions")) self.clearcopper_label.setToolTip( diff --git a/AppGUI/preferences/gerber/GerberPreferencesUI.py b/AppGUI/preferences/gerber/GerberPreferencesUI.py index 10c62978..cee2a54c 100644 --- a/AppGUI/preferences/gerber/GerberPreferencesUI.py +++ b/AppGUI/preferences/gerber/GerberPreferencesUI.py @@ -44,6 +44,7 @@ class GerberPreferencesUI(QtWidgets.QWidget): self.vlay = QtWidgets.QVBoxLayout() self.vlay.addWidget(self.gerber_opt_group) self.vlay.addWidget(self.gerber_exp_group) + self.vlay.addStretch() self.layout.addWidget(self.gerber_gen_group) self.layout.addLayout(self.vlay) diff --git a/AppGUI/preferences/tools/Tools2CalPrefGroupUI.py b/AppGUI/preferences/tools/Tools2CalPrefGroupUI.py index 97100998..04d54387 100644 --- a/AppGUI/preferences/tools/Tools2CalPrefGroupUI.py +++ b/AppGUI/preferences/tools/Tools2CalPrefGroupUI.py @@ -1,7 +1,7 @@ from PyQt5 import QtWidgets from PyQt5.QtCore import QSettings -from AppGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, FCEntry +from AppGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, NumericalEvalTupleEntry from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI import gettext @@ -116,7 +116,7 @@ class Tools2CalPrefGroupUI(OptionsGroupUI): "(x, y) point will be used,") ) - self.toolchange_xy_entry = FCEntry() + self.toolchange_xy_entry = NumericalEvalTupleEntry(border_color='#0069A9') grid_lay.addWidget(toolchangexy_lbl, 7, 0) grid_lay.addWidget(self.toolchange_xy_entry, 7, 1, 1, 2) diff --git a/AppGUI/preferences/tools/ToolsISOPrefGroupUI.py b/AppGUI/preferences/tools/ToolsISOPrefGroupUI.py index 3a73b4ec..86064b56 100644 --- a/AppGUI/preferences/tools/ToolsISOPrefGroupUI.py +++ b/AppGUI/preferences/tools/ToolsISOPrefGroupUI.py @@ -1,7 +1,7 @@ from PyQt5 import QtWidgets from PyQt5.QtCore import QSettings -from AppGUI.GUIElements import FCEntry, RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox, FCSpinner +from AppGUI.GUIElements import RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox, FCSpinner, NumericalEvalTupleEntry from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI import gettext @@ -32,7 +32,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): _("Create a Geometry object with\n" "toolpaths to cut around polygons.") ) - self.layout.addWidget(self.clearcopper_label) + self.layout.addWidget(self.iso_label) grid0 = QtWidgets.QGridLayout() self.layout.addLayout(grid0) @@ -44,11 +44,11 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): "The value of the diameter has to use the dot decimals separator.\n" "Valid values: 0.3, 1.0") ) - self.tool_dia_entry = FCEntry(border_color='#0069A9') + self.tool_dia_entry = NumericalEvalTupleEntry(border_color='#0069A9') self.tool_dia_entry.setPlaceholderText(_("Comma separated values")) grid0.addWidget(isotdlabel, 0, 0) - grid0.addWidget(self.tool_dia_entry, 0, 1) + grid0.addWidget(self.tool_dia_entry, 0, 1, 1, 2) # Tool order Radio Button self.order_label = QtWidgets.QLabel('%s:' % _('Tool order')) @@ -64,7 +64,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): {'label': _('Reverse'), 'value': 'rev'}]) grid0.addWidget(self.order_label, 1, 0) - grid0.addWidget(self.order_radio, 1, 1) + grid0.addWidget(self.order_radio, 1, 1, 1, 2) # Tool Type Radio Button self.tool_type_label = QtWidgets.QLabel('%s:' % _('Tool Type')) @@ -83,7 +83,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): ) grid0.addWidget(self.tool_type_label, 2, 0) - grid0.addWidget(self.tool_type_radio, 2, 1) + grid0.addWidget(self.tool_type_radio, 2, 1, 1, 2) # Tip Dia self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia')) @@ -95,7 +95,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): self.tipdia_entry.setSingleStep(0.1) grid0.addWidget(self.tipdialabel, 3, 0) - grid0.addWidget(self.tipdia_entry, 3, 1) + grid0.addWidget(self.tipdia_entry, 3, 1, 1, 2) # Tip Angle self.tipanglelabel = QtWidgets.QLabel('%s:' % _('V-Tip Angle')) @@ -109,7 +109,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): self.tipangle_entry.setWrapping(True) grid0.addWidget(self.tipanglelabel, 4, 0) - grid0.addWidget(self.tipangle_entry, 4, 1) + grid0.addWidget(self.tipangle_entry, 4, 1, 1, 2) # Cut Z entry cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z')) @@ -128,7 +128,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): ) grid0.addWidget(cutzlabel, 5, 0) - grid0.addWidget(self.cutz_entry, 5, 1) + grid0.addWidget(self.cutz_entry, 5, 1, 1, 2) # New Diameter self.newdialabel = QtWidgets.QLabel('%s:' % _('New Dia')) @@ -143,12 +143,12 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): self.newdia_entry.setSingleStep(0.1) grid0.addWidget(self.newdialabel, 6, 0) - grid0.addWidget(self.newdia_entry, 6, 1) + grid0.addWidget(self.newdia_entry, 6, 1, 1, 2) separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) - grid0.addWidget(separator_line, 7, 0, 1, 2) + grid0.addWidget(separator_line, 7, 0, 1, 3) # Passes passlabel = QtWidgets.QLabel('%s:' % _('Passes')) @@ -161,7 +161,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): self.passes_entry.setObjectName("i_passes") grid0.addWidget(passlabel, 8, 0) - grid0.addWidget(self.passes_entry, 8, 1) + grid0.addWidget(self.passes_entry, 8, 1, 1, 2) # Overlap Entry overlabel = QtWidgets.QLabel('%s:' % _('Overlap')) @@ -176,7 +176,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): self.overlap_entry.setObjectName("i_overlap") grid0.addWidget(overlabel, 9, 0) - grid0.addWidget(self.overlap_entry, 9, 1) + grid0.addWidget(self.overlap_entry, 9, 1, 1, 2) # Milling Type Radio Button self.milling_type_label = QtWidgets.QLabel('%s:' % _('Milling Type')) @@ -195,7 +195,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): ) grid0.addWidget(self.milling_type_label, 10, 0) - grid0.addWidget(self.milling_type_radio, 10, 1) + grid0.addWidget(self.milling_type_radio, 10, 1, 1, 2) # Follow self.follow_label = QtWidgets.QLabel('%s:' % _('Follow')) @@ -212,7 +212,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): self.follow_cb.setObjectName("i_follow") grid0.addWidget(self.follow_label, 11, 0) - grid0.addWidget(self.follow_cb, 11, 1) + grid0.addWidget(self.follow_cb, 11, 1, 1, 2) # Isolation Type self.iso_type_label = QtWidgets.QLabel('%s:' % _('Isolation Type')) @@ -232,15 +232,15 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): self.iso_type_radio.setObjectName("i_type") grid0.addWidget(self.iso_type_label, 12, 0) - grid0.addWidget(self.iso_type_radio, 12, 1) + grid0.addWidget(self.iso_type_radio, 12, 1, 1, 2) separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) - grid0.addWidget(separator_line, 13, 0, 1, 2) + grid0.addWidget(separator_line, 13, 0, 1, 3) # Rest machining CheckBox - self.rest_cb = FCCheckBox('%s' % _("Rest Machining")) + self.rest_cb = FCCheckBox('%s' % _("Rest")) self.rest_cb.setObjectName("i_rest_machining") self.rest_cb.setToolTip( _("If checked, use 'rest machining'.\n" @@ -252,7 +252,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): "If not checked, use the standard algorithm.") ) - grid0.addWidget(self.ncc_rest_cb, 17, 0, 1, 2) + grid0.addWidget(self.rest_cb, 17, 0) # Combine All Passes self.combine_passes_cb = FCCheckBox(label=_('Combine')) @@ -261,7 +261,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): ) self.combine_passes_cb.setObjectName("i_combine") - grid0.addWidget(self.combine_passes_cb, 18, 0, 1, 2) + grid0.addWidget(self.combine_passes_cb, 17, 1) # Exception Areas self.except_cb = FCCheckBox(label=_('Except')) @@ -269,7 +269,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): "by checking this, the area of the object below\n" "will be subtracted from the isolation geometry.")) self.except_cb.setObjectName("i_except") - grid0.addWidget(self.except_cb, 19, 0, 1, 2) + grid0.addWidget(self.except_cb, 17, 2) # Isolation Scope self.select_label = QtWidgets.QLabel('%s:' % _("Selection")) @@ -286,7 +286,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): self.select_combo.setObjectName("i_selection") grid0.addWidget(self.select_label, 20, 0) - grid0.addWidget(self.select_combo, 20, 1) + grid0.addWidget(self.select_combo, 20, 1, 1, 2) # Area Shape self.area_shape_label = QtWidgets.QLabel('%s:' % _("Shape")) @@ -298,22 +298,22 @@ class ToolsISOPrefGroupUI(OptionsGroupUI): {'label': _("Polygon"), 'value': 'polygon'}]) grid0.addWidget(self.area_shape_label, 21, 0) - grid0.addWidget(self.area_shape_radio, 21, 1) + grid0.addWidget(self.area_shape_radio, 21, 1, 1, 2) separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) - grid0.addWidget(separator_line, 22, 0, 1, 2) + grid0.addWidget(separator_line, 22, 0, 1, 3) # ## Plotting type - self.ncc_plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'}, - {"label": _("Progressive"), "value": "progressive"}]) - plotting_label = QtWidgets.QLabel('%s:' % _("ISO Plotting")) + self.plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'}, + {"label": _("Progressive"), "value": "progressive"}]) + plotting_label = QtWidgets.QLabel('%s:' % _("Plotting")) plotting_label.setToolTip( _("- 'Normal' - normal plotting, done at the end of the job\n" "- 'Progressive' - each shape is plotted after it is generated") ) - grid0.addWidget(plotting_label, 21, 0) - grid0.addWidget(self.ncc_plotting_radio, 21, 1) + grid0.addWidget(plotting_label, 23, 0) + grid0.addWidget(self.plotting_radio, 23, 1, 1, 2) self.layout.addStretch() diff --git a/AppGUI/preferences/tools/ToolsNCCPrefGroupUI.py b/AppGUI/preferences/tools/ToolsNCCPrefGroupUI.py index a1d7598f..28c300da 100644 --- a/AppGUI/preferences/tools/ToolsNCCPrefGroupUI.py +++ b/AppGUI/preferences/tools/ToolsNCCPrefGroupUI.py @@ -1,7 +1,7 @@ from PyQt5 import QtWidgets from PyQt5.QtCore import QSettings -from AppGUI.GUIElements import FCEntry, RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox +from AppGUI.GUIElements import RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox, NumericalEvalTupleEntry from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI import gettext @@ -45,7 +45,7 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI): "Valid values: 0.3, 1.0") ) grid0.addWidget(ncctdlabel, 0, 0) - self.ncc_tool_dia_entry = FCEntry(border_color='#0069A9') + self.ncc_tool_dia_entry = NumericalEvalTupleEntry(border_color='#0069A9') self.ncc_tool_dia_entry.setPlaceholderText(_("Comma separated values")) grid0.addWidget(self.ncc_tool_dia_entry, 0, 1) @@ -285,7 +285,7 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI): grid0.addWidget(separator_line, 16, 0, 1, 2) # Rest machining CheckBox - self.ncc_rest_cb = FCCheckBox('%s' % _("Rest Machining")) + self.ncc_rest_cb = FCCheckBox('%s' % _("Rest")) self.ncc_rest_cb.setToolTip( _("If checked, use 'rest machining'.\n" "Basically it will clear copper outside PCB features,\n" @@ -336,14 +336,14 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI): grid0.addWidget(separator_line, 20, 0, 1, 2) # ## Plotting type - self.ncc_plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'}, - {"label": _("Progressive"), "value": "progressive"}]) - plotting_label = QtWidgets.QLabel('%s:' % _("NCC Plotting")) + self.plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'}, + {"label": _("Progressive"), "value": "progressive"}]) + plotting_label = QtWidgets.QLabel('%s:' % _("Plotting")) plotting_label.setToolTip( _("- 'Normal' - normal plotting, done at the end of the job\n" "- 'Progressive' - each shape is plotted after it is generated") ) grid0.addWidget(plotting_label, 21, 0) - grid0.addWidget(self.ncc_plotting_radio, 21, 1) + grid0.addWidget(self.plotting_radio, 21, 1) self.layout.addStretch() diff --git a/AppGUI/preferences/tools/ToolsPaintPrefGroupUI.py b/AppGUI/preferences/tools/ToolsPaintPrefGroupUI.py index 9557c5b5..9634ebb7 100644 --- a/AppGUI/preferences/tools/ToolsPaintPrefGroupUI.py +++ b/AppGUI/preferences/tools/ToolsPaintPrefGroupUI.py @@ -1,7 +1,7 @@ from PyQt5 import QtWidgets from PyQt5.QtCore import QSettings -from AppGUI.GUIElements import FCEntry, RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox +from AppGUI.GUIElements import RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox, NumericalEvalTupleEntry from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI import gettext @@ -53,7 +53,7 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI): ) grid0.addWidget(ptdlabel, 0, 0) - self.painttooldia_entry = FCEntry(border_color='#0069A9') + self.painttooldia_entry = NumericalEvalTupleEntry(border_color='#0069A9') self.painttooldia_entry.setPlaceholderText(_("Comma separated values")) grid0.addWidget(self.painttooldia_entry, 0, 1) @@ -241,8 +241,8 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI): separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) grid0.addWidget(separator_line, 13, 0, 1, 2) - self.rest_cb = FCCheckBox('%s' % _("Rest Machining")) - self.rest_cb.setObjectName(_("Rest Machining")) + self.rest_cb = FCCheckBox('%s' % _("Rest")) + self.rest_cb.setObjectName(_("Rest")) self.rest_cb.setToolTip( _("If checked, use 'rest machining'.\n" "Basically it will clear copper outside PCB features,\n" @@ -302,7 +302,7 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI): # ## Plotting type self.paint_plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'}, {"label": _("Progressive"), "value": "progressive"}]) - plotting_label = QtWidgets.QLabel('%s:' % _("Paint Plotting")) + plotting_label = QtWidgets.QLabel('%s:' % _("Plotting")) plotting_label.setToolTip( _("- 'Normal' - normal plotting, done at the end of the job\n" "- 'Progressive' - each shape is plotted after it is generated") diff --git a/AppGUI/preferences/tools/ToolsSolderpastePrefGroupUI.py b/AppGUI/preferences/tools/ToolsSolderpastePrefGroupUI.py index c615fcb9..832b76d9 100644 --- a/AppGUI/preferences/tools/ToolsSolderpastePrefGroupUI.py +++ b/AppGUI/preferences/tools/ToolsSolderpastePrefGroupUI.py @@ -1,7 +1,7 @@ from PyQt5 import QtWidgets from PyQt5.QtCore import QSettings -from AppGUI.GUIElements import FCEntry, FCDoubleSpinner, FCSpinner, FCComboBox +from AppGUI.GUIElements import FCDoubleSpinner, FCSpinner, FCComboBox, NumericalEvalTupleEntry from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI import gettext @@ -45,7 +45,7 @@ class ToolsSolderpastePrefGroupUI(OptionsGroupUI): "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 = NumericalEvalTupleEntry(border_color='#0069A9') grid0.addWidget(nozzletdlabel, 0, 0) grid0.addWidget(self.nozzle_tool_dia_entry, 0, 1) @@ -130,7 +130,7 @@ class ToolsSolderpastePrefGroupUI(OptionsGroupUI): grid0.addWidget(self.z_toolchange_entry, 6, 1) # X,Y Toolchange location - self.xy_toolchange_entry = FCEntry() + self.xy_toolchange_entry = NumericalEvalTupleEntry(border_color='#0069A9') self.xy_toolchange_label = QtWidgets.QLabel('%s:' % _("Toolchange X-Y")) self.xy_toolchange_label.setToolTip( _("The X,Y location for tool (nozzle) change.\n" diff --git a/AppGUI/preferences/tools/ToolsTransformPrefGroupUI.py b/AppGUI/preferences/tools/ToolsTransformPrefGroupUI.py index 58304a07..cecff2bf 100644 --- a/AppGUI/preferences/tools/ToolsTransformPrefGroupUI.py +++ b/AppGUI/preferences/tools/ToolsTransformPrefGroupUI.py @@ -1,7 +1,7 @@ from PyQt5 import QtWidgets from PyQt5.QtCore import QSettings -from AppGUI.GUIElements import FCDoubleSpinner, FCCheckBox, FCEntry +from AppGUI.GUIElements import FCDoubleSpinner, FCCheckBox, NumericalEvalTupleEntry from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI import gettext @@ -191,7 +191,7 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI): "The 'x' in (x, y) will be used when using Flip on X and\n" "the 'y' in (x, y) will be used when using Flip on Y and") ) - self.flip_ref_entry = FCEntry() + self.flip_ref_entry = NumericalEvalTupleEntry(border_color='#0069A9') grid0.addWidget(self.flip_ref_label, 14, 0, 1, 2) grid0.addWidget(self.flip_ref_entry, 15, 0, 1, 2) diff --git a/AppObjects/FlatCAMExcellon.py b/AppObjects/FlatCAMExcellon.py index 262ba293..2dfd4c60 100644 --- a/AppObjects/FlatCAMExcellon.py +++ b/AppObjects/FlatCAMExcellon.py @@ -1180,13 +1180,25 @@ class ExcellonObject(FlatCAMObj, Excellon): def generate_milling_drills(self, tools=None, outname=None, tooldia=None, plot=False, use_thread=False): """ + Will generate an Geometry Object allowing to cut a drill hole instead of drilling it. + Note: This method is a good template for generic operations as it takes it's options from parameters or otherwise from the object's options and returns a (success, msg) tuple as feedback for shell operations. - :return: Success/failure condition tuple (bool, str). - :rtype: tuple + :param tools: A list of tools where the drills are to be milled or a string: "all" + :type tools: + :param outname: the name of the resulting Geometry object + :type outname: str + :param tooldia: the tool diameter to be used in creation of the milling path (Geometry Object) + :type tooldia: float + :param plot: if to plot the resulting object + :type plot: bool + :param use_thread: if to use threading for creation of the Geometry object + :type use_thread: bool + :return: Success/failure condition tuple (bool, str). + :rtype: tuple """ # Get the tools from the list. These are keys @@ -1250,7 +1262,7 @@ class ExcellonObject(FlatCAMObj, Excellon): geo_obj.options['Tools_in_use'] = tool_table_items geo_obj.options['type'] = 'Excellon Geometry' geo_obj.options["cnctooldia"] = str(tooldia) - + geo_obj.options["multidepth"] = self.options["multidepth"] geo_obj.solid_geometry = [] # in case that the tool used has the same diameter with the hole, and since the maximum resolution @@ -1280,15 +1292,27 @@ class ExcellonObject(FlatCAMObj, Excellon): return True, "" - def generate_milling_slots(self, tools=None, outname=None, tooldia=None, plot=True, use_thread=False): + def generate_milling_slots(self, tools=None, outname=None, tooldia=None, plot=False, use_thread=False): """ + Will generate an Geometry Object allowing to cut/mill a slot hole. + Note: This method is a good template for generic operations as it takes it's options from parameters or otherwise from the object's options and returns a (success, msg) tuple as feedback for shell operations. - :return: Success/failure condition tuple (bool, str). - :rtype: tuple + :param tools: A list of tools where the drills are to be milled or a string: "all" + :type tools: + :param outname: the name of the resulting Geometry object + :type outname: str + :param tooldia: the tool diameter to be used in creation of the milling path (Geometry Object) + :type tooldia: float + :param plot: if to plot the resulting object + :type plot: bool + :param use_thread: if to use threading for creation of the Geometry object + :type use_thread: bool + :return: Success/failure condition tuple (bool, str). + :rtype: tuple """ # Get the tools from the list. These are keys @@ -1341,7 +1365,7 @@ class ExcellonObject(FlatCAMObj, Excellon): geo_obj.options['Tools_in_use'] = tool_table_items geo_obj.options['type'] = 'Excellon Geometry' geo_obj.options["cnctooldia"] = str(tooldia) - + geo_obj.options["multidepth"] = self.options["multidepth"] geo_obj.solid_geometry = [] # in case that the tool used has the same diameter with the hole, and since the maximum resolution @@ -1388,13 +1412,13 @@ class ExcellonObject(FlatCAMObj, Excellon): self.app.defaults.report_usage("excellon_on_create_milling_drills button") self.read_form() - self.generate_milling_drills(use_thread=False) + self.generate_milling_drills(use_thread=False, plot=True) def on_generate_milling_slots_button_click(self, *args): self.app.defaults.report_usage("excellon_on_create_milling_slots_button") self.read_form() - self.generate_milling_slots(use_thread=False) + self.generate_milling_slots(use_thread=False, plot=True) def on_pp_changed(self): current_pp = self.ui.pp_excellon_name_cb.get_value() diff --git a/AppObjects/FlatCAMGerber.py b/AppObjects/FlatCAMGerber.py index aa537009..cd90cad4 100644 --- a/AppObjects/FlatCAMGerber.py +++ b/AppObjects/FlatCAMGerber.py @@ -115,23 +115,12 @@ class GerberObject(FlatCAMObj, Gerber): "plot": True, "multicolored": False, "solid": False, - "tool_type": 'circular', - "vtipdia": 0.1, - "vtipangle": 30, - "vcutz": -0.05, - "isotooldia": 0.016, - "isopasses": 1, - "isooverlap": 15, - "milling_type": "cl", - "combine_passes": True, "noncoppermargin": 0.0, "noncopperrounded": False, "bboxmargin": 0.0, "bboxrounded": False, "aperture_display": False, "follow": False, - "iso_scope": 'all', - "iso_type": 'full' }) # type of isolation: 0 = exteriors, 1 = interiors, 2 = complete isolation (both interiors and exteriors) @@ -197,33 +186,22 @@ class GerberObject(FlatCAMObj, Gerber): "plot": self.ui.plot_cb, "multicolored": self.ui.multicolored_cb, "solid": self.ui.solid_cb, - "tool_type": self.ui.tool_type_radio, - "vtipdia": self.ui.tipdia_spinner, - "vtipangle": self.ui.tipangle_spinner, - "vcutz": self.ui.cutz_spinner, - "isotooldia": self.ui.iso_tool_dia_entry, - "isopasses": self.ui.iso_width_entry, - "isooverlap": self.ui.iso_overlap_entry, - "milling_type": self.ui.milling_type_radio, - "combine_passes": self.ui.combine_passes_cb, "noncoppermargin": self.ui.noncopper_margin_entry, "noncopperrounded": self.ui.noncopper_rounded_cb, "bboxmargin": self.ui.bbmargin_entry, "bboxrounded": self.ui.bbrounded_cb, "aperture_display": self.ui.aperture_table_visibility_cb, - "follow": self.ui.follow_cb, - "iso_scope": self.ui.iso_scope_radio, - "iso_type": self.ui.iso_type_radio + "follow": self.ui.follow_cb }) # Fill form fields only on object create self.to_form() assert isinstance(self.ui, GerberObjectUI) + self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click) self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click) - self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click) # Tools self.ui.iso_button.clicked.connect(self.app.isolation_tool.run) @@ -235,54 +213,17 @@ class GerberObject(FlatCAMObj, Gerber): self.ui.aperture_table_visibility_cb.stateChanged.connect(self.on_aperture_table_visibility_change) self.ui.follow_cb.stateChanged.connect(self.on_follow_cb_click) - # set the model for the Area Exception comboboxes - self.ui.obj_combo.setModel(self.app.collection) - self.ui.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) - self.ui.obj_combo.is_last = True - self.ui.obj_combo.obj_type = { - _("Gerber"): "Gerber", _("Geometry"): "Geometry" - }[self.ui.type_obj_combo.get_value()] - self.on_type_obj_index_changed() - - self.ui.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed) - - self.ui.tool_type_radio.activated_custom.connect(self.on_tool_type_change) - # establish visibility for the GUI elements found in the slot function - self.ui.tool_type_radio.activated_custom.emit(self.options['tool_type']) - # Show/Hide Advanced Options if self.app.defaults["global_app_level"] == 'b': self.ui.level.setText('%s' % _('Basic')) - self.options['tool_type'] = 'circular' - - self.ui.tool_type_label.hide() - self.ui.tool_type_radio.hide() - - # override the Preferences Value; in Basic mode the Tool Type is always Circular ('C1') - self.ui.tool_type_radio.set_value('circular') - - self.ui.tipdialabel.hide() - self.ui.tipdia_spinner.hide() - self.ui.tipanglelabel.hide() - self.ui.tipangle_spinner.hide() - self.ui.cutzlabel.hide() - self.ui.cutz_spinner.hide() self.ui.apertures_table_label.hide() self.ui.aperture_table_visibility_cb.hide() - self.ui.milling_type_label.hide() - self.ui.milling_type_radio.hide() - self.ui.iso_type_label.hide() - self.ui.iso_type_radio.hide() self.ui.follow_cb.hide() - self.ui.except_cb.setChecked(False) - self.ui.except_cb.hide() + else: self.ui.level.setText('%s' % _('Advanced')) - self.ui.tipdia_spinner.valueChanged.connect(self.on_calculate_tooldia) - self.ui.tipangle_spinner.valueChanged.connect(self.on_calculate_tooldia) - self.ui.cutz_spinner.valueChanged.connect(self.on_calculate_tooldia) if self.app.defaults["gerber_buffering"] == 'no': self.ui.create_buffer_button.show() @@ -300,58 +241,6 @@ class GerberObject(FlatCAMObj, Gerber): self.build_ui() self.units_found = self.app.defaults['units'] - def on_calculate_tooldia(self): - try: - tdia = float(self.ui.tipdia_spinner.get_value()) - except Exception: - return - try: - dang = float(self.ui.tipangle_spinner.get_value()) - except Exception: - return - try: - cutz = float(self.ui.cutz_spinner.get_value()) - except Exception: - return - - cutz *= -1 - if cutz < 0: - cutz *= -1 - - half_tip_angle = dang / 2 - - tool_diameter = tdia + (2 * cutz * math.tan(math.radians(half_tip_angle))) - self.ui.iso_tool_dia_entry.set_value(tool_diameter) - - def on_type_obj_index_changed(self): - val = self.ui.type_obj_combo.get_value() - obj_type = {"Gerber": 0, "Geometry": 2}[val] - self.ui.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) - self.ui.obj_combo.setCurrentIndex(0) - self.ui.obj_combo.obj_type = {_("Gerber"): "Gerber", _("Geometry"): "Geometry"}[val] - - def on_tool_type_change(self, state): - if state == 'circular': - self.ui.tipdialabel.hide() - self.ui.tipdia_spinner.hide() - self.ui.tipanglelabel.hide() - self.ui.tipangle_spinner.hide() - self.ui.cutzlabel.hide() - self.ui.cutz_spinner.hide() - self.ui.iso_tool_dia_entry.setDisabled(False) - # update the value in the self.iso_tool_dia_entry once this is selected - self.ui.iso_tool_dia_entry.set_value(self.options['isotooldia']) - else: - self.ui.tipdialabel.show() - self.ui.tipdia_spinner.show() - self.ui.tipanglelabel.show() - self.ui.tipangle_spinner.show() - self.ui.cutzlabel.show() - self.ui.cutz_spinner.show() - self.ui.iso_tool_dia_entry.setDisabled(True) - # update the value in the self.iso_tool_dia_entry once this is selected - self.on_calculate_tooldia() - def build_ui(self): FlatCAMObj.build_ui(self) @@ -562,521 +451,6 @@ class GerberObject(FlatCAMObj, Gerber): self.app.app_obj.new_object("geometry", name, geo_init) - def on_iso_button_click(self, *args): - - obj = self.app.collection.get_active() - - self.iso_type = 2 - if self.ui.iso_type_radio.get_value() == 'ext': - self.iso_type = 0 - if self.ui.iso_type_radio.get_value() == 'int': - self.iso_type = 1 - - def worker_task(iso_obj, app_obj): - with self.app.proc_container.new(_("Isolating...")): - if self.ui.follow_cb.get_value() is True: - iso_obj.follow_geo() - # in the end toggle the visibility of the origin object so we can see the generated Geometry - iso_obj.ui.plot_cb.toggle() - else: - app_obj.defaults.report_usage("gerber_on_iso_button") - self.read_form() - - iso_scope = 'all' if self.ui.iso_scope_radio.get_value() == 'all' else 'single' - self.isolate_handler(iso_type=self.iso_type, iso_scope=iso_scope) - - self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]}) - - def follow_geo(self, outname=None): - """ - Creates a geometry object "following" the gerber paths. - - :return: None - """ - - # default_name = self.options["name"] + "_follow" - # follow_name = outname or default_name - - if outname is None: - follow_name = self.options["name"] + "_follow" - else: - follow_name = outname - - def follow_init(follow_obj, app): - # Propagate options - follow_obj.options["cnctooldia"] = str(self.options["isotooldia"]) - follow_obj.solid_geometry = self.follow_geometry - - # TODO: Do something if this is None. Offer changing name? - try: - self.app.app_obj.new_object("geometry", follow_name, follow_init) - except Exception as e: - return "Operation failed: %s" % str(e) - - def isolate_handler(self, iso_type, iso_scope): - - if iso_scope == 'all': - self.isolate(iso_type=iso_type) - else: - # disengage the grid snapping since it may be hard to click on polygons with grid snapping on - if self.app.ui.grid_snap_btn.isChecked(): - self.grid_status_memory = True - self.app.ui.grid_snap_btn.trigger() - else: - self.grid_status_memory = False - - self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release) - - if self.app.is_legacy is False: - self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot) - else: - self.app.plotcanvas.graph_event_disconnect(self.app.mr) - - self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click on a polygon to isolate it.")) - - def on_mouse_click_release(self, event): - if self.app.is_legacy is False: - event_pos = event.pos - right_button = 2 - self.app.event_is_dragging = self.app.event_is_dragging - else: - event_pos = (event.xdata, event.ydata) - right_button = 3 - self.app.event_is_dragging = self.app.ui.popMenu.mouse_is_panning - - try: - x = float(event_pos[0]) - y = float(event_pos[1]) - except TypeError: - return - - event_pos = (x, y) - 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]) - else: - curr_pos = (curr_pos[0], curr_pos[1]) - - if event.button == 1: - clicked_poly = self.find_polygon(point=(curr_pos[0], curr_pos[1])) - - if self.app.selection_type is not None: - self.selection_area_handler(self.app.pos, curr_pos, self.app.selection_type) - self.app.selection_type = None - elif clicked_poly: - if clicked_poly not in self.poly_dict.values(): - shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0, shape=clicked_poly, - color=self.app.defaults['global_sel_draw_color'] + 'AF', - face_color=self.app.defaults['global_sel_draw_color'] + 'AF', - visible=True) - self.poly_dict[shape_id] = clicked_poly - self.app.inform.emit( - '%s: %d. %s' % (_("Added polygon"), int(len(self.poly_dict)), - _("Click to add next polygon or right click to start isolation.")) - ) - else: - try: - for k, v in list(self.poly_dict.items()): - if v == clicked_poly: - self.app.tool_shapes.remove(k) - self.poly_dict.pop(k) - break - except TypeError: - return - self.app.inform.emit( - '%s. %s' % (_("Removed polygon"), - _("Click to add/remove next polygon or right click to start isolation.")) - ) - - self.app.tool_shapes.redraw() - else: - self.app.inform.emit(_("No polygon detected under click position.")) - elif event.button == right_button and self.app.event_is_dragging is False: - # 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_mouse_click_release) - else: - self.app.plotcanvas.graph_event_disconnect(self.mr) - - self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release', - self.app.on_mouse_click_release_over_plot) - - self.app.tool_shapes.clear(update=True) - - if self.poly_dict: - poly_list = deepcopy(list(self.poly_dict.values())) - self.isolate(iso_type=self.iso_type, geometry=poly_list) - self.poly_dict.clear() - else: - self.app.inform.emit('[ERROR_NOTCL] %s' % _("List of single polygons is empty. Aborting.")) - - def selection_area_handler(self, start_pos, end_pos, sel_type): - """ - :param start_pos: mouse position when the selection LMB click was done - :param end_pos: mouse position when the left mouse button is released - :param sel_type: if True it's a left to right selection (enclosure), if False it's a 'touch' selection - :return: - """ - poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])]) - - # delete previous selection shape - self.app.delete_selection_shape() - - added_poly_count = 0 - try: - for geo in self.solid_geometry: - if geo not in self.poly_dict.values(): - if sel_type is True: - if geo.within(poly_selection): - shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0, - shape=geo, - color=self.app.defaults['global_sel_draw_color'] + 'AF', - face_color=self.app.defaults[ - 'global_sel_draw_color'] + 'AF', - visible=True) - self.poly_dict[shape_id] = geo - added_poly_count += 1 - else: - if poly_selection.intersects(geo): - shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0, - shape=geo, - color=self.app.defaults['global_sel_draw_color'] + 'AF', - face_color=self.app.defaults[ - 'global_sel_draw_color'] + 'AF', - visible=True) - self.poly_dict[shape_id] = geo - added_poly_count += 1 - except TypeError: - if self.solid_geometry not in self.poly_dict.values(): - if sel_type is True: - if self.solid_geometry.within(poly_selection): - shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0, - shape=self.solid_geometry, - color=self.app.defaults['global_sel_draw_color'] + 'AF', - face_color=self.app.defaults[ - 'global_sel_draw_color'] + 'AF', - visible=True) - self.poly_dict[shape_id] = self.solid_geometry - added_poly_count += 1 - else: - if poly_selection.intersects(self.solid_geometry): - shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0, - shape=self.solid_geometry, - color=self.app.defaults['global_sel_draw_color'] + 'AF', - face_color=self.app.defaults[ - 'global_sel_draw_color'] + 'AF', - visible=True) - self.poly_dict[shape_id] = self.solid_geometry - added_poly_count += 1 - - if added_poly_count > 0: - self.app.tool_shapes.redraw() - self.app.inform.emit( - '%s: %d. %s' % (_("Added polygon"), - int(added_poly_count), - _("Click to add next polygon or right click to start isolation.")) - ) - else: - self.app.inform.emit(_("No polygon in selection.")) - - def isolate(self, iso_type=None, geometry=None, dia=None, passes=None, overlap=None, outname=None, combine=None, - milling_type=None, follow=None, plot=True): - """ - Creates an isolation routing geometry object in the project. - - :param iso_type: type of isolation to be done: 0 = exteriors, 1 = interiors and 2 = both - :param geometry: specific geometry to isolate - :param dia: Tool diameter - :param passes: Number of tool widths to cut - :param overlap: Overlap between passes in fraction of tool diameter - :param outname: Base name of the output object - :param combine: Boolean: if to combine passes in one resulting object in case of multiple passes - :param milling_type: type of milling: conventional or climbing - :param follow: Boolean: if to generate a 'follow' geometry - :param plot: Boolean: if to plot the resulting geometry object - :return: None - """ - - if geometry is None: - work_geo = self.follow_geometry if follow is True else self.solid_geometry - else: - work_geo = geometry - - if dia is None: - dia = float(self.options["isotooldia"]) - - if passes is None: - passes = int(self.options["isopasses"]) - - if overlap is None: - overlap = float(self.options["isooverlap"]) - - overlap /= 100.0 - - combine = self.options["combine_passes"] if combine is None else bool(combine) - - if milling_type is None: - milling_type = self.options["milling_type"] - - if iso_type is None: - iso_t = 2 - else: - iso_t = iso_type - - base_name = self.options["name"] - - if combine: - if outname is None: - if self.iso_type == 0: - iso_name = base_name + "_ext_iso" - elif self.iso_type == 1: - iso_name = base_name + "_int_iso" - else: - iso_name = base_name + "_iso" - else: - iso_name = outname - - def iso_init(geo_obj, app_obj): - # Propagate options - geo_obj.options["cnctooldia"] = str(self.options["isotooldia"]) - geo_obj.tool_type = self.ui.tool_type_radio.get_value().upper() - - geo_obj.solid_geometry = [] - - # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI - if self.ui.tool_type_radio.get_value() == 'v': - new_cutz = self.ui.cutz_spinner.get_value() - new_vtipdia = self.ui.tipdia_spinner.get_value() - new_vtipangle = self.ui.tipangle_spinner.get_value() - tool_type = 'V' - else: - new_cutz = self.app.defaults['geometry_cutz'] - new_vtipdia = self.app.defaults['geometry_vtipdia'] - new_vtipangle = self.app.defaults['geometry_vtipangle'] - tool_type = 'C1' - - # store here the default data for Geometry Data - default_data = {} - default_data.update({ - "name": iso_name, - "plot": self.app.defaults['geometry_plot'], - "cutz": new_cutz, - "vtipdia": new_vtipdia, - "vtipangle": new_vtipangle, - "travelz": self.app.defaults['geometry_travelz'], - "feedrate": self.app.defaults['geometry_feedrate'], - "feedrate_z": self.app.defaults['geometry_feedrate_z'], - "feedrate_rapid": self.app.defaults['geometry_feedrate_rapid'], - "dwell": self.app.defaults['geometry_dwell'], - "dwelltime": self.app.defaults['geometry_dwelltime'], - "multidepth": self.app.defaults['geometry_multidepth'], - "ppname_g": self.app.defaults['geometry_ppname_g'], - "depthperpass": self.app.defaults['geometry_depthperpass'], - "extracut": self.app.defaults['geometry_extracut'], - "extracut_length": self.app.defaults['geometry_extracut_length'], - "toolchange": self.app.defaults['geometry_toolchange'], - "toolchangez": self.app.defaults['geometry_toolchangez'], - "endz": self.app.defaults['geometry_endz'], - "spindlespeed": self.app.defaults['geometry_spindlespeed'], - "toolchangexy": self.app.defaults['geometry_toolchangexy'], - "startz": self.app.defaults['geometry_startz'] - }) - - geo_obj.tools = {} - geo_obj.tools['1'] = {} - geo_obj.tools.update({ - '1': { - 'tooldia': float(self.options["isotooldia"]), - 'offset': 'Path', - 'offset_value': 0.0, - 'type': _('Rough'), - 'tool_type': tool_type, - 'data': default_data, - 'solid_geometry': geo_obj.solid_geometry - } - }) - - for nr_pass in range(passes): - 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 - geom = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t, - follow=follow, nr_passes=nr_pass) - - if geom == 'fail': - app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated.")) - return 'fail' - geo_obj.solid_geometry.append(geom) - - # update the geometry in the tools - geo_obj.tools['1']['solid_geometry'] = geo_obj.solid_geometry - - # detect if solid_geometry is empty and this require list flattening which is "heavy" - # or just looking in the lists (they are one level depth) and if any is not empty - # proceed with object creation, if there are empty and the number of them is the length - # of the list then we have an empty solid_geometry which should raise a Custom Exception - empty_cnt = 0 - if not isinstance(geo_obj.solid_geometry, list) and \ - not isinstance(geo_obj.solid_geometry, MultiPolygon): - geo_obj.solid_geometry = [geo_obj.solid_geometry] - - for g in geo_obj.solid_geometry: - if g: - break - else: - empty_cnt += 1 - - if empty_cnt == len(geo_obj.solid_geometry): - raise ValidationError("Empty Geometry", None) - else: - app_obj.inform.emit('[success] %s" %s' % (_("Isolation geometry created"), geo_obj.options["name"])) - - # even if combine is checked, one pass is still single-geo - geo_obj.multigeo = True if passes > 1 else False - - # ############################################################ - # ########## AREA SUBTRACTION ################################ - # ############################################################ - if self.ui.except_cb.get_value(): - self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo")) - geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry) - - # TODO: Do something if this is None. Offer changing name? - 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.0000001) - (i * overlap * dia) - if passes > 1: - if outname is None: - if self.iso_type == 0: - iso_name = base_name + "_ext_iso" + str(i + 1) - elif self.iso_type == 1: - iso_name = base_name + "_int_iso" + str(i + 1) - else: - iso_name = base_name + "_iso" + str(i + 1) - else: - iso_name = outname - else: - if outname is None: - if self.iso_type == 0: - iso_name = base_name + "_ext_iso" - elif self.iso_type == 1: - iso_name = base_name + "_int_iso" - else: - iso_name = base_name + "_iso" - else: - iso_name = outname - - def iso_init(geo_obj, fc_obj): - # Propagate options - geo_obj.options["cnctooldia"] = str(self.options["isotooldia"]) - if self.ui.tool_type_radio.get_value() == 'v': - geo_obj.tool_type = 'V' - else: - geo_obj.tool_type = 'C1' - - # if milling type is climb then the move is counter-clockwise around features - mill_dir = 1 if milling_type == 'cl' else 0 - geom = self.generate_envelope(offset, mill_dir, geometry=work_geo, env_iso_type=iso_t, - follow=follow, - nr_passes=i) - - if geom == 'fail': - fc_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated.")) - return 'fail' - - geo_obj.solid_geometry = geom - - # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI - # even if the resulting geometry is not multigeo we add the tools dict which will hold the data - # required to be transfered to the Geometry object - if self.ui.tool_type_radio.get_value() == 'v': - new_cutz = self.ui.cutz_spinner.get_value() - new_vtipdia = self.ui.tipdia_spinner.get_value() - new_vtipangle = self.ui.tipangle_spinner.get_value() - tool_type = 'V' - else: - new_cutz = self.app.defaults['geometry_cutz'] - new_vtipdia = self.app.defaults['geometry_vtipdia'] - new_vtipangle = self.app.defaults['geometry_vtipangle'] - tool_type = 'C1' - - # store here the default data for Geometry Data - default_data = {} - default_data.update({ - "name": iso_name, - "plot": self.app.defaults['geometry_plot'], - "cutz": new_cutz, - "vtipdia": new_vtipdia, - "vtipangle": new_vtipangle, - "travelz": self.app.defaults['geometry_travelz'], - "feedrate": self.app.defaults['geometry_feedrate'], - "feedrate_z": self.app.defaults['geometry_feedrate_z'], - "feedrate_rapid": self.app.defaults['geometry_feedrate_rapid'], - "dwell": self.app.defaults['geometry_dwell'], - "dwelltime": self.app.defaults['geometry_dwelltime'], - "multidepth": self.app.defaults['geometry_multidepth'], - "ppname_g": self.app.defaults['geometry_ppname_g'], - "depthperpass": self.app.defaults['geometry_depthperpass'], - "extracut": self.app.defaults['geometry_extracut'], - "extracut_length": self.app.defaults['geometry_extracut_length'], - "toolchange": self.app.defaults['geometry_toolchange'], - "toolchangez": self.app.defaults['geometry_toolchangez'], - "endz": self.app.defaults['geometry_endz'], - "spindlespeed": self.app.defaults['geometry_spindlespeed'], - "toolchangexy": self.app.defaults['geometry_toolchangexy'], - "startz": self.app.defaults['geometry_startz'] - }) - - geo_obj.tools = {} - geo_obj.tools['1'] = {} - geo_obj.tools.update({ - '1': { - 'tooldia': float(self.options["isotooldia"]), - 'offset': 'Path', - 'offset_value': 0.0, - 'type': _('Rough'), - 'tool_type': tool_type, - 'data': default_data, - 'solid_geometry': geo_obj.solid_geometry - } - }) - - # detect if solid_geometry is empty and this require list flattening which is "heavy" - # or just looking in the lists (they are one level depth) and if any is not empty - # proceed with object creation, if there are empty and the number of them is the length - # of the list then we have an empty solid_geometry which should raise a Custom Exception - empty_cnt = 0 - if not isinstance(geo_obj.solid_geometry, list): - geo_obj.solid_geometry = [geo_obj.solid_geometry] - - for g in geo_obj.solid_geometry: - if g: - break - else: - empty_cnt += 1 - - if empty_cnt == len(geo_obj.solid_geometry): - raise ValidationError("Empty Geometry", None) - else: - fc_obj.inform.emit('[success] %s: %s' % - (_("Isolation geometry created"), geo_obj.options["name"])) - geo_obj.multigeo = False - - # ############################################################ - # ########## AREA SUBTRACTION ################################ - # ############################################################ - if self.ui.except_cb.get_value(): - self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo")) - geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry) - - # TODO: Do something if this is None. Offer changing name? - self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot) - def generate_envelope(self, offset, invert, geometry=None, env_iso_type=2, follow=None, nr_passes=0): # isolation_geometry produces an envelope that is going on the left of the geometry # (the copper features). To leave the least amount of burrs on the features @@ -1117,65 +491,6 @@ class GerberObject(FlatCAMObj, Gerber): return 'fail' return geom - def area_subtraction(self, geo, subtractor_geo=None): - """ - Subtracts the subtractor_geo (if present else self.solid_geometry) from the geo - - :param geo: target geometry from which to subtract - :param subtractor_geo: geometry that acts as subtractor - :return: - """ - new_geometry = [] - target_geo = geo - - if subtractor_geo: - sub_union = cascaded_union(subtractor_geo) - else: - name = self.ui.obj_combo.currentText() - subtractor_obj = self.app.collection.get_by_name(name) - sub_union = cascaded_union(subtractor_obj.solid_geometry) - - try: - for geo_elem in target_geo: - if isinstance(geo_elem, Polygon): - for ring in self.poly2rings(geo_elem): - new_geo = ring.difference(sub_union) - if new_geo and not new_geo.is_empty: - new_geometry.append(new_geo) - elif isinstance(geo_elem, MultiPolygon): - for poly in geo_elem: - for ring in self.poly2rings(poly): - new_geo = ring.difference(sub_union) - if new_geo and not new_geo.is_empty: - new_geometry.append(new_geo) - elif isinstance(geo_elem, LineString): - new_geo = geo_elem.difference(sub_union) - if new_geo: - if not new_geo.is_empty: - new_geometry.append(new_geo) - elif isinstance(geo_elem, MultiLineString): - for line_elem in geo_elem: - new_geo = line_elem.difference(sub_union) - if new_geo and not new_geo.is_empty: - new_geometry.append(new_geo) - except TypeError: - if isinstance(target_geo, Polygon): - for ring in self.poly2rings(target_geo): - new_geo = ring.difference(sub_union) - if new_geo: - if not new_geo.is_empty: - new_geometry.append(new_geo) - elif isinstance(target_geo, LineString): - new_geo = target_geo.difference(sub_union) - if new_geo and not new_geo.is_empty: - new_geometry.append(new_geo) - elif isinstance(target_geo, MultiLineString): - for line_elem in target_geo: - new_geo = line_elem.difference(sub_union) - if new_geo and not new_geo.is_empty: - new_geometry.append(new_geo) - return new_geometry - def on_plot_cb_click(self, *args): if self.muted_ui: return diff --git a/AppTools/ToolEtchCompensation.py b/AppTools/ToolEtchCompensation.py index e41e46be..403be024 100644 --- a/AppTools/ToolEtchCompensation.py +++ b/AppTools/ToolEtchCompensation.py @@ -95,7 +95,7 @@ class ToolEtchCompensation(AppTool): hlay_1 = QtWidgets.QHBoxLayout() - self.oz_entry = NumericalEvalEntry() + self.oz_entry = NumericalEvalEntry(border_color='#0069A9') self.oz_entry.setPlaceholderText(_("Oz value")) self.oz_to_um_entry = FCEntry() self.oz_to_um_entry.setPlaceholderText(_("Microns value")) @@ -116,7 +116,7 @@ class ToolEtchCompensation(AppTool): hlay_2 = QtWidgets.QHBoxLayout() - self.mils_entry = NumericalEvalEntry() + self.mils_entry = NumericalEvalEntry(border_color='#0069A9') self.mils_entry.setPlaceholderText(_("Mils value")) self.mils_to_um_entry = FCEntry() self.mils_to_um_entry.setPlaceholderText(_("Microns value")) @@ -180,7 +180,7 @@ class ToolEtchCompensation(AppTool): _("The ratio between depth etch and lateral etch .\n" "Accepts real numbers and formulas using the operators: /,*,+,-,%") ) - self.factor_entry = NumericalEvalEntry() + self.factor_entry = NumericalEvalEntry(border_color='#0069A9') self.factor_entry.setPlaceholderText(_("Real number or formula")) self.factor_entry.setObjectName(_("Etch_factor")) diff --git a/AppTools/ToolIsolation.py b/AppTools/ToolIsolation.py index d64cf016..915cfb53 100644 --- a/AppTools/ToolIsolation.py +++ b/AppTools/ToolIsolation.py @@ -213,7 +213,7 @@ class ToolIsolation(AppTool, Gerber): "- 'V-shape'\n" "- Circular") ) - self.tool_type_radio.setObjectName(_("Tool Type")) + self.tool_type_radio.setObjectName("i_tool_type") self.grid3.addWidget(self.tool_type_label, 2, 0) self.grid3.addWidget(self.tool_type_radio, 2, 1) @@ -226,7 +226,7 @@ class ToolIsolation(AppTool, Gerber): self.tipdia_entry.set_precision(self.decimals) self.tipdia_entry.set_range(0.0000, 9999.9999) self.tipdia_entry.setSingleStep(0.1) - self.tipdia_entry.setObjectName(_("V-Tip Dia")) + self.tipdia_entry.setObjectName("i_vtipdia") self.grid3.addWidget(self.tipdialabel, 3, 0) self.grid3.addWidget(self.tipdia_entry, 3, 1) @@ -240,7 +240,7 @@ class ToolIsolation(AppTool, Gerber): self.tipangle_entry.set_precision(self.decimals) self.tipangle_entry.set_range(0.0000, 180.0000) self.tipangle_entry.setSingleStep(5) - self.tipangle_entry.setObjectName(_("V-Tip Angle")) + self.tipangle_entry.setObjectName("i_vtipangle") self.grid3.addWidget(self.tipanglelabel, 4, 0) self.grid3.addWidget(self.tipangle_entry, 4, 1) @@ -254,7 +254,7 @@ class ToolIsolation(AppTool, Gerber): self.cutz_entry = FCDoubleSpinner(callback=self.confirmation_message) self.cutz_entry.set_precision(self.decimals) self.cutz_entry.set_range(-99999.9999, 0.0000) - self.cutz_entry.setObjectName(_("Cut Z")) + self.cutz_entry.setObjectName("i_vcutz") self.grid3.addWidget(cutzlabel, 5, 0) self.grid3.addWidget(self.cutz_entry, 5, 1) @@ -269,7 +269,7 @@ class ToolIsolation(AppTool, Gerber): self.addtool_entry = FCDoubleSpinner(callback=self.confirmation_message) self.addtool_entry.set_precision(self.decimals) self.addtool_entry.set_range(0.000, 9999.9999) - self.addtool_entry.setObjectName(_("Tool Dia")) + self.addtool_entry.setObjectName("i_new_tooldia") self.grid3.addWidget(self.addtool_entry_lbl, 6, 0) self.grid3.addWidget(self.addtool_entry, 6, 1) @@ -402,7 +402,7 @@ class ToolIsolation(AppTool, Gerber): self.iso_type_radio = RadioSet([{'label': _('Full'), 'value': 'full'}, {'label': _('Ext'), 'value': 'ext'}, {'label': _('Int'), 'value': 'int'}]) - self.iso_type_radio.setObjectName("i_type") + self.iso_type_radio.setObjectName("i_iso_type") self.grid3.addWidget(self.iso_type_label, 17, 0) self.grid3.addWidget(self.iso_type_radio, 17, 1) @@ -432,8 +432,8 @@ class ToolIsolation(AppTool, Gerber): self.grid3.addWidget(self.gen_param_label, 24, 0, 1, 2) # Rest Machining - self.rest_cb = FCCheckBox('%s' % _("Rest Machining")) - self.rest_cb.setObjectName("i_rest_machining") + self.rest_cb = FCCheckBox('%s' % _("Rest")) + self.rest_cb.setObjectName("i_rest") self.rest_cb.setToolTip( _("If checked, use 'rest machining'.\n" "Basically it will isolate outside PCB features,\n" @@ -677,21 +677,21 @@ class ToolIsolation(AppTool, Gerber): self.tooldia = None self.form_fields = { - "tools_iso_passes": self.passes_entry, - "tools_iso_overlap": self.iso_overlap_entry, - "tools_iso_milling_type": self.milling_type_radio, - "tools_iso_combine": self.combine_passes_cb, - "tools_iso_follow": self.follow_cb, - "tools_iso_isotype": self.iso_type_radio + "tools_iso_passes": self.passes_entry, + "tools_iso_overlap": self.iso_overlap_entry, + "tools_iso_milling_type": self.milling_type_radio, + "tools_iso_combine": self.combine_passes_cb, + "tools_iso_follow": self.follow_cb, + "tools_iso_isotype": self.iso_type_radio } self.name2option = { - "i_passes": "tools_iso_passes", - "i_overlap": "tools_iso_overlap", - "i_milling_type": "tools_iso_milling_type", - "i_combine": "tools_iso_combine", - "i_follow": "tools_iso_follow", - "i_type": "tools_iso_isotype" + "i_passes": "tools_iso_passes", + "i_overlap": "tools_iso_overlap", + "i_milling_type": "tools_iso_milling_type", + "i_combine": "tools_iso_combine", + "i_follow": "tools_iso_follow", + "i_iso_type": "tools_iso_isotype" } self.old_tool_dia = None @@ -715,7 +715,7 @@ class ToolIsolation(AppTool, Gerber): self.type_excobj_radio.activated_custom.connect(self.on_type_excobj_index_changed) self.apply_param_to_all.clicked.connect(self.on_apply_param_to_all_clicked) - self.addtool_from_db_btn.clicked.connect(self.on_ncc_tool_add_from_db_clicked) + self.addtool_from_db_btn.clicked.connect(self.on_tool_add_from_db_clicked) self.generate_iso_button.clicked.connect(self.on_isolate_click) self.reset_button.clicked.connect(self.set_tool_ui) @@ -731,21 +731,6 @@ class ToolIsolation(AppTool, Gerber): "gerber": "Gerber", "geometry": "Geometry" }[self.type_excobj_radio.get_value()] - def on_operation_change(self, val): - if val == 'iso': - self.milling_type_label.setEnabled(True) - self.milling_type_radio.setEnabled(True) - else: - self.milling_type_label.setEnabled(False) - self.milling_type_radio.setEnabled(False) - - current_row = self.tools_table.currentRow() - try: - current_uid = int(self.tools_table.item(current_row, 3).text()) - self.iso_tools[current_uid]['data']['tools_nccoperation'] = val - except AttributeError: - return - def on_row_selection_change(self): self.blockSignals(True) @@ -782,7 +767,7 @@ class ToolIsolation(AppTool, Gerber): form_value_storage = tooluid_value[key] self.storage_to_form(form_value_storage) except Exception as e: - log.debug("NonCopperClear ---> update_ui() " + str(e)) + log.debug("ToolIsolation ---> update_ui() " + str(e)) else: self.tool_data_label.setText( "%s: %s" % (_('Parameters for'), _("Multiple Tools")) @@ -797,7 +782,7 @@ class ToolIsolation(AppTool, Gerber): try: self.form_fields[form_key].set_value(dict_storage[form_key]) except Exception as e: - log.debug("NonCopperClear.storage_to_form() --> %s" % str(e)) + log.debug("ToolIsolation.storage_to_form() --> %s" % str(e)) pass def form_to_storage(self): @@ -831,7 +816,7 @@ class ToolIsolation(AppTool, Gerber): def on_apply_param_to_all_clicked(self): if self.tools_table.rowCount() == 0: # there is no tool in tool table so we can't save the GUI elements values to storage - log.debug("NonCopperClear.on_apply_param_to_all_clicked() --> no tool in Tools Table, aborting.") + log.debug("ToolIsolation.on_apply_param_to_all_clicked() --> no tool in Tools Table, aborting.") return self.blockSignals(True) @@ -853,47 +838,7 @@ class ToolIsolation(AppTool, Gerber): for tooluid_key, tooluid_val in self.iso_tools.items(): tooluid_val['data'] = deepcopy(temp_tool_data) - # store all the data associated with the row parameter to the self.tools storage - # tooldia_item = float(self.tools_table.item(row, 1).text()) - # type_item = self.tools_table.cellWidget(row, 2).currentText() - # operation_type_item = self.tools_table.cellWidget(row, 4).currentText() - # - # nccoffset_item = self.ncc_choice_offset_cb.get_value() - # nccoffset_value_item = float(self.ncc_offset_spinner.get_value()) - - # this new dict will hold the actual useful data, another dict that is the value of key 'data' - # temp_tools = {} - # temp_dia = {} - # temp_data = {} - # - # for tooluid_key, tooluid_value in self.iso_tools.items(): - # for key, value in tooluid_value.items(): - # if key == 'data': - # # update the 'data' section - # for data_key in tooluid_value[key].keys(): - # for form_key, form_value in self.form_fields.items(): - # if form_key == data_key: - # temp_data[data_key] = form_value.get_value() - # # make sure we make a copy of the keys not in the form (we may use 'data' keys that are - # # updated from self.app.defaults - # if data_key not in self.form_fields: - # temp_data[data_key] = value[data_key] - # temp_dia[key] = deepcopy(temp_data) - # temp_data.clear() - # - # elif key == 'solid_geometry': - # temp_dia[key] = deepcopy(self.tools[tooluid_key]['solid_geometry']) - # else: - # temp_dia[key] = deepcopy(value) - # - # temp_tools[tooluid_key] = deepcopy(temp_dia) - # - # self.iso_tools.clear() - # self.iso_tools = deepcopy(temp_tools) - # temp_tools.clear() - self.app.inform.emit('[success] %s' % _("Current Tool parameters were applied to all tools.")) - self.blockSignals(False) def on_add_tool_by_key(self): @@ -979,14 +924,14 @@ class ToolIsolation(AppTool, Gerber): self.iso_type_radio.set_value('full') self.iso_type_radio.hide() - self.follow_cb.setChecked(False) + self.follow_cb.set_value(False) self.follow_cb.hide() self.follow_label.hide() - self.rest_cb.setChecked(False) + self.rest_cb.set_value(False) self.rest_cb.hide() - self.except_cb.setChecked(False) + self.except_cb.set_value(False) self.except_cb.hide() self.select_combo.setCurrentIndex(0) @@ -995,7 +940,6 @@ class ToolIsolation(AppTool, Gerber): else: self.level.setText('%s' % _('Advanced')) - # TODO remember to set the GUI elements to values from app.defaults dict self.tool_type_radio.set_value(self.app.defaults["tools_iso_tool_type"]) self.tool_type_label.show() self.tool_type_radio.show() @@ -1007,17 +951,17 @@ class ToolIsolation(AppTool, Gerber): self.iso_type_radio.set_value(self.app.defaults["tools_iso_isotype"]) self.iso_type_radio.show() - self.follow_cb.setChecked(self.app.defaults["tools_iso_follow"]) + self.follow_cb.set_value(self.app.defaults["tools_iso_follow"]) self.follow_cb.show() self.follow_label.show() - self.rest_cb.setChecked(self.app.defaults["tools_iso_rest"]) + self.rest_cb.set_value(self.app.defaults["tools_iso_rest"]) self.rest_cb.show() - self.except_cb.setChecked(self.app.defaults["tools_iso_isoexcept"]) + self.except_cb.set_value(self.app.defaults["tools_iso_isoexcept"]) self.except_cb.show() - self.select_combo.setCurrentIndex(self.app.defaults["tools_iso_selection"]) + self.select_combo.set_value(self.app.defaults["tools_iso_selection"]) self.select_combo.show() self.select_label.show() @@ -1185,21 +1129,12 @@ class ToolIsolation(AppTool, Gerber): tool_uid_item = QtWidgets.QTableWidgetItem(str(int(tooluid_key))) - # operation_type = FCComboBox() - # operation_type.addItems(['iso_op', 'clear_op']) - # - # # operation_type.setStyleSheet('background-color: rgb(255,255,255)') - # op_idx = operation_type.findText(tooluid_value['operation']) - # operation_type.setCurrentIndex(op_idx) - self.tools_table.setItem(row_no, 1, dia) # Diameter self.tools_table.setCellWidget(row_no, 2, tool_type_item) # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ## self.tools_table.setItem(row_no, 3, tool_uid_item) # Tool unique ID - # self.tools_table.setCellWidget(row_no, 4, operation_type) - # make the diameter column editable for row in range(tool_id): self.tools_table.item(row, 1).setFlags( @@ -1746,6 +1681,596 @@ class ToolIsolation(AppTool, Gerber): isotooldia=self.iso_dia_list, outname=self.o_name) + + + + # ########################################### + # ########################################### + # ########################################### + # ########################################### + + def on_iso_button_click(self, *args): + + obj = self.app.collection.get_active() + + self.iso_type = 2 + if self.ui.iso_type_radio.get_value() == 'ext': + self.iso_type = 0 + if self.ui.iso_type_radio.get_value() == 'int': + self.iso_type = 1 + + def worker_task(iso_obj, app_obj): + with self.app.proc_container.new(_("Isolating...")): + if self.ui.follow_cb.get_value() is True: + iso_obj.follow_geo() + # in the end toggle the visibility of the origin object so we can see the generated Geometry + iso_obj.ui.plot_cb.toggle() + else: + app_obj.defaults.report_usage("gerber_on_iso_button") + self.read_form() + + iso_scope = 'all' if self.ui.iso_scope_radio.get_value() == 'all' else 'single' + self.isolate_handler(iso_type=self.iso_type, iso_scope=iso_scope) + + self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]}) + + def follow_geo(self, outname=None): + """ + Creates a geometry object "following" the gerber paths. + + :return: None + """ + + # default_name = self.options["name"] + "_follow" + # follow_name = outname or default_name + + if outname is None: + follow_name = self.options["name"] + "_follow" + else: + follow_name = outname + + def follow_init(follow_obj, app): + # Propagate options + follow_obj.options["cnctooldia"] = str(self.options["isotooldia"]) + follow_obj.solid_geometry = self.follow_geometry + + # TODO: Do something if this is None. Offer changing name? + try: + self.app.app_obj.new_object("geometry", follow_name, follow_init) + except Exception as e: + return "Operation failed: %s" % str(e) + + def isolate_handler(self, iso_type, iso_scope): + + if iso_scope == 'all': + self.isolate(iso_type=iso_type) + else: + # disengage the grid snapping since it may be hard to click on polygons with grid snapping on + if self.app.ui.grid_snap_btn.isChecked(): + self.grid_status_memory = True + self.app.ui.grid_snap_btn.trigger() + else: + self.grid_status_memory = False + + self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release) + + if self.app.is_legacy is False: + self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot) + else: + self.app.plotcanvas.graph_event_disconnect(self.app.mr) + + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click on a polygon to isolate it.")) + + def isolate(self, iso_type=None, geometry=None, dia=None, passes=None, overlap=None, outname=None, combine=None, + milling_type=None, follow=None, plot=True): + """ + Creates an isolation routing geometry object in the project. + + :param iso_type: type of isolation to be done: 0 = exteriors, 1 = interiors and 2 = both + :param geometry: specific geometry to isolate + :param dia: Tool diameter + :param passes: Number of tool widths to cut + :param overlap: Overlap between passes in fraction of tool diameter + :param outname: Base name of the output object + :param combine: Boolean: if to combine passes in one resulting object in case of multiple passes + :param milling_type: type of milling: conventional or climbing + :param follow: Boolean: if to generate a 'follow' geometry + :param plot: Boolean: if to plot the resulting geometry object + :return: None + """ + + if geometry is None: + work_geo = self.follow_geometry if follow is True else self.solid_geometry + else: + work_geo = geometry + + if dia is None: + dia = float(self.options["isotooldia"]) + + if passes is None: + passes = int(self.options["isopasses"]) + + if overlap is None: + overlap = float(self.options["isooverlap"]) + + overlap /= 100.0 + + combine = self.options["combine_passes"] if combine is None else bool(combine) + + if milling_type is None: + milling_type = self.options["milling_type"] + + if iso_type is None: + iso_t = 2 + else: + iso_t = iso_type + + base_name = self.options["name"] + + if combine: + if outname is None: + if self.iso_type == 0: + iso_name = base_name + "_ext_iso" + elif self.iso_type == 1: + iso_name = base_name + "_int_iso" + else: + iso_name = base_name + "_iso" + else: + iso_name = outname + + def iso_init(geo_obj, app_obj): + # Propagate options + geo_obj.options["cnctooldia"] = str(self.options["isotooldia"]) + geo_obj.tool_type = self.ui.tool_type_radio.get_value().upper() + + geo_obj.solid_geometry = [] + + # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI + if self.ui.tool_type_radio.get_value() == 'v': + new_cutz = self.ui.cutz_spinner.get_value() + new_vtipdia = self.ui.tipdia_spinner.get_value() + new_vtipangle = self.ui.tipangle_spinner.get_value() + tool_type = 'V' + else: + new_cutz = self.app.defaults['geometry_cutz'] + new_vtipdia = self.app.defaults['geometry_vtipdia'] + new_vtipangle = self.app.defaults['geometry_vtipangle'] + tool_type = 'C1' + + # store here the default data for Geometry Data + default_data = {} + default_data.update({ + "name": iso_name, + "plot": self.app.defaults['geometry_plot'], + "cutz": new_cutz, + "vtipdia": new_vtipdia, + "vtipangle": new_vtipangle, + "travelz": self.app.defaults['geometry_travelz'], + "feedrate": self.app.defaults['geometry_feedrate'], + "feedrate_z": self.app.defaults['geometry_feedrate_z'], + "feedrate_rapid": self.app.defaults['geometry_feedrate_rapid'], + "dwell": self.app.defaults['geometry_dwell'], + "dwelltime": self.app.defaults['geometry_dwelltime'], + "multidepth": self.app.defaults['geometry_multidepth'], + "ppname_g": self.app.defaults['geometry_ppname_g'], + "depthperpass": self.app.defaults['geometry_depthperpass'], + "extracut": self.app.defaults['geometry_extracut'], + "extracut_length": self.app.defaults['geometry_extracut_length'], + "toolchange": self.app.defaults['geometry_toolchange'], + "toolchangez": self.app.defaults['geometry_toolchangez'], + "endz": self.app.defaults['geometry_endz'], + "spindlespeed": self.app.defaults['geometry_spindlespeed'], + "toolchangexy": self.app.defaults['geometry_toolchangexy'], + "startz": self.app.defaults['geometry_startz'] + }) + + geo_obj.tools = {} + geo_obj.tools['1'] = {} + geo_obj.tools.update({ + '1': { + 'tooldia': float(self.options["isotooldia"]), + 'offset': 'Path', + 'offset_value': 0.0, + 'type': _('Rough'), + 'tool_type': tool_type, + 'data': default_data, + 'solid_geometry': geo_obj.solid_geometry + } + }) + + for nr_pass in range(passes): + 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 + geom = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t, + follow=follow, nr_passes=nr_pass) + + if geom == 'fail': + app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated.")) + return 'fail' + geo_obj.solid_geometry.append(geom) + + # update the geometry in the tools + geo_obj.tools['1']['solid_geometry'] = geo_obj.solid_geometry + + # detect if solid_geometry is empty and this require list flattening which is "heavy" + # or just looking in the lists (they are one level depth) and if any is not empty + # proceed with object creation, if there are empty and the number of them is the length + # of the list then we have an empty solid_geometry which should raise a Custom Exception + empty_cnt = 0 + if not isinstance(geo_obj.solid_geometry, list) and \ + not isinstance(geo_obj.solid_geometry, MultiPolygon): + geo_obj.solid_geometry = [geo_obj.solid_geometry] + + for g in geo_obj.solid_geometry: + if g: + break + else: + empty_cnt += 1 + + if empty_cnt == len(geo_obj.solid_geometry): + raise ValidationError("Empty Geometry", None) + else: + app_obj.inform.emit('[success] %s" %s' % (_("Isolation geometry created"), geo_obj.options["name"])) + + # even if combine is checked, one pass is still single-geo + geo_obj.multigeo = True if passes > 1 else False + + # ############################################################ + # ########## AREA SUBTRACTION ################################ + # ############################################################ + if self.ui.except_cb.get_value(): + self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo")) + geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry) + + # TODO: Do something if this is None. Offer changing name? + 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.0000001) - (i * overlap * dia) + if passes > 1: + if outname is None: + if self.iso_type == 0: + iso_name = base_name + "_ext_iso" + str(i + 1) + elif self.iso_type == 1: + iso_name = base_name + "_int_iso" + str(i + 1) + else: + iso_name = base_name + "_iso" + str(i + 1) + else: + iso_name = outname + else: + if outname is None: + if self.iso_type == 0: + iso_name = base_name + "_ext_iso" + elif self.iso_type == 1: + iso_name = base_name + "_int_iso" + else: + iso_name = base_name + "_iso" + else: + iso_name = outname + + def iso_init(geo_obj, fc_obj): + # Propagate options + geo_obj.options["cnctooldia"] = str(self.options["isotooldia"]) + if self.ui.tool_type_radio.get_value() == 'v': + geo_obj.tool_type = 'V' + else: + geo_obj.tool_type = 'C1' + + # if milling type is climb then the move is counter-clockwise around features + mill_dir = 1 if milling_type == 'cl' else 0 + geom = self.generate_envelope(offset, mill_dir, geometry=work_geo, env_iso_type=iso_t, + follow=follow, + nr_passes=i) + + if geom == 'fail': + fc_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated.")) + return 'fail' + + geo_obj.solid_geometry = geom + + # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI + # even if the resulting geometry is not multigeo we add the tools dict which will hold the data + # required to be transfered to the Geometry object + if self.ui.tool_type_radio.get_value() == 'v': + new_cutz = self.ui.cutz_spinner.get_value() + new_vtipdia = self.ui.tipdia_spinner.get_value() + new_vtipangle = self.ui.tipangle_spinner.get_value() + tool_type = 'V' + else: + new_cutz = self.app.defaults['geometry_cutz'] + new_vtipdia = self.app.defaults['geometry_vtipdia'] + new_vtipangle = self.app.defaults['geometry_vtipangle'] + tool_type = 'C1' + + # store here the default data for Geometry Data + default_data = {} + default_data.update({ + "name": iso_name, + "plot": self.app.defaults['geometry_plot'], + "cutz": new_cutz, + "vtipdia": new_vtipdia, + "vtipangle": new_vtipangle, + "travelz": self.app.defaults['geometry_travelz'], + "feedrate": self.app.defaults['geometry_feedrate'], + "feedrate_z": self.app.defaults['geometry_feedrate_z'], + "feedrate_rapid": self.app.defaults['geometry_feedrate_rapid'], + "dwell": self.app.defaults['geometry_dwell'], + "dwelltime": self.app.defaults['geometry_dwelltime'], + "multidepth": self.app.defaults['geometry_multidepth'], + "ppname_g": self.app.defaults['geometry_ppname_g'], + "depthperpass": self.app.defaults['geometry_depthperpass'], + "extracut": self.app.defaults['geometry_extracut'], + "extracut_length": self.app.defaults['geometry_extracut_length'], + "toolchange": self.app.defaults['geometry_toolchange'], + "toolchangez": self.app.defaults['geometry_toolchangez'], + "endz": self.app.defaults['geometry_endz'], + "spindlespeed": self.app.defaults['geometry_spindlespeed'], + "toolchangexy": self.app.defaults['geometry_toolchangexy'], + "startz": self.app.defaults['geometry_startz'] + }) + + geo_obj.tools = {} + geo_obj.tools['1'] = {} + geo_obj.tools.update({ + '1': { + 'tooldia': float(self.options["isotooldia"]), + 'offset': 'Path', + 'offset_value': 0.0, + 'type': _('Rough'), + 'tool_type': tool_type, + 'data': default_data, + 'solid_geometry': geo_obj.solid_geometry + } + }) + + # detect if solid_geometry is empty and this require list flattening which is "heavy" + # or just looking in the lists (they are one level depth) and if any is not empty + # proceed with object creation, if there are empty and the number of them is the length + # of the list then we have an empty solid_geometry which should raise a Custom Exception + empty_cnt = 0 + if not isinstance(geo_obj.solid_geometry, list): + geo_obj.solid_geometry = [geo_obj.solid_geometry] + + for g in geo_obj.solid_geometry: + if g: + break + else: + empty_cnt += 1 + + if empty_cnt == len(geo_obj.solid_geometry): + raise ValidationError("Empty Geometry", None) + else: + fc_obj.inform.emit('[success] %s: %s' % + (_("Isolation geometry created"), geo_obj.options["name"])) + geo_obj.multigeo = False + + # ############################################################ + # ########## AREA SUBTRACTION ################################ + # ############################################################ + if self.ui.except_cb.get_value(): + self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo")) + geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry) + + # TODO: Do something if this is None. Offer changing name? + self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot) + + def area_subtraction(self, geo, subtractor_geo=None): + """ + Subtracts the subtractor_geo (if present else self.solid_geometry) from the geo + + :param geo: target geometry from which to subtract + :param subtractor_geo: geometry that acts as subtractor + :return: + """ + new_geometry = [] + target_geo = geo + + if subtractor_geo: + sub_union = cascaded_union(subtractor_geo) + else: + name = self.ui.obj_combo.currentText() + subtractor_obj = self.app.collection.get_by_name(name) + sub_union = cascaded_union(subtractor_obj.solid_geometry) + + try: + for geo_elem in target_geo: + if isinstance(geo_elem, Polygon): + for ring in self.poly2rings(geo_elem): + new_geo = ring.difference(sub_union) + if new_geo and not new_geo.is_empty: + new_geometry.append(new_geo) + elif isinstance(geo_elem, MultiPolygon): + for poly in geo_elem: + for ring in self.poly2rings(poly): + new_geo = ring.difference(sub_union) + if new_geo and not new_geo.is_empty: + new_geometry.append(new_geo) + elif isinstance(geo_elem, LineString): + new_geo = geo_elem.difference(sub_union) + if new_geo: + if not new_geo.is_empty: + new_geometry.append(new_geo) + elif isinstance(geo_elem, MultiLineString): + for line_elem in geo_elem: + new_geo = line_elem.difference(sub_union) + if new_geo and not new_geo.is_empty: + new_geometry.append(new_geo) + except TypeError: + if isinstance(target_geo, Polygon): + for ring in self.poly2rings(target_geo): + new_geo = ring.difference(sub_union) + if new_geo: + if not new_geo.is_empty: + new_geometry.append(new_geo) + elif isinstance(target_geo, LineString): + new_geo = target_geo.difference(sub_union) + if new_geo and not new_geo.is_empty: + new_geometry.append(new_geo) + elif isinstance(target_geo, MultiLineString): + for line_elem in target_geo: + new_geo = line_elem.difference(sub_union) + if new_geo and not new_geo.is_empty: + new_geometry.append(new_geo) + return new_geometry + + def on_mouse_click_release(self, event): + if self.app.is_legacy is False: + event_pos = event.pos + right_button = 2 + self.app.event_is_dragging = self.app.event_is_dragging + else: + event_pos = (event.xdata, event.ydata) + right_button = 3 + self.app.event_is_dragging = self.app.ui.popMenu.mouse_is_panning + + try: + x = float(event_pos[0]) + y = float(event_pos[1]) + except TypeError: + return + + event_pos = (x, y) + 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]) + else: + curr_pos = (curr_pos[0], curr_pos[1]) + + if event.button == 1: + clicked_poly = self.find_polygon(point=(curr_pos[0], curr_pos[1])) + + if self.app.selection_type is not None: + self.selection_area_handler(self.app.pos, curr_pos, self.app.selection_type) + self.app.selection_type = None + elif clicked_poly: + if clicked_poly not in self.poly_dict.values(): + shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0, shape=clicked_poly, + color=self.app.defaults['global_sel_draw_color'] + 'AF', + face_color=self.app.defaults['global_sel_draw_color'] + 'AF', + visible=True) + self.poly_dict[shape_id] = clicked_poly + self.app.inform.emit( + '%s: %d. %s' % (_("Added polygon"), int(len(self.poly_dict)), + _("Click to add next polygon or right click to start isolation.")) + ) + else: + try: + for k, v in list(self.poly_dict.items()): + if v == clicked_poly: + self.app.tool_shapes.remove(k) + self.poly_dict.pop(k) + break + except TypeError: + return + self.app.inform.emit( + '%s. %s' % (_("Removed polygon"), + _("Click to add/remove next polygon or right click to start isolation.")) + ) + + self.app.tool_shapes.redraw() + else: + self.app.inform.emit(_("No polygon detected under click position.")) + elif event.button == right_button and self.app.event_is_dragging is False: + # 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_mouse_click_release) + else: + self.app.plotcanvas.graph_event_disconnect(self.mr) + + self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release', + self.app.on_mouse_click_release_over_plot) + + self.app.tool_shapes.clear(update=True) + + if self.poly_dict: + poly_list = deepcopy(list(self.poly_dict.values())) + self.isolate(iso_type=self.iso_type, geometry=poly_list) + self.poly_dict.clear() + else: + self.app.inform.emit('[ERROR_NOTCL] %s' % _("List of single polygons is empty. Aborting.")) + + def selection_area_handler(self, start_pos, end_pos, sel_type): + """ + :param start_pos: mouse position when the selection LMB click was done + :param end_pos: mouse position when the left mouse button is released + :param sel_type: if True it's a left to right selection (enclosure), if False it's a 'touch' selection + :return: + """ + poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])]) + + # delete previous selection shape + self.app.delete_selection_shape() + + added_poly_count = 0 + try: + for geo in self.solid_geometry: + if geo not in self.poly_dict.values(): + if sel_type is True: + if geo.within(poly_selection): + shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0, + shape=geo, + color=self.app.defaults['global_sel_draw_color'] + 'AF', + face_color=self.app.defaults[ + 'global_sel_draw_color'] + 'AF', + visible=True) + self.poly_dict[shape_id] = geo + added_poly_count += 1 + else: + if poly_selection.intersects(geo): + shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0, + shape=geo, + color=self.app.defaults['global_sel_draw_color'] + 'AF', + face_color=self.app.defaults[ + 'global_sel_draw_color'] + 'AF', + visible=True) + self.poly_dict[shape_id] = geo + added_poly_count += 1 + except TypeError: + if self.solid_geometry not in self.poly_dict.values(): + if sel_type is True: + if self.solid_geometry.within(poly_selection): + shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0, + shape=self.solid_geometry, + color=self.app.defaults['global_sel_draw_color'] + 'AF', + face_color=self.app.defaults[ + 'global_sel_draw_color'] + 'AF', + visible=True) + self.poly_dict[shape_id] = self.solid_geometry + added_poly_count += 1 + else: + if poly_selection.intersects(self.solid_geometry): + shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0, + shape=self.solid_geometry, + color=self.app.defaults['global_sel_draw_color'] + 'AF', + face_color=self.app.defaults[ + 'global_sel_draw_color'] + 'AF', + visible=True) + self.poly_dict[shape_id] = self.solid_geometry + added_poly_count += 1 + + if added_poly_count > 0: + self.app.tool_shapes.redraw() + self.app.inform.emit( + '%s: %d. %s' % (_("Added polygon"), + int(added_poly_count), + _("Click to add next polygon or right click to start isolation.")) + ) + else: + self.app.inform.emit(_("No polygon in selection.")) + + # ########################################### + # ########################################### + # ########################################### + # ########################################### + + + + # To be called after clicking on the plot. def on_mouse_release(self, event): if self.app.is_legacy is False: @@ -2802,14 +3327,14 @@ class ToolIsolation(AppTool, Gerber): return 'fail' return geom - def on_ncc_tool_add_from_db_executed(self, tool): + def on_tool_add_from_db_executed(self, tool): """ Here add the tool from DB in the selected geometry object :return: """ tool_from_db = deepcopy(tool) - res = self.on_ncc_tool_from_db_inserted(tool=tool_from_db) + res = self.on_tool_from_db_inserted(tool=tool_from_db) for idx in range(self.app.ui.plot_tab_area.count()): if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"): @@ -2828,7 +3353,7 @@ class ToolIsolation(AppTool, Gerber): self.tools_table.selectRow(row) self.on_row_selection_change() - def on_ncc_tool_from_db_inserted(self, tool): + def on_tool_from_db_inserted(self, tool): """ Called from the Tools DB object through a App method when adding a tool from Tools Database :param tool: a dict with the tool data @@ -2887,7 +3412,7 @@ class ToolIsolation(AppTool, Gerber): # if self.tools_table.rowCount() != 0: # self.param_frame.setDisabled(False) - def on_ncc_tool_add_from_db_clicked(self): + def on_tool_add_from_db_clicked(self): """ Called when the user wants to add a new tool from Tools Database. It will create the Tools Database object and display the Tools Database tab in the form needed for the Tool adding @@ -2917,7 +3442,7 @@ class ToolIsolation(AppTool, Gerber): self.cursor_pos = None self.mouse_is_dragging = False - prog_plot = True if self.app.defaults["tools_ncc_plotting"] == 'progressive' else False + prog_plot = True if self.app.defaults["tools_iso_plotting"] == 'progressive' else False if prog_plot: self.temp_shapes.clear(update=True) diff --git a/AppTools/ToolNCC.py b/AppTools/ToolNCC.py index 1756eca1..c666629b 100644 --- a/AppTools/ToolNCC.py +++ b/AppTools/ToolNCC.py @@ -509,7 +509,7 @@ class NonCopperClear(AppTool, Gerber): self.grid3.addWidget(self.gen_param_label, 24, 0, 1, 2) # Rest Machining - self.ncc_rest_cb = FCCheckBox('%s' % _("Rest Machining")) + self.ncc_rest_cb = FCCheckBox('%s' % _("Rest")) self.ncc_rest_cb.setObjectName("n_rest_machining") self.ncc_rest_cb.setToolTip( diff --git a/AppTools/ToolPaint.py b/AppTools/ToolPaint.py index 9f929533..b2b68625 100644 --- a/AppTools/ToolPaint.py +++ b/AppTools/ToolPaint.py @@ -438,7 +438,7 @@ class ToolPaint(AppTool, Gerber): ) grid4.addWidget(self.gen_param_label, 15, 0, 1, 2) - self.rest_cb = FCCheckBox('%s' % _("Rest Machining")) + self.rest_cb = FCCheckBox('%s' % _("Rest")) self.rest_cb.setObjectName('p_rest_machining') self.rest_cb.setToolTip( _("If checked, use 'rest machining'.\n" diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ba27d7..1d201bb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ CHANGELOG for FlatCAM beta - if there is a Gerber object selected then in Isolation Tool the Gerber object combobox will show that object name as current - made the Project Tree items not editable by clicking on selected Tree items (the object rename can still be done in the Selected tab) - working on Isolation Tool: added a Preferences section in Edit -> Preferences and updated their usage within the Isolation tool +- fixed milling drills not plotting the resulting Geometry object +- all tuple entries in the Preferences UI are now protected against letter entry +- all entries in the Preferences UI that have numerical entry are protected now against letters +- cleaned the Preferences UI in the Gerber area 25.05.2020 diff --git a/camlib.py b/camlib.py index 5e8e328f..5e461043 100644 --- a/camlib.py +++ b/camlib.py @@ -4374,7 +4374,7 @@ class CNCjob(Geometry): self.gcode += self.create_gcode_multi_pass(geo, current_tooldia, extracut, self.extracut_length, tolerance, - z_move=z_move,_postproc=p, + z_move=z_move, postproc=p, old_point=current_pt) # calculate the travel distance diff --git a/defaults.py b/defaults.py index 4e7ba01a..326d9524 100644 --- a/defaults.py +++ b/defaults.py @@ -395,11 +395,12 @@ class FlatCAMDefaults: "tools_iso_follow": False, "tools_iso_isotype": "full", - "tools_iso_rest": False, + "tools_iso_rest": False, "tools_iso_combine_passes": False, - "tools_iso_isoexcept": False, - "tools_iso_selection": _("All"), - "tools_iso_area_shape": "square", + "tools_iso_isoexcept": False, + "tools_iso_selection": _("All"), + "tools_iso_area_shape": "square", + "tools_iso_plotting": 'normal', # NCC Tool "tools_ncctools": "1.0, 0.5", @@ -415,13 +416,13 @@ class FlatCAMDefaults: "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', "tools_ncccutz": -0.05, "tools_ncctipdia": 0.1, "tools_ncctipangle": 30, "tools_nccnewdia": 0.1, + "tools_ncc_plotting": 'normal', # Cutout Tool "tools_cutouttooldia": 2.4,