diff --git a/FlatCAMApp.py b/FlatCAMApp.py
index 9ddc1770..05cc7136 100644
--- a/FlatCAMApp.py
+++ b/FlatCAMApp.py
@@ -2517,6 +2517,7 @@ class App(QtCore.QObject):
self.fiducial_tool = None
self.edrills_tool = None
self.align_objects_tool = None
+ self.punch_tool = None
# always install tools only after the shell is initialized because the self.inform.emit() depends on shell
try:
@@ -3150,6 +3151,9 @@ class App(QtCore.QObject):
self.qrcode_tool.install(icon=QtGui.QIcon(self.resource_location + '/qrcode32.png'),
pos=self.ui.menutool)
+ self.punch_tool = ToolPunchGerber(self)
+ self.punch_tool.install(icon=QtGui.QIcon(self.resource_location + '/punch32.png'), pos=self.ui.menutool)
+
self.transform_tool = ToolTransform(self)
self.transform_tool.install(icon=QtGui.QIcon(self.resource_location + '/transform.png'),
pos=self.ui.menuoptions, separator=True)
@@ -3291,6 +3295,7 @@ class App(QtCore.QObject):
self.ui.qrcode_btn.triggered.connect(lambda: self.qrcode_tool.run(toggle=True))
self.ui.copperfill_btn.triggered.connect(lambda: self.copper_thieving_tool.run(toggle=True))
self.ui.fiducials_btn.triggered.connect(lambda: self.fiducial_tool.run(toggle=True))
+ self.ui.punch_btn.triggered.connect(lambda: self.punch_tool.run(toggle=True))
def object2editor(self):
"""
diff --git a/FlatCAMObj.py b/FlatCAMObj.py
index 751063a7..8cbc596a 100644
--- a/FlatCAMObj.py
+++ b/FlatCAMObj.py
@@ -1004,15 +1004,21 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
def geo_init(geo_obj, app_obj):
assert isinstance(geo_obj, FlatCAMGeometry)
if isinstance(self.solid_geometry, list):
- self.solid_geometry = cascaded_union(self.solid_geometry)
+ try:
+ self.solid_geometry = MultiPolygon(self.solid_geometry)
+ except Exception:
+ self.solid_geometry = cascaded_union(self.solid_geometry)
bounding_box = self.solid_geometry.envelope.buffer(float(self.options["noncoppermargin"]))
if not self.options["noncopperrounded"]:
bounding_box = bounding_box.envelope
non_copper = bounding_box.difference(self.solid_geometry)
+
+ if non_copper is None or non_copper.is_empty:
+ self.app.inform.emit("[ERROR_NOTCL] %s" % _("Operation could not be done."))
+ return "fail"
geo_obj.solid_geometry = non_copper
- # TODO: Check for None
self.app.new_object("geometry", name, geo_init)
def on_generatebb_button_click(self, *args):
@@ -1024,12 +1030,19 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
assert isinstance(geo_obj, FlatCAMGeometry)
if isinstance(self.solid_geometry, list):
- self.solid_geometry = MultiPolygon(self.solid_geometry)
+ try:
+ self.solid_geometry = MultiPolygon(self.solid_geometry)
+ except Exception:
+ self.solid_geometry = cascaded_union(self.solid_geometry)
# Bounding box with rounded corners
bounding_box = self.solid_geometry.envelope.buffer(float(self.options["bboxmargin"]))
if not self.options["bboxrounded"]: # Remove rounded corners
bounding_box = bounding_box.envelope
+
+ if bounding_box is None or bounding_box.is_empty:
+ self.app.inform.emit("[ERROR_NOTCL] %s" % _("Operation could not be done."))
+ return "fail"
geo_obj.solid_geometry = bounding_box
self.app.new_object("geometry", name, geo_init)
@@ -3641,8 +3654,8 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
if self.options["solid"]:
for geo in self.solid_geometry:
self.add_shape(shape=geo,
- color=self.app.defaults["excellon_plot_line"],
- face_color=self.app.defaults["excellon_plot_fill"],
+ color=self.outline_color,
+ face_color=self.fill_color,
visible=visible,
layer=2)
else:
diff --git a/README.md b/README.md
index 8c96a14b..33637b91 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,12 @@ CAD program, and create G-Code for Isolation routing.
=================================================
+24.02.2020
+
+- small changes to the Toolchange manual preprocessor
+- fix for plotting Excellon objects if the color is changed and then the object is moved
+- laying the GUI for a new Tool: Punch Gerber Tool which will add holes in the Gerber apertures
+
22.01.2020
- fixed a bug in the bounding box generation
diff --git a/flatcamGUI/FlatCAMGUI.py b/flatcamGUI/FlatCAMGUI.py
index 9f7700d3..2c921197 100644
--- a/flatcamGUI/FlatCAMGUI.py
+++ b/flatcamGUI/FlatCAMGUI.py
@@ -920,6 +920,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
QtGui.QIcon(self.app.resource_location + '/fiducials_32.png'), _("Fiducials Tool"))
self.cal_btn = self.toolbartools.addAction(
QtGui.QIcon(self.app.resource_location + '/calibrate_32.png'), _("Calibration Tool"))
+ self.punch_btn = self.toolbartools.addAction(
+ QtGui.QIcon(self.app.resource_location + '/punch32.png'), _("Punch Gerber Tool"))
# ########################################################################
# ########################## Excellon Editor Toolbar# ####################
diff --git a/flatcamTools/ToolExtractDrills.py b/flatcamTools/ToolExtractDrills.py
index d75a61b5..6973aecc 100644
--- a/flatcamTools/ToolExtractDrills.py
+++ b/flatcamTools/ToolExtractDrills.py
@@ -43,8 +43,7 @@ class ToolExtractDrills(FlatCAMTool):
""")
self.layout.addWidget(title_label)
- self.empty_lb = QtWidgets.QLabel("")
- self.layout.addWidget(self.empty_lb)
+ self.layout.addWidget(QtWidgets.QLabel(""))
# ## Grid Layout
grid_lay = QtWidgets.QGridLayout()
@@ -128,7 +127,7 @@ class ToolExtractDrills(FlatCAMTool):
self.method_label = QtWidgets.QLabel('%s' % _("Method"))
grid1.addWidget(self.method_label, 2, 0, 1, 2)
- # ## Axis
+ # ## Holes Size
self.hole_size_radio = RadioSet(
[
{'label': _("Fixed Diameter"), 'value': 'fixed'},
diff --git a/flatcamTools/ToolPunchGerber.py b/flatcamTools/ToolPunchGerber.py
new file mode 100644
index 00000000..17fa3a80
--- /dev/null
+++ b/flatcamTools/ToolPunchGerber.py
@@ -0,0 +1,373 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing #
+# File Author: Marius Adrian Stanciu (c) #
+# Date: 1/24/2020 #
+# MIT Licence #
+# ##########################################################
+
+from PyQt5 import QtGui, QtCore, QtWidgets
+
+from FlatCAMTool import FlatCAMTool
+from flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, \
+ OptionalHideInputSection, OptionalInputSection, FCComboBox
+
+from copy import deepcopy
+import logging
+from shapely.geometry import Polygon, MultiPolygon, Point
+
+from reportlab.graphics import renderPDF
+from reportlab.pdfgen import canvas
+from reportlab.graphics import renderPM
+from reportlab.lib.units import inch, mm
+from reportlab.lib.pagesizes import landscape, portrait
+
+from svglib.svglib import svg2rlg
+from xml.dom.minidom import parseString as parse_xml_string
+from lxml import etree as ET
+from io import StringIO
+
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+ _ = gettext.gettext
+
+log = logging.getLogger('base')
+
+
+class ToolPunchGerber(FlatCAMTool):
+
+ toolName = _("Punch Gerber")
+
+ def __init__(self, app):
+ FlatCAMTool.__init__(self, app)
+
+ 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)
+
+ # Punch Drill holes
+ self.layout.addWidget(QtWidgets.QLabel(""))
+
+ # ## Grid Layout
+ grid_lay = QtWidgets.QGridLayout()
+ self.layout.addLayout(grid_lay)
+ grid_lay.setColumnStretch(0, 1)
+ grid_lay.setColumnStretch(1, 0)
+
+ # ## Gerber Object
+ self.gerber_object_combo = QtWidgets.QComboBox()
+ self.gerber_object_combo.setModel(self.app.collection)
+ self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+ self.gerber_object_combo.setCurrentIndex(1)
+
+ self.grb_label = QtWidgets.QLabel("%s:" % _("GERBER"))
+ self.grb_label.setToolTip('%s.' % _("Gerber into which to punch holes"))
+
+ grid_lay.addWidget(self.grb_label, 0, 0, 1, 2)
+ grid_lay.addWidget(self.gerber_object_combo, 1, 0, 1, 2)
+
+ separator_line = QtWidgets.QFrame()
+ separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+ separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+ grid_lay.addWidget(separator_line, 2, 0, 1, 2)
+
+ # Grid Layout
+ grid0 = QtWidgets.QGridLayout()
+ self.layout.addLayout(grid0)
+ grid0.setColumnStretch(0, 0)
+ grid0.setColumnStretch(1, 1)
+
+ self.method_label = QtWidgets.QLabel('%s:' % _("Method"))
+ self.method_label.setToolTip(
+ _("The punch hole source can be:\n"
+ "- Excellon -> an Excellon holes center will serve as reference.\n"
+ "- Fixed Diameter -> will try to use the pads center as reference.\n"
+ "- Fixed Annular Ring -> will try to use the pads center as reference.\n"
+ "- Proportional -> will try to use the pads center as reference.\n")
+ )
+ self.method_punch = RadioSet(
+ [
+ {'label': _('Excellon'), 'value': 'exc'},
+ {'label': _("Fixed Diameter"), 'value': 'fixed'},
+ {'label': _("Fixed Annular Ring"), 'value': 'ring'},
+ {'label': _("Proportional"), 'value': 'prop'}
+ ],
+ orientation='vertical',
+ stretch=False)
+ grid0.addWidget(self.method_label, 0, 0)
+ grid0.addWidget(self.method_punch, 0, 1)
+
+ separator_line = QtWidgets.QFrame()
+ separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+ separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+ grid0.addWidget(separator_line, 1, 0, 1, 2)
+
+ self.exc_label = QtWidgets.QLabel('%s' % _("Excellon"))
+ self.exc_label.setToolTip(
+ _("Remove the geometry of Excellon from the Gerber to create the holes in pads.")
+ )
+
+ self.exc_combo = QtWidgets.QComboBox()
+ self.exc_combo.setModel(self.app.collection)
+ self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+ self.exc_combo.setCurrentIndex(1)
+
+ grid0.addWidget(self.exc_label, 2, 0, 1, 2)
+ grid0.addWidget(self.exc_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, 5, 0, 1, 2)
+
+ # Fixed Dia
+ self.fixed_label = QtWidgets.QLabel('%s' % _("Fixed Diameter"))
+ grid0.addWidget(self.fixed_label, 6, 0, 1, 2)
+
+ # Diameter value
+ self.dia_entry = FCDoubleSpinner()
+ self.dia_entry.set_precision(self.decimals)
+ self.dia_entry.set_range(0.0000, 9999.9999)
+
+ self.dia_label = QtWidgets.QLabel('%s:' % _("Value"))
+ self.dia_label.setToolTip(
+ _("Fixed hole diameter.")
+ )
+
+ grid0.addWidget(self.dia_label, 8, 0)
+ grid0.addWidget(self.dia_entry, 8, 1)
+
+ separator_line = QtWidgets.QFrame()
+ separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+ separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+ grid0.addWidget(separator_line, 9, 0, 1, 2)
+
+ self.ring_frame = QtWidgets.QFrame()
+ self.ring_frame.setContentsMargins(0, 0, 0, 0)
+ grid0.addWidget(self.ring_frame, 10, 0, 1, 2)
+
+ self.ring_box = QtWidgets.QVBoxLayout()
+ self.ring_box.setContentsMargins(0, 0, 0, 0)
+ self.ring_frame.setLayout(self.ring_box)
+
+ # ## Grid Layout
+ grid1 = QtWidgets.QGridLayout()
+ grid1.setColumnStretch(0, 0)
+ grid1.setColumnStretch(1, 1)
+ self.ring_box.addLayout(grid1)
+
+ # Annular Ring value
+ self.ring_label = QtWidgets.QLabel('%s' % _("Fixed Annular Ring"))
+ self.ring_label.setToolTip(
+ _("The size of annular ring.\n"
+ "The copper sliver between the drill hole exterior\n"
+ "and the margin of the copper pad.")
+ )
+ grid1.addWidget(self.ring_label, 0, 0, 1, 2)
+
+ # Circular Annular Ring Value
+ self.circular_ring_label = QtWidgets.QLabel('%s:' % _("Circular"))
+ self.circular_ring_label.setToolTip(
+ _("The size of annular ring for circular pads.")
+ )
+
+ self.circular_ring_entry = FCDoubleSpinner()
+ self.circular_ring_entry.set_precision(self.decimals)
+ self.circular_ring_entry.set_range(0.0000, 9999.9999)
+
+ grid1.addWidget(self.circular_ring_label, 1, 0)
+ grid1.addWidget(self.circular_ring_entry, 1, 1)
+
+ # Oblong Annular Ring Value
+ self.oblong_ring_label = QtWidgets.QLabel('%s:' % _("Oblong"))
+ self.oblong_ring_label.setToolTip(
+ _("The size of annular ring for oblong pads.")
+ )
+
+ self.oblong_ring_entry = FCDoubleSpinner()
+ self.oblong_ring_entry.set_precision(self.decimals)
+ self.oblong_ring_entry.set_range(0.0000, 9999.9999)
+
+ grid1.addWidget(self.oblong_ring_label, 2, 0)
+ grid1.addWidget(self.oblong_ring_entry, 2, 1)
+
+ # Square Annular Ring Value
+ self.square_ring_label = QtWidgets.QLabel('%s:' % _("Square"))
+ self.square_ring_label.setToolTip(
+ _("The size of annular ring for square pads.")
+ )
+
+ self.square_ring_entry = FCDoubleSpinner()
+ self.square_ring_entry.set_precision(self.decimals)
+ self.square_ring_entry.set_range(0.0000, 9999.9999)
+
+ grid1.addWidget(self.square_ring_label, 3, 0)
+ grid1.addWidget(self.square_ring_entry, 3, 1)
+
+ # Rectangular Annular Ring Value
+ self.rectangular_ring_label = QtWidgets.QLabel('%s:' % _("Rectangular"))
+ self.rectangular_ring_label.setToolTip(
+ _("The size of annular ring for rectangular pads.")
+ )
+
+ self.rectangular_ring_entry = FCDoubleSpinner()
+ self.rectangular_ring_entry.set_precision(self.decimals)
+ self.rectangular_ring_entry.set_range(0.0000, 9999.9999)
+
+ grid1.addWidget(self.rectangular_ring_label, 4, 0)
+ grid1.addWidget(self.rectangular_ring_entry, 4, 1)
+
+ # Others Annular Ring Value
+ self.other_ring_label = QtWidgets.QLabel('%s:' % _("Others"))
+ self.other_ring_label.setToolTip(
+ _("The size of annular ring for other pads.")
+ )
+
+ self.other_ring_entry = FCDoubleSpinner()
+ self.other_ring_entry.set_precision(self.decimals)
+ self.other_ring_entry.set_range(0.0000, 9999.9999)
+
+ grid1.addWidget(self.other_ring_label, 5, 0)
+ grid1.addWidget(self.other_ring_entry, 5, 1)
+
+ separator_line = QtWidgets.QFrame()
+ separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+ separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+ grid0.addWidget(separator_line, 11, 0, 1, 2)
+
+ # Proportional value
+ self.prop_label = QtWidgets.QLabel('%s' % _("Proportional Diameter"))
+ grid0.addWidget(self.prop_label, 12, 0, 1, 2)
+
+ # Diameter value
+ self.factor_entry = FCDoubleSpinner(suffix='%')
+ self.factor_entry.set_precision(self.decimals)
+ self.factor_entry.set_range(0.0000, 100.0000)
+ self.factor_entry.setSingleStep(0.1)
+
+ self.factor_label = QtWidgets.QLabel('%s:' % _("Value"))
+ self.factor_label.setToolTip(
+ _("Proportional Diameter.\n"
+ "The drill diameter will be a fraction of the pad size.")
+ )
+
+ grid0.addWidget(self.factor_label, 13, 0)
+ grid0.addWidget(self.factor_entry, 13, 1)
+
+ separator_line3 = QtWidgets.QFrame()
+ separator_line3.setFrameShape(QtWidgets.QFrame.HLine)
+ separator_line3.setFrameShadow(QtWidgets.QFrame.Sunken)
+ grid0.addWidget(separator_line3, 14, 0, 1, 2)
+
+ # Buttons
+ self.punch_object_button = QtWidgets.QPushButton(_("Punch Gerber"))
+ self.punch_object_button.setToolTip(
+ _("Create a Gerber object from the selected object, within\n"
+ "the specified box.")
+ )
+ self.punch_object_button.setStyleSheet("""
+ QPushButton
+ {
+ font-weight: bold;
+ }
+ """)
+ self.layout.addWidget(self.punch_object_button)
+
+ self.layout.addStretch()
+
+ # ## Reset Tool
+ self.reset_button = QtWidgets.QPushButton(_("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)
+
+ self.units = self.app.defaults['units']
+
+ # ## Signals
+
+ self.method_punch.activated_custom.connect(self.on_method)
+ self.reset_button.clicked.connect(self.set_tool_ui)
+
+ def run(self, toggle=True):
+ self.app.report_usage("ToolPunchGerber()")
+
+ if toggle:
+ # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+ if self.app.ui.splitter.sizes()[0] == 0:
+ self.app.ui.splitter.setSizes([1, 1])
+ else:
+ try:
+ if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+ # if tab is populated with the tool but it does not have the focus, focus on it
+ if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+ # focus on Tool Tab
+ self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+ else:
+ self.app.ui.splitter.setSizes([0, 1])
+ except AttributeError:
+ pass
+ else:
+ if self.app.ui.splitter.sizes()[0] == 0:
+ self.app.ui.splitter.setSizes([1, 1])
+
+ FlatCAMTool.run(self)
+
+ self.set_tool_ui()
+
+ self.app.ui.notebook.setTabText(2, _("Punch Tool"))
+
+ def install(self, icon=None, separator=None, **kwargs):
+ FlatCAMTool.install(self, icon, separator, shortcut='ALT+H', **kwargs)
+
+ def set_tool_ui(self):
+ self.reset_fields()
+
+ self.method_punch.set_value('exc')
+
+ def on_method(self, val):
+ self.exc_label.setEnabled(False)
+ self.exc_combo.setEnabled(False)
+ self.fixed_label.setEnabled(False)
+ self.dia_label.setEnabled(False)
+ self.dia_entry.setEnabled(False)
+ self.ring_frame.setEnabled(False)
+ self.prop_label.setEnabled(False)
+ self.factor_label.setEnabled(False)
+ self.factor_entry.setEnabled(False)
+
+ if val == 'exc':
+ self.exc_label.setEnabled(True)
+ self.exc_combo.setEnabled(True)
+ elif val == 'fixed':
+ self.fixed_label.setEnabled(True)
+ self.dia_label.setEnabled(True)
+ self.dia_entry.setEnabled(True)
+ elif val == 'ring':
+ self.ring_frame.setEnabled(True)
+ elif val == 'prop':
+ self.prop_label.setEnabled(True)
+ self.factor_label.setEnabled(True)
+ self.factor_entry.setEnabled(True)
+
+ def reset_fields(self):
+ self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+ self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
diff --git a/flatcamTools/__init__.py b/flatcamTools/__init__.py
index a593e6c4..b503ca38 100644
--- a/flatcamTools/__init__.py
+++ b/flatcamTools/__init__.py
@@ -38,3 +38,4 @@ from flatcamTools.ToolSolderPaste import SolderPaste
from flatcamTools.ToolSub import ToolSub
from flatcamTools.ToolTransform import ToolTransform
+from flatcamTools.ToolPunchGerber import ToolPunchGerber
diff --git a/preprocessors/Toolchange_manual.py b/preprocessors/Toolchange_manual.py
index dc7a304d..ef583a7c 100644
--- a/preprocessors/Toolchange_manual.py
+++ b/preprocessors/Toolchange_manual.py
@@ -119,6 +119,7 @@ M0
G00 Z{z_toolchange}
(MSG, Now the tool can be tightened more securely.)
M0
+(MSG, Drilling with Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
""".format(x_toolchange=self.coordinate_format % (p.coords_decimals, x_toolchange),
y_toolchange=self.coordinate_format % (p.coords_decimals, y_toolchange),
z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
@@ -139,6 +140,7 @@ M0
G00 Z{z_toolchange}
(MSG, Now the tool can be tightened more securely.)
M0
+(MSG, Milling with Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
""".format(
z_toolchange=self.coordinate_format % (p.coords_decimals, z_toolchange),
tool=int(p.tool),
diff --git a/share/punch16.png b/share/punch16.png
new file mode 100644
index 00000000..65c25810
Binary files /dev/null and b/share/punch16.png differ
diff --git a/share/punch32.png b/share/punch32.png
new file mode 100644
index 00000000..b3c130ba
Binary files /dev/null and b/share/punch32.png differ