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