فهرست منبع

- 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

Marius Stanciu 6 سال پیش
والد
کامیت
d775e999fe
5فایلهای تغییر یافته به همراه430 افزوده شده و 135 حذف شده
  1. 1 4
      FlatCAMApp.py
  2. 0 1
      FlatCAMEditor.py
  3. 6 19
      FlatCAMGUI.py
  4. 4 0
      README.md
  5. 419 111
      flatcamTools/ToolCutOut.py

+ 1 - 4
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,

+ 0 - 1
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)

+ 6 - 19
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:

+ 4 - 0
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
 

+ 419 - 111
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("<font size=4><b>A. Automatic Cutout</b></font>")
-        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("<b>FreeForm Cutout</b>")
-        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("<font size=4><b>A. Automatic Bridge Gaps</b></font>")
+        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("<b>FreeForm:</b>")
+        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("<b>Rectangular Cutout</b>")
-        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("<b>Rectangular:</b>")
+        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("<font size=4><b>B. Manual Cutout</b></font>")
+        title_manual_label = QtWidgets.QLabel("<font size=4><b>B. Manual Bridge Gaps</b></font>")
+        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)
 
-        self.layout.addStretch()
+        ## 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)
 
-        ## 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.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('')
 
-        ## Signals
-        self.ff_cutout_object_btn.clicked.connect(self.on_freeform_cutout)
-        self.rect_cutout_object_btn.clicked.connect(self.on_rectangular_cutout)
+        form_layout_3.addRow(self.man_object_label, self.man_object_combo)
+        # form_layout_3.addRow(e_lab_0)
 
-        self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
+        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()
+
+        self.cutting_gapsize = 0.0
+        self.cutting_dia = 0.0
+
+        # 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
-
-            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])
-
-        # TODO: Check for None
-        self.app.new_object("geometry", name + "_cutout", geo_init)
-        self.app.inform.emit("[success] Rectangular CutOut operation finished.")
+            geo_obj.solid_geometry = geo.buffer(margin + abs(dia / 2))
+
+        outname = cutout_obj.options["name"] + "_cutout"
+        self.app.new_object('geometry', outname, geo_init)
+
+        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()))