Browse Source

- 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 years ago
parent
commit
d775e999fe
5 changed files with 430 additions and 135 deletions
  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_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_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_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,
             "tools_gaps_ff": self.ui.tools_defaults_form.tools_cutout_group.gaps_combo,
 
 
             # Paint Area Tool
             # Paint Area Tool
@@ -744,7 +743,6 @@ class App(QtCore.QObject):
             "tools_cutouttooldia": 0.00393701,
             "tools_cutouttooldia": 0.00393701,
             "tools_cutoutmargin": 0.00393701,
             "tools_cutoutmargin": 0.00393701,
             "tools_cutoutgapsize": 0.005905512,
             "tools_cutoutgapsize": 0.005905512,
-            "tools_gaps_rect": "4",
             "tools_gaps_ff": "8",
             "tools_gaps_ff": "8",
 
 
             "tools_painttooldia": 0.07,
             "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_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_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_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_gaps_ff": self.ui.tools_options_form.tools_cutout_group.gaps_combo,
 
 
             "tools_painttooldia": self.ui.tools_options_form.tools_paint_group.painttooldia_entry,
             "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_ncctools": "1.0, 0.5",
             "tools_nccoverlap": 0.4,
             "tools_nccoverlap": 0.4,
             "tools_nccmargin": 1,
             "tools_nccmargin": 1,
+
             "tools_cutouttooldia": 0.07,
             "tools_cutouttooldia": 0.07,
             "tools_cutoutmargin": 0.1,
             "tools_cutoutmargin": 0.1,
             "tools_cutoutgapsize": 0.15,
             "tools_cutoutgapsize": 0.15,
-            "tools_gaps_rect": "4",
             "tools_gaps_ff": "8",
             "tools_gaps_ff": "8",
 
 
             "tools_painttooldia": 0.07,
             "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))
         geo = self.active_tool.utility_geometry(data=(x, y))
 
 
         if isinstance(geo, DrawToolShape) and geo.geo is not None:
         if isinstance(geo, DrawToolShape) and geo.geo is not None:
-
             # Remove any previous utility shape
             # Remove any previous utility shape
             self.tool_shape.clear(update=True)
             self.tool_shape.clear(update=True)
             self.draw_utility_geometry(geo=geo)
             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 = GerberGenPrefGroupUI()
         self.gerber_gen_group.setFixedWidth(250)
         self.gerber_gen_group.setFixedWidth(250)
         self.gerber_opt_group = GerberOptPrefGroupUI()
         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 = GerberAdvOptPrefGroupUI()
         self.gerber_adv_opt_group.setFixedWidth(200)
         self.gerber_adv_opt_group.setFixedWidth(200)
 
 
@@ -4853,22 +4853,9 @@ class ToolsCutoutPrefGroupUI(OptionsGroupUI):
         self.cutout_gap_entry = LengthEntry()
         self.cutout_gap_entry = LengthEntry()
         grid0.addWidget(self.cutout_gap_entry, 2, 1)
         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"
             "There can be maximum 8 bridges/gaps.\n"
             "The choices are:\n"
             "The choices are:\n"
             "- lr    - left + right\n"
             "- lr    - left + right\n"
@@ -4878,9 +4865,9 @@ class ToolsCutoutPrefGroupUI(OptionsGroupUI):
             "- 2tb  - 2*top + 2*bottom\n"
             "- 2tb  - 2*top + 2*bottom\n"
             "- 8     - 2*left + 2*right +2*top + 2*bottom"
             "- 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()
         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']
         gaps_items = ['LR', 'TB', '4', '2LR', '2TB', '8']
         for it in gaps_items:
         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
 - finished work on object hovering
 - fixed Excellon object move and all the other transformations
 - fixed Excellon object move and all the other transformations
 - starting to work on Manual Cutout Tool
 - 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
 3.03.2019
 
 

+ 419 - 111
flatcamTools/ToolCutOut.py

@@ -1,15 +1,20 @@
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
 from ObjectCollection import *
 from ObjectCollection import *
 from FlatCAMApp import *
 from FlatCAMApp import *
+from shapely.geometry import box
 
 
 
 
 class CutOut(FlatCAMTool):
 class CutOut(FlatCAMTool):
 
 
     toolName = "Cutout PCB"
     toolName = "Cutout PCB"
+    gapFinished = pyqtSignal()
 
 
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
 
 
+        self.app = app
+        self.canvas = app.plotcanvas
+
         ## Title
         ## Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label.setStyleSheet("""
         title_label.setStyleSheet("""
@@ -44,6 +49,7 @@ class CutOut(FlatCAMTool):
             "What is selected here will dictate the kind\n"
             "What is selected here will dictate the kind\n"
             "of objects that will populate the 'Object' combobox."
             "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)
         form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo)
 
 
         ## Object to be cutout
         ## Object to be cutout
@@ -58,14 +64,6 @@ class CutOut(FlatCAMTool):
         )
         )
         form_layout.addRow(self.object_label, self.obj_combo)
         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
         # Tool Diameter
         self.dia = FCEntry()
         self.dia = FCEntry()
         self.dia_label = QtWidgets.QLabel("Tool Dia:")
         self.dia_label = QtWidgets.QLabel("Tool Dia:")
@@ -73,7 +71,7 @@ class CutOut(FlatCAMTool):
             "Diameter of the tool used to cutout\n"
             "Diameter of the tool used to cutout\n"
             "the PCB shape out of the surrounding material."
             "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
         # Margin
         self.margin = FCEntry()
         self.margin = FCEntry()
@@ -83,26 +81,18 @@ class CutOut(FlatCAMTool):
             "will make the cutout of the PCB further from\n"
             "will make the cutout of the PCB further from\n"
             "the actual PCB border"
             "the actual PCB border"
         )
         )
-        form_layout_2.addRow(self.margin_label, self.margin)
+        form_layout.addRow(self.margin_label, self.margin)
 
 
         # Gapsize
         # Gapsize
         self.gapsize = FCEntry()
         self.gapsize = FCEntry()
         self.gapsize_label = QtWidgets.QLabel("Gap size:")
         self.gapsize_label = QtWidgets.QLabel("Gap size:")
         self.gapsize_label.setToolTip(
         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"
             "used to keep the board connected to\n"
             "the surrounding material (the one \n"
             "the surrounding material (the one \n"
             "from which the PCB is cutout)."
             "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:
         # How gaps wil be rendered:
         # lr    - left + right
         # lr    - left + right
@@ -112,10 +102,21 @@ class CutOut(FlatCAMTool):
         # 2tb   - 2*top + 2*bottom
         # 2tb   - 2*top + 2*bottom
         # 8     - 2*left + 2*right +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
-        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"
             "There can be maximum 8 bridges/gaps.\n"
             "The choices are:\n"
             "The choices are:\n"
             "- lr    - left + right\n"
             "- lr    - left + right\n"
@@ -125,84 +126,137 @@ class CutOut(FlatCAMTool):
             "- 2tb  - 2*top + 2*bottom\n"
             "- 2tb  - 2*top + 2*bottom\n"
             "- 8     - 2*left + 2*right +2*top + 2*bottom"
             "- 8     - 2*left + 2*right +2*top + 2*bottom"
         )
         )
+        gaps_label.setFixedWidth(60)
 
 
         self.gaps = FCComboBox()
         self.gaps = FCComboBox()
         gaps_items = ['LR', 'TB', '4', '2LR', '2TB', '8']
         gaps_items = ['LR', 'TB', '4', '2LR', '2TB', '8']
         for it in gaps_items:
         for it in gaps_items:
             self.gaps.addItem(it)
             self.gaps.addItem(it)
             self.gaps.setStyleSheet('background-color: rgb(255,255,255)')
             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
         ## Buttons
         hlay = QtWidgets.QHBoxLayout()
         hlay = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay)
         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()
         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(
         self.ff_cutout_object_btn.setToolTip(
             "Cutout the selected object.\n"
             "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)
         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()
         hlay2 = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay2)
         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()
         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(
         self.rect_cutout_object_btn.setToolTip(
             "Cutout the selected object.\n"
             "Cutout the selected object.\n"
             "The resulting cutout shape is\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."
             "the bounding box of the Object."
         )
         )
         hlay2.addWidget(self.rect_cutout_object_btn)
         hlay2.addWidget(self.rect_cutout_object_btn)
 
 
         ## Title5
         ## 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)
         self.layout.addWidget(title_manual_label)
 
 
         ## Form Layout
         ## 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):
     def on_type_obj_index_changed(self, index):
         obj_type = self.type_obj_combo.currentIndex()
         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.margin.set_value(float(self.app.defaults["tools_cutoutmargin"]))
         self.gapsize.set_value(float(self.app.defaults["tools_cutoutgapsize"]))
         self.gapsize.set_value(float(self.app.defaults["tools_cutoutgapsize"]))
         self.gaps.set_value(4)
         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):
     def on_freeform_cutout(self):
 
 
@@ -268,6 +331,11 @@ class CutOut(FlatCAMTool):
                                      "Add it and retry.")
                                      "Add it and retry.")
                 return
                 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:
         try:
             margin = float(self.margin.get_value())
             margin = float(self.margin.get_value())
         except ValueError:
         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.")
             self.app.inform.emit("[WARNING_NOTCL] Number of gaps value is missing. Add it and retry.")
             return
             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']:
         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. "
             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. ")
                                  "Fill in a correct value and retry. ")
@@ -377,6 +441,11 @@ class CutOut(FlatCAMTool):
         self.app.should_we_save = True
         self.app.should_we_save = True
 
 
     def on_rectangular_cutout(self):
     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()
         name = self.obj_combo.currentText()
 
 
         # Get source object.
         # Get source object.
@@ -400,6 +469,10 @@ class CutOut(FlatCAMTool):
                                      "Add it and retry.")
                                      "Add it and retry.")
                 return
                 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:
         try:
             margin = float(self.margin.get_value())
             margin = float(self.margin.get_value())
         except ValueError:
         except ValueError:
@@ -423,14 +496,15 @@ class CutOut(FlatCAMTool):
                 return
                 return
 
 
         try:
         try:
-            gaps = self.gaps_rect_radio.get_value()
+            gaps = self.gaps.get_value()
         except TypeError:
         except TypeError:
             self.app.inform.emit("[WARNING_NOTCL] Number of gaps value is missing. Add it and retry.")
             self.app.inform.emit("[WARNING_NOTCL] Number of gaps value is missing. Add it and retry.")
             return
             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:
         if cutout_obj.multigeo is True:
             self.app.inform.emit("[ERROR]Cutout operation cannot be done on a multi-geo Geometry.\n"
             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.")
                                  "and after that perform Cutout.")
             return
             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):
         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.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):
     def reset_fields(self):
         self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))