diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 548fa754..379020e0 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -457,7 +457,6 @@ class App(QtCore.QObject): "tools_cutouttooldia": self.ui.tools_defaults_form.tools_cutout_group.cutout_tooldia_entry, "tools_cutoutmargin": self.ui.tools_defaults_form.tools_cutout_group.cutout_margin_entry, "tools_cutoutgapsize": self.ui.tools_defaults_form.tools_cutout_group.cutout_gap_entry, - "tools_gaps_rect": self.ui.tools_defaults_form.tools_cutout_group.gaps_radio, "tools_gaps_ff": self.ui.tools_defaults_form.tools_cutout_group.gaps_combo, # Paint Area Tool @@ -744,7 +743,6 @@ class App(QtCore.QObject): "tools_cutouttooldia": 0.00393701, "tools_cutoutmargin": 0.00393701, "tools_cutoutgapsize": 0.005905512, - "tools_gaps_rect": "4", "tools_gaps_ff": "8", "tools_painttooldia": 0.07, @@ -920,7 +918,6 @@ class App(QtCore.QObject): "tools_cutouttooldia": self.ui.tools_options_form.tools_cutout_group.cutout_tooldia_entry, "tools_cutoutmargin": self.ui.tools_options_form.tools_cutout_group.cutout_margin_entry, "tools_cutoutgapsize": self.ui.tools_options_form.tools_cutout_group.cutout_gap_entry, - "tools_gaps_rect": self.ui.tools_options_form.tools_cutout_group.gaps_radio, "tools_gaps_ff": self.ui.tools_options_form.tools_cutout_group.gaps_combo, "tools_painttooldia": self.ui.tools_options_form.tools_paint_group.painttooldia_entry, @@ -1035,10 +1032,10 @@ class App(QtCore.QObject): "tools_ncctools": "1.0, 0.5", "tools_nccoverlap": 0.4, "tools_nccmargin": 1, + "tools_cutouttooldia": 0.07, "tools_cutoutmargin": 0.1, "tools_cutoutgapsize": 0.15, - "tools_gaps_rect": "4", "tools_gaps_ff": "8", "tools_painttooldia": 0.07, diff --git a/FlatCAMEditor.py b/FlatCAMEditor.py index a81bd1bf..c9ff97a3 100644 --- a/FlatCAMEditor.py +++ b/FlatCAMEditor.py @@ -3849,7 +3849,6 @@ class FlatCAMGeoEditor(QtCore.QObject): geo = self.active_tool.utility_geometry(data=(x, y)) if isinstance(geo, DrawToolShape) and geo.geo is not None: - # Remove any previous utility shape self.tool_shape.clear(update=True) self.draw_utility_geometry(geo=geo) diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 588ad270..cf4f55b2 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -2576,7 +2576,7 @@ class GerberPreferencesUI(QtWidgets.QWidget): self.gerber_gen_group = GerberGenPrefGroupUI() self.gerber_gen_group.setFixedWidth(250) self.gerber_opt_group = GerberOptPrefGroupUI() - self.gerber_opt_group.setFixedWidth(200) + self.gerber_opt_group.setFixedWidth(230) self.gerber_adv_opt_group = GerberAdvOptPrefGroupUI() self.gerber_adv_opt_group.setFixedWidth(200) @@ -4853,22 +4853,9 @@ class ToolsCutoutPrefGroupUI(OptionsGroupUI): self.cutout_gap_entry = LengthEntry() grid0.addWidget(self.cutout_gap_entry, 2, 1) - gapslabel = QtWidgets.QLabel('Gaps Rect:') - gapslabel.setToolTip( - "Where to place the gaps when doing a Rectangular Cutout:\n" - " - 2 (T/B) --> Top/Bottom\n" - " - 2 (L/R) --> Left/Rigt\n" - " - 4 --> on each of all 4 sides." - ) - grid0.addWidget(gapslabel, 3, 0) - self.gaps_radio = RadioSet([{'label': '2 (T/B)', 'value': 'tb'}, - {'label': '2 (L/R)', 'value': 'lr'}, - {'label': '4', 'value': '4'}]) - grid0.addWidget(self.gaps_radio, 3, 1) - - gaps_ff_label = QtWidgets.QLabel('Gaps FF:') - gaps_ff_label.setToolTip( - "Number of gaps used for the FreeForm cutout.\n" + gaps_label = QtWidgets.QLabel('Gaps:') + gaps_label.setToolTip( + "Number of bridge gaps used for the cutout.\n" "There can be maximum 8 bridges/gaps.\n" "The choices are:\n" "- lr - left + right\n" @@ -4878,9 +4865,9 @@ class ToolsCutoutPrefGroupUI(OptionsGroupUI): "- 2tb - 2*top + 2*bottom\n" "- 8 - 2*left + 2*right +2*top + 2*bottom" ) - grid0.addWidget(gaps_ff_label, 4, 0) + grid0.addWidget(gaps_label, 3, 0) self.gaps_combo = FCComboBox() - grid0.addWidget(self.gaps_combo, 4, 1) + grid0.addWidget(self.gaps_combo, 3, 1) gaps_items = ['LR', 'TB', '4', '2LR', '2TB', '8'] for it in gaps_items: diff --git a/README.md b/README.md index 6d563cf2..67b1b419 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ CAD program, and create G-Code for Isolation routing. - finished work on object hovering - fixed Excellon object move and all the other transformations - starting to work on Manual Cutout Tool +- remade the CutOut Tool +- finished Manual Cutout Tool by adding utility geometry to the cutting geometry +- added CTRL + click behavior for adding manual bridge gaps in Cutout Tool +- in Tool Cutout added shortcut key 'Escape' to cancel the current adding of bridge gaps 3.03.2019 diff --git a/flatcamTools/ToolCutOut.py b/flatcamTools/ToolCutOut.py index 43d9d241..69cd7311 100644 --- a/flatcamTools/ToolCutOut.py +++ b/flatcamTools/ToolCutOut.py @@ -1,15 +1,20 @@ from FlatCAMTool import FlatCAMTool from ObjectCollection import * from FlatCAMApp import * +from shapely.geometry import box class CutOut(FlatCAMTool): toolName = "Cutout PCB" + gapFinished = pyqtSignal() def __init__(self, app): FlatCAMTool.__init__(self, app) + self.app = app + self.canvas = app.plotcanvas + ## Title title_label = QtWidgets.QLabel("%s" % self.toolName) title_label.setStyleSheet(""" @@ -44,6 +49,7 @@ class CutOut(FlatCAMTool): "What is selected here will dictate the kind\n" "of objects that will populate the 'Object' combobox." ) + self.type_obj_combo_label.setFixedWidth(60) form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo) ## Object to be cutout @@ -58,14 +64,6 @@ class CutOut(FlatCAMTool): ) form_layout.addRow(self.object_label, self.obj_combo) - ## Title2 - title_param_label = QtWidgets.QLabel("A. Automatic Cutout") - self.layout.addWidget(title_param_label) - - ## Form Layout - form_layout_2 = QtWidgets.QFormLayout() - self.layout.addLayout(form_layout_2) - # Tool Diameter self.dia = FCEntry() self.dia_label = QtWidgets.QLabel("Tool Dia:") @@ -73,7 +71,7 @@ class CutOut(FlatCAMTool): "Diameter of the tool used to cutout\n" "the PCB shape out of the surrounding material." ) - form_layout_2.addRow(self.dia_label, self.dia) + form_layout.addRow(self.dia_label, self.dia) # Margin self.margin = FCEntry() @@ -83,26 +81,18 @@ class CutOut(FlatCAMTool): "will make the cutout of the PCB further from\n" "the actual PCB border" ) - form_layout_2.addRow(self.margin_label, self.margin) + form_layout.addRow(self.margin_label, self.margin) # Gapsize self.gapsize = FCEntry() self.gapsize_label = QtWidgets.QLabel("Gap size:") self.gapsize_label.setToolTip( - "The size of the gaps in the cutout\n" + "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)." ) - form_layout_2.addRow(self.gapsize_label, self.gapsize) - - ## Title3 - title_ff_label = QtWidgets.QLabel("FreeForm Cutout") - self.layout.addWidget(title_ff_label) - - ## Form Layout - form_layout_3 = QtWidgets.QFormLayout() - self.layout.addLayout(form_layout_3) + form_layout.addRow(self.gapsize_label, self.gapsize) # How gaps wil be rendered: # lr - left + right @@ -112,10 +102,21 @@ class CutOut(FlatCAMTool): # 2tb - 2*top + 2*bottom # 8 - 2*left + 2*right +2*top + 2*bottom + ## Title2 + title_param_label = QtWidgets.QLabel("A. Automatic Bridge Gaps") + title_param_label.setToolTip( + "This section handle creation of automatic bridge gaps." + ) + self.layout.addWidget(title_param_label) + + ## Form Layout + form_layout_2 = QtWidgets.QFormLayout() + self.layout.addLayout(form_layout_2) + # Gaps - gaps_ff_label = QtWidgets.QLabel('Gaps FF: ') - gaps_ff_label.setToolTip( - "Number of gaps used for the FreeForm cutout.\n" + gaps_label = QtWidgets.QLabel('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" "- lr - left + right\n" @@ -125,84 +126,137 @@ class CutOut(FlatCAMTool): "- 2tb - 2*top + 2*bottom\n" "- 8 - 2*left + 2*right +2*top + 2*bottom" ) + gaps_label.setFixedWidth(60) self.gaps = FCComboBox() gaps_items = ['LR', 'TB', '4', '2LR', '2TB', '8'] for it in gaps_items: self.gaps.addItem(it) self.gaps.setStyleSheet('background-color: rgb(255,255,255)') - form_layout_3.addRow(gaps_ff_label, self.gaps) + form_layout_2.addRow(gaps_label, self.gaps) ## Buttons hlay = QtWidgets.QHBoxLayout() self.layout.addLayout(hlay) + title_ff_label = QtWidgets.QLabel("FreeForm:") + title_ff_label.setToolTip( + "The cutout shape can be of ny shape.\n" + "Useful when the PCB has a non-rectangular shape." + ) + hlay.addWidget(title_ff_label) + hlay.addStretch() - self.ff_cutout_object_btn = QtWidgets.QPushButton(" FreeForm Cutout Object ") + + self.ff_cutout_object_btn = QtWidgets.QPushButton("Generate Geo") self.ff_cutout_object_btn.setToolTip( "Cutout the selected object.\n" - "The cutout shape can be any shape.\n" - "Useful when the PCB has a non-rectangular shape.\n" - "But if the object to be cutout is of Gerber Type,\n" - "it needs to be an outline of the actual board shape." + "The cutout shape can be of any shape.\n" + "Useful when the PCB has a non-rectangular shape." ) hlay.addWidget(self.ff_cutout_object_btn) - ## Title4 - title_rct_label = QtWidgets.QLabel("Rectangular Cutout") - self.layout.addWidget(title_rct_label) - - ## Form Layout - form_layout_4 = QtWidgets.QFormLayout() - self.layout.addLayout(form_layout_4) - - gapslabel_rect = QtWidgets.QLabel('Type of gaps:') - gapslabel_rect.setToolTip( - "Where to place the gaps:\n" - "- one gap Top / one gap Bottom\n" - "- one gap Left / one gap Right\n" - "- one gap on each of the 4 sides." - ) - self.gaps_rect_radio = RadioSet([{'label': '2(T/B)', 'value': 'TB'}, - {'label': '2(L/R)', 'value': 'LR'}, - {'label': '4', 'value': '4'}]) - form_layout_4.addRow(gapslabel_rect, self.gaps_rect_radio) - hlay2 = QtWidgets.QHBoxLayout() self.layout.addLayout(hlay2) + title_rct_label = QtWidgets.QLabel("Rectangular:") + title_rct_label.setToolTip( + "The resulting cutout shape is\n" + "always a rectangle shape and it will be\n" + "the bounding box of the Object." + ) + hlay2.addWidget(title_rct_label) + hlay2.addStretch() - self.rect_cutout_object_btn = QtWidgets.QPushButton("Rectangular Cutout Object") + self.rect_cutout_object_btn = QtWidgets.QPushButton("Generate Geo") self.rect_cutout_object_btn.setToolTip( "Cutout the selected object.\n" "The resulting cutout shape is\n" - "always of a rectangle form and it will be\n" + "always a rectangle shape and it will be\n" "the bounding box of the Object." ) hlay2.addWidget(self.rect_cutout_object_btn) ## Title5 - title_manual_label = QtWidgets.QLabel("B. Manual Cutout") + title_manual_label = QtWidgets.QLabel("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. " + ) self.layout.addWidget(title_manual_label) ## Form Layout - form_layout_5 = QtWidgets.QFormLayout() - self.layout.addLayout(form_layout_4) + form_layout_3 = QtWidgets.QFormLayout() + self.layout.addLayout(form_layout_3) + + ## Manual Geo Object + self.man_object_combo = QtWidgets.QComboBox() + 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.setCurrentIndex(1) + + self.man_object_label = QtWidgets.QLabel("Geo Obj:") + self.man_object_label.setToolTip( + "Geometry object used to create the manual cutout." + ) + self.man_object_label.setFixedWidth(60) + # e_lab_0 = QtWidgets.QLabel('') + + form_layout_3.addRow(self.man_object_label, self.man_object_combo) + # form_layout_3.addRow(e_lab_0) + + hlay3 = QtWidgets.QHBoxLayout() + self.layout.addLayout(hlay3) + + self.man_geo_label = QtWidgets.QLabel("Manual Geo:") + self.man_geo_label.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." + ) + hlay3.addWidget(self.man_geo_label) + + hlay3.addStretch() + self.man_geo_creation_btn = QtWidgets.QPushButton("Generate Geo") + 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." + ) + hlay3.addWidget(self.man_geo_creation_btn) + + hlay4 = QtWidgets.QHBoxLayout() + self.layout.addLayout(hlay4) + + self.man_bridge_gaps_label = QtWidgets.QLabel("Manual Add Bridge Gaps:") + self.man_bridge_gaps_label.setToolTip( + "Use the left mouse button (LMB) click\n" + "to create a bridge gap to separate the PCB from\n" + "the surrounding material." + ) + hlay4.addWidget(self.man_bridge_gaps_label) + + hlay4.addStretch() + self.man_gaps_creation_btn = QtWidgets.QPushButton("Generate Gap") + 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." + ) + hlay4.addWidget(self.man_gaps_creation_btn) self.layout.addStretch() - ## Init GUI - # self.dia.set_value(1) - # self.margin.set_value(0) - # self.gapsize.set_value(1) - # self.gaps.set_value(4) - # self.gaps_rect_radio.set_value("4") + self.cutting_gapsize = 0.0 + self.cutting_dia = 0.0 - ## Signals - self.ff_cutout_object_btn.clicked.connect(self.on_freeform_cutout) - self.rect_cutout_object_btn.clicked.connect(self.on_rectangular_cutout) - - self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed) + # true if we want to repeat the gap without clicking again on the button + self.repeat_gap = False def on_type_obj_index_changed(self, index): obj_type = self.type_obj_combo.currentIndex() @@ -236,7 +290,16 @@ class CutOut(FlatCAMTool): self.margin.set_value(float(self.app.defaults["tools_cutoutmargin"])) self.gapsize.set_value(float(self.app.defaults["tools_cutoutgapsize"])) self.gaps.set_value(4) - self.gaps_rect_radio.set_value(str(self.app.defaults["tools_gaps_rect"])) + + ## Signals + self.ff_cutout_object_btn.clicked.connect(self.on_freeform_cutout) + self.rect_cutout_object_btn.clicked.connect(self.on_rectangular_cutout) + + self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_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.gapFinished.connect(self.on_gap_finished) def on_freeform_cutout(self): @@ -268,6 +331,11 @@ class CutOut(FlatCAMTool): "Add it and retry.") return + + if 0 in {dia}: + self.app.inform.emit("[WARNING_NOTCL]Tool Diameter is zero value. Change it to a positive integer.") + return "Tool Diameter is zero value. Change it to a positive integer." + try: margin = float(self.margin.get_value()) except ValueError: @@ -296,10 +364,6 @@ class CutOut(FlatCAMTool): self.app.inform.emit("[WARNING_NOTCL] Number of gaps value is missing. Add it and retry.") return - if 0 in {dia}: - self.app.inform.emit("[WARNING_NOTCL]Tool Diameter is zero value. Change it to a positive integer.") - return "Tool Diameter is zero value. Change it to a positive integer." - if gaps not in ['LR', 'TB', '2LR', '2TB', '4', '8']: self.app.inform.emit("[WARNING_NOTCL] Gaps value can be only one of: 'lr', 'tb', '2lr', '2tb', 4 or 8. " "Fill in a correct value and retry. ") @@ -377,6 +441,11 @@ class CutOut(FlatCAMTool): self.app.should_we_save = True def on_rectangular_cutout(self): + + 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() # Get source object. @@ -400,6 +469,10 @@ class CutOut(FlatCAMTool): "Add it and retry.") return + if 0 in {dia}: + self.app.inform.emit("[ERROR_NOTCL]Tool Diameter is zero value. Change it to a positive integer.") + return "Tool Diameter is zero value. Change it to a positive integer." + try: margin = float(self.margin.get_value()) except ValueError: @@ -423,14 +496,15 @@ class CutOut(FlatCAMTool): return try: - gaps = self.gaps_rect_radio.get_value() + gaps = self.gaps.get_value() except TypeError: self.app.inform.emit("[WARNING_NOTCL] Number of gaps value is missing. Add it and retry.") return - if 0 in {dia}: - self.app.inform.emit("[ERROR_NOTCL]Tool Diameter is zero value. Change it to a positive integer.") - return "Tool Diameter is zero value. Change it to a positive integer." + if gaps not in ['LR', 'TB', '2LR', '2TB', '4', '8']: + self.app.inform.emit("[WARNING_NOTCL] Gaps value can be only one of: 'lr', 'tb', '2lr', '2tb', 4 or 8. " + "Fill in a correct value and retry. ") + return if cutout_obj.multigeo is True: self.app.inform.emit("[ERROR]Cutout operation cannot be done on a multi-geo Geometry.\n" @@ -438,45 +512,279 @@ class CutOut(FlatCAMTool): "and after that perform Cutout.") return + # Get min and max data for each object as we just cut rectangles across X or Y + xmin, ymin, xmax, ymax = cutout_obj.bounds() + geo = box(xmin, ymin, xmax, ymax) + + px = 0.5 * (xmin + xmax) + margin + py = 0.5 * (ymin + ymax) + margin + lenghtx = (xmax - xmin) + (margin * 2) + lenghty = (ymax - ymin) + (margin * 2) + + gapsize = gapsize / 2 + (dia / 2) + def geo_init(geo_obj, app_obj): - real_margin = margin + (dia / 2) - real_gap_size = gapsize + dia + geo_obj.solid_geometry = geo.buffer(margin + abs(dia / 2)) - minx, miny, maxx, maxy = cutout_obj.bounds() - minx -= real_margin - maxx += real_margin - miny -= real_margin - maxy += real_margin - midx = 0.5 * (minx + maxx) - midy = 0.5 * (miny + maxy) - hgap = 0.5 * real_gap_size - pts = [[midx - hgap, maxy], - [minx, maxy], - [minx, midy + hgap], - [minx, midy - hgap], - [minx, miny], - [midx - hgap, miny], - [midx + hgap, miny], - [maxx, miny], - [maxx, midy - hgap], - [maxx, midy + hgap], - [maxx, maxy], - [midx + hgap, maxy]] - cases = {"TB": [[pts[0], pts[1], pts[4], pts[5]], - [pts[6], pts[7], pts[10], pts[11]]], - "LR": [[pts[9], pts[10], pts[1], pts[2]], - [pts[3], pts[4], pts[7], pts[8]]], - "4": [[pts[0], pts[1], pts[2]], - [pts[3], pts[4], pts[5]], - [pts[6], pts[7], pts[8]], - [pts[9], pts[10], pts[11]]]} - cuts = cases[gaps] - geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts]) + outname = cutout_obj.options["name"] + "_cutout" + self.app.new_object('geometry', outname, geo_init) - # TODO: Check for None - self.app.new_object("geometry", name + "_cutout", geo_init) - self.app.inform.emit("[success] Rectangular CutOut operation finished.") + cutout_obj = self.app.collection.get_by_name(outname) + + if gaps == '8' or gaps == '2LR': + subtract_rectangle(cutout_obj, + xmin - gapsize, # botleft_x + py - gapsize + lenghty / 4, # botleft_y + xmax + gapsize, # topright_x + py + gapsize + lenghty / 4) # topright_y + subtract_rectangle(cutout_obj, + xmin - gapsize, + py - gapsize - lenghty / 4, + xmax + gapsize, + py + gapsize - lenghty / 4) + + if gaps == '8' or gaps == '2TB': + subtract_rectangle(cutout_obj, + px - gapsize + lenghtx / 4, + ymin - gapsize, + px + gapsize + lenghtx / 4, + ymax + gapsize) + subtract_rectangle(cutout_obj, + px - gapsize - lenghtx / 4, + ymin - gapsize, + px + gapsize - lenghtx / 4, + ymax + gapsize) + + if gaps == '4' or gaps == 'LR': + subtract_rectangle(cutout_obj, + xmin - gapsize, + py - gapsize, + xmax + gapsize, + py + gapsize) + + if gaps == '4' or gaps == 'TB': + subtract_rectangle(cutout_obj, + px - gapsize, + ymin - gapsize, + px + gapsize, + ymax + gapsize) + + cutout_obj.plot() + self.app.inform.emit("[success] Any form CutOut operation finished.") self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) + self.app.should_we_save = True + + def on_manual_gap_click(self): + self.app.inform.emit("Click on the selected geometry object perimeter to create a bridge gap ...") + self.app.geo_editor.tool_shape.enabled = True + + try: + self.cutting_dia = float(self.dia.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + self.cutting_dia = float(self.dia.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Tool diameter value is missing or wrong format. " + "Add it and retry.") + return + + if 0 in {self.cutting_dia}: + self.app.inform.emit("[ERROR_NOTCL]Tool Diameter is zero value. Change it to a positive integer.") + return "Tool Diameter is zero value. Change it to a positive integer." + + try: + self.cutting_gapsize = float(self.gapsize.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + self.cutting_gapsize = float(self.gapsize.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Gap size value is missing or wrong format. " + "Add it and retry.") + return + + self.app.plotcanvas.vis_disconnect('key_press', self.app.ui.keyPressEvent) + self.app.plotcanvas.vis_disconnect('mouse_press', self.app.on_mouse_click_over_plot) + self.app.plotcanvas.vis_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot) + self.app.plotcanvas.vis_disconnect('mouse_move', self.app.on_mouse_move_over_plot) + self.app.plotcanvas.vis_connect('key_press', self.on_key_press) + self.app.plotcanvas.vis_connect('mouse_move', self.on_mouse_move) + self.app.plotcanvas.vis_connect('mouse_release', self.doit) + + # To be called after clicking on the plot. + def doit(self, event): + # do paint single only for left mouse clicks + if event.button == 1: + self.app.inform.emit("Making manual bridge gap...") + pos = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos) + self.on_manual_cutout(click_pos=pos) + + self.app.plotcanvas.vis_disconnect('key_press', self.on_key_press) + self.app.plotcanvas.vis_disconnect('mouse_move', self.on_mouse_move) + self.app.plotcanvas.vis_disconnect('mouse_release', self.doit) + self.app.plotcanvas.vis_connect('key_press', self.app.ui.keyPressEvent) + self.app.plotcanvas.vis_connect('mouse_press', self.app.on_mouse_click_over_plot) + self.app.plotcanvas.vis_connect('mouse_release', self.app.on_mouse_click_release_over_plot) + self.app.plotcanvas.vis_connect('mouse_move', self.app.on_mouse_move_over_plot) + + self.app.geo_editor.tool_shape.clear(update=True) + self.app.geo_editor.tool_shape.enabled = False + self.gapFinished.emit() + + def on_manual_cutout(self, click_pos): + name = self.man_object_combo.currentText() + + # Get source object. + try: + cutout_obj = self.app.collection.get_by_name(str(name)) + except: + self.app.inform.emit("[ERROR_NOTCL]Could not retrieve Geoemtry object: %s" % name) + return "Could not retrieve object: %s" % name + + if cutout_obj is None: + self.app.inform.emit("[ERROR_NOTCL]Geometry object for manual cutout not found: %s" % cutout_obj) + return + + # use the snapped position as reference + 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])) + cutout_obj.subtract_polygon(cut_poly) + + cutout_obj.plot() + self.app.inform.emit("[success] Added manual Bridge Gap.") + + self.app.should_we_save = True + + def on_gap_finished(self): + # if CTRL key modifier is pressed then repeat the bridge gap cut + key_modifier = QtWidgets.QApplication.keyboardModifiers() + if key_modifier == Qt.ControlModifier: + self.on_manual_gap_click() + + def on_manual_geo(self): + name = self.obj_combo.currentText() + + # Get source object. + try: + cutout_obj = self.app.collection.get_by_name(str(name)) + except: + self.app.inform.emit("[ERROR_NOTCL]Could not retrieve Gerber object: %s" % name) + return "Could not retrieve object: %s" % name + + if cutout_obj is None: + self.app.inform.emit("[ERROR_NOTCL]There is no Gerber object selected for Cutout.\n" + "Select one and try again.") + return + + if not isinstance(cutout_obj, FlatCAMGerber): + self.app.inform.emit("[ERROR_NOTCL]The selected object has to be of Gerber type.\n" + "Select a Gerber file and try again.") + return + + try: + dia = float(self.dia.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + dia = float(self.dia.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Tool diameter value is missing or wrong format. " + "Add it and retry.") + return + + if 0 in {dia}: + self.app.inform.emit("[ERROR_NOTCL]Tool Diameter is zero value. Change it to a positive integer.") + return "Tool Diameter is zero value. Change it to a positive integer." + + try: + margin = float(self.margin.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + margin = float(self.margin.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Margin value is missing or wrong format. " + "Add it and retry.") + return + + def geo_init(geo_obj, app_obj): + geo = cutout_obj.solid_geometry.convex_hull + geo_obj.solid_geometry = geo.buffer(margin + abs(dia / 2)) + + outname = cutout_obj.options["name"] + "_cutout" + self.app.new_object('geometry', outname, geo_init) + + def cutting_geo(self, pos): + self.cutting_gapsize = self.cutting_gapsize / 2 + (self.cutting_dia / 2) + offset = self.cutting_gapsize / 2 + + # cutting area definition + orig_x = pos[0] + orig_y = pos[1] + xmin = orig_x - offset + ymin = orig_y - offset + xmax = orig_x + offset + ymax = orig_y + offset + + cut_poly = box(xmin, ymin, xmax, ymax) + return cut_poly + + def on_mouse_move(self, event): + + self.app.on_mouse_move_over_plot(event=event) + + pos = self.canvas.vispy_canvas.translate_coords(event.pos) + event.xdata, event.ydata = pos[0], pos[1] + + try: + x = float(event.xdata) + y = float(event.ydata) + except TypeError: + return + + snap_x, snap_y = self.app.geo_editor.snap(x, y) + + geo = self.cutting_geo(pos=(snap_x, snap_y)) + + # Remove any previous utility shape + self.app.geo_editor.tool_shape.clear(update=True) + self.draw_utility_geometry(geo=geo) + + def draw_utility_geometry(self, geo): + self.app.geo_editor.tool_shape.add( + shape=geo, + color=(self.app.defaults["global_draw_color"] + '80'), + update=False, + layer=0, + tolerance=None) + self.app.geo_editor.tool_shape.redraw() + + def on_key_press(self, event): + # events out of the self.app.collection view (it's about Project Tab) are of type int + if type(event) is int: + key = event + # events from the GUI are of type QKeyEvent + elif type(event) == QtGui.QKeyEvent: + key = event.key() + # events from Vispy are of type KeyEvent + else: + key = event.key + + # Escape = Deselect All + if key == QtCore.Qt.Key_Escape or key == 'Escape': + self.app.plotcanvas.vis_disconnect('key_press', self.on_key_press) + self.app.plotcanvas.vis_disconnect('mouse_move', self.on_mouse_move) + self.app.plotcanvas.vis_disconnect('mouse_release', self.doit) + self.app.plotcanvas.vis_connect('key_press', self.app.ui.keyPressEvent) + self.app.plotcanvas.vis_connect('mouse_press', self.app.on_mouse_click_over_plot) + self.app.plotcanvas.vis_connect('mouse_release', self.app.on_mouse_click_release_over_plot) + self.app.plotcanvas.vis_connect('mouse_move', self.app.on_mouse_move_over_plot) + + # Remove any previous utility shape + self.app.geo_editor.tool_shape.clear(update=True) + self.app.geo_editor.tool_shape.enabled = False def reset_fields(self): self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))