From 79fec61934b93a8b5362ad35951a3bbe0b16e5c7 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Thu, 18 Jun 2020 14:26:24 +0300 Subject: [PATCH] - fixed bug in the Cutout Tool that did not allowed the manual cutous to be added on a Geometry created in the Tool - fixed bug that made the selection box show in the stage of adding manual gaps - updated Cutout Tool UI - Cutout Tool - in manual gap adding there is now an option to automatically turn on the big cursor which could help - Cutout Tool - fixed errors when trying to add a manual gap without having a geometry object selected in the combobox --- CHANGELOG.md | 8 + appGUI/preferences/PreferencesUIManager.py | 19 +- .../tools/ToolsCutoutPrefGroupUI.py | 5 + appObjects/FlatCAMExcellon.py | 38 +- appTools/ToolCutOut.py | 1137 +++++++++-------- appTools/ToolNCC.py | 5 +- app_Main.py | 36 +- camlib.py | 14 +- defaults.py | 1 + 9 files changed, 717 insertions(+), 546 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28a0f71a..714f16ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ CHANGELOG for FlatCAM beta ================================================= +18.06.2020 + +- fixed bug in the Cutout Tool that did not allowed the manual cutous to be added on a Geometry created in the Tool +- fixed bug that made the selection box show in the stage of adding manual gaps +- updated Cutout Tool UI +- Cutout Tool - in manual gap adding there is now an option to automatically turn on the big cursor which could help +- Cutout Tool - fixed errors when trying to add a manual gap without having a geometry object selected in the combobox + 17.06.2020 - added the multi-save capability if multiple CNCJob objects are selected in Project tab but only if all are of type CNCJob diff --git a/appGUI/preferences/PreferencesUIManager.py b/appGUI/preferences/PreferencesUIManager.py index 6b7d9e18..0e8f1a38 100644 --- a/appGUI/preferences/PreferencesUIManager.py +++ b/appGUI/preferences/PreferencesUIManager.py @@ -378,15 +378,16 @@ class PreferencesUIManager: "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, - "tools_cutoutkind": self.ui.tools_defaults_form.tools_cutout_group.obj_kind_combo, - "tools_cutoutmargin": self.ui.tools_defaults_form.tools_cutout_group.cutout_margin_entry, - "tools_cutout_z": self.ui.tools_defaults_form.tools_cutout_group.cutz_entry, - "tools_cutout_depthperpass": self.ui.tools_defaults_form.tools_cutout_group.maxdepth_entry, - "tools_cutout_mdepth": self.ui.tools_defaults_form.tools_cutout_group.mpass_cb, - "tools_cutoutgapsize": self.ui.tools_defaults_form.tools_cutout_group.cutout_gap_entry, - "tools_gaps_ff": self.ui.tools_defaults_form.tools_cutout_group.gaps_combo, - "tools_cutout_convexshape": self.ui.tools_defaults_form.tools_cutout_group.convex_box, + "tools_cutouttooldia": self.ui.tools_defaults_form.tools_cutout_group.cutout_tooldia_entry, + "tools_cutoutkind": self.ui.tools_defaults_form.tools_cutout_group.obj_kind_combo, + "tools_cutoutmargin": self.ui.tools_defaults_form.tools_cutout_group.cutout_margin_entry, + "tools_cutout_z": self.ui.tools_defaults_form.tools_cutout_group.cutz_entry, + "tools_cutout_depthperpass": self.ui.tools_defaults_form.tools_cutout_group.maxdepth_entry, + "tools_cutout_mdepth": self.ui.tools_defaults_form.tools_cutout_group.mpass_cb, + "tools_cutoutgapsize": self.ui.tools_defaults_form.tools_cutout_group.cutout_gap_entry, + "tools_gaps_ff": self.ui.tools_defaults_form.tools_cutout_group.gaps_combo, + "tools_cutout_convexshape": self.ui.tools_defaults_form.tools_cutout_group.convex_box, + "tools_cutout_big_cursor": self.ui.tools_defaults_form.tools_cutout_group.big_cursor_cb, # Paint Area Tool "tools_painttooldia": self.ui.tools_defaults_form.tools_paint_group.painttooldia_entry, diff --git a/appGUI/preferences/tools/ToolsCutoutPrefGroupUI.py b/appGUI/preferences/tools/ToolsCutoutPrefGroupUI.py index e081586a..1f73f6fe 100644 --- a/appGUI/preferences/tools/ToolsCutoutPrefGroupUI.py +++ b/appGUI/preferences/tools/ToolsCutoutPrefGroupUI.py @@ -174,4 +174,9 @@ class ToolsCutoutPrefGroupUI(OptionsGroupUI): ) grid0.addWidget(self.convex_box, 7, 0, 1, 2) + self.big_cursor_cb = FCCheckBox('%s' % _("Big cursor")) + self.big_cursor_cb.setToolTip( + _("Use a big cursor when adding manual gaps.")) + grid0.addWidget(self.big_cursor_cb, 8, 0, 1, 2) + self.layout.addStretch() diff --git a/appObjects/FlatCAMExcellon.py b/appObjects/FlatCAMExcellon.py index 6b33d477..85f9bdc5 100644 --- a/appObjects/FlatCAMExcellon.py +++ b/appObjects/FlatCAMExcellon.py @@ -269,7 +269,12 @@ class ExcellonObject(FlatCAMObj, Excellon): sort = [] for k, v in list(self.tools.items()): - sort.append((k, v['tooldia'])) + try: + sort.append((k, v['tooldia'])) + except KeyError: + # for old projects to be opened + sort.append((k, v['C'])) + sorted_tools = sorted(sort, key=lambda t1: t1[1]) tools = [i[0] for i in sorted_tools] @@ -278,11 +283,14 @@ class ExcellonObject(FlatCAMObj, Excellon): new_options[opt] = self.options[opt] for tool_no in tools: + try: + dia_val = self.tools[tool_no]['tooldia'] + except KeyError: + # for old projects to be opened + dia_val = self.tools[tool_no]['C'] # add the data dictionary for each tool with the default values self.tools[tool_no]['data'] = deepcopy(new_options) - # self.tools[tool_no]['data']["tooldia"] = self.tools[tool_no]["C"] - # self.tools[tool_no]['data']["slot_tooldia"] = self.tools[tool_no]["C"] drill_cnt = 0 # variable to store the nr of drills per tool slot_cnt = 0 # variable to store the nr of slots per tool @@ -301,32 +309,38 @@ class ExcellonObject(FlatCAMObj, Excellon): slot_cnt = 0 self.tot_slot_cnt += slot_cnt + # Tool ID exc_id_item = QtWidgets.QTableWidgetItem('%d' % int(tool_no)) exc_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + self.ui.tools_table.setItem(self.tool_row, 0, exc_id_item) # Tool name/id - dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, self.tools[tool_no]['tooldia'])) + # Diameter + dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, dia_val)) dia_item.setFlags(QtCore.Qt.ItemIsEnabled) + self.ui.tools_table.setItem(self.tool_row, 1, dia_item) # Diameter + # Drill count drill_count_item = QtWidgets.QTableWidgetItem('%d' % drill_cnt) drill_count_item.setFlags(QtCore.Qt.ItemIsEnabled) + self.ui.tools_table.setItem(self.tool_row, 2, drill_count_item) # Number of drills per tool + # Slot Count # if the slot number is zero is better to not clutter the GUI with zero's so we print a space slot_count_str = '%d' % slot_cnt if slot_cnt > 0 else '' slot_count_item = QtWidgets.QTableWidgetItem(slot_count_str) slot_count_item.setFlags(QtCore.Qt.ItemIsEnabled) + self.ui.tools_table.setItem(self.tool_row, 3, slot_count_item) # Number of drills per tool + # Empty Plot Item + empty_plot_item = QtWidgets.QTableWidgetItem('') + empty_plot_item.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + self.ui.tools_table.setItem(self.tool_row, 5, empty_plot_item) + + # Plot Item plot_item = FCCheckBox() plot_item.setLayoutDirection(QtCore.Qt.RightToLeft) if self.ui.plot_cb.isChecked(): plot_item.setChecked(True) - - self.ui.tools_table.setItem(self.tool_row, 0, exc_id_item) # Tool name/id - self.ui.tools_table.setItem(self.tool_row, 1, dia_item) # Diameter - self.ui.tools_table.setItem(self.tool_row, 2, drill_count_item) # Number of drills per tool - self.ui.tools_table.setItem(self.tool_row, 3, slot_count_item) # Number of drills per tool - empty_plot_item = QtWidgets.QTableWidgetItem('') - empty_plot_item.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) - self.ui.tools_table.setItem(self.tool_row, 5, empty_plot_item) self.ui.tools_table.setCellWidget(self.tool_row, 5, plot_item) self.tool_row += 1 diff --git a/appTools/ToolCutOut.py b/appTools/ToolCutOut.py index 4b3b7a47..eafc9155 100644 --- a/appTools/ToolCutOut.py +++ b/appTools/ToolCutOut.py @@ -9,7 +9,7 @@ from PyQt5 import QtWidgets, QtGui, QtCore from appTool import AppTool from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, RadioSet, FCComboBox, OptionalInputSection, FCButton -from shapely.geometry import box, MultiPolygon, Polygon, LineString, LinearRing +from shapely.geometry import box, MultiPolygon, Polygon, LineString, LinearRing, MultiLineString from shapely.ops import cascaded_union, unary_union import shapely.affinity as affinity @@ -38,8 +38,6 @@ else: class CutOut(AppTool): - toolName = _("Cutout PCB") - def __init__(self, app): AppTool.__init__(self, app) @@ -47,329 +45,11 @@ class CutOut(AppTool): self.canvas = app.plotcanvas self.decimals = self.app.decimals - # Title - title_label = QtWidgets.QLabel("%s" % self.toolName) - title_label.setStyleSheet(""" - QLabel - { - font-size: 16px; - font-weight: bold; - } - """) - self.layout.addWidget(title_label) - - self.layout.addWidget(QtWidgets.QLabel('')) - - # Form Layout - grid0 = QtWidgets.QGridLayout() - grid0.setColumnStretch(0, 0) - grid0.setColumnStretch(1, 1) - self.layout.addLayout(grid0) - - self.object_label = QtWidgets.QLabel('%s:' % _("Source Object")) - self.object_label.setToolTip('%s.' % _("Object to be cutout")) - - grid0.addWidget(self.object_label, 0, 0, 1, 2) - - # Object kind - self.kindlabel = QtWidgets.QLabel('%s:' % _('Kind')) - self.kindlabel.setToolTip( - _("Choice of what kind the object we want to cutout is.
" - "- Single: contain a single PCB Gerber outline object.
" - "- Panel: a panel PCB Gerber object, which is made\n" - "out of many individual PCB outlines.") - ) - self.obj_kind_combo = RadioSet([ - {"label": _("Single"), "value": "single"}, - {"label": _("Panel"), "value": "panel"}, - ]) - grid0.addWidget(self.kindlabel, 1, 0) - grid0.addWidget(self.obj_kind_combo, 1, 1) - - # Type of object to be cutout - self.type_obj_radio = RadioSet([ - {"label": _("Gerber"), "value": "grb"}, - {"label": _("Geometry"), "value": "geo"}, - ]) - - self.type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Type")) - self.type_obj_combo_label.setToolTip( - _("Specify the type of object to be cutout.\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.") - ) - - grid0.addWidget(self.type_obj_combo_label, 2, 0) - grid0.addWidget(self.type_obj_radio, 2, 1) - - # Object to be cutout - self.obj_combo = FCComboBox() - self.obj_combo.setModel(self.app.collection) - self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) - self.obj_combo.is_last = True - - grid0.addWidget(self.obj_combo, 3, 0, 1, 2) - - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) - grid0.addWidget(separator_line, 4, 0, 1, 2) - - grid0.addWidget(QtWidgets.QLabel(''), 5, 0, 1, 2) - - self.param_label = QtWidgets.QLabel('%s:' % _("Tool Parameters")) - grid0.addWidget(self.param_label, 6, 0, 1, 2) - - # Tool Diameter - self.dia = FCDoubleSpinner(callback=self.confirmation_message) - self.dia.set_precision(self.decimals) - self.dia.set_range(0.0000, 9999.9999) - - self.dia_label = QtWidgets.QLabel('%s:' % _("Tool Diameter")) - self.dia_label.setToolTip( - _("Diameter of the tool used to cutout\n" - "the PCB shape out of the surrounding material.") - ) - grid0.addWidget(self.dia_label, 8, 0) - grid0.addWidget(self.dia, 8, 1) - - # Cut Z - cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z')) - cutzlabel.setToolTip( - _( - "Cutting depth (negative)\n" - "below the copper surface." - ) - ) - self.cutz_entry = FCDoubleSpinner(callback=self.confirmation_message) - self.cutz_entry.set_precision(self.decimals) - - if machinist_setting == 0: - self.cutz_entry.setRange(-9999.9999, -0.00001) - else: - self.cutz_entry.setRange(-9999.9999, 9999.9999) - - self.cutz_entry.setSingleStep(0.1) - - grid0.addWidget(cutzlabel, 9, 0) - grid0.addWidget(self.cutz_entry, 9, 1) - - # Multi-pass - self.mpass_cb = FCCheckBox('%s:' % _("Multi-Depth")) - self.mpass_cb.setToolTip( - _( - "Use multiple passes to limit\n" - "the cut depth in each pass. Will\n" - "cut multiple times until Cut Z is\n" - "reached." - ) - ) - - self.maxdepth_entry = FCDoubleSpinner(callback=self.confirmation_message) - self.maxdepth_entry.set_precision(self.decimals) - self.maxdepth_entry.setRange(0, 9999.9999) - self.maxdepth_entry.setSingleStep(0.1) - - self.maxdepth_entry.setToolTip( - _( - "Depth of each pass (positive)." - ) - ) - self.ois_mpass_geo = OptionalInputSection(self.mpass_cb, [self.maxdepth_entry]) - - grid0.addWidget(self.mpass_cb, 10, 0) - grid0.addWidget(self.maxdepth_entry, 10, 1) - - # Margin - self.margin = FCDoubleSpinner(callback=self.confirmation_message) - self.margin.set_range(-9999.9999, 9999.9999) - self.margin.setSingleStep(0.1) - self.margin.set_precision(self.decimals) - - self.margin_label = QtWidgets.QLabel('%s:' % _("Margin")) - self.margin_label.setToolTip( - _("Margin over bounds. A positive value here\n" - "will make the cutout of the PCB further from\n" - "the actual PCB border") - ) - grid0.addWidget(self.margin_label, 11, 0) - grid0.addWidget(self.margin, 11, 1) - - # Gapsize - self.gapsize = FCDoubleSpinner(callback=self.confirmation_message) - self.gapsize.set_precision(self.decimals) - - self.gapsize_label = QtWidgets.QLabel('%s:' % _("Gap size")) - self.gapsize_label.setToolTip( - _("The size of the bridge gaps in the cutout\n" - "used to keep the board connected to\n" - "the surrounding material (the one \n" - "from which the PCB is cutout).") - ) - grid0.addWidget(self.gapsize_label, 13, 0) - grid0.addWidget(self.gapsize, 13, 1) - - # How gaps wil be rendered: - # lr - left + right - # tb - top + bottom - # 4 - left + right +top + bottom - # 2lr - 2*left + 2*right - # 2tb - 2*top + 2*bottom - # 8 - 2*left + 2*right +2*top + 2*bottom - - # Surrounding convex box shape - self.convex_box = FCCheckBox('%s' % _("Convex Shape")) - # self.convex_box_label = QtWidgets.QLabel('%s' % _("Convex Sh.")) - self.convex_box.setToolTip( - _("Create a convex shape surrounding the entire PCB.\n" - "Used only if the source object type is Gerber.") - ) - grid0.addWidget(self.convex_box, 15, 0, 1, 2) - - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) - grid0.addWidget(separator_line, 16, 0, 1, 2) - - grid0.addWidget(QtWidgets.QLabel(''), 17, 0, 1, 2) - - # Title2 - title_param_label = QtWidgets.QLabel("%s" % _('A. Automatic Bridge Gaps')) - title_param_label.setToolTip( - _("This section handle creation of automatic bridge gaps.") - ) - grid0.addWidget(title_param_label, 18, 0, 1, 2) - - # Gaps - gaps_label = QtWidgets.QLabel('%s:' % _('Gaps')) - gaps_label.setToolTip( - _("Number of gaps used for the Automatic cutout.\n" - "There can be maximum 8 bridges/gaps.\n" - "The choices are:\n" - "- None - no gaps\n" - "- lr - left + right\n" - "- tb - top + bottom\n" - "- 4 - left + right +top + bottom\n" - "- 2lr - 2*left + 2*right\n" - "- 2tb - 2*top + 2*bottom\n" - "- 8 - 2*left + 2*right +2*top + 2*bottom") - ) - # gaps_label.setMinimumWidth(60) - - self.gaps = FCComboBox() - gaps_items = ['None', 'LR', 'TB', '4', '2LR', '2TB', '8'] - for it in gaps_items: - self.gaps.addItem(it) - self.gaps.setStyleSheet('background-color: rgb(255,255,255)') - grid0.addWidget(gaps_label, 19, 0) - grid0.addWidget(self.gaps, 19, 1) - - # Buttons - self.ff_cutout_object_btn = FCButton(_("Generate Freeform Geometry")) - self.ff_cutout_object_btn.setToolTip( - _("Cutout the selected object.\n" - "The cutout shape can be of any shape.\n" - "Useful when the PCB has a non-rectangular shape.") - ) - self.ff_cutout_object_btn.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) - grid0.addWidget(self.ff_cutout_object_btn, 20, 0, 1, 2) - - self.rect_cutout_object_btn = FCButton(_("Generate Rectangular Geometry")) - self.rect_cutout_object_btn.setToolTip( - _("Cutout the selected object.\n" - "The resulting cutout shape is\n" - "always a rectangle shape and it will be\n" - "the bounding box of the Object.") - ) - self.rect_cutout_object_btn.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) - grid0.addWidget(self.rect_cutout_object_btn, 21, 0, 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) - - # Title5 - title_manual_label = QtWidgets.QLabel("%s" % _('B. Manual Bridge Gaps')) - title_manual_label.setToolTip( - _("This section handle creation of manual bridge gaps.\n" - "This is done by mouse clicking on the perimeter of the\n" - "Geometry object that is used as a cutout object. ") - ) - grid0.addWidget(title_manual_label, 23, 0, 1, 2) - - # Manual Geo Object - self.man_object_combo = FCComboBox() - self.man_object_combo.setModel(self.app.collection) - self.man_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex())) - self.man_object_combo.is_last = True - self.man_object_combo.obj_type = "Geometry" - - self.man_object_label = QtWidgets.QLabel('%s:' % _("Geometry Object")) - self.man_object_label.setToolTip( - _("Geometry object used to create the manual cutout.") - ) - # self.man_object_label.setMinimumWidth(60) - - grid0.addWidget(self.man_object_label, 25, 0, 1, 2) - grid0.addWidget(self.man_object_combo, 26, 0, 1, 2) - - self.man_geo_creation_btn = FCButton(_("Generate Manual Geometry")) - self.man_geo_creation_btn.setToolTip( - _("If the object to be cutout is a Gerber\n" - "first create a Geometry that surrounds it,\n" - "to be used as the cutout, if one doesn't exist yet.\n" - "Select the source Gerber file in the top object combobox.") - ) - self.man_geo_creation_btn.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) - grid0.addWidget(self.man_geo_creation_btn, 28, 0, 1, 2) - - self.man_gaps_creation_btn = FCButton(_("Manual Add Bridge Gaps")) - self.man_gaps_creation_btn.setToolTip( - _("Use the left mouse button (LMB) click\n" - "to create a bridge gap to separate the PCB from\n" - "the surrounding material.\n" - "The LMB click has to be done on the perimeter of\n" - "the Geometry object used as a cutout geometry.") - ) - self.man_gaps_creation_btn.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) - grid0.addWidget(self.man_gaps_creation_btn, 30, 0, 1, 2) - - self.layout.addStretch() - - # ## Reset Tool - self.reset_button = FCButton(_("Reset Tool")) - self.reset_button.setToolTip( - _("Will reset the tool parameters.") - ) - self.reset_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) - self.layout.addWidget(self.reset_button) + # ############################################################################# + # ######################### Tool GUI ########################################## + # ############################################################################# + self.ui = CutoutUI(layout=self.layout, app=self.app) + self.toolName = self.ui.toolName self.cutting_gapsize = 0.0 self.cutting_dia = 0.0 @@ -385,6 +65,9 @@ class CutOut(AppTool): # if mouse is dragging set the object True self.mouse_is_dragging = False + # if mouse events are bound to local methods + self.mouse_events_connected = False + # event handlers references self.kp = None self.mm = None @@ -397,20 +80,26 @@ class CutOut(AppTool): # store the default data for the resulting Geometry Object self.default_data = {} - # Signals - self.ff_cutout_object_btn.clicked.connect(self.on_freeform_cutout) - self.rect_cutout_object_btn.clicked.connect(self.on_rectangular_cutout) + # store the current cursor type to be restored after manual geo + self.old_cursor_type = self.app.defaults["global_cursor_type"] - self.type_obj_radio.activated_custom.connect(self.on_type_obj_changed) - self.man_geo_creation_btn.clicked.connect(self.on_manual_geo) - self.man_gaps_creation_btn.clicked.connect(self.on_manual_gap_click) - self.reset_button.clicked.connect(self.set_tool_ui) + # store the current selection shape status to be restored after manual geo + self.old_selection_state = self.app.defaults['global_selection_shape'] + + # Signals + self.ui.ff_cutout_object_btn.clicked.connect(self.on_freeform_cutout) + self.ui.rect_cutout_object_btn.clicked.connect(self.on_rectangular_cutout) + + self.ui.type_obj_radio.activated_custom.connect(self.on_type_obj_changed) + self.ui.man_geo_creation_btn.clicked.connect(self.on_manual_geo) + self.ui.man_gaps_creation_btn.clicked.connect(self.on_manual_gap_click) + self.ui.reset_button.clicked.connect(self.set_tool_ui) def on_type_obj_changed(self, val): obj_type = {'grb': 0, 'geo': 2}[val] - self.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) - self.obj_combo.setCurrentIndex(0) - self.obj_combo.obj_type = {"grb": "Gerber", "geo": "Geometry"}[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 = {"grb": "Gerber", "geo": "Geometry"}[val] def run(self, toggle=True): self.app.defaults.report_usage("ToolCutOut()") @@ -445,16 +134,17 @@ class CutOut(AppTool): def set_tool_ui(self): self.reset_fields() - self.dia.set_value(float(self.app.defaults["tools_cutouttooldia"])) - self.obj_kind_combo.set_value(self.app.defaults["tools_cutoutkind"]) - self.margin.set_value(float(self.app.defaults["tools_cutoutmargin"])) - self.cutz_entry.set_value(float(self.app.defaults["tools_cutout_z"])) - self.mpass_cb.set_value(float(self.app.defaults["tools_cutout_mdepth"])) - self.maxdepth_entry.set_value(float(self.app.defaults["tools_cutout_depthperpass"])) + self.ui.dia.set_value(float(self.app.defaults["tools_cutouttooldia"])) + self.ui.obj_kind_combo.set_value(self.app.defaults["tools_cutoutkind"]) + self.ui.margin.set_value(float(self.app.defaults["tools_cutoutmargin"])) + self.ui.cutz_entry.set_value(float(self.app.defaults["tools_cutout_z"])) + self.ui.mpass_cb.set_value(float(self.app.defaults["tools_cutout_mdepth"])) + self.ui.maxdepth_entry.set_value(float(self.app.defaults["tools_cutout_depthperpass"])) - self.gapsize.set_value(float(self.app.defaults["tools_cutoutgapsize"])) - self.gaps.set_value(self.app.defaults["tools_gaps_ff"]) - self.convex_box.set_value(self.app.defaults['tools_cutout_convexshape']) + self.ui.gapsize.set_value(float(self.app.defaults["tools_cutoutgapsize"])) + self.ui.gaps.set_value(self.app.defaults["tools_gaps_ff"]) + self.ui.convex_box.set_value(self.app.defaults['tools_cutout_convexshape']) + self.ui.big_cursor_cb.set_value(self.app.defaults['tools_cutout_big_cursor']) # use the current selected object and make it visible in the Paint object combobox sel_list = self.app.collection.get_selected() @@ -462,9 +152,9 @@ class CutOut(AppTool): active = self.app.collection.get_active() kind = active.kind if kind == 'gerber': - self.type_obj_radio.set_value('grb') + self.ui.type_obj_radio.set_value('grb') else: - self.type_obj_radio.set_value('geo') + self.ui.type_obj_radio.set_value('geo') # run those once so the obj_type attribute is updated for the FCComboboxes # so the last loaded object is displayed @@ -473,10 +163,10 @@ class CutOut(AppTool): else: self.on_type_obj_changed(val='geo') - self.obj_combo.set_value(active.options['name']) + self.ui.obj_combo.set_value(active.options['name']) else: kind = 'gerber' - self.type_obj_radio.set_value('grb') + self.ui.type_obj_radio.set_value('grb') # run those once so the obj_type attribute is updated for the FCComboboxes # so the last loaded object is displayed @@ -546,7 +236,7 @@ class CutOut(AppTool): # def subtract_rectangle(obj_, x0, y0, x1, y1): # pts = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)] # obj_.subtract_polygon(pts) - name = self.obj_combo.currentText() + name = self.ui.obj_combo.currentText() # Get source object. try: @@ -561,22 +251,22 @@ class CutOut(AppTool): _("There is no object selected for Cutout.\nSelect one and try again.")) return - dia = float(self.dia.get_value()) + dia = float(self.ui.dia.get_value()) if 0 in {dia}: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Tool Diameter is zero value. Change it to a positive real number.")) return "Tool Diameter is zero value. Change it to a positive real number." try: - kind = self.obj_kind_combo.get_value() + kind = self.ui.obj_kind_combo.get_value() except ValueError: return - margin = float(self.margin.get_value()) - gapsize = float(self.gapsize.get_value()) + margin = float(self.ui.margin.get_value()) + gapsize = float(self.ui.gapsize.get_value()) try: - gaps = self.gaps.get_value() + gaps = self.ui.gaps.get_value() except TypeError: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Number of gaps value is missing. Add it and retry.")) return @@ -594,7 +284,7 @@ class CutOut(AppTool): "and after that perform Cutout.")) return - convex_box = self.convex_box.get_value() + convex_box = self.ui.convex_box.get_value() gapsize = gapsize / 2 + (dia / 2) @@ -618,7 +308,7 @@ class CutOut(AppTool): def cutout_handler(geom): # Get min and max data for each object as we just cut rectangles across X or Y - xxmin, yymin, xxmax, yymax = recursive_bounds(geom) + xxmin, yymin, xxmax, yymax = CutOut.recursive_bounds(geom) px = 0.5 * (xxmin + xxmax) + margin py = 0.5 * (yymin + yymax) + margin @@ -669,9 +359,11 @@ class CutOut(AppTool): try: for g in geom: - proc_geometry.append(g) + if g and not g.is_empty: + proc_geometry.append(g) except TypeError: - proc_geometry.append(geom) + if geom and not geom.is_empty: + proc_geometry.append(geom) return proc_geometry @@ -708,37 +400,44 @@ class CutOut(AppTool): solid_geo += cutout_handler(geom=geom_struct) + if not solid_geo: + app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) + return "fail" geo_obj.solid_geometry = deepcopy(solid_geo) - xmin, ymin, xmax, ymax = recursive_bounds(geo_obj.solid_geometry) + xmin, ymin, xmax, ymax = CutOut.recursive_bounds(geo_obj.solid_geometry) geo_obj.options['xmin'] = xmin geo_obj.options['ymin'] = ymin geo_obj.options['xmax'] = xmax geo_obj.options['ymax'] = ymax geo_obj.options['cnctooldia'] = str(dia) - geo_obj.options['cutz'] = self.cutz_entry.get_value() - geo_obj.options['multidepth'] = self.mpass_cb.get_value() - geo_obj.options['depthperpass'] = self.maxdepth_entry.get_value() + geo_obj.options['cutz'] = self.ui.cutz_entry.get_value() + geo_obj.options['multidepth'] = self.ui.mpass_cb.get_value() + geo_obj.options['depthperpass'] = self.ui.maxdepth_entry.get_value() geo_obj.tools.update({ 1: { - 'tooldia': str(dia), - 'offset': 'Path', - 'offset_value': 0.0, - 'type': _('Rough'), - 'tool_type': 'C1', - 'data': self.default_data, - 'solid_geometry': geo_obj.solid_geometry + 'tooldia': str(dia), + 'offset': 'Path', + 'offset_value': 0.0, + 'type': _('Rough'), + 'tool_type': 'C1', + 'data': self.default_data, + 'solid_geometry': geo_obj.solid_geometry } }) geo_obj.multigeo = True geo_obj.tools[1]['data']['name'] = outname - geo_obj.tools[1]['data']['cutz'] = self.cutz_entry.get_value() - geo_obj.tools[1]['data']['multidepth'] = self.mpass_cb.get_value() - geo_obj.tools[1]['data']['depthperpass'] = self.maxdepth_entry.get_value() + geo_obj.tools[1]['data']['cutz'] = self.ui.cutz_entry.get_value() + geo_obj.tools[1]['data']['multidepth'] = self.ui.mpass_cb.get_value() + geo_obj.tools[1]['data']['depthperpass'] = self.ui.maxdepth_entry.get_value() outname = cutout_obj.options["name"] + "_cutout" - self.app.app_obj.new_object('geometry', outname, geo_init) + ret = self.app.app_obj.new_object('geometry', outname, geo_init) + + if ret == 'fail': + self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) + return cutout_obj.plot() self.app.inform.emit('[success] %s' % _("Any form CutOut operation finished.")) @@ -752,7 +451,7 @@ class CutOut(AppTool): # pts = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)] # obj_.subtract_polygon(pts) - name = self.obj_combo.currentText() + name = self.ui.obj_combo.currentText() # Get source object. try: @@ -765,22 +464,22 @@ class CutOut(AppTool): if cutout_obj is None: self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(name))) - dia = float(self.dia.get_value()) + dia = float(self.ui.dia.get_value()) if 0 in {dia}: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Tool Diameter is zero value. Change it to a positive real number.")) return "Tool Diameter is zero value. Change it to a positive real number." try: - kind = self.obj_kind_combo.get_value() + kind = self.ui.obj_kind_combo.get_value() except ValueError: return - margin = float(self.margin.get_value()) - gapsize = float(self.gapsize.get_value()) + margin = float(self.ui.margin.get_value()) + gapsize = float(self.ui.gapsize.get_value()) try: - gaps = self.gaps.get_value() + gaps = self.ui.gaps.get_value() except TypeError: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Number of gaps value is missing. Add it and retry.")) @@ -906,32 +605,32 @@ class CutOut(AppTool): solid_geo += cutout_rect_handler(geom=geom_struct) elif cutout_obj.kind == 'gerber' and margin < 0: - self.app.inform.emit('[WARNING_NOTCL] %s' % - _("Rectangular cutout with negative margin is not possible.")) + app_obj.inform.emit( + '[WARNING_NOTCL] %s' % _("Rectangular cutout with negative margin is not possible.")) return "fail" geo_obj.options['cnctooldia'] = str(dia) - geo_obj.options['cutz'] = self.cutz_entry.get_value() - geo_obj.options['multidepth'] = self.mpass_cb.get_value() - geo_obj.options['depthperpass'] = self.maxdepth_entry.get_value() + geo_obj.options['cutz'] = self.ui.cutz_entry.get_value() + geo_obj.options['multidepth'] = self.ui.mpass_cb.get_value() + geo_obj.options['depthperpass'] = self.ui.maxdepth_entry.get_value() geo_obj.solid_geometry = deepcopy(solid_geo) geo_obj.tools.update({ 1: { - 'tooldia': str(dia), - 'offset': 'Path', - 'offset_value': 0.0, - 'type': _('Rough'), - 'tool_type': 'C1', - 'data': self.default_data, - 'solid_geometry': geo_obj.solid_geometry + 'tooldia': str(dia), + 'offset': 'Path', + 'offset_value': 0.0, + 'type': _('Rough'), + 'tool_type': 'C1', + 'data': self.default_data, + 'solid_geometry': geo_obj.solid_geometry } }) geo_obj.multigeo = True geo_obj.tools[1]['data']['name'] = outname - geo_obj.tools[1]['data']['cutz'] = self.cutz_entry.get_value() - geo_obj.tools[1]['data']['multidepth'] = self.mpass_cb.get_value() - geo_obj.tools[1]['data']['depthperpass'] = self.maxdepth_entry.get_value() + geo_obj.tools[1]['data']['cutz'] = self.ui.cutz_entry.get_value() + geo_obj.tools[1]['data']['multidepth'] = self.ui.mpass_cb.get_value() + geo_obj.tools[1]['data']['depthperpass'] = self.ui.maxdepth_entry.get_value() outname = cutout_obj.options["name"] + "_cutout" ret = self.app.app_obj.new_object('geometry', outname, geo_init) @@ -939,22 +638,41 @@ class CutOut(AppTool): if ret != 'fail': # cutout_obj.plot() self.app.inform.emit('[success] %s' % _("Any form CutOut operation finished.")) + else: + self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) + return + # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) self.app.should_we_save = True def on_manual_gap_click(self): + name = self.ui.man_object_combo.currentText() + + # Get source object. + try: + self.man_cutout_obj = self.app.collection.get_by_name(str(name)) + except Exception as e: + log.debug("CutOut.on_manual_cutout() --> %s" % str(e)) + self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve Geometry object"), name)) + return + + if self.man_cutout_obj is None: + self.app.inform.emit('[ERROR_NOTCL] %s: %s' % + (_("Geometry object for manual cutout not found"), self.man_cutout_obj)) + return + self.app.inform.emit(_("Click on the selected geometry object perimeter to create a bridge gap ...")) self.app.geo_editor.tool_shape.enabled = True - self.cutting_dia = float(self.dia.get_value()) + self.cutting_dia = float(self.ui.dia.get_value()) if 0 in {self.cutting_dia}: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Tool Diameter is zero value. Change it to a positive real number.")) return - self.cutting_gapsize = float(self.gapsize.get_value()) + self.cutting_gapsize = float(self.ui.gapsize.get_value()) - name = self.man_object_combo.currentText() + name = self.ui.man_object_combo.currentText() # Get Geometry source object to be used as target for Manual adding Gaps try: self.man_cutout_obj = self.app.collection.get_by_name(str(name)) @@ -978,16 +696,14 @@ class CutOut(AppTool): self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move) self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release) - def on_manual_cutout(self, click_pos): - name = self.man_object_combo.currentText() + self.mouse_events_connected = True - # Get source object. - try: - self.man_cutout_obj = self.app.collection.get_by_name(str(name)) - except Exception as e: - log.debug("CutOut.on_manual_cutout() --> %s" % str(e)) - self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve Geometry object"), name)) - return "Could not retrieve object: %s" % name + if self.ui.big_cursor_cb.get_value(): + self.old_cursor_type = self.app.defaults["global_cursor_type"] + self.app.on_cursor_type(val="big") + self.app.defaults['global_selection_shape'] = False + + def on_manual_cutout(self, click_pos): if self.man_cutout_obj is None: self.app.inform.emit('[ERROR_NOTCL] %s: %s' % @@ -998,7 +714,14 @@ class CutOut(AppTool): snapped_pos = self.app.geo_editor.snap(click_pos[0], click_pos[1]) cut_poly = self.cutting_geo(pos=(snapped_pos[0], snapped_pos[1])) - self.man_cutout_obj.subtract_polygon(cut_poly) + + # first subtract geometry for the total solid_geometry + new_solid_geometry = CutOut.subtract_polygon(self.man_cutout_obj.solid_geometry, cut_poly) + self.man_cutout_obj.solid_geometry = new_solid_geometry + + # then do it or each tool in the manual cutout Geometry object + for tool in self.man_cutout_obj.tools: + self.man_cutout_obj.tools[tool]['solid_geometry'] = new_solid_geometry self.man_cutout_obj.plot() self.app.inform.emit('[success] %s' % _("Added manual Bridge Gap.")) @@ -1006,7 +729,7 @@ class CutOut(AppTool): self.app.should_we_save = True def on_manual_geo(self): - name = self.obj_combo.currentText() + name = self.ui.obj_combo.currentText() # Get source object. try: @@ -1028,19 +751,19 @@ class CutOut(AppTool): "Select a Gerber file and try again.")) return - dia = float(self.dia.get_value()) + dia = float(self.ui.dia.get_value()) if 0 in {dia}: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Tool Diameter is zero value. Change it to a positive real number.")) - return "Tool Diameter is zero value. Change it to a positive real number." + return try: - kind = self.obj_kind_combo.get_value() + kind = self.ui.obj_kind_combo.get_value() except ValueError: return - margin = float(self.margin.get_value()) - convex_box = self.convex_box.get_value() + margin = float(self.ui.margin.get_value()) + convex_box = self.ui.convex_box.get_value() def geo_init(geo_obj, app_obj): geo_union = unary_union(cutout_obj.solid_geometry) @@ -1058,8 +781,8 @@ class CutOut(AppTool): geo = box(x0, y0, x1, y1) geo_obj.solid_geometry = geo.buffer(margin + abs(dia / 2)) else: - self.app.inform.emit('[ERROR_NOTCL] %s: %s' % - (_("Geometry not supported for cutout"), type(geo_union))) + app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % ( + _("Geometry not supported for cutout"), type(geo_union))) return 'fail' else: geo = geo_union @@ -1071,34 +794,35 @@ class CutOut(AppTool): for poly in geo: solid_geo.append(poly.exterior) geo_obj.solid_geometry = deepcopy(solid_geo) + geo_obj.options['cnctooldia'] = str(dia) - geo_obj.options['cutz'] = self.cutz_entry.get_value() - geo_obj.options['multidepth'] = self.mpass_cb.get_value() - geo_obj.options['depthperpass'] = self.maxdepth_entry.get_value() + geo_obj.options['cutz'] = self.ui.cutz_entry.get_value() + geo_obj.options['multidepth'] = self.ui.mpass_cb.get_value() + geo_obj.options['depthperpass'] = self.ui.maxdepth_entry.get_value() geo_obj.tools.update({ 1: { - 'tooldia': str(dia), - 'offset': 'Path', - 'offset_value': 0.0, - 'type': _('Rough'), - 'tool_type': 'C1', - 'data': self.default_data, - 'solid_geometry': geo_obj.solid_geometry + 'tooldia': str(dia), + 'offset': 'Path', + 'offset_value': 0.0, + 'type': _('Rough'), + 'tool_type': 'C1', + 'data': self.default_data, + 'solid_geometry': geo_obj.solid_geometry } }) geo_obj.multigeo = True geo_obj.tools[1]['data']['name'] = outname - geo_obj.tools[1]['data']['cutz'] = self.cutz_entry.get_value() - geo_obj.tools[1]['data']['multidepth'] = self.mpass_cb.get_value() - geo_obj.tools[1]['data']['depthperpass'] = self.maxdepth_entry.get_value() + geo_obj.tools[1]['data']['cutz'] = self.ui.cutz_entry.get_value() + geo_obj.tools[1]['data']['multidepth'] = self.ui.mpass_cb.get_value() + geo_obj.tools[1]['data']['depthperpass'] = self.ui.maxdepth_entry.get_value() outname = cutout_obj.options["name"] + "_cutout" self.app.app_obj.new_object('geometry', outname, geo_init) def cutting_geo(self, pos): - self.cutting_dia = float(self.dia.get_value()) - self.cutting_gapsize = float(self.gapsize.get_value()) + self.cutting_dia = float(self.ui.dia.get_value()) + self.cutting_gapsize = float(self.ui.gapsize.get_value()) offset = self.cutting_dia / 2 + self.cutting_gapsize / 2 @@ -1161,6 +885,15 @@ class CutOut(AppTool): self.app.geo_editor.tool_shape.clear(update=True) self.app.geo_editor.tool_shape.enabled = False + # signal that the mouse events are disconnected from local methods + self.mouse_events_connected = False + + if self.ui.big_cursor_cb.get_value(): + # restore cursor + self.app.on_cursor_type(val=self.old_cursor_type) + # restore selection + self.app.defaults['global_selection_shape'] = self.old_selection_state + def on_mouse_move(self, event): self.app.on_mouse_move_over_plot(event=event) @@ -1307,20 +1040,28 @@ class CutOut(AppTool): # Escape = Deselect All if key == QtCore.Qt.Key_Escape or key == 'Escape': - if self.app.is_legacy is False: - self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press) - self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move) - self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release) - else: - self.app.plotcanvas.graph_event_disconnect(self.kp) - self.app.plotcanvas.graph_event_disconnect(self.mm) - self.app.plotcanvas.graph_event_disconnect(self.mr) + if self.mouse_events_connected is True: + self.mouse_events_connected = False + if self.app.is_legacy is False: + self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press) + self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move) + self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release) + else: + self.app.plotcanvas.graph_event_disconnect(self.kp) + self.app.plotcanvas.graph_event_disconnect(self.mm) + self.app.plotcanvas.graph_event_disconnect(self.mr) - self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent) - self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot) - self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release', - self.app.on_mouse_click_release_over_plot) - self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.app.on_mouse_move_over_plot) + self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent) + self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot) + self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release', + self.app.on_mouse_click_release_over_plot) + self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.app.on_mouse_move_over_plot) + + if self.ui.big_cursor_cb.get_value(): + # restore cursor + self.app.on_cursor_type(val=self.old_cursor_type) + # restore selection + self.app.defaults['global_selection_shape'] = self.old_selection_state # Remove any previous utility shape self.app.geo_editor.tool_shape.clear(update=True) @@ -1355,7 +1096,7 @@ class CutOut(AppTool): points = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)] # pathonly should be allways True, otherwise polygons are not subtracted - flat_geometry = flatten(geometry=solid_geo) + flat_geometry = CutOut.flatten(geometry=solid_geo) log.debug("%d paths" % len(flat_geometry)) @@ -1370,66 +1111,456 @@ class CutOut(AppTool): return unary_union(diffs) - def reset_fields(self): - self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + @staticmethod + def flatten(geometry): + """ + Creates a list of non-iterable linear geometry objects. + Polygons are expanded into its exterior and interiors. + Results are placed in self.flat_geometry -def flatten(geometry): - """ - Creates a list of non-iterable linear geometry objects. - Polygons are expanded into its exterior and interiors. - - Results are placed in self.flat_geometry - - :param geometry: Shapely type or list or list of list of such. - """ - flat_geo = [] - try: - for geo in geometry: - if type(geo) == Polygon: - flat_geo.append(geo.exterior) - for subgeo in geo.interiors: - flat_geo.append(subgeo) - else: - flat_geo.append(geo) - except TypeError: - if type(geometry) == Polygon: - flat_geo.append(geometry.exterior) - for subgeo in geometry.interiors: - flat_geo.append(subgeo) - else: - flat_geo.append(geometry) - - return flat_geo - - -def recursive_bounds(geometry): - """ - Return the bounds of the biggest bounding box in geometry, one that include all. - - :param geometry: a iterable object that holds geometry - :return: Returns coordinates of rectangular bounds of geometry: (xmin, ymin, xmax, ymax). - """ - - # now it can get bounds for nested lists of objects - - def bounds_rec(obj): + :param geometry: Shapely type or list or list of list of such. + """ + flat_geo = [] try: - minx = Inf - miny = Inf - maxx = -Inf - maxy = -Inf - - for k in obj: - minx_, miny_, maxx_, maxy_ = bounds_rec(k) - minx = min(minx, minx_) - miny = min(miny, miny_) - maxx = max(maxx, maxx_) - maxy = max(maxy, maxy_) - return minx, miny, maxx, maxy + for geo in geometry: + if geo and not geo.is_empty: + flat_geo += CutOut.flatten(geometry=geo) except TypeError: - # it's a Shapely object, return it's bounds - if obj: - return obj.bounds + if isinstance(geometry, Polygon): + flat_geo.append(geometry.exterior) + CutOut.flatten(geometry=geometry.interiors) + else: + flat_geo.append(geometry) - return bounds_rec(geometry) + return flat_geo + + @staticmethod + def recursive_bounds(geometry): + """ + Return the bounds of the biggest bounding box in geometry, one that include all. + + :param geometry: a iterable object that holds geometry + :return: Returns coordinates of rectangular bounds of geometry: (xmin, ymin, xmax, ymax). + """ + + # now it can get bounds for nested lists of objects + + def bounds_rec(obj): + try: + minx = Inf + miny = Inf + maxx = -Inf + maxy = -Inf + + for k in obj: + minx_, miny_, maxx_, maxy_ = bounds_rec(k) + minx = min(minx, minx_) + miny = min(miny, miny_) + maxx = max(maxx, maxx_) + maxy = max(maxy, maxy_) + return minx, miny, maxx, maxy + except TypeError: + # it's a Shapely object, return it's bounds + if obj: + return obj.bounds + + return bounds_rec(geometry) + + @staticmethod + def subtract_polygon(target_geo, subtractor): + """ + Subtract subtractor polygon from the target_geo. This only operates on the paths in the target_geo, + i.e. it converts polygons into paths. + + :param target_geo: geometry from which to subtract + :param subtractor: a list of Points, a LinearRing or a Polygon that will be subtracted from target_geo + :return: a cascaded union of the resulting geometry + """ + + if target_geo is None: + target_geo = [] + + # flatten() takes care of possible empty geometry making sure that is filtered + flat_geometry = CutOut.flatten(target_geo) + log.debug("%d paths" % len(flat_geometry)) + + toolgeo = unary_union(subtractor) + + diffs = [] + for target in flat_geometry: + if isinstance(target, LineString) or isinstance(target, LinearRing) or isinstance(target, MultiLineString): + diffs.append(target.difference(toolgeo)) + else: + log.warning("Not implemented.") + + return unary_union(diffs) + + def reset_fields(self): + self.ui.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + + +class CutoutUI: + + toolName = _("Cutout PCB") + + def __init__(self, layout, app): + self.app = app + self.decimals = self.app.decimals + self.layout = layout + + # Title + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) + self.layout.addWidget(title_label) + + self.layout.addWidget(QtWidgets.QLabel('')) + + # Form Layout + grid0 = QtWidgets.QGridLayout() + grid0.setColumnStretch(0, 0) + grid0.setColumnStretch(1, 1) + self.layout.addLayout(grid0) + + self.object_label = QtWidgets.QLabel('%s:' % _("Source Object")) + self.object_label.setToolTip('%s.' % _("Object to be cutout")) + + grid0.addWidget(self.object_label, 0, 0, 1, 2) + + # Object kind + self.kindlabel = QtWidgets.QLabel('%s:' % _('Kind')) + self.kindlabel.setToolTip( + _("Choice of what kind the object we want to cutout is.
" + "- Single: contain a single PCB Gerber outline object.
" + "- Panel: a panel PCB Gerber object, which is made\n" + "out of many individual PCB outlines.") + ) + self.obj_kind_combo = RadioSet([ + {"label": _("Single"), "value": "single"}, + {"label": _("Panel"), "value": "panel"}, + ]) + grid0.addWidget(self.kindlabel, 1, 0) + grid0.addWidget(self.obj_kind_combo, 1, 1) + + # Type of object to be cutout + self.type_obj_radio = RadioSet([ + {"label": _("Gerber"), "value": "grb"}, + {"label": _("Geometry"), "value": "geo"}, + ]) + + self.type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Type")) + self.type_obj_combo_label.setToolTip( + _("Specify the type of object to be cutout.\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.") + ) + + grid0.addWidget(self.type_obj_combo_label, 2, 0) + grid0.addWidget(self.type_obj_radio, 2, 1) + + # Object to be cutout + self.obj_combo = FCComboBox() + self.obj_combo.setModel(self.app.collection) + self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + self.obj_combo.is_last = True + + grid0.addWidget(self.obj_combo, 3, 0, 1, 2) + + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) + grid0.addWidget(separator_line, 4, 0, 1, 2) + + grid0.addWidget(QtWidgets.QLabel(''), 5, 0, 1, 2) + + self.param_label = QtWidgets.QLabel('%s:' % _("Tool Parameters")) + grid0.addWidget(self.param_label, 6, 0, 1, 2) + + # Tool Diameter + self.dia = FCDoubleSpinner(callback=self.confirmation_message) + self.dia.set_precision(self.decimals) + self.dia.set_range(0.0000, 9999.9999) + + self.dia_label = QtWidgets.QLabel('%s:' % _("Tool Diameter")) + self.dia_label.setToolTip( + _("Diameter of the tool used to cutout\n" + "the PCB shape out of the surrounding material.") + ) + grid0.addWidget(self.dia_label, 8, 0) + grid0.addWidget(self.dia, 8, 1) + + # Cut Z + cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z')) + cutzlabel.setToolTip( + _( + "Cutting depth (negative)\n" + "below the copper surface." + ) + ) + self.cutz_entry = FCDoubleSpinner(callback=self.confirmation_message) + self.cutz_entry.set_precision(self.decimals) + + if machinist_setting == 0: + self.cutz_entry.setRange(-9999.9999, -0.00001) + else: + self.cutz_entry.setRange(-9999.9999, 9999.9999) + + self.cutz_entry.setSingleStep(0.1) + + grid0.addWidget(cutzlabel, 9, 0) + grid0.addWidget(self.cutz_entry, 9, 1) + + # Multi-pass + self.mpass_cb = FCCheckBox('%s:' % _("Multi-Depth")) + self.mpass_cb.setToolTip( + _( + "Use multiple passes to limit\n" + "the cut depth in each pass. Will\n" + "cut multiple times until Cut Z is\n" + "reached." + ) + ) + + self.maxdepth_entry = FCDoubleSpinner(callback=self.confirmation_message) + self.maxdepth_entry.set_precision(self.decimals) + self.maxdepth_entry.setRange(0, 9999.9999) + self.maxdepth_entry.setSingleStep(0.1) + + self.maxdepth_entry.setToolTip( + _( + "Depth of each pass (positive)." + ) + ) + self.ois_mpass_geo = OptionalInputSection(self.mpass_cb, [self.maxdepth_entry]) + + grid0.addWidget(self.mpass_cb, 10, 0) + grid0.addWidget(self.maxdepth_entry, 10, 1) + + # Margin + self.margin = FCDoubleSpinner(callback=self.confirmation_message) + self.margin.set_range(-9999.9999, 9999.9999) + self.margin.setSingleStep(0.1) + self.margin.set_precision(self.decimals) + + self.margin_label = QtWidgets.QLabel('%s:' % _("Margin")) + self.margin_label.setToolTip( + _("Margin over bounds. A positive value here\n" + "will make the cutout of the PCB further from\n" + "the actual PCB border") + ) + grid0.addWidget(self.margin_label, 11, 0) + grid0.addWidget(self.margin, 11, 1) + + # Gapsize + self.gapsize = FCDoubleSpinner(callback=self.confirmation_message) + self.gapsize.set_precision(self.decimals) + + self.gapsize_label = QtWidgets.QLabel('%s:' % _("Gap size")) + self.gapsize_label.setToolTip( + _("The size of the bridge gaps in the cutout\n" + "used to keep the board connected to\n" + "the surrounding material (the one \n" + "from which the PCB is cutout).") + ) + grid0.addWidget(self.gapsize_label, 13, 0) + grid0.addWidget(self.gapsize, 13, 1) + + # How gaps wil be rendered: + # lr - left + right + # tb - top + bottom + # 4 - left + right +top + bottom + # 2lr - 2*left + 2*right + # 2tb - 2*top + 2*bottom + # 8 - 2*left + 2*right +2*top + 2*bottom + + # Surrounding convex box shape + self.convex_box = FCCheckBox('%s' % _("Convex Shape")) + # self.convex_box_label = QtWidgets.QLabel('%s' % _("Convex Sh.")) + self.convex_box.setToolTip( + _("Create a convex shape surrounding the entire PCB.\n" + "Used only if the source object type is Gerber.") + ) + grid0.addWidget(self.convex_box, 15, 0, 1, 2) + + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) + grid0.addWidget(separator_line, 16, 0, 1, 2) + + grid0.addWidget(QtWidgets.QLabel(''), 17, 0, 1, 2) + + # Title2 + title_param_label = QtWidgets.QLabel("%s %s:" % (_('Automatic'), _("Bridge Gaps"))) + title_param_label.setToolTip( + _("This section handle creation of automatic bridge gaps.") + ) + grid0.addWidget(title_param_label, 18, 0, 1, 2) + + # Gaps + gaps_label = QtWidgets.QLabel('%s:' % _('Gaps')) + gaps_label.setToolTip( + _("Number of gaps used for the Automatic cutout.\n" + "There can be maximum 8 bridges/gaps.\n" + "The choices are:\n" + "- None - no gaps\n" + "- lr - left + right\n" + "- tb - top + bottom\n" + "- 4 - left + right +top + bottom\n" + "- 2lr - 2*left + 2*right\n" + "- 2tb - 2*top + 2*bottom\n" + "- 8 - 2*left + 2*right +2*top + 2*bottom") + ) + # gaps_label.setMinimumWidth(60) + + self.gaps = FCComboBox() + gaps_items = ['None', 'LR', 'TB', '4', '2LR', '2TB', '8'] + for it in gaps_items: + self.gaps.addItem(it) + # self.gaps.setStyleSheet('background-color: rgb(255,255,255)') + grid0.addWidget(gaps_label, 19, 0) + grid0.addWidget(self.gaps, 19, 1) + + # Buttons + self.ff_cutout_object_btn = FCButton(_("Generate Freeform Geometry")) + self.ff_cutout_object_btn.setToolTip( + _("Cutout the selected object.\n" + "The cutout shape can be of any shape.\n" + "Useful when the PCB has a non-rectangular shape.") + ) + self.ff_cutout_object_btn.setStyleSheet(""" + QPushButton + { + font-weight: bold; + } + """) + grid0.addWidget(self.ff_cutout_object_btn, 20, 0, 1, 2) + + self.rect_cutout_object_btn = FCButton(_("Generate Rectangular Geometry")) + self.rect_cutout_object_btn.setToolTip( + _("Cutout the selected object.\n" + "The resulting cutout shape is\n" + "always a rectangle shape and it will be\n" + "the bounding box of the Object.") + ) + self.rect_cutout_object_btn.setStyleSheet(""" + QPushButton + { + font-weight: bold; + } + """) + grid0.addWidget(self.rect_cutout_object_btn, 21, 0, 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(QtWidgets.QLabel(''), 24, 0, 1, 2) + + # MANUAL BRIDGE GAPS + title_manual_label = QtWidgets.QLabel("%s %s:" % (_('Manual'), _("Bridge Gaps"))) + title_manual_label.setToolTip( + _("This section handle creation of manual bridge gaps.\n" + "This is done by mouse clicking on the perimeter of the\n" + "Geometry object that is used as a cutout object. ") + ) + grid0.addWidget(title_manual_label, 25, 0, 1, 2) + + # Big Cursor + big_cursor_label = QtWidgets.QLabel('%s:' % _("Big cursor")) + big_cursor_label.setToolTip( + _("Use a big cursor when adding manual gaps.")) + self.big_cursor_cb = FCCheckBox() + + grid0.addWidget(big_cursor_label, 27, 0) + grid0.addWidget(self.big_cursor_cb, 27, 1) + + # Generate a surrounding Geometry object + self.man_geo_creation_btn = FCButton(_("Generate Manual Geometry")) + self.man_geo_creation_btn.setToolTip( + _("If the object to be cutout is a Gerber\n" + "first create a Geometry that surrounds it,\n" + "to be used as the cutout, if one doesn't exist yet.\n" + "Select the source Gerber file in the top object combobox.") + ) + # self.man_geo_creation_btn.setStyleSheet(""" + # QPushButton + # { + # font-weight: bold; + # } + # """) + grid0.addWidget(self.man_geo_creation_btn, 28, 0, 1, 2) + + # Manual Geo Object + self.man_object_combo = FCComboBox() + self.man_object_combo.setModel(self.app.collection) + self.man_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex())) + self.man_object_combo.is_last = True + self.man_object_combo.obj_type = "Geometry" + + self.man_object_label = QtWidgets.QLabel('%s:' % _("Manual cutout Geometry")) + self.man_object_label.setToolTip( + _("Geometry object used to create the manual cutout.") + ) + # self.man_object_label.setMinimumWidth(60) + + grid0.addWidget(self.man_object_label, 30, 0, 1, 2) + grid0.addWidget(self.man_object_combo, 31, 0, 1, 2) + + self.man_gaps_creation_btn = FCButton(_("Manual Add Bridge Gaps")) + self.man_gaps_creation_btn.setToolTip( + _("Use the left mouse button (LMB) click\n" + "to create a bridge gap to separate the PCB from\n" + "the surrounding material.\n" + "The LMB click has to be done on the perimeter of\n" + "the Geometry object used as a cutout geometry.") + ) + self.man_gaps_creation_btn.setStyleSheet(""" + QPushButton + { + font-weight: bold; + } + """) + grid0.addWidget(self.man_gaps_creation_btn, 35, 0, 1, 2) + + self.layout.addStretch() + + # ## Reset Tool + self.reset_button = FCButton(_("Reset Tool")) + self.reset_button.setToolTip( + _("Will reset the tool parameters.") + ) + self.reset_button.setStyleSheet(""" + QPushButton + { + font-weight: bold; + } + """) + self.layout.addWidget(self.reset_button) + + # ############################ FINSIHED GUI ################################### + # ############################################################################# + + def confirmation_message(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"), + self.decimals, + minval, + self.decimals, + maxval), False) + else: + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) + + def confirmation_message_int(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' % + (_("Edited value is out of range"), minval, maxval), False) + else: + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) diff --git a/appTools/ToolNCC.py b/appTools/ToolNCC.py index 0f4ee4f0..5ed875c4 100644 --- a/appTools/ToolNCC.py +++ b/appTools/ToolNCC.py @@ -557,7 +557,10 @@ class NonCopperClear(AppTool, Gerber): try: dias = [float(self.app.defaults["tools_ncctools"])] except (ValueError, TypeError): - dias = [float(eval(dia)) for dia in self.app.defaults["tools_ncctools"].split(",") if dia != ''] + try: + dias = [float(eval(dia)) for dia in self.app.defaults["tools_ncctools"].split(",") if dia != ''] + except AttributeError: + dias = self.app.defaults["tools_ncctools"] except Exception: dias = [] diff --git a/app_Main.py b/app_Main.py index 39e09f1b..cebc165c 100644 --- a/app_Main.py +++ b/app_Main.py @@ -6068,6 +6068,10 @@ class App(QtCore.QObject): self.mouse = [pos[0], pos[1]] + if self.defaults['global_selection_shape'] is False: + self.selection_type = None + return + # if the mouse is moved and the LMB is clicked then the action is a selection if self.event_is_dragging == 1 and event.button == 1: self.delete_selection_shape() @@ -9104,26 +9108,26 @@ class App(QtCore.QObject): App.log.debug(" **************** Started PROEJCT loading... **************** ") for obj in d['objs']: - try: - def obj_init(obj_inst, app_inst): - + def obj_init(obj_inst, app_inst): + try: obj_inst.from_dict(obj) + except Exception as e: + print('App.open_project() --> ' + str(e)) + return 'fail' - App.log.debug("Recreating from opened project an %s object: %s" % - (obj['kind'].capitalize(), obj['options']['name'])) + App.log.debug("Recreating from opened project an %s object: %s" % + (obj['kind'].capitalize(), obj['options']['name'])) - # for some reason, setting ui_title does not work when this method is called from Tcl Shell - # it's because the TclCommand is run in another thread (it inherit TclCommandSignaled) - if cli is None: - self.set_ui_title(name="{} {}: {}".format(_("Loading Project ... restoring"), - obj['kind'].upper(), - obj['options']['name'] - ) - ) + # for some reason, setting ui_title does not work when this method is called from Tcl Shell + # it's because the TclCommand is run in another thread (it inherit TclCommandSignaled) + if cli is None: + self.set_ui_title(name="{} {}: {}".format(_("Loading Project ... restoring"), + obj['kind'].upper(), + obj['options']['name'] + ) + ) - self.app_obj.new_object(obj['kind'], obj['options']['name'], obj_init, plot=plot) - except Exception as e: - print('App.open_project() --> ' + str(e)) + self.app_obj.new_object(obj['kind'], obj['options']['name'], obj_init, plot=plot) self.inform.emit('[success] %s: %s' % (_("Project loaded from"), filename)) diff --git a/camlib.py b/camlib.py index 13106ec2..7e73e389 100644 --- a/camlib.py +++ b/camlib.py @@ -592,8 +592,7 @@ class Geometry(object): if isinstance(self.solid_geometry, list): return len(self.solid_geometry) == 0 - self.app.inform.emit('[ERROR_NOTCL] %s' % - _("self.solid_geometry is neither BaseGeometry or list.")) + self.app.inform.emit('[ERROR_NOTCL] %s' % _("self.solid_geometry is neither BaseGeometry or list.")) return def subtract_polygon(self, points): @@ -610,15 +609,20 @@ class Geometry(object): # pathonly should be allways True, otherwise polygons are not subtracted flat_geometry = self.flatten(pathonly=True) log.debug("%d paths" % len(flat_geometry)) - polygon = Polygon(points) + + if not isinstance(points, Polygon): + polygon = Polygon(points) + else: + polygon = points toolgeo = cascaded_union(polygon) diffs = [] for target in flat_geometry: - if type(target) == LineString or type(target) == LinearRing: + if isinstance(target, LineString) or isinstance(target, LineString) or isinstance(target, MultiLineString): diffs.append(target.difference(toolgeo)) else: log.warning("Not implemented.") - self.solid_geometry = cascaded_union(diffs) + + self.solid_geometry = unary_union(diffs) def bounds(self, flatten=False): """ diff --git a/defaults.py b/defaults.py index 882a26fb..ba3c8ab5 100644 --- a/defaults.py +++ b/defaults.py @@ -457,6 +457,7 @@ class FlatCAMDefaults: "tools_cutoutgapsize": 4, "tools_gaps_ff": "4", "tools_cutout_convexshape": False, + "tools_cutout_big_cursor": True, # Paint Tool "tools_painttooldia": 0.3,