- remade file names in the app

- fixed the issue with factory_defaults being saved every time the app start
- fixed the preferences not being saved to a file when the Save button is pressed in Edit -> Preferences
- fixed and updated the Transform Tools in the Editors
This commit is contained in:
Marius Stanciu
2020-06-03 20:35:59 +03:00
committed by Marius
parent 378a497935
commit 2eecb20e95
190 changed files with 1940 additions and 1990 deletions

View File

@@ -0,0 +1,495 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 1/13/2020 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets, QtCore
from appTool import AppTool
from appGUI.GUIElements import FCComboBox, RadioSet
import math
from shapely.geometry import Point
from shapely.affinity import translate
import gettext
import appTranslation as fcTranslate
import builtins
import logging
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class AlignObjects(AppTool):
toolName = _("Align Objects")
def __init__(self, app):
AppTool.__init__(self, app)
self.app = app
self.decimals = app.decimals
self.canvas = self.app.plotcanvas
# ## 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.aligned_label = QtWidgets.QLabel('<b>%s:</b>' % _("MOVING object"))
grid0.addWidget(self.aligned_label, 0, 0, 1, 2)
self.aligned_label.setToolTip(
_("Specify the type of object to be aligned.\n"
"It can be of type: Gerber or Excellon.\n"
"The selection here decide the type of objects that will be\n"
"in the Object combobox.")
)
# Type of object to be aligned
self.type_obj_radio = RadioSet([
{"label": _("Gerber"), "value": "grb"},
{"label": _("Excellon"), "value": "exc"},
], orientation='vertical', stretch=False)
grid0.addWidget(self.type_obj_radio, 3, 0, 1, 2)
# Object to be aligned
self.object_combo = FCComboBox()
self.object_combo.setModel(self.app.collection)
self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.object_combo.is_last = True
self.object_combo.setToolTip(
_("Object to be aligned.")
)
grid0.addWidget(self.object_combo, 4, 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)
grid0.addWidget(QtWidgets.QLabel(''), 6, 0, 1, 2)
self.aligned_label = QtWidgets.QLabel('<b>%s:</b>' % _("TARGET object"))
self.aligned_label.setToolTip(
_("Specify the type of object to be aligned to.\n"
"It can be of type: Gerber or Excellon.\n"
"The selection here decide the type of objects that will be\n"
"in the Object combobox.")
)
grid0.addWidget(self.aligned_label, 7, 0, 1, 2)
# Type of object to be aligned to = aligner
self.type_aligner_obj_radio = RadioSet([
{"label": _("Gerber"), "value": "grb"},
{"label": _("Excellon"), "value": "exc"},
], orientation='vertical', stretch=False)
grid0.addWidget(self.type_aligner_obj_radio, 8, 0, 1, 2)
# Object to be aligned to = aligner
self.aligner_object_combo = FCComboBox()
self.aligner_object_combo.setModel(self.app.collection)
self.aligner_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.aligner_object_combo.is_last = True
self.aligner_object_combo.setToolTip(
_("Object to be aligned to. Aligner.")
)
grid0.addWidget(self.aligner_object_combo, 9, 0, 1, 2)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid0.addWidget(separator_line, 10, 0, 1, 2)
grid0.addWidget(QtWidgets.QLabel(''), 11, 0, 1, 2)
# Alignment Type
self.a_type_lbl = QtWidgets.QLabel('<b>%s:</b>' % _("Alignment Type"))
self.a_type_lbl.setToolTip(
_("The type of alignment can be:\n"
"- Single Point -> it require a single point of sync, the action will be a translation\n"
"- Dual Point -> it require two points of sync, the action will be translation followed by rotation")
)
self.a_type_radio = RadioSet(
[
{'label': _('Single Point'), 'value': 'sp'},
{'label': _('Dual Point'), 'value': 'dp'}
],
orientation='vertical',
stretch=False
)
grid0.addWidget(self.a_type_lbl, 12, 0, 1, 2)
grid0.addWidget(self.a_type_radio, 13, 0, 1, 2)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid0.addWidget(separator_line, 14, 0, 1, 2)
# Buttons
self.align_object_button = QtWidgets.QPushButton(_("Align Object"))
self.align_object_button.setToolTip(
_("Align the specified object to the aligner object.\n"
"If only one point is used then it assumes translation.\n"
"If tho points are used it assume translation and rotation.")
)
self.align_object_button.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
self.layout.addWidget(self.align_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)
# Signals
self.align_object_button.clicked.connect(self.on_align)
self.type_obj_radio.activated_custom.connect(self.on_type_obj_changed)
self.type_aligner_obj_radio.activated_custom.connect(self.on_type_aligner_changed)
self.reset_button.clicked.connect(self.set_tool_ui)
self.mr = None
# if the mouse events are connected to a local method set this True
self.local_connected = False
# store the status of the grid
self.grid_status_memory = None
self.aligned_obj = None
self.aligner_obj = None
# this is one of the objects: self.aligned_obj or self.aligner_obj
self.target_obj = None
# here store the alignment points
self.clicked_points = []
self.align_type = None
# old colors of objects involved in the alignment
self.aligner_old_fill_color = None
self.aligner_old_line_color = None
self.aligned_old_fill_color = None
self.aligned_old_line_color = None
def run(self, toggle=True):
self.app.defaults.report_usage("ToolAlignObjects()")
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])
AppTool.run(self)
self.set_tool_ui()
self.app.ui.notebook.setTabText(2, _("Align Tool"))
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='Alt+A', **kwargs)
def set_tool_ui(self):
self.reset_fields()
self.clicked_points = []
self.target_obj = None
self.aligned_obj = None
self.aligner_obj = None
self.aligner_old_fill_color = None
self.aligner_old_line_color = None
self.aligned_old_fill_color = None
self.aligned_old_line_color = None
self.a_type_radio.set_value(self.app.defaults["tools_align_objects_align_type"])
self.type_obj_radio.set_value('grb')
self.type_aligner_obj_radio.set_value('grb')
if self.local_connected is True:
self.disconnect_cal_events()
def on_type_obj_changed(self, val):
obj_type = {'grb': 0, 'exc': 1}[val]
self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
self.object_combo.setCurrentIndex(0)
self.object_combo.obj_type = {'grb': "Gerber", 'exc': "Excellon"}[val]
def on_type_aligner_changed(self, val):
obj_type = {'grb': 0, 'exc': 1}[val]
self.aligner_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
self.aligner_object_combo.setCurrentIndex(0)
self.aligner_object_combo.obj_type = {'grb': "Gerber", 'exc': "Excellon"}[val]
def on_align(self):
self.app.delete_selection_shape()
obj_sel_index = self.object_combo.currentIndex()
obj_model_index = self.app.collection.index(obj_sel_index, 0, self.object_combo.rootModelIndex())
try:
self.aligned_obj = obj_model_index.internalPointer().obj
except AttributeError:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no aligned FlatCAM object selected..."))
return
aligner_obj_sel_index = self.aligner_object_combo.currentIndex()
aligner_obj_model_index = self.app.collection.index(
aligner_obj_sel_index, 0, self.aligner_object_combo.rootModelIndex())
try:
self.aligner_obj = aligner_obj_model_index.internalPointer().obj
except AttributeError:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no aligner FlatCAM object selected..."))
return
self.align_type = self.a_type_radio.get_value()
# disengage the grid snapping since it will be hard to find the drills or pads on grid
if self.app.ui.grid_snap_btn.isChecked():
self.grid_status_memory = True
self.app.ui.grid_snap_btn.trigger()
else:
self.grid_status_memory = False
self.local_connected = True
self.aligner_old_fill_color = self.aligner_obj.fill_color
self.aligner_old_line_color = self.aligner_obj.outline_color
self.aligned_old_fill_color = self.aligned_obj.fill_color
self.aligned_old_line_color = self.aligned_obj.outline_color
self.target_obj = self.aligned_obj
self.set_color()
self.app.inform.emit('%s: %s' % (_("First Point"), _("Click on the START point.")))
self.mr = self.canvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
if self.app.is_legacy is False:
self.canvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
else:
self.canvas.graph_event_disconnect(self.app.mr)
def on_mouse_click_release(self, event):
if self.app.is_legacy is False:
event_pos = event.pos
right_button = 2
self.app.event_is_dragging = self.app.event_is_dragging
else:
event_pos = (event.xdata, event.ydata)
right_button = 3
self.app.event_is_dragging = self.app.ui.popMenu.mouse_is_panning
pos_canvas = self.canvas.translate_coords(event_pos)
if event.button == 1:
click_pt = Point([pos_canvas[0], pos_canvas[1]])
if self.app.selection_type is not None:
# delete previous selection shape
self.app.delete_selection_shape()
self.app.selection_type = None
else:
if self.target_obj.kind.lower() == 'excellon':
for tool, tool_dict in self.target_obj.tools.items():
for geo in tool_dict['solid_geometry']:
if click_pt.within(geo):
center_pt = geo.centroid
self.clicked_points.append(
[
float('%.*f' % (self.decimals, center_pt.x)),
float('%.*f' % (self.decimals, center_pt.y))
]
)
self.check_points()
elif self.target_obj.kind.lower() == 'gerber':
for apid, apid_val in self.target_obj.apertures.items():
for geo_el in apid_val['geometry']:
if 'solid' in geo_el:
if click_pt.within(geo_el['solid']):
if isinstance(geo_el['follow'], Point):
center_pt = geo_el['solid'].centroid
self.clicked_points.append(
[
float('%.*f' % (self.decimals, center_pt.x)),
float('%.*f' % (self.decimals, center_pt.y))
]
)
self.check_points()
elif event.button == right_button and self.app.event_is_dragging is False:
self.reset_color()
self.clicked_points = []
self.disconnect_cal_events()
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled by user request."))
def check_points(self):
if len(self.clicked_points) == 1:
self.app.inform.emit('%s: %s. %s' % (
_("First Point"), _("Click on the DESTINATION point."), _("Or right click to cancel.")))
self.target_obj = self.aligner_obj
self.reset_color()
self.set_color()
if len(self.clicked_points) == 2:
if self.align_type == 'sp':
self.align_translate()
self.app.inform.emit('[success] %s' % _("Done."))
self.app.plot_all()
self.disconnect_cal_events()
return
else:
self.app.inform.emit('%s: %s. %s' % (
_("Second Point"), _("Click on the START point."), _("Or right click to cancel.")))
self.target_obj = self.aligned_obj
self.reset_color()
self.set_color()
if len(self.clicked_points) == 3:
self.app.inform.emit('%s: %s. %s' % (
_("Second Point"), _("Click on the DESTINATION point."), _("Or right click to cancel.")))
self.target_obj = self.aligner_obj
self.reset_color()
self.set_color()
if len(self.clicked_points) == 4:
self.align_translate()
self.align_rotate()
self.app.inform.emit('[success] %s' % _("Done."))
self.disconnect_cal_events()
self.app.plot_all()
def align_translate(self):
dx = self.clicked_points[1][0] - self.clicked_points[0][0]
dy = self.clicked_points[1][1] - self.clicked_points[0][1]
self.aligned_obj.offset((dx, dy))
# Update the object bounding box options
a, b, c, d = self.aligned_obj.bounds()
self.aligned_obj.options['xmin'] = a
self.aligned_obj.options['ymin'] = b
self.aligned_obj.options['xmax'] = c
self.aligned_obj.options['ymax'] = d
def align_rotate(self):
dx = self.clicked_points[1][0] - self.clicked_points[0][0]
dy = self.clicked_points[1][1] - self.clicked_points[0][1]
test_rotation_pt = translate(Point(self.clicked_points[2]), xoff=dx, yoff=dy)
new_start = (test_rotation_pt.x, test_rotation_pt.y)
new_dest = self.clicked_points[3]
origin_pt = self.clicked_points[1]
dxd = new_dest[0] - origin_pt[0]
dyd = new_dest[1] - origin_pt[1]
dxs = new_start[0] - origin_pt[0]
dys = new_start[1] - origin_pt[1]
rotation_not_needed = (abs(new_start[0] - new_dest[0]) <= (10 ** -self.decimals)) or \
(abs(new_start[1] - new_dest[1]) <= (10 ** -self.decimals))
if rotation_not_needed is False:
# calculate rotation angle
angle_dest = math.degrees(math.atan(dyd / dxd))
angle_start = math.degrees(math.atan(dys / dxs))
angle = angle_dest - angle_start
self.aligned_obj.rotate(angle=angle, point=origin_pt)
def disconnect_cal_events(self):
# restore the Grid snapping if it was active before
if self.grid_status_memory is True:
self.app.ui.grid_snap_btn.trigger()
self.app.mr = self.canvas.graph_event_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
if self.app.is_legacy is False:
self.canvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
else:
self.canvas.graph_event_disconnect(self.mr)
self.local_connected = False
self.aligner_old_fill_color = None
self.aligner_old_line_color = None
self.aligned_old_fill_color = None
self.aligned_old_line_color = None
def set_color(self):
new_color = "#15678abf"
new_line_color = new_color
self.target_obj.shapes.redraw(
update_colors=(new_color, new_line_color)
)
def reset_color(self):
self.aligned_obj.shapes.redraw(
update_colors=(self.aligned_old_fill_color, self.aligned_old_line_color)
)
self.aligner_obj.shapes.redraw(
update_colors=(self.aligner_old_fill_color, self.aligner_old_line_color)
)
def reset_fields(self):
self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.aligner_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))

370
appTools/ToolCalculators.py Normal file
View File

@@ -0,0 +1,370 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 3/10/2019 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets
from appTool import AppTool
from appGUI.GUIElements import FCSpinner, FCDoubleSpinner, FCEntry
import math
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
class ToolCalculator(AppTool):
toolName = _("Calculators")
v_shapeName = _("V-Shape Tool Calculator")
unitsName = _("Units Calculator")
eplateName = _("ElectroPlating Calculator")
def __init__(self, app):
AppTool.__init__(self, app)
self.app = 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)
# #####################
# ## Units Calculator #
# #####################
self.unists_spacer_label = QtWidgets.QLabel(" ")
self.layout.addWidget(self.unists_spacer_label)
# ## Title of the Units Calculator
units_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.unitsName)
self.layout.addWidget(units_label)
# Grid Layout
grid_units_layout = QtWidgets.QGridLayout()
self.layout.addLayout(grid_units_layout)
inch_label = QtWidgets.QLabel(_("INCH"))
mm_label = QtWidgets.QLabel(_("MM"))
grid_units_layout.addWidget(mm_label, 0, 0)
grid_units_layout.addWidget(inch_label, 0, 1)
self.inch_entry = FCEntry()
# self.inch_entry.setFixedWidth(70)
# self.inch_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.inch_entry.setToolTip(_("Here you enter the value to be converted from INCH to MM"))
self.mm_entry = FCEntry()
# self.mm_entry.setFixedWidth(130)
# self.mm_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.mm_entry.setToolTip(_("Here you enter the value to be converted from MM to INCH"))
grid_units_layout.addWidget(self.mm_entry, 1, 0)
grid_units_layout.addWidget(self.inch_entry, 1, 1)
# ##############################
# ## V-shape Tool Calculator ###
# ##############################
self.v_shape_spacer_label = QtWidgets.QLabel(" ")
self.layout.addWidget(self.v_shape_spacer_label)
# ## Title of the V-shape Tools Calculator
v_shape_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.v_shapeName)
self.layout.addWidget(v_shape_title_label)
# ## Form Layout
form_layout = QtWidgets.QFormLayout()
self.layout.addLayout(form_layout)
self.tipDia_label = QtWidgets.QLabel('%s:' % _("Tip Diameter"))
self.tipDia_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.tipDia_entry.set_precision(self.decimals)
self.tipDia_entry.set_range(0.0, 9999.9999)
self.tipDia_entry.setSingleStep(0.1)
# self.tipDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.tipDia_label.setToolTip(
_("This is the tool tip diameter.\n"
"It is specified by manufacturer.")
)
self.tipAngle_label = QtWidgets.QLabel('%s:' % _("Tip Angle"))
self.tipAngle_entry = FCSpinner(callback=self.confirmation_message_int)
self.tipAngle_entry.set_range(0,180)
self.tipAngle_entry.set_step(5)
# self.tipAngle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.tipAngle_label.setToolTip(_("This is the angle of the tip of the tool.\n"
"It is specified by manufacturer."))
self.cutDepth_label = QtWidgets.QLabel('%s:' % _("Cut Z"))
self.cutDepth_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.cutDepth_entry.set_range(-9999.9999, 9999.9999)
self.cutDepth_entry.set_precision(self.decimals)
# self.cutDepth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.cutDepth_label.setToolTip(_("This is the depth to cut into the material.\n"
"In the CNCJob is the CutZ parameter."))
self.effectiveToolDia_label = QtWidgets.QLabel('%s:' % _("Tool Diameter"))
self.effectiveToolDia_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.effectiveToolDia_entry.set_precision(self.decimals)
# self.effectiveToolDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.effectiveToolDia_label.setToolTip(_("This is the tool diameter to be entered into\n"
"FlatCAM Gerber section.\n"
"In the CNCJob section it is called >Tool dia<."))
# self.effectiveToolDia_entry.setEnabled(False)
form_layout.addRow(self.tipDia_label, self.tipDia_entry)
form_layout.addRow(self.tipAngle_label, self.tipAngle_entry)
form_layout.addRow(self.cutDepth_label, self.cutDepth_entry)
form_layout.addRow(self.effectiveToolDia_label, self.effectiveToolDia_entry)
# ## Buttons
self.calculate_vshape_button = QtWidgets.QPushButton(_("Calculate"))
# self.calculate_button.setFixedWidth(70)
self.calculate_vshape_button.setToolTip(
_("Calculate either the Cut Z or the effective tool diameter,\n "
"depending on which is desired and which is known. ")
)
self.layout.addWidget(self.calculate_vshape_button)
# ####################################
# ## ElectroPlating Tool Calculator ##
# ####################################
self.plate_spacer_label = QtWidgets.QLabel(" ")
self.layout.addWidget(self.plate_spacer_label)
# ## Title of the ElectroPlating Tools Calculator
plate_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.eplateName)
plate_title_label.setToolTip(
_("This calculator is useful for those who plate the via/pad/drill holes,\n"
"using a method like graphite ink or calcium hypophosphite ink or palladium chloride.")
)
self.layout.addWidget(plate_title_label)
# ## Plate Form Layout
plate_form_layout = QtWidgets.QFormLayout()
self.layout.addLayout(plate_form_layout)
self.pcblengthlabel = QtWidgets.QLabel('%s:' % _("Board Length"))
self.pcblength_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.pcblength_entry.set_precision(self.decimals)
self.pcblength_entry.set_range(0.0, 9999.9999)
# self.pcblength_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.pcblengthlabel.setToolTip(_('This is the board length. In centimeters.'))
self.pcbwidthlabel = QtWidgets.QLabel('%s:' % _("Board Width"))
self.pcbwidth_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.pcbwidth_entry.set_precision(self.decimals)
self.pcbwidth_entry.set_range(0.0, 9999.9999)
# self.pcbwidth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.pcbwidthlabel.setToolTip(_('This is the board width.In centimeters.'))
self.cdensity_label = QtWidgets.QLabel('%s:' % _("Current Density"))
self.cdensity_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.cdensity_entry.set_precision(self.decimals)
self.cdensity_entry.set_range(0.0, 9999.9999)
self.cdensity_entry.setSingleStep(0.1)
# self.cdensity_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.cdensity_label.setToolTip(_("Current density to pass through the board. \n"
"In Amps per Square Feet ASF."))
self.growth_label = QtWidgets.QLabel('%s:' % _("Copper Growth"))
self.growth_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.growth_entry.set_precision(self.decimals)
self.growth_entry.set_range(0.0, 9999.9999)
self.growth_entry.setSingleStep(0.01)
# self.growth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.growth_label.setToolTip(_("How thick the copper growth is intended to be.\n"
"In microns."))
# self.growth_entry.setEnabled(False)
self.cvaluelabel = QtWidgets.QLabel('%s:' % _("Current Value"))
self.cvalue_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.cvalue_entry.set_precision(self.decimals)
self.cvalue_entry.set_range(0.0, 9999.9999)
self.cvalue_entry.setSingleStep(0.1)
# self.cvalue_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.cvaluelabel.setToolTip(_('This is the current intensity value\n'
'to be set on the Power Supply. In Amps.'))
self.cvalue_entry.setReadOnly(True)
self.timelabel = QtWidgets.QLabel('%s:' % _("Time"))
self.time_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.time_entry.set_precision(self.decimals)
self.time_entry.set_range(0.0, 9999.9999)
self.time_entry.setSingleStep(0.1)
# self.time_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.timelabel.setToolTip(_('This is the calculated time required for the procedure.\n'
'In minutes.'))
self.time_entry.setReadOnly(True)
plate_form_layout.addRow(self.pcblengthlabel, self.pcblength_entry)
plate_form_layout.addRow(self.pcbwidthlabel, self.pcbwidth_entry)
plate_form_layout.addRow(self.cdensity_label, self.cdensity_entry)
plate_form_layout.addRow(self.growth_label, self.growth_entry)
plate_form_layout.addRow(self.cvaluelabel, self.cvalue_entry)
plate_form_layout.addRow(self.timelabel, self.time_entry)
# ## Buttons
self.calculate_plate_button = QtWidgets.QPushButton(_("Calculate"))
# self.calculate_button.setFixedWidth(70)
self.calculate_plate_button.setToolTip(
_("Calculate the current intensity value and the procedure time,\n"
"depending on the parameters above")
)
self.layout.addWidget(self.calculate_plate_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 = ''
# ## Signals
self.cutDepth_entry.valueChanged.connect(self.on_calculate_tool_dia)
self.cutDepth_entry.returnPressed.connect(self.on_calculate_tool_dia)
self.tipDia_entry.returnPressed.connect(self.on_calculate_tool_dia)
self.tipAngle_entry.returnPressed.connect(self.on_calculate_tool_dia)
self.calculate_vshape_button.clicked.connect(self.on_calculate_tool_dia)
self.mm_entry.editingFinished.connect(self.on_calculate_inch_units)
self.inch_entry.editingFinished.connect(self.on_calculate_mm_units)
self.calculate_plate_button.clicked.connect(self.on_calculate_eplate)
self.reset_button.clicked.connect(self.set_tool_ui)
def run(self, toggle=True):
self.app.defaults.report_usage("ToolCalculators()")
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])
AppTool.run(self)
self.set_tool_ui()
self.app.ui.notebook.setTabText(2, _("Calc. Tool"))
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='Alt+C', **kwargs)
def set_tool_ui(self):
self.units = self.app.defaults['units'].upper()
# ## Initialize form
self.mm_entry.set_value('%.*f' % (self.decimals, 0))
self.inch_entry.set_value('%.*f' % (self.decimals, 0))
length = self.app.defaults["tools_calc_electro_length"]
width = self.app.defaults["tools_calc_electro_width"]
density = self.app.defaults["tools_calc_electro_cdensity"]
growth = self.app.defaults["tools_calc_electro_growth"]
self.pcblength_entry.set_value(length)
self.pcbwidth_entry.set_value(width)
self.cdensity_entry.set_value(density)
self.growth_entry.set_value(growth)
self.cvalue_entry.set_value(0.00)
self.time_entry.set_value(0.0)
tip_dia = self.app.defaults["tools_calc_vshape_tip_dia"]
tip_angle = self.app.defaults["tools_calc_vshape_tip_angle"]
cut_z = self.app.defaults["tools_calc_vshape_cut_z"]
self.tipDia_entry.set_value(tip_dia)
self.tipAngle_entry.set_value(tip_angle)
self.cutDepth_entry.set_value(cut_z)
self.effectiveToolDia_entry.set_value('0.0000')
def on_calculate_tool_dia(self):
# Calculation:
# Manufacturer gives total angle of the the tip but we need only half of it
# tangent(half_tip_angle) = opposite side / adjacent = part_of _real_dia / depth_of_cut
# effective_diameter = tip_diameter + part_of_real_dia_left_side + part_of_real_dia_right_side
# tool is symmetrical therefore: part_of_real_dia_left_side = part_of_real_dia_right_side
# effective_diameter = tip_diameter + (2 * part_of_real_dia_left_side)
# effective diameter = tip_diameter + (2 * depth_of_cut * tangent(half_tip_angle))
tip_diameter = float(self.tipDia_entry.get_value())
half_tip_angle = float(self.tipAngle_entry.get_value()) / 2.0
cut_depth = float(self.cutDepth_entry.get_value())
cut_depth = -cut_depth if cut_depth < 0 else cut_depth
tool_diameter = tip_diameter + (2 * cut_depth * math.tan(math.radians(half_tip_angle)))
self.effectiveToolDia_entry.set_value("%.*f" % (self.decimals, tool_diameter))
def on_calculate_inch_units(self):
mm_val = float(self.mm_entry.get_value())
self.inch_entry.set_value('%.*f' % (self.decimals, (mm_val / 25.4)))
def on_calculate_mm_units(self):
inch_val = float(self.inch_entry.get_value())
self.mm_entry.set_value('%.*f' % (self.decimals, (inch_val * 25.4)))
def on_calculate_eplate(self):
length = float(self.pcblength_entry.get_value())
width = float(self.pcbwidth_entry.get_value())
density = float(self.cdensity_entry.get_value())
copper = float(self.growth_entry.get_value())
calculated_current = (length * width * density) * 0.0021527820833419
calculated_time = copper * 2.142857142857143 * float(20 / density)
self.cvalue_entry.set_value('%.2f' % calculated_current)
self.time_entry.set_value('%.1f' % calculated_time)
# end of file

1383
appTools/ToolCalibration.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

440
appTools/ToolCorners.py Normal file
View File

@@ -0,0 +1,440 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 5/17/2020 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets, QtCore
from appTool import AppTool
from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, FCComboBox, FCButton
from shapely.geometry import MultiPolygon, LineString
from copy import deepcopy
import logging
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class ToolCorners(AppTool):
toolName = _("Corner Markers Tool")
def __init__(self, app):
AppTool.__init__(self, app)
self.app = app
self.canvas = self.app.plotcanvas
self.decimals = self.app.decimals
self.units = ''
# ## 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(''))
# Gerber object #
self.object_label = QtWidgets.QLabel('<b>%s:</b>' % _("GERBER"))
self.object_label.setToolTip(
_("The Gerber object to which will be added corner markers.")
)
self.object_combo = FCComboBox()
self.object_combo.setModel(self.app.collection)
self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.object_combo.is_last = True
self.object_combo.obj_type = "Gerber"
self.layout.addWidget(self.object_label)
self.layout.addWidget(self.object_combo)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
self.layout.addWidget(separator_line)
self.points_label = QtWidgets.QLabel('<b>%s:</b>' % _('Locations'))
self.points_label.setToolTip(
_("Locations where to place corner markers.")
)
self.layout.addWidget(self.points_label)
# BOTTOM LEFT
self.bl_cb = FCCheckBox(_("Bottom Left"))
self.layout.addWidget(self.bl_cb)
# BOTTOM RIGHT
self.br_cb = FCCheckBox(_("Bottom Right"))
self.layout.addWidget(self.br_cb)
# TOP LEFT
self.tl_cb = FCCheckBox(_("Top Left"))
self.layout.addWidget(self.tl_cb)
# TOP RIGHT
self.tr_cb = FCCheckBox(_("Top Right"))
self.layout.addWidget(self.tr_cb)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
self.layout.addWidget(separator_line)
# Toggle ALL
self.toggle_all_cb = FCCheckBox(_("Toggle ALL"))
self.layout.addWidget(self.toggle_all_cb)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
self.layout.addWidget(separator_line)
# ## Grid Layout
grid_lay = QtWidgets.QGridLayout()
self.layout.addLayout(grid_lay)
grid_lay.setColumnStretch(0, 0)
grid_lay.setColumnStretch(1, 1)
self.param_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
self.param_label.setToolTip(
_("Parameters used for this tool.")
)
grid_lay.addWidget(self.param_label, 0, 0, 1, 2)
# Thickness #
self.thick_label = QtWidgets.QLabel('%s:' % _("Thickness"))
self.thick_label.setToolTip(
_("The thickness of the line that makes the corner marker.")
)
self.thick_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.thick_entry.set_range(0.0000, 9.9999)
self.thick_entry.set_precision(self.decimals)
self.thick_entry.setWrapping(True)
self.thick_entry.setSingleStep(10 ** -self.decimals)
grid_lay.addWidget(self.thick_label, 1, 0)
grid_lay.addWidget(self.thick_entry, 1, 1)
# Length #
self.l_label = QtWidgets.QLabel('%s:' % _("Length"))
self.l_label.setToolTip(
_("The length of the line that makes the corner marker.")
)
self.l_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.l_entry.set_range(-9999.9999, 9999.9999)
self.l_entry.set_precision(self.decimals)
self.l_entry.setSingleStep(10 ** -self.decimals)
grid_lay.addWidget(self.l_label, 2, 0)
grid_lay.addWidget(self.l_entry, 2, 1)
# Margin #
self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
self.margin_label.setToolTip(
_("Bounding box margin.")
)
self.margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.margin_entry.set_range(-9999.9999, 9999.9999)
self.margin_entry.set_precision(self.decimals)
self.margin_entry.setSingleStep(0.1)
grid_lay.addWidget(self.margin_label, 3, 0)
grid_lay.addWidget(self.margin_entry, 3, 1)
separator_line_2 = QtWidgets.QFrame()
separator_line_2.setFrameShape(QtWidgets.QFrame.HLine)
separator_line_2.setFrameShadow(QtWidgets.QFrame.Sunken)
grid_lay.addWidget(separator_line_2, 4, 0, 1, 2)
# ## Insert Corner Marker
self.add_marker_button = FCButton(_("Add Marker"))
self.add_marker_button.setToolTip(
_("Will add corner markers to the selected Gerber file.")
)
self.add_marker_button.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
grid_lay.addWidget(self.add_marker_button, 11, 0, 1, 2)
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)
# Objects involved in Copper thieving
self.grb_object = None
# store the flattened geometry here:
self.flat_geometry = []
# Tool properties
self.fid_dia = None
self.grb_steps_per_circle = self.app.defaults["gerber_circle_steps"]
# SIGNALS
self.add_marker_button.clicked.connect(self.add_markers)
self.toggle_all_cb.toggled.connect(self.on_toggle_all)
def run(self, toggle=True):
self.app.defaults.report_usage("ToolCorners()")
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])
AppTool.run(self)
self.set_tool_ui()
self.app.ui.notebook.setTabText(2, _("Corners Tool"))
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='Alt+M', **kwargs)
def set_tool_ui(self):
self.units = self.app.defaults['units']
self.thick_entry.set_value(self.app.defaults["tools_corners_thickness"])
self.l_entry.set_value(float(self.app.defaults["tools_corners_length"]))
self.margin_entry.set_value(float(self.app.defaults["tools_corners_margin"]))
self.toggle_all_cb.set_value(False)
def on_toggle_all(self, val):
self.bl_cb.set_value(val)
self.br_cb.set_value(val)
self.tl_cb.set_value(val)
self.tr_cb.set_value(val)
def add_markers(self):
self.app.call_source = "corners_tool"
tl_state = self.tl_cb.get_value()
tr_state = self.tr_cb.get_value()
bl_state = self.bl_cb.get_value()
br_state = self.br_cb.get_value()
# get the Gerber object on which the corner marker will be inserted
selection_index = self.object_combo.currentIndex()
model_index = self.app.collection.index(selection_index, 0, self.object_combo.rootModelIndex())
try:
self.grb_object = model_index.internalPointer().obj
except Exception as e:
log.debug("ToolCorners.add_markers() --> %s" % str(e))
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
return
xmin, ymin, xmax, ymax = self.grb_object.bounds()
points = {}
if tl_state:
points['tl'] = (xmin, ymax)
if tr_state:
points['tr'] = (xmax, ymax)
if bl_state:
points['bl'] = (xmin, ymin)
if br_state:
points['br'] = (xmax, ymin)
self.add_corners_geo(points, g_obj=self.grb_object)
self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'],
filename=None,
local_use=self.grb_object, use_thread=False)
self.on_exit()
def add_corners_geo(self, points_storage, g_obj):
"""
Add geometry to the solid_geometry of the copper Gerber object
:param points_storage: a dictionary holding the points where to add corners
:param g_obj: the Gerber object where to add the geometry
:return: None
"""
line_thickness = self.thick_entry.get_value()
line_length = self.l_entry.get_value()
margin = self.margin_entry.get_value()
geo_list = []
if not points_storage:
self.app.inform.emit("[ERROR_NOTCL] %s." % _("Please select at least a location"))
return
for key in points_storage:
if key == 'tl':
pt = points_storage[key]
x = pt[0] - margin - line_thickness / 2.0
y = pt[1] + margin + line_thickness / 2.0
line_geo_hor = LineString([
(x, y), (x + line_length, y)
])
line_geo_vert = LineString([
(x, y), (x, y - line_length)
])
geo_list.append(line_geo_hor)
geo_list.append(line_geo_vert)
if key == 'tr':
pt = points_storage[key]
x = pt[0] + margin + line_thickness / 2.0
y = pt[1] + margin + line_thickness / 2.0
line_geo_hor = LineString([
(x, y), (x - line_length, y)
])
line_geo_vert = LineString([
(x, y), (x, y - line_length)
])
geo_list.append(line_geo_hor)
geo_list.append(line_geo_vert)
if key == 'bl':
pt = points_storage[key]
x = pt[0] - margin - line_thickness / 2.0
y = pt[1] - margin - line_thickness / 2.0
line_geo_hor = LineString([
(x, y), (x + line_length, y)
])
line_geo_vert = LineString([
(x, y), (x, y + line_length)
])
geo_list.append(line_geo_hor)
geo_list.append(line_geo_vert)
if key == 'br':
pt = points_storage[key]
x = pt[0] + margin + line_thickness / 2.0
y = pt[1] - margin - line_thickness / 2.0
line_geo_hor = LineString([
(x, y), (x - line_length, y)
])
line_geo_vert = LineString([
(x, y), (x, y + line_length)
])
geo_list.append(line_geo_hor)
geo_list.append(line_geo_vert)
aperture_found = None
for ap_id, ap_val in g_obj.apertures.items():
if ap_val['type'] == 'C' and ap_val['size'] == line_thickness:
aperture_found = ap_id
break
geo_buff_list = []
if aperture_found:
for geo in geo_list:
geo_buff = geo.buffer(line_thickness / 2.0, resolution=self.grb_steps_per_circle, join_style=2)
geo_buff_list.append(geo_buff)
dict_el = {}
dict_el['follow'] = geo
dict_el['solid'] = geo_buff
g_obj.apertures[aperture_found]['geometry'].append(deepcopy(dict_el))
else:
ap_keys = list(g_obj.apertures.keys())
if ap_keys:
new_apid = str(int(max(ap_keys)) + 1)
else:
new_apid = '10'
g_obj.apertures[new_apid] = {}
g_obj.apertures[new_apid]['type'] = 'C'
g_obj.apertures[new_apid]['size'] = line_thickness
g_obj.apertures[new_apid]['geometry'] = []
for geo in geo_list:
geo_buff = geo.buffer(line_thickness / 2.0, resolution=self.grb_steps_per_circle, join_style=3)
geo_buff_list.append(geo_buff)
dict_el = {}
dict_el['follow'] = geo
dict_el['solid'] = geo_buff
g_obj.apertures[new_apid]['geometry'].append(deepcopy(dict_el))
s_list = []
if g_obj.solid_geometry:
try:
for poly in g_obj.solid_geometry:
s_list.append(poly)
except TypeError:
s_list.append(g_obj.solid_geometry)
geo_buff_list = MultiPolygon(geo_buff_list)
geo_buff_list = geo_buff_list.buffer(0)
for poly in geo_buff_list:
s_list.append(poly)
g_obj.solid_geometry = MultiPolygon(s_list)
def replot(self, obj, run_thread=True):
def worker_task():
with self.app.proc_container.new('%s...' % _("Plotting")):
obj.plot()
if run_thread:
self.app.worker_task.emit({'fcn': worker_task, 'params': []})
else:
worker_task()
def on_exit(self):
# plot the object
try:
self.replot(obj=self.grb_object)
except (AttributeError, TypeError):
return
# update the bounding box values
try:
a, b, c, d = self.grb_object.bounds()
self.grb_object.options['xmin'] = a
self.grb_object.options['ymin'] = b
self.grb_object.options['xmax'] = c
self.grb_object.options['ymax'] = d
except Exception as e:
log.debug("ToolCorners.on_exit() copper_obj bounds error --> %s" % str(e))
# reset the variables
self.grb_object = None
self.app.call_source = "app"
self.app.inform.emit('[success] %s' % _("Corners Tool exit."))

1433
appTools/ToolCutOut.py Normal file

File diff suppressed because it is too large Load Diff

901
appTools/ToolDblSided.py Normal file
View File

@@ -0,0 +1,901 @@
from PyQt5 import QtWidgets, QtCore
from appTool import AppTool
from appGUI.GUIElements import RadioSet, FCDoubleSpinner, EvalEntry, FCEntry, FCButton, FCComboBox
from numpy import Inf
from shapely.geometry import Point
from shapely import affinity
import logging
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class DblSidedTool(AppTool):
toolName = _("2-Sided PCB")
def __init__(self, app):
AppTool.__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)
self.layout.addWidget(QtWidgets.QLabel(""))
# ## Grid Layout
grid_lay = QtWidgets.QGridLayout()
grid_lay.setColumnStretch(0, 1)
grid_lay.setColumnStretch(1, 0)
self.layout.addLayout(grid_lay)
# Objects to be mirrored
self.m_objects_label = QtWidgets.QLabel("<b>%s:</b>" % _("Mirror Operation"))
self.m_objects_label.setToolTip('%s.' % _("Objects to be mirrored"))
grid_lay.addWidget(self.m_objects_label, 0, 0, 1, 2)
# ## Gerber Object to mirror
self.gerber_object_combo = FCComboBox()
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.is_last = True
self.gerber_object_combo.obj_type = "Gerber"
self.botlay_label = QtWidgets.QLabel("%s:" % _("GERBER"))
self.botlay_label.setToolTip('%s.' % _("Gerber to be mirrored"))
self.mirror_gerber_button = QtWidgets.QPushButton(_("Mirror"))
self.mirror_gerber_button.setToolTip(
_("Mirrors (flips) the specified object around \n"
"the specified axis. Does not create a new \n"
"object, but modifies it.")
)
self.mirror_gerber_button.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
self.mirror_gerber_button.setMinimumWidth(60)
grid_lay.addWidget(self.botlay_label, 1, 0)
grid_lay.addWidget(self.gerber_object_combo, 2, 0)
grid_lay.addWidget(self.mirror_gerber_button, 2, 1)
# ## Excellon Object to mirror
self.exc_object_combo = FCComboBox()
self.exc_object_combo.setModel(self.app.collection)
self.exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
self.exc_object_combo.is_last = True
self.exc_object_combo.obj_type = "Excellon"
self.excobj_label = QtWidgets.QLabel("%s:" % _("EXCELLON"))
self.excobj_label.setToolTip(_("Excellon Object to be mirrored."))
self.mirror_exc_button = QtWidgets.QPushButton(_("Mirror"))
self.mirror_exc_button.setToolTip(
_("Mirrors (flips) the specified object around \n"
"the specified axis. Does not create a new \n"
"object, but modifies it.")
)
self.mirror_exc_button.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
self.mirror_exc_button.setMinimumWidth(60)
grid_lay.addWidget(self.excobj_label, 3, 0)
grid_lay.addWidget(self.exc_object_combo, 4, 0)
grid_lay.addWidget(self.mirror_exc_button, 4, 1)
# ## Geometry Object to mirror
self.geo_object_combo = FCComboBox()
self.geo_object_combo.setModel(self.app.collection)
self.geo_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
self.geo_object_combo.is_last = True
self.geo_object_combo.obj_type = "Geometry"
self.geoobj_label = QtWidgets.QLabel("%s:" % _("GEOMETRY"))
self.geoobj_label.setToolTip(
_("Geometry Obj to be mirrored.")
)
self.mirror_geo_button = QtWidgets.QPushButton(_("Mirror"))
self.mirror_geo_button.setToolTip(
_("Mirrors (flips) the specified object around \n"
"the specified axis. Does not create a new \n"
"object, but modifies it.")
)
self.mirror_geo_button.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
self.mirror_geo_button.setMinimumWidth(60)
# grid_lay.addRow("Bottom Layer:", self.object_combo)
grid_lay.addWidget(self.geoobj_label, 5, 0)
grid_lay.addWidget(self.geo_object_combo, 6, 0)
grid_lay.addWidget(self.mirror_geo_button, 6, 1)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid_lay.addWidget(separator_line, 7, 0, 1, 2)
self.layout.addWidget(QtWidgets.QLabel(""))
# ## Grid Layout
grid_lay1 = QtWidgets.QGridLayout()
grid_lay1.setColumnStretch(0, 0)
grid_lay1.setColumnStretch(1, 1)
self.layout.addLayout(grid_lay1)
# Objects to be mirrored
self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Mirror Parameters"))
self.param_label.setToolTip('%s.' % _("Parameters for the mirror operation"))
grid_lay1.addWidget(self.param_label, 0, 0, 1, 2)
# ## Axis
self.mirax_label = QtWidgets.QLabel('%s:' % _("Mirror Axis"))
self.mirax_label.setToolTip(_("Mirror vertically (X) or horizontally (Y)."))
self.mirror_axis = RadioSet([{'label': 'X', 'value': 'X'},
{'label': 'Y', 'value': 'Y'}])
grid_lay1.addWidget(self.mirax_label, 2, 0)
grid_lay1.addWidget(self.mirror_axis, 2, 1, 1, 2)
# ## Axis Location
self.axloc_label = QtWidgets.QLabel('%s:' % _("Reference"))
self.axloc_label.setToolTip(
_("The coordinates used as reference for the mirror operation.\n"
"Can be:\n"
"- Point -> a set of coordinates (x,y) around which the object is mirrored\n"
"- Box -> a set of coordinates (x, y) obtained from the center of the\n"
"bounding box of another object selected below")
)
self.axis_location = RadioSet([{'label': _('Point'), 'value': 'point'},
{'label': _('Box'), 'value': 'box'}])
grid_lay1.addWidget(self.axloc_label, 4, 0)
grid_lay1.addWidget(self.axis_location, 4, 1, 1, 2)
# ## Point/Box
self.point_entry = EvalEntry()
self.point_entry.setPlaceholderText(_("Point coordinates"))
# Add a reference
self.add_point_button = QtWidgets.QPushButton(_("Add"))
self.add_point_button.setToolTip(
_("Add the coordinates in format <b>(x, y)</b> through which the mirroring axis\n "
"selected in 'MIRROR AXIS' pass.\n"
"The (x, y) coordinates are captured by pressing SHIFT key\n"
"and left mouse button click on canvas or you can enter the coordinates manually.")
)
self.add_point_button.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
self.add_point_button.setMinimumWidth(60)
grid_lay1.addWidget(self.point_entry, 7, 0, 1, 2)
grid_lay1.addWidget(self.add_point_button, 7, 2)
# ## Grid Layout
grid_lay2 = QtWidgets.QGridLayout()
grid_lay2.setColumnStretch(0, 0)
grid_lay2.setColumnStretch(1, 1)
self.layout.addLayout(grid_lay2)
self.box_type_label = QtWidgets.QLabel('%s:' % _("Reference Object"))
self.box_type_label.setToolTip(
_("It can be of type: Gerber or Excellon or Geometry.\n"
"The coordinates of the center of the bounding box are used\n"
"as reference for mirror operation.")
)
# Type of object used as BOX reference
self.box_type_radio = RadioSet([{'label': _('Gerber'), 'value': 'grb'},
{'label': _('Excellon'), 'value': 'exc'},
{'label': _('Geometry'), 'value': 'geo'}])
self.box_type_label.hide()
self.box_type_radio.hide()
grid_lay2.addWidget(self.box_type_label, 0, 0, 1, 2)
grid_lay2.addWidget(self.box_type_radio, 1, 0, 1, 2)
# Object used as BOX reference
self.box_combo = FCComboBox()
self.box_combo.setModel(self.app.collection)
self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.box_combo.is_last = True
self.box_combo.hide()
grid_lay2.addWidget(self.box_combo, 3, 0, 1, 2)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid_lay2.addWidget(separator_line, 4, 0, 1, 2)
grid_lay2.addWidget(QtWidgets.QLabel(""), 5, 0, 1, 2)
# ## Title Bounds Values
self.bv_label = QtWidgets.QLabel("<b>%s:</b>" % _('Bounds Values'))
self.bv_label.setToolTip(
_("Select on canvas the object(s)\n"
"for which to calculate bounds values.")
)
grid_lay2.addWidget(self.bv_label, 6, 0, 1, 2)
# Xmin value
self.xmin_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.xmin_entry.set_precision(self.decimals)
self.xmin_entry.set_range(-9999.9999, 9999.9999)
self.xmin_btn = FCButton('%s:' % _("X min"))
self.xmin_btn.setToolTip(
_("Minimum location.")
)
self.xmin_entry.setReadOnly(True)
grid_lay2.addWidget(self.xmin_btn, 7, 0)
grid_lay2.addWidget(self.xmin_entry, 7, 1)
# Ymin value
self.ymin_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.ymin_entry.set_precision(self.decimals)
self.ymin_entry.set_range(-9999.9999, 9999.9999)
self.ymin_btn = FCButton('%s:' % _("Y min"))
self.ymin_btn.setToolTip(
_("Minimum location.")
)
self.ymin_entry.setReadOnly(True)
grid_lay2.addWidget(self.ymin_btn, 8, 0)
grid_lay2.addWidget(self.ymin_entry, 8, 1)
# Xmax value
self.xmax_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.xmax_entry.set_precision(self.decimals)
self.xmax_entry.set_range(-9999.9999, 9999.9999)
self.xmax_btn = FCButton('%s:' % _("X max"))
self.xmax_btn.setToolTip(
_("Maximum location.")
)
self.xmax_entry.setReadOnly(True)
grid_lay2.addWidget(self.xmax_btn, 9, 0)
grid_lay2.addWidget(self.xmax_entry, 9, 1)
# Ymax value
self.ymax_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.ymax_entry.set_precision(self.decimals)
self.ymax_entry.set_range(-9999.9999, 9999.9999)
self.ymax_btn = FCButton('%s:' % _("Y max"))
self.ymax_btn.setToolTip(
_("Maximum location.")
)
self.ymax_entry.setReadOnly(True)
grid_lay2.addWidget(self.ymax_btn, 10, 0)
grid_lay2.addWidget(self.ymax_entry, 10, 1)
# Center point value
self.center_entry = FCEntry()
self.center_entry.setPlaceholderText(_("Center point coordinates"))
self.center_btn = FCButton('%s:' % _("Centroid"))
self.center_btn.setToolTip(
_("The center point location for the rectangular\n"
"bounding shape. Centroid. Format is (x, y).")
)
self.center_entry.setReadOnly(True)
grid_lay2.addWidget(self.center_btn, 12, 0)
grid_lay2.addWidget(self.center_entry, 12, 1)
# Calculate Bounding box
self.calculate_bb_button = QtWidgets.QPushButton(_("Calculate Bounds Values"))
self.calculate_bb_button.setToolTip(
_("Calculate the enveloping rectangular shape coordinates,\n"
"for the selection of objects.\n"
"The envelope shape is parallel with the X, Y axis.")
)
self.calculate_bb_button.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
grid_lay2.addWidget(self.calculate_bb_button, 13, 0, 1, 2)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid_lay2.addWidget(separator_line, 14, 0, 1, 2)
grid_lay2.addWidget(QtWidgets.QLabel(""), 15, 0, 1, 2)
# ## Alignment holes
self.alignment_label = QtWidgets.QLabel("<b>%s:</b>" % _('PCB Alignment'))
self.alignment_label.setToolTip(
_("Creates an Excellon Object containing the\n"
"specified alignment holes and their mirror\n"
"images.")
)
grid_lay2.addWidget(self.alignment_label, 25, 0, 1, 2)
# ## Drill diameter for alignment holes
self.dt_label = QtWidgets.QLabel("%s:" % _('Drill Diameter'))
self.dt_label.setToolTip(
_("Diameter of the drill for the alignment holes.")
)
self.drill_dia = FCDoubleSpinner(callback=self.confirmation_message)
self.drill_dia.setToolTip(
_("Diameter of the drill for the alignment holes.")
)
self.drill_dia.set_precision(self.decimals)
self.drill_dia.set_range(0.0000, 9999.9999)
grid_lay2.addWidget(self.dt_label, 26, 0)
grid_lay2.addWidget(self.drill_dia, 26, 1)
# ## Alignment Axis
self.align_ax_label = QtWidgets.QLabel('%s:' % _("Align Axis"))
self.align_ax_label.setToolTip(
_("Mirror vertically (X) or horizontally (Y).")
)
self.align_axis_radio = RadioSet([{'label': 'X', 'value': 'X'},
{'label': 'Y', 'value': 'Y'}])
grid_lay2.addWidget(self.align_ax_label, 27, 0)
grid_lay2.addWidget(self.align_axis_radio, 27, 1)
# ## Alignment Reference Point
self.align_ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
self.align_ref_label.setToolTip(
_("The reference point used to create the second alignment drill\n"
"from the first alignment drill, by doing mirror.\n"
"It can be modified in the Mirror Parameters -> Reference section")
)
self.align_ref_label_val = EvalEntry()
self.align_ref_label_val.setToolTip(
_("The reference point used to create the second alignment drill\n"
"from the first alignment drill, by doing mirror.\n"
"It can be modified in the Mirror Parameters -> Reference section")
)
self.align_ref_label_val.setDisabled(True)
grid_lay2.addWidget(self.align_ref_label, 28, 0)
grid_lay2.addWidget(self.align_ref_label_val, 28, 1)
grid_lay4 = QtWidgets.QGridLayout()
self.layout.addLayout(grid_lay4)
# ## Alignment holes
self.ah_label = QtWidgets.QLabel("%s:" % _('Alignment Drill Coordinates'))
self.ah_label.setToolTip(
_("Alignment holes (x1, y1), (x2, y2), ... "
"on one side of the mirror axis. For each set of (x, y) coordinates\n"
"entered here, a pair of drills will be created:\n\n"
"- one drill at the coordinates from the field\n"
"- one drill in mirror position over the axis selected above in the 'Align Axis'.")
)
self.alignment_holes = EvalEntry()
self.alignment_holes.setPlaceholderText(_("Drill coordinates"))
grid_lay4.addWidget(self.ah_label, 0, 0, 1, 2)
grid_lay4.addWidget(self.alignment_holes, 1, 0, 1, 2)
self.add_drill_point_button = FCButton(_("Add"))
self.add_drill_point_button.setToolTip(
_("Add alignment drill holes coordinates in the format: (x1, y1), (x2, y2), ... \n"
"on one side of the alignment axis.\n\n"
"The coordinates set can be obtained:\n"
"- press SHIFT key and left mouse clicking on canvas. Then click Add.\n"
"- press SHIFT key and left mouse clicking on canvas. Then Ctrl+V in the field.\n"
"- press SHIFT key and left mouse clicking on canvas. Then RMB click in the field and click Paste.\n"
"- by entering the coords manually in the format: (x1, y1), (x2, y2), ...")
)
# self.add_drill_point_button.setStyleSheet("""
# QPushButton
# {
# font-weight: bold;
# }
# """)
self.delete_drill_point_button = FCButton(_("Delete Last"))
self.delete_drill_point_button.setToolTip(
_("Delete the last coordinates tuple in the list.")
)
drill_hlay = QtWidgets.QHBoxLayout()
drill_hlay.addWidget(self.add_drill_point_button)
drill_hlay.addWidget(self.delete_drill_point_button)
grid_lay4.addLayout(drill_hlay, 2, 0, 1, 2)
# ## Buttons
self.create_alignment_hole_button = QtWidgets.QPushButton(_("Create Excellon Object"))
self.create_alignment_hole_button.setToolTip(
_("Creates an Excellon Object containing the\n"
"specified alignment holes and their mirror\n"
"images.")
)
self.create_alignment_hole_button.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
self.layout.addWidget(self.create_alignment_hole_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)
# ## Signals
self.mirror_gerber_button.clicked.connect(self.on_mirror_gerber)
self.mirror_exc_button.clicked.connect(self.on_mirror_exc)
self.mirror_geo_button.clicked.connect(self.on_mirror_geo)
self.add_point_button.clicked.connect(self.on_point_add)
self.add_drill_point_button.clicked.connect(self.on_drill_add)
self.delete_drill_point_button.clicked.connect(self.on_drill_delete_last)
self.box_type_radio.activated_custom.connect(self.on_combo_box_type)
self.axis_location.group_toggle_fn = self.on_toggle_pointbox
self.point_entry.textChanged.connect(lambda val: self.align_ref_label_val.set_value(val))
self.xmin_btn.clicked.connect(self.on_xmin_clicked)
self.ymin_btn.clicked.connect(self.on_ymin_clicked)
self.xmax_btn.clicked.connect(self.on_xmax_clicked)
self.ymax_btn.clicked.connect(self.on_ymax_clicked)
self.center_btn.clicked.connect(
lambda: self.point_entry.set_value(self.center_entry.get_value())
)
self.create_alignment_hole_button.clicked.connect(self.on_create_alignment_holes)
self.calculate_bb_button.clicked.connect(self.on_bbox_coordinates)
self.reset_button.clicked.connect(self.set_tool_ui)
self.drill_values = ""
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='Alt+D', **kwargs)
def run(self, toggle=True):
self.app.defaults.report_usage("Tool2Sided()")
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])
AppTool.run(self)
self.set_tool_ui()
self.app.ui.notebook.setTabText(2, _("2-Sided Tool"))
def set_tool_ui(self):
self.reset_fields()
self.point_entry.set_value("")
self.alignment_holes.set_value("")
self.mirror_axis.set_value(self.app.defaults["tools_2sided_mirror_axis"])
self.axis_location.set_value(self.app.defaults["tools_2sided_axis_loc"])
self.drill_dia.set_value(self.app.defaults["tools_2sided_drilldia"])
self.align_axis_radio.set_value(self.app.defaults["tools_2sided_allign_axis"])
self.xmin_entry.set_value(0.0)
self.ymin_entry.set_value(0.0)
self.xmax_entry.set_value(0.0)
self.ymax_entry.set_value(0.0)
self.center_entry.set_value('')
self.align_ref_label_val.set_value('%.*f' % (self.decimals, 0.0))
# run once to make sure that the obj_type attribute is updated in the FCComboBox
self.box_type_radio.set_value('grb')
self.on_combo_box_type('grb')
def on_combo_box_type(self, val):
obj_type = {'grb': 0, 'exc': 1, 'geo': 2}[val]
self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
self.box_combo.setCurrentIndex(0)
self.box_combo.obj_type = {
"grb": "Gerber", "exc": "Excellon", "geo": "Geometry"}[val]
def on_create_alignment_holes(self):
axis = self.align_axis_radio.get_value()
mode = self.axis_location.get_value()
if mode == "point":
try:
px, py = self.point_entry.get_value()
except TypeError:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("'Point' reference is selected and 'Point' coordinates "
"are missing. Add them and retry."))
return
else:
selection_index = self.box_combo.currentIndex()
model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
try:
bb_obj = model_index.internalPointer().obj
except AttributeError:
model_index = self.app.collection.index(selection_index, 0, self.exc_object_combo.rootModelIndex())
try:
bb_obj = model_index.internalPointer().obj
except AttributeError:
model_index = self.app.collection.index(selection_index, 0,
self.geo_object_combo.rootModelIndex())
try:
bb_obj = model_index.internalPointer().obj
except AttributeError:
self.app.inform.emit(
'[WARNING_NOTCL] %s' % _("There is no Box reference object loaded. Load one and retry."))
return
xmin, ymin, xmax, ymax = bb_obj.bounds()
px = 0.5 * (xmin + xmax)
py = 0.5 * (ymin + ymax)
xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
dia = float(self.drill_dia.get_value())
if dia == '':
self.app.inform.emit('[WARNING_NOTCL] %s' %
_("No value or wrong format in Drill Dia entry. Add it and retry."))
return
tools = {}
tools["1"] = {}
tools["1"]["C"] = dia
tools["1"]['solid_geometry'] = []
# holes = self.alignment_holes.get_value()
holes = eval('[{}]'.format(self.alignment_holes.text()))
if not holes:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There are no Alignment Drill Coordinates to use. "
"Add them and retry."))
return
drills = []
for hole in holes:
point = Point(hole)
point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
drills.append({"point": point, "tool": "1"})
drills.append({"point": point_mirror, "tool": "1"})
tools["1"]['solid_geometry'].append(point)
tools["1"]['solid_geometry'].append(point_mirror)
def obj_init(obj_inst, app_inst):
obj_inst.tools = tools
obj_inst.drills = drills
obj_inst.create_geometry()
obj_inst.source_file = app_inst.export_excellon(obj_name=obj_inst.options['name'], local_use=obj_inst,
filename=None, use_thread=False)
self.app.app_obj.new_object("excellon", "Alignment Drills", obj_init)
self.drill_values = ''
self.app.inform.emit('[success] %s' % _("Excellon object with alignment drills created..."))
def on_mirror_gerber(self):
selection_index = self.gerber_object_combo.currentIndex()
# fcobj = self.app.collection.object_list[selection_index]
model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
try:
fcobj = model_index.internalPointer().obj
except Exception:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
return
if fcobj.kind != 'gerber':
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber, Excellon and Geometry objects can be mirrored."))
return
axis = self.mirror_axis.get_value()
mode = self.axis_location.get_value()
if mode == "point":
try:
px, py = self.point_entry.get_value()
except TypeError:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There are no Point coordinates in the Point field. "
"Add coords and try again ..."))
return
else:
selection_index_box = self.box_combo.currentIndex()
model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex())
try:
bb_obj = model_index_box.internalPointer().obj
except Exception:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Box object loaded ..."))
return
xmin, ymin, xmax, ymax = bb_obj.bounds()
px = 0.5 * (xmin + xmax)
py = 0.5 * (ymin + ymax)
fcobj.mirror(axis, [px, py])
self.app.app_obj.object_changed.emit(fcobj)
fcobj.plot()
self.app.inform.emit('[success] Gerber %s %s...' % (str(fcobj.options['name']), _("was mirrored")))
def on_mirror_exc(self):
selection_index = self.exc_object_combo.currentIndex()
# fcobj = self.app.collection.object_list[selection_index]
model_index = self.app.collection.index(selection_index, 0, self.exc_object_combo.rootModelIndex())
try:
fcobj = model_index.internalPointer().obj
except Exception:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Excellon object loaded ..."))
return
if fcobj.kind != 'excellon':
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber, Excellon and Geometry objects can be mirrored."))
return
axis = self.mirror_axis.get_value()
mode = self.axis_location.get_value()
if mode == "point":
try:
px, py = self.point_entry.get_value()
except Exception as e:
log.debug("DblSidedTool.on_mirror_geo() --> %s" % str(e))
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There are no Point coordinates in the Point field. "
"Add coords and try again ..."))
return
else:
selection_index_box = self.box_combo.currentIndex()
model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex())
try:
bb_obj = model_index_box.internalPointer().obj
except Exception as e:
log.debug("DblSidedTool.on_mirror_geo() --> %s" % str(e))
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Box object loaded ..."))
return
xmin, ymin, xmax, ymax = bb_obj.bounds()
px = 0.5 * (xmin + xmax)
py = 0.5 * (ymin + ymax)
fcobj.mirror(axis, [px, py])
self.app.app_obj.object_changed.emit(fcobj)
fcobj.plot()
self.app.inform.emit('[success] Excellon %s %s...' % (str(fcobj.options['name']), _("was mirrored")))
def on_mirror_geo(self):
selection_index = self.geo_object_combo.currentIndex()
# fcobj = self.app.collection.object_list[selection_index]
model_index = self.app.collection.index(selection_index, 0, self.geo_object_combo.rootModelIndex())
try:
fcobj = model_index.internalPointer().obj
except Exception:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Geometry object loaded ..."))
return
if fcobj.kind != 'geometry':
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber, Excellon and Geometry objects can be mirrored."))
return
axis = self.mirror_axis.get_value()
mode = self.axis_location.get_value()
if mode == "point":
px, py = self.point_entry.get_value()
else:
selection_index_box = self.box_combo.currentIndex()
model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex())
try:
bb_obj = model_index_box.internalPointer().obj
except Exception:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Box object loaded ..."))
return
xmin, ymin, xmax, ymax = bb_obj.bounds()
px = 0.5 * (xmin + xmax)
py = 0.5 * (ymin + ymax)
fcobj.mirror(axis, [px, py])
self.app.app_obj.object_changed.emit(fcobj)
fcobj.plot()
self.app.inform.emit('[success] Geometry %s %s...' % (str(fcobj.options['name']), _("was mirrored")))
def on_point_add(self):
val = self.app.defaults["global_point_clipboard_format"] % \
(self.decimals, self.app.pos[0], self.decimals, self.app.pos[1])
self.point_entry.set_value(val)
def on_drill_add(self):
self.drill_values += (self.app.defaults["global_point_clipboard_format"] %
(self.decimals, self.app.pos[0], self.decimals, self.app.pos[1])) + ','
self.alignment_holes.set_value(self.drill_values)
def on_drill_delete_last(self):
drill_values_without_last_tupple = self.drill_values.rpartition('(')[0]
self.drill_values = drill_values_without_last_tupple
self.alignment_holes.set_value(self.drill_values)
def on_toggle_pointbox(self):
if self.axis_location.get_value() == "point":
self.point_entry.show()
self.add_point_button.show()
self.box_type_label.hide()
self.box_type_radio.hide()
self.box_combo.hide()
self.align_ref_label_val.set_value(self.point_entry.get_value())
else:
self.point_entry.hide()
self.add_point_button.hide()
self.box_type_label.show()
self.box_type_radio.show()
self.box_combo.show()
self.align_ref_label_val.set_value("Box centroid")
def on_bbox_coordinates(self):
xmin = Inf
ymin = Inf
xmax = -Inf
ymax = -Inf
obj_list = self.app.collection.get_selected()
if not obj_list:
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed. No object(s) selected..."))
return
for obj in obj_list:
try:
gxmin, gymin, gxmax, gymax = obj.bounds()
xmin = min([xmin, gxmin])
ymin = min([ymin, gymin])
xmax = max([xmax, gxmax])
ymax = max([ymax, gymax])
except Exception as e:
log.warning("DEV WARNING: Tried to get bounds of empty geometry in DblSidedTool. %s" % str(e))
self.xmin_entry.set_value(xmin)
self.ymin_entry.set_value(ymin)
self.xmax_entry.set_value(xmax)
self.ymax_entry.set_value(ymax)
cx = '%.*f' % (self.decimals, (((xmax - xmin) / 2.0) + xmin))
cy = '%.*f' % (self.decimals, (((ymax - ymin) / 2.0) + ymin))
val_txt = '(%s, %s)' % (cx, cy)
self.center_entry.set_value(val_txt)
self.axis_location.set_value('point')
self.point_entry.set_value(val_txt)
self.app.delete_selection_shape()
def on_xmin_clicked(self):
xmin = self.xmin_entry.get_value()
self.axis_location.set_value('point')
try:
px, py = self.point_entry.get_value()
val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmin, self.decimals, py)
except TypeError:
val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmin, self.decimals, 0.0)
self.point_entry.set_value(val)
def on_ymin_clicked(self):
ymin = self.ymin_entry.get_value()
self.axis_location.set_value('point')
try:
px, py = self.point_entry.get_value()
val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, px, self.decimals, ymin)
except TypeError:
val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, 0.0, self.decimals, ymin)
self.point_entry.set_value(val)
def on_xmax_clicked(self):
xmax = self.xmax_entry.get_value()
self.axis_location.set_value('point')
try:
px, py = self.point_entry.get_value()
val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmax, self.decimals, py)
except TypeError:
val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmax, self.decimals, 0.0)
self.point_entry.set_value(val)
def on_ymax_clicked(self):
ymax = self.ymax_entry.get_value()
self.axis_location.set_value('point')
try:
px, py = self.point_entry.get_value()
val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, px, self.decimals, ymax)
except TypeError:
val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, 0.0, self.decimals, ymax)
self.point_entry.set_value(val)
def reset_fields(self):
self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
self.geo_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.gerber_object_combo.setCurrentIndex(0)
self.exc_object_combo.setCurrentIndex(0)
self.geo_object_combo.setCurrentIndex(0)
self.box_combo.setCurrentIndex(0)
self.box_type_radio.set_value('grb')
self.drill_values = ""
self.align_ref_label_val.set_value('')

638
appTools/ToolDistance.py Normal file
View File

@@ -0,0 +1,638 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 3/10/2019 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets, QtCore
from appTool import AppTool
from appGUI.VisPyVisuals import *
from appGUI.GUIElements import FCEntry, FCButton, FCCheckBox
from shapely.geometry import Point, MultiLineString, Polygon
import appTranslation as fcTranslate
from camlib import FlatCAMRTreeStorage
from appEditors.FlatCAMGeoEditor import DrawToolShape
from copy import copy
import math
import logging
import gettext
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class Distance(AppTool):
toolName = _("Distance Tool")
def __init__(self, app):
AppTool.__init__(self, app)
self.app = app
self.decimals = self.app.decimals
self.canvas = self.app.plotcanvas
self.units = self.app.defaults['units'].lower()
# ## Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
self.layout.addWidget(title_label)
# ## Form Layout
grid0 = QtWidgets.QGridLayout()
grid0.setColumnStretch(0, 0)
grid0.setColumnStretch(1, 1)
self.layout.addLayout(grid0)
self.units_label = QtWidgets.QLabel('%s:' % _("Units"))
self.units_label.setToolTip(_("Those are the units in which the distance is measured."))
self.units_value = QtWidgets.QLabel("%s" % str({'mm': _("METRIC (mm)"), 'in': _("INCH (in)")}[self.units]))
self.units_value.setDisabled(True)
grid0.addWidget(self.units_label, 0, 0)
grid0.addWidget(self.units_value, 0, 1)
self.snap_center_cb = FCCheckBox(_("Snap to center"))
self.snap_center_cb.setToolTip(
_("Mouse cursor will snap to the center of the pad/drill\n"
"when it is hovering over the geometry of the pad/drill.")
)
grid0.addWidget(self.snap_center_cb, 1, 0, 1, 2)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid0.addWidget(separator_line, 2, 0, 1, 2)
self.start_label = QtWidgets.QLabel("%s:" % _('Start Coords'))
self.start_label.setToolTip(_("This is measuring Start point coordinates."))
self.start_entry = FCEntry()
self.start_entry.setReadOnly(True)
self.start_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.start_entry.setToolTip(_("This is measuring Start point coordinates."))
grid0.addWidget(self.start_label, 3, 0)
grid0.addWidget(self.start_entry, 3, 1)
self.stop_label = QtWidgets.QLabel("%s:" % _('Stop Coords'))
self.stop_label.setToolTip(_("This is the measuring Stop point coordinates."))
self.stop_entry = FCEntry()
self.stop_entry.setReadOnly(True)
self.stop_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.stop_entry.setToolTip(_("This is the measuring Stop point coordinates."))
grid0.addWidget(self.stop_label, 4, 0)
grid0.addWidget(self.stop_entry, 4, 1)
self.distance_x_label = QtWidgets.QLabel('%s:' % _("Dx"))
self.distance_x_label.setToolTip(_("This is the distance measured over the X axis."))
self.distance_x_entry = FCEntry()
self.distance_x_entry.setReadOnly(True)
self.distance_x_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.distance_x_entry.setToolTip(_("This is the distance measured over the X axis."))
grid0.addWidget(self.distance_x_label, 5, 0)
grid0.addWidget(self.distance_x_entry, 5, 1)
self.distance_y_label = QtWidgets.QLabel('%s:' % _("Dy"))
self.distance_y_label.setToolTip(_("This is the distance measured over the Y axis."))
self.distance_y_entry = FCEntry()
self.distance_y_entry.setReadOnly(True)
self.distance_y_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.distance_y_entry.setToolTip(_("This is the distance measured over the Y axis."))
grid0.addWidget(self.distance_y_label, 6, 0)
grid0.addWidget(self.distance_y_entry, 6, 1)
self.angle_label = QtWidgets.QLabel('%s:' % _("Angle"))
self.angle_label.setToolTip(_("This is orientation angle of the measuring line."))
self.angle_entry = FCEntry()
self.angle_entry.setReadOnly(True)
self.angle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.angle_entry.setToolTip(_("This is orientation angle of the measuring line."))
grid0.addWidget(self.angle_label, 7, 0)
grid0.addWidget(self.angle_entry, 7, 1)
self.total_distance_label = QtWidgets.QLabel("<b>%s:</b>" % _('DISTANCE'))
self.total_distance_label.setToolTip(_("This is the point to point Euclidian distance."))
self.total_distance_entry = FCEntry()
self.total_distance_entry.setReadOnly(True)
self.total_distance_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.total_distance_entry.setToolTip(_("This is the point to point Euclidian distance."))
grid0.addWidget(self.total_distance_label, 8, 0)
grid0.addWidget(self.total_distance_entry, 8, 1)
self.measure_btn = FCButton(_("Measure"))
# self.measure_btn.setFixedWidth(70)
self.layout.addWidget(self.measure_btn)
self.layout.addStretch()
# store here the first click and second click of the measurement process
self.points = []
self.rel_point1 = None
self.rel_point2 = None
self.active = False
self.clicked_meas = None
self.meas_line = None
self.original_call_source = 'app'
# store here the event connection ID's
self.mm = None
self.mr = None
# monitor if the tool was used
self.tool_done = False
# store the grid status here
self.grid_status_memory = False
# store here if the snap button was clicked
self.snap_toggled = None
self.mouse_is_dragging = False
# VisPy visuals
if self.app.is_legacy is False:
self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
else:
from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy
self.sel_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name='measurement')
self.measure_btn.clicked.connect(self.activate_measure_tool)
def run(self, toggle=False):
self.app.defaults.report_usage("ToolDistance()")
self.points[:] = []
self.rel_point1 = None
self.rel_point2 = None
self.tool_done = False
if self.app.tool_tab_locked is True:
return
self.app.ui.notebook.setTabText(2, _("Distance Tool"))
# if the splitter is hidden, display it
if self.app.ui.splitter.sizes()[0] == 0:
self.app.ui.splitter.setSizes([1, 1])
if toggle:
pass
if self.active is False:
self.activate_measure_tool()
else:
self.deactivate_measure_tool()
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='Ctrl+M', **kwargs)
def set_tool_ui(self):
# Remove anything else in the appGUI
self.app.ui.tool_scroll_area.takeWidget()
# Put ourselves in the appGUI
self.app.ui.tool_scroll_area.setWidget(self)
# Switch notebook to tool page
self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
self.units = self.app.defaults['units'].lower()
self.app.command_active = "Distance"
# initial view of the layout
self.start_entry.set_value('(0, 0)')
self.stop_entry.set_value('(0, 0)')
self.distance_x_entry.set_value('0.0')
self.distance_y_entry.set_value('0.0')
self.angle_entry.set_value('0.0')
self.total_distance_entry.set_value('0.0')
self.snap_center_cb.set_value(self.app.defaults['tools_dist_snap_center'])
# snap center works only for Gerber and Execellon Editor's
if self.original_call_source == 'exc_editor' or self.original_call_source == 'grb_editor':
self.snap_center_cb.show()
snap_center = self.app.defaults['tools_dist_snap_center']
self.on_snap_toggled(snap_center)
self.snap_center_cb.toggled.connect(self.on_snap_toggled)
else:
self.snap_center_cb.hide()
try:
self.snap_center_cb.toggled.disconnect(self.on_snap_toggled)
except (TypeError, AttributeError):
pass
# this is a hack; seems that triggering the grid will make the visuals better
# trigger it twice to return to the original state
self.app.ui.grid_snap_btn.trigger()
self.app.ui.grid_snap_btn.trigger()
if self.app.ui.grid_snap_btn.isChecked():
self.grid_status_memory = True
log.debug("Distance Tool --> tool initialized")
def on_snap_toggled(self, state):
self.app.defaults['tools_dist_snap_center'] = state
if state:
# disengage the grid snapping since it will be hard to find the drills or pads on grid
if self.app.ui.grid_snap_btn.isChecked():
self.app.ui.grid_snap_btn.trigger()
def activate_measure_tool(self):
# ENABLE the Measuring TOOL
self.active = True
# disable the measuring button
self.measure_btn.setDisabled(True)
self.measure_btn.setText('%s...' % _("Working"))
self.clicked_meas = 0
self.original_call_source = copy(self.app.call_source)
self.app.inform.emit(_("MEASURING: Click on the Start point ..."))
self.units = self.app.defaults['units'].lower()
# we can connect the app mouse events to the measurement tool
# NEVER DISCONNECT THOSE before connecting some other handlers; it breaks something in VisPy
self.mm = self.canvas.graph_event_connect('mouse_move', self.on_mouse_move_meas)
self.mr = self.canvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
# we disconnect the mouse/key handlers from wherever the measurement tool was called
if self.app.call_source == 'app':
if self.app.is_legacy is False:
self.canvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
self.canvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
self.canvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
else:
self.canvas.graph_event_disconnect(self.app.mm)
self.canvas.graph_event_disconnect(self.app.mp)
self.canvas.graph_event_disconnect(self.app.mr)
elif self.app.call_source == 'geo_editor':
if self.app.is_legacy is False:
self.canvas.graph_event_disconnect('mouse_move', self.app.geo_editor.on_canvas_move)
self.canvas.graph_event_disconnect('mouse_press', self.app.geo_editor.on_canvas_click)
self.canvas.graph_event_disconnect('mouse_release', self.app.geo_editor.on_geo_click_release)
else:
self.canvas.graph_event_disconnect(self.app.geo_editor.mm)
self.canvas.graph_event_disconnect(self.app.geo_editor.mp)
self.canvas.graph_event_disconnect(self.app.geo_editor.mr)
elif self.app.call_source == 'exc_editor':
if self.app.is_legacy is False:
self.canvas.graph_event_disconnect('mouse_move', self.app.exc_editor.on_canvas_move)
self.canvas.graph_event_disconnect('mouse_press', self.app.exc_editor.on_canvas_click)
self.canvas.graph_event_disconnect('mouse_release', self.app.exc_editor.on_exc_click_release)
else:
self.canvas.graph_event_disconnect(self.app.exc_editor.mm)
self.canvas.graph_event_disconnect(self.app.exc_editor.mp)
self.canvas.graph_event_disconnect(self.app.exc_editor.mr)
elif self.app.call_source == 'grb_editor':
if self.app.is_legacy is False:
self.canvas.graph_event_disconnect('mouse_move', self.app.grb_editor.on_canvas_move)
self.canvas.graph_event_disconnect('mouse_press', self.app.grb_editor.on_canvas_click)
self.canvas.graph_event_disconnect('mouse_release', self.app.grb_editor.on_grb_click_release)
else:
self.canvas.graph_event_disconnect(self.app.grb_editor.mm)
self.canvas.graph_event_disconnect(self.app.grb_editor.mp)
self.canvas.graph_event_disconnect(self.app.grb_editor.mr)
self.app.call_source = 'measurement'
self.set_tool_ui()
def deactivate_measure_tool(self):
# DISABLE the Measuring TOOL
self.active = False
self.points = []
# disable the measuring button
self.measure_btn.setDisabled(False)
self.measure_btn.setText(_("Measure"))
self.app.call_source = copy(self.original_call_source)
if self.original_call_source == 'app':
self.app.mm = self.canvas.graph_event_connect('mouse_move', self.app.on_mouse_move_over_plot)
self.app.mp = self.canvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
self.app.mr = self.canvas.graph_event_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
elif self.original_call_source == 'geo_editor':
self.app.geo_editor.mm = self.canvas.graph_event_connect('mouse_move', self.app.geo_editor.on_canvas_move)
self.app.geo_editor.mp = self.canvas.graph_event_connect('mouse_press', self.app.geo_editor.on_canvas_click)
self.app.geo_editor.mr = self.canvas.graph_event_connect('mouse_release',
self.app.geo_editor.on_geo_click_release)
elif self.original_call_source == 'exc_editor':
self.app.exc_editor.mm = self.canvas.graph_event_connect('mouse_move', self.app.exc_editor.on_canvas_move)
self.app.exc_editor.mp = self.canvas.graph_event_connect('mouse_press', self.app.exc_editor.on_canvas_click)
self.app.exc_editor.mr = self.canvas.graph_event_connect('mouse_release',
self.app.exc_editor.on_exc_click_release)
elif self.original_call_source == 'grb_editor':
self.app.grb_editor.mm = self.canvas.graph_event_connect('mouse_move', self.app.grb_editor.on_canvas_move)
self.app.grb_editor.mp = self.canvas.graph_event_connect('mouse_press', self.app.grb_editor.on_canvas_click)
self.app.grb_editor.mr = self.canvas.graph_event_connect('mouse_release',
self.app.grb_editor.on_grb_click_release)
# disconnect the mouse/key events from functions of measurement tool
if self.app.is_legacy is False:
self.canvas.graph_event_disconnect('mouse_move', self.on_mouse_move_meas)
self.canvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
else:
self.canvas.graph_event_disconnect(self.mm)
self.canvas.graph_event_disconnect(self.mr)
# self.app.ui.notebook.setTabText(2, _("Tools"))
# self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
self.app.command_active = None
# delete the measuring line
self.delete_shape()
# restore the grid status
if (self.app.ui.grid_snap_btn.isChecked() and self.grid_status_memory is False) or \
(not self.app.ui.grid_snap_btn.isChecked() and self.grid_status_memory is True):
self.app.ui.grid_snap_btn.trigger()
log.debug("Distance Tool --> exit tool")
if self.tool_done is False:
self.app.inform.emit('%s' % _("Distance Tool finished."))
def on_mouse_click_release(self, event):
# mouse click releases will be accepted only if the left button is clicked
# this is necessary because right mouse click or middle mouse click
# are used for panning on the canvas
log.debug("Distance Tool --> mouse click release")
if self.app.is_legacy is False:
event_pos = event.pos
right_button = 2
event_is_dragging = self.mouse_is_dragging
else:
event_pos = (event.xdata, event.ydata)
right_button = 3
event_is_dragging = self.app.plotcanvas.is_dragging
if event.button == 1:
pos_canvas = self.canvas.translate_coords(event_pos)
if self.snap_center_cb.get_value() is False:
# if GRID is active we need to get the snapped positions
if self.app.grid_status():
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
else:
pos = pos_canvas[0], pos_canvas[1]
else:
pos = (pos_canvas[0], pos_canvas[1])
current_pt = Point(pos)
shapes_storage = self.make_storage()
if self.original_call_source == 'exc_editor':
for storage in self.app.exc_editor.storage_dict:
__, st_closest_shape = self.app.exc_editor.storage_dict[storage].nearest(pos)
shapes_storage.insert(st_closest_shape)
__, closest_shape = shapes_storage.nearest(pos)
# if it's a drill
if isinstance(closest_shape.geo, MultiLineString):
radius = closest_shape.geo[0].length / 2.0
center_pt = closest_shape.geo.centroid
geo_buffered = center_pt.buffer(radius)
if current_pt.within(geo_buffered):
pos = (center_pt.x, center_pt.y)
# if it's a slot
elif isinstance(closest_shape.geo, Polygon):
geo_buffered = closest_shape.geo.buffer(0)
center_pt = geo_buffered.centroid
if current_pt.within(geo_buffered):
pos = (center_pt.x, center_pt.y)
elif self.original_call_source == 'grb_editor':
clicked_pads = []
for storage in self.app.grb_editor.storage_dict:
try:
for shape_stored in self.app.grb_editor.storage_dict[storage]['geometry']:
if 'solid' in shape_stored.geo:
geometric_data = shape_stored.geo['solid']
if Point(current_pt).within(geometric_data):
if isinstance(shape_stored.geo['follow'], Point):
clicked_pads.append(shape_stored.geo['follow'])
except KeyError:
pass
if len(clicked_pads) > 1:
self.tool_done = True
self.deactivate_measure_tool()
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Pads overlapped. Aborting."))
return
pos = (clicked_pads[0].x, clicked_pads[0].y)
self.app.on_jump_to(custom_location=pos, fit_center=False)
# Update cursor
self.app.app_cursor.enabled = True
self.app.app_cursor.set_data(np.asarray([(pos[0], pos[1])]),
symbol='++', edge_color='#000000',
edge_width=self.app.defaults["global_cursor_width"],
size=self.app.defaults["global_cursor_size"])
self.points.append(pos)
# Reset here the relative coordinates so there is a new reference on the click position
if self.rel_point1 is None:
# self.app.ui.rel_position_label.setText("<b>Dx</b>: %.*f&nbsp;&nbsp; <b>Dy</b>: "
# "%.*f&nbsp;&nbsp;&nbsp;&nbsp;" %
# (self.decimals, 0.0, self.decimals, 0.0))
self.rel_point1 = pos
else:
self.rel_point2 = copy(self.rel_point1)
self.rel_point1 = pos
self.calculate_distance(pos=pos)
elif event.button == right_button and event_is_dragging is False:
self.deactivate_measure_tool()
self.app.inform.emit(_("Distance Tool cancelled."))
def calculate_distance(self, pos):
if len(self.points) == 1:
self.start_entry.set_value("(%.*f, %.*f)" % (self.decimals, pos[0], self.decimals, pos[1]))
self.app.inform.emit(_("MEASURING: Click on the Destination point ..."))
elif len(self.points) == 2:
# self.app.app_cursor.enabled = False
dx = self.points[1][0] - self.points[0][0]
dy = self.points[1][1] - self.points[0][1]
d = math.sqrt(dx ** 2 + dy ** 2)
self.stop_entry.set_value("(%.*f, %.*f)" % (self.decimals, pos[0], self.decimals, pos[1]))
self.app.inform.emit("{tx1}: {tx2} D(x) = {d_x} | D(y) = {d_y} | {tx3} = {d_z}".format(
tx1=_("MEASURING"),
tx2=_("Result"),
tx3=_("Distance"),
d_x='%*f' % (self.decimals, abs(dx)),
d_y='%*f' % (self.decimals, abs(dy)),
d_z='%*f' % (self.decimals, abs(d)))
)
self.distance_x_entry.set_value('%.*f' % (self.decimals, abs(dx)))
self.distance_y_entry.set_value('%.*f' % (self.decimals, abs(dy)))
try:
angle = math.degrees(math.atan2(dy, dx))
if angle < 0:
angle += 360
self.angle_entry.set_value('%.*f' % (self.decimals, angle))
except Exception:
pass
self.total_distance_entry.set_value('%.*f' % (self.decimals, abs(d)))
# self.app.ui.rel_position_label.setText(
# "<b>Dx</b>: {}&nbsp;&nbsp; <b>Dy</b>: {}&nbsp;&nbsp;&nbsp;&nbsp;".format(
# '%.*f' % (self.decimals, pos[0]), '%.*f' % (self.decimals, pos[1])
# )
# )
self.tool_done = True
self.deactivate_measure_tool()
def on_mouse_move_meas(self, event):
try: # May fail in case mouse not within axes
if self.app.is_legacy is False:
event_pos = event.pos
self.mouse_is_dragging = event.is_dragging
else:
event_pos = (event.xdata, event.ydata)
try:
x = float(event_pos[0])
y = float(event_pos[1])
except TypeError:
return
pos_canvas = self.app.plotcanvas.translate_coords((x, y))
if self.app.grid_status():
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
# Update cursor
self.app.app_cursor.set_data(np.asarray([(pos[0], pos[1])]),
symbol='++', edge_color=self.app.cursor_color_3D,
edge_width=self.app.defaults["global_cursor_width"],
size=self.app.defaults["global_cursor_size"])
else:
pos = (pos_canvas[0], pos_canvas[1])
self.app.ui.position_label.setText(
"&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: {}&nbsp;&nbsp; <b>Y</b>: {}".format(
'%.*f' % (self.decimals, pos[0]), '%.*f' % (self.decimals, pos[1])
)
)
units = self.app.defaults["units"].lower()
self.app.plotcanvas.text_hud.text = \
'Dx:\t{:<.4f} [{:s}]\nDy:\t{:<.4f} [{:s}]\n\nX: \t{:<.4f} [{:s}]\nY: \t{:<.4f} [{:s}]'.format(
0.0000, units, 0.0000, units, pos[0], units, pos[1], units)
if self.rel_point1 is not None:
dx = pos[0] - float(self.rel_point1[0])
dy = pos[1] - float(self.rel_point1[1])
else:
dx = pos[0]
dy = pos[1]
# self.app.ui.rel_position_label.setText(
# "<b>Dx</b>: {}&nbsp;&nbsp; <b>Dy</b>: {}&nbsp;&nbsp;&nbsp;&nbsp;".format(
# '%.*f' % (self.decimals, dx), '%.*f' % (self.decimals, dy)
# )
# )
# update utility geometry
if len(self.points) == 1:
self.utility_geometry(pos=pos)
# and display the temporary angle
try:
angle = math.degrees(math.atan2(dy, dx))
if angle < 0:
angle += 360
self.angle_entry.set_value('%.*f' % (self.decimals, angle))
except Exception as e:
log.debug("Distance.on_mouse_move_meas() -> update utility geometry -> %s" % str(e))
pass
except Exception as e:
log.debug("Distance.on_mouse_move_meas() --> %s" % str(e))
self.app.ui.position_label.setText("")
# self.app.ui.rel_position_label.setText("")
def utility_geometry(self, pos):
# first delete old shape
self.delete_shape()
# second draw the new shape of the utility geometry
meas_line = LineString([pos, self.points[0]])
settings = QtCore.QSettings("Open Source", "FlatCAM")
if settings.contains("theme"):
theme = settings.value('theme', type=str)
else:
theme = 'white'
if theme == 'white':
color = '#000000FF'
else:
color = '#FFFFFFFF'
self.sel_shapes.add(meas_line, color=color, update=True, layer=0, tolerance=None, linewidth=2)
if self.app.is_legacy is True:
self.sel_shapes.redraw()
def delete_shape(self):
self.sel_shapes.clear()
self.sel_shapes.redraw()
@staticmethod
def make_storage():
# ## Shape storage.
storage = FlatCAMRTreeStorage()
storage.get_points = DrawToolShape.get_pts
return storage
# def set_meas_units(self, units):
# self.meas.units_label.setText("[" + self.app.options["units"].lower() + "]")
# end of file

305
appTools/ToolDistanceMin.py Normal file
View File

@@ -0,0 +1,305 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 09/29/2019 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets, QtCore
from appTool import AppTool
from appGUI.GUIElements import FCEntry
from shapely.ops import nearest_points
from shapely.geometry import Point, MultiPolygon
from shapely.ops import cascaded_union
import math
import logging
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class DistanceMin(AppTool):
toolName = _("Minimum Distance Tool")
def __init__(self, app):
AppTool.__init__(self, app)
self.app = app
self.canvas = self.app.plotcanvas
self.units = self.app.defaults['units'].lower()
self.decimals = self.app.decimals
# ## Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
self.layout.addWidget(title_label)
# ## Form Layout
form_layout = QtWidgets.QFormLayout()
self.layout.addLayout(form_layout)
self.units_label = QtWidgets.QLabel('%s:' % _("Units"))
self.units_label.setToolTip(_("Those are the units in which the distance is measured."))
self.units_value = QtWidgets.QLabel("%s" % str({'mm': _("METRIC (mm)"), 'in': _("INCH (in)")}[self.units]))
self.units_value.setDisabled(True)
self.start_label = QtWidgets.QLabel("%s:" % _('First object point'))
self.start_label.setToolTip(_("This is first object point coordinates.\n"
"This is the start point for measuring distance."))
self.stop_label = QtWidgets.QLabel("%s:" % _('Second object point'))
self.stop_label.setToolTip(_("This is second object point coordinates.\n"
"This is the end point for measuring distance."))
self.distance_x_label = QtWidgets.QLabel('%s:' % _("Dx"))
self.distance_x_label.setToolTip(_("This is the distance measured over the X axis."))
self.distance_y_label = QtWidgets.QLabel('%s:' % _("Dy"))
self.distance_y_label.setToolTip(_("This is the distance measured over the Y axis."))
self.angle_label = QtWidgets.QLabel('%s:' % _("Angle"))
self.angle_label.setToolTip(_("This is orientation angle of the measuring line."))
self.total_distance_label = QtWidgets.QLabel("<b>%s:</b>" % _('DISTANCE'))
self.total_distance_label.setToolTip(_("This is the point to point Euclidean distance."))
self.half_point_label = QtWidgets.QLabel("<b>%s:</b>" % _('Half Point'))
self.half_point_label.setToolTip(_("This is the middle point of the point to point Euclidean distance."))
self.start_entry = FCEntry()
self.start_entry.setReadOnly(True)
self.start_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.start_entry.setToolTip(_("This is first object point coordinates.\n"
"This is the start point for measuring distance."))
self.stop_entry = FCEntry()
self.stop_entry.setReadOnly(True)
self.stop_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.stop_entry.setToolTip(_("This is second object point coordinates.\n"
"This is the end point for measuring distance."))
self.distance_x_entry = FCEntry()
self.distance_x_entry.setReadOnly(True)
self.distance_x_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.distance_x_entry.setToolTip(_("This is the distance measured over the X axis."))
self.distance_y_entry = FCEntry()
self.distance_y_entry.setReadOnly(True)
self.distance_y_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.distance_y_entry.setToolTip(_("This is the distance measured over the Y axis."))
self.angle_entry = FCEntry()
self.angle_entry.setReadOnly(True)
self.angle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.angle_entry.setToolTip(_("This is orientation angle of the measuring line."))
self.total_distance_entry = FCEntry()
self.total_distance_entry.setReadOnly(True)
self.total_distance_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.total_distance_entry.setToolTip(_("This is the point to point Euclidean distance."))
self.half_point_entry = FCEntry()
self.half_point_entry.setReadOnly(True)
self.half_point_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.half_point_entry.setToolTip(_("This is the middle point of the point to point Euclidean distance."))
self.measure_btn = QtWidgets.QPushButton(_("Measure"))
self.layout.addWidget(self.measure_btn)
self.jump_hp_btn = QtWidgets.QPushButton(_("Jump to Half Point"))
self.layout.addWidget(self.jump_hp_btn)
self.jump_hp_btn.setDisabled(True)
form_layout.addRow(self.units_label, self.units_value)
form_layout.addRow(self.start_label, self.start_entry)
form_layout.addRow(self.stop_label, self.stop_entry)
form_layout.addRow(self.distance_x_label, self.distance_x_entry)
form_layout.addRow(self.distance_y_label, self.distance_y_entry)
form_layout.addRow(self.angle_label, self.angle_entry)
form_layout.addRow(self.total_distance_label, self.total_distance_entry)
form_layout.addRow(self.half_point_label, self.half_point_entry)
self.layout.addStretch()
self.h_point = (0, 0)
self.measure_btn.clicked.connect(self.activate_measure_tool)
self.jump_hp_btn.clicked.connect(self.on_jump_to_half_point)
def run(self, toggle=False):
self.app.defaults.report_usage("ToolDistanceMin()")
if self.app.tool_tab_locked is True:
return
self.app.ui.notebook.setTabText(2, _("Minimum Distance Tool"))
# if the splitter is hidden, display it
if self.app.ui.splitter.sizes()[0] == 0:
self.app.ui.splitter.setSizes([1, 1])
if toggle:
pass
self.set_tool_ui()
self.app.inform.emit('MEASURING: %s' %
_("Select two objects and no more, to measure the distance between them ..."))
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='Shift+M', **kwargs)
def set_tool_ui(self):
# Remove anything else in the appGUI
self.app.ui.tool_scroll_area.takeWidget()
# Put oneself in the appGUI
self.app.ui.tool_scroll_area.setWidget(self)
# Switch notebook to tool page
self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
self.units = self.app.defaults['units'].lower()
# initial view of the layout
self.start_entry.set_value('(0, 0)')
self.stop_entry.set_value('(0, 0)')
self.distance_x_entry.set_value('0.0')
self.distance_y_entry.set_value('0.0')
self.angle_entry.set_value('0.0')
self.total_distance_entry.set_value('0.0')
self.half_point_entry.set_value('(0, 0)')
self.jump_hp_btn.setDisabled(True)
log.debug("Minimum Distance Tool --> tool initialized")
def activate_measure_tool(self):
# ENABLE the Measuring TOOL
self.jump_hp_btn.setDisabled(False)
self.units = self.app.defaults['units'].lower()
if self.app.call_source == 'app':
selected_objs = self.app.collection.get_selected()
if len(selected_objs) != 2:
self.app.inform.emit('[WARNING_NOTCL] %s %s' %
(_("Select two objects and no more. Currently the selection has objects: "),
str(len(selected_objs))))
return
else:
if isinstance(selected_objs[0].solid_geometry, list):
try:
selected_objs[0].solid_geometry = MultiPolygon(selected_objs[0].solid_geometry)
except Exception:
selected_objs[0].solid_geometry = cascaded_union(selected_objs[0].solid_geometry)
try:
selected_objs[1].solid_geometry = MultiPolygon(selected_objs[1].solid_geometry)
except Exception:
selected_objs[1].solid_geometry = cascaded_union(selected_objs[1].solid_geometry)
first_pos, last_pos = nearest_points(selected_objs[0].solid_geometry, selected_objs[1].solid_geometry)
elif self.app.call_source == 'geo_editor':
selected_objs = self.app.geo_editor.selected
if len(selected_objs) != 2:
self.app.inform.emit('[WARNING_NOTCL] %s %s' %
(_("Select two objects and no more. Currently the selection has objects: "),
str(len(selected_objs))))
return
else:
first_pos, last_pos = nearest_points(selected_objs[0].geo, selected_objs[1].geo)
elif self.app.call_source == 'exc_editor':
selected_objs = self.app.exc_editor.selected
if len(selected_objs) != 2:
self.app.inform.emit('[WARNING_NOTCL] %s %s' %
(_("Select two objects and no more. Currently the selection has objects: "),
str(len(selected_objs))))
return
else:
# the objects are really MultiLinesStrings made out of 2 lines in cross shape
xmin, ymin, xmax, ymax = selected_objs[0].geo.bounds
first_geo_radius = (xmax - xmin) / 2
first_geo_center = Point(xmin + first_geo_radius, ymin + first_geo_radius)
first_geo = first_geo_center.buffer(first_geo_radius)
# the objects are really MultiLinesStrings made out of 2 lines in cross shape
xmin, ymin, xmax, ymax = selected_objs[1].geo.bounds
last_geo_radius = (xmax - xmin) / 2
last_geo_center = Point(xmin + last_geo_radius, ymin + last_geo_radius)
last_geo = last_geo_center.buffer(last_geo_radius)
first_pos, last_pos = nearest_points(first_geo, last_geo)
elif self.app.call_source == 'grb_editor':
selected_objs = self.app.grb_editor.selected
if len(selected_objs) != 2:
self.app.inform.emit('[WARNING_NOTCL] %s %s' %
(_("Select two objects and no more. Currently the selection has objects: "),
str(len(selected_objs))))
return
else:
first_pos, last_pos = nearest_points(selected_objs[0].geo['solid'], selected_objs[1].geo['solid'])
else:
first_pos, last_pos = 0, 0
self.start_entry.set_value("(%.*f, %.*f)" % (self.decimals, first_pos.x, self.decimals, first_pos.y))
self.stop_entry.set_value("(%.*f, %.*f)" % (self.decimals, last_pos.x, self.decimals, last_pos.y))
dx = first_pos.x - last_pos.x
dy = first_pos.y - last_pos.y
self.distance_x_entry.set_value('%.*f' % (self.decimals, abs(dx)))
self.distance_y_entry.set_value('%.*f' % (self.decimals, abs(dy)))
try:
angle = math.degrees(math.atan(dy / dx))
self.angle_entry.set_value('%.*f' % (self.decimals, angle))
except Exception as e:
pass
d = math.sqrt(dx ** 2 + dy ** 2)
self.total_distance_entry.set_value('%.*f' % (self.decimals, abs(d)))
self.h_point = (min(first_pos.x, last_pos.x) + (abs(dx) / 2), min(first_pos.y, last_pos.y) + (abs(dy) / 2))
if d != 0:
self.half_point_entry.set_value(
"(%.*f, %.*f)" % (self.decimals, self.h_point[0], self.decimals, self.h_point[1])
)
else:
self.half_point_entry.set_value(
"(%.*f, %.*f)" % (self.decimals, 0.0, self.decimals, 0.0)
)
if d != 0:
self.app.inform.emit("{tx1}: {tx2} D(x) = {d_x} | D(y) = {d_y} | {tx3} = {d_z}".format(
tx1=_("MEASURING"),
tx2=_("Result"),
tx3=_("Distance"),
d_x='%*f' % (self.decimals, abs(dx)),
d_y='%*f' % (self.decimals, abs(dy)),
d_z='%*f' % (self.decimals, abs(d)))
)
else:
self.app.inform.emit('[WARNING_NOTCL] %s: %s' %
(_("Objects intersects or touch at"),
"(%.*f, %.*f)" % (self.decimals, self.h_point[0], self.decimals, self.h_point[1])))
def on_jump_to_half_point(self):
self.app.on_jump_to(custom_location=self.h_point)
self.app.inform.emit('[success] %s: %s' %
(_("Jumped to the half point between the two selected objects"),
"(%.*f, %.*f)" % (self.decimals, self.h_point[0], self.decimals, self.h_point[1])))
def set_meas_units(self, units):
self.meas.units_label.setText("[" + self.app.options["units"].lower() + "]")
# end of file

View File

@@ -0,0 +1,455 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 2/14/2020 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets, QtCore
from appTool import AppTool
from appGUI.GUIElements import FCButton, FCDoubleSpinner, RadioSet, FCComboBox, NumericalEvalEntry, FCEntry
from shapely.ops import unary_union
from copy import deepcopy
import math
import logging
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class ToolEtchCompensation(AppTool):
toolName = _("Etch Compensation Tool")
def __init__(self, app):
self.app = app
self.decimals = self.app.decimals
AppTool.__init__(self, app)
self.tools_frame = QtWidgets.QFrame()
self.tools_frame.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(self.tools_frame)
self.tools_box = QtWidgets.QVBoxLayout()
self.tools_box.setContentsMargins(0, 0, 0, 0)
self.tools_frame.setLayout(self.tools_box)
# Title
title_label = QtWidgets.QLabel("%s" % self.toolName)
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.tools_box.addWidget(title_label)
# Grid Layout
grid0 = QtWidgets.QGridLayout()
grid0.setColumnStretch(0, 0)
grid0.setColumnStretch(1, 1)
self.tools_box.addLayout(grid0)
grid0.addWidget(QtWidgets.QLabel(''), 0, 0, 1, 2)
# Target Gerber Object
self.gerber_combo = FCComboBox()
self.gerber_combo.setModel(self.app.collection)
self.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.gerber_combo.is_last = True
self.gerber_combo.obj_type = "Gerber"
self.gerber_label = QtWidgets.QLabel('<b>%s:</b>' % _("GERBER"))
self.gerber_label.setToolTip(
_("Gerber object that will be inverted.")
)
grid0.addWidget(self.gerber_label, 1, 0, 1, 2)
grid0.addWidget(self.gerber_combo, 2, 0, 1, 2)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid0.addWidget(separator_line, 3, 0, 1, 2)
self.util_label = QtWidgets.QLabel("<b>%s:</b>" % _("Utilities"))
self.util_label.setToolTip('%s.' % _("Conversion utilities"))
grid0.addWidget(self.util_label, 4, 0, 1, 2)
# Oz to um conversion
self.oz_um_label = QtWidgets.QLabel('%s:' % _('Oz to Microns'))
self.oz_um_label.setToolTip(
_("Will convert from oz thickness to microns [um].\n"
"Can use formulas with operators: /, *, +, -, %, .\n"
"The real numbers use the dot decimals separator.")
)
grid0.addWidget(self.oz_um_label, 5, 0, 1, 2)
hlay_1 = QtWidgets.QHBoxLayout()
self.oz_entry = NumericalEvalEntry(border_color='#0069A9')
self.oz_entry.setPlaceholderText(_("Oz value"))
self.oz_to_um_entry = FCEntry()
self.oz_to_um_entry.setPlaceholderText(_("Microns value"))
self.oz_to_um_entry.setReadOnly(True)
hlay_1.addWidget(self.oz_entry)
hlay_1.addWidget(self.oz_to_um_entry)
grid0.addLayout(hlay_1, 6, 0, 1, 2)
# Mils to um conversion
self.mils_um_label = QtWidgets.QLabel('%s:' % _('Mils to Microns'))
self.mils_um_label.setToolTip(
_("Will convert from mils to microns [um].\n"
"Can use formulas with operators: /, *, +, -, %, .\n"
"The real numbers use the dot decimals separator.")
)
grid0.addWidget(self.mils_um_label, 7, 0, 1, 2)
hlay_2 = QtWidgets.QHBoxLayout()
self.mils_entry = NumericalEvalEntry(border_color='#0069A9')
self.mils_entry.setPlaceholderText(_("Mils value"))
self.mils_to_um_entry = FCEntry()
self.mils_to_um_entry.setPlaceholderText(_("Microns value"))
self.mils_to_um_entry.setReadOnly(True)
hlay_2.addWidget(self.mils_entry)
hlay_2.addWidget(self.mils_to_um_entry)
grid0.addLayout(hlay_2, 8, 0, 1, 2)
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.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
self.param_label.setToolTip('%s.' % _("Parameters for this tool"))
grid0.addWidget(self.param_label, 10, 0, 1, 2)
# Thickness
self.thick_label = QtWidgets.QLabel('%s:' % _('Copper Thickness'))
self.thick_label.setToolTip(
_("The thickness of the copper foil.\n"
"In microns [um].")
)
self.thick_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.thick_entry.set_precision(self.decimals)
self.thick_entry.set_range(0.0000, 9999.9999)
self.thick_entry.setObjectName(_("Thickness"))
grid0.addWidget(self.thick_label, 12, 0)
grid0.addWidget(self.thick_entry, 12, 1)
self.ratio_label = QtWidgets.QLabel('%s:' % _("Ratio"))
self.ratio_label.setToolTip(
_("The ratio of lateral etch versus depth etch.\n"
"Can be:\n"
"- custom -> the user will enter a custom value\n"
"- preselection -> value which depends on a selection of etchants")
)
self.ratio_radio = RadioSet([
{'label': _('Etch Factor'), 'value': 'factor'},
{'label': _('Etchants list'), 'value': 'etch_list'},
{'label': _('Manual offset'), 'value': 'manual'}
], orientation='vertical', stretch=False)
grid0.addWidget(self.ratio_label, 14, 0, 1, 2)
grid0.addWidget(self.ratio_radio, 16, 0, 1, 2)
# Etchants
self.etchants_label = QtWidgets.QLabel('%s:' % _('Etchants'))
self.etchants_label.setToolTip(
_("A list of etchants.")
)
self.etchants_combo = FCComboBox(callback=self.confirmation_message)
self.etchants_combo.setObjectName(_("Etchants"))
self.etchants_combo.addItems(["CuCl2", "Fe3Cl", _("Alkaline baths")])
grid0.addWidget(self.etchants_label, 18, 0)
grid0.addWidget(self.etchants_combo, 18, 1)
# Etch Factor
self.factor_label = QtWidgets.QLabel('%s:' % _('Etch factor'))
self.factor_label.setToolTip(
_("The ratio between depth etch and lateral etch .\n"
"Accepts real numbers and formulas using the operators: /,*,+,-,%")
)
self.factor_entry = NumericalEvalEntry(border_color='#0069A9')
self.factor_entry.setPlaceholderText(_("Real number or formula"))
self.factor_entry.setObjectName(_("Etch_factor"))
grid0.addWidget(self.factor_label, 19, 0)
grid0.addWidget(self.factor_entry, 19, 1)
# Manual Offset
self.offset_label = QtWidgets.QLabel('%s:' % _('Offset'))
self.offset_label.setToolTip(
_("Value with which to increase or decrease (buffer)\n"
"the copper features. In microns [um].")
)
self.offset_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.offset_entry.set_precision(self.decimals)
self.offset_entry.set_range(-9999.9999, 9999.9999)
self.offset_entry.setObjectName(_("Offset"))
grid0.addWidget(self.offset_label, 20, 0)
grid0.addWidget(self.offset_entry, 20, 1)
# Hide the Etchants and Etch factor
self.etchants_label.hide()
self.etchants_combo.hide()
self.factor_label.hide()
self.factor_entry.hide()
self.offset_label.hide()
self.offset_entry.hide()
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid0.addWidget(separator_line, 22, 0, 1, 2)
self.compensate_btn = FCButton(_('Compensate'))
self.compensate_btn.setToolTip(
_("Will increase the copper features thickness to compensate the lateral etch.")
)
self.compensate_btn.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
grid0.addWidget(self.compensate_btn, 24, 0, 1, 2)
self.tools_box.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.tools_box.addWidget(self.reset_button)
self.compensate_btn.clicked.connect(self.on_compensate)
self.reset_button.clicked.connect(self.set_tool_ui)
self.ratio_radio.activated_custom.connect(self.on_ratio_change)
self.oz_entry.textChanged.connect(self.on_oz_conversion)
self.mils_entry.textChanged.connect(self.on_mils_conversion)
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='', **kwargs)
def run(self, toggle=True):
self.app.defaults.report_usage("ToolEtchCompensation()")
log.debug("ToolEtchCompensation() is running ...")
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])
AppTool.run(self)
self.set_tool_ui()
self.app.ui.notebook.setTabText(2, _("Etch Compensation Tool"))
def set_tool_ui(self):
self.thick_entry.set_value(18.0)
self.ratio_radio.set_value('factor')
def on_ratio_change(self, val):
"""
Called on activated_custom signal of the RadioSet GUI element self.radio_ratio
:param val: 'c' or 'p': 'c' means custom factor and 'p' means preselected etchants
:type val: str
:return: None
:rtype:
"""
if val == 'factor':
self.etchants_label.hide()
self.etchants_combo.hide()
self.factor_label.show()
self.factor_entry.show()
self.offset_label.hide()
self.offset_entry.hide()
elif val == 'etch_list':
self.etchants_label.show()
self.etchants_combo.show()
self.factor_label.hide()
self.factor_entry.hide()
self.offset_label.hide()
self.offset_entry.hide()
else:
self.etchants_label.hide()
self.etchants_combo.hide()
self.factor_label.hide()
self.factor_entry.hide()
self.offset_label.show()
self.offset_entry.show()
def on_oz_conversion(self, txt):
try:
val = eval(txt)
# oz thickness to mils by multiplying with 1.37
# mils to microns by multiplying with 25.4
val *= 34.798
except Exception:
self.oz_to_um_entry.set_value('')
return
self.oz_to_um_entry.set_value(val, self.decimals)
def on_mils_conversion(self, txt):
try:
val = eval(txt)
val *= 25.4
except Exception:
self.mils_to_um_entry.set_value('')
return
self.mils_to_um_entry.set_value(val, self.decimals)
def on_compensate(self):
log.debug("ToolEtchCompensation.on_compensate()")
ratio_type = self.ratio_radio.get_value()
thickness = self.thick_entry.get_value() / 1000 # in microns
grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
obj_name = self.gerber_combo.currentText()
outname = obj_name + "_comp"
# Get source object.
try:
grb_obj = self.app.collection.get_by_name(obj_name)
except Exception as e:
self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(obj_name)))
return "Could not retrieve object: %s with error: %s" % (obj_name, str(e))
if grb_obj is None:
if obj_name == '':
obj_name = 'None'
self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(obj_name)))
return
if ratio_type == 'factor':
etch_factor = 1 / self.factor_entry.get_value()
offset = thickness / etch_factor
elif ratio_type == 'etch_list':
etchant = self.etchants_combo.get_value()
if etchant == "CuCl2":
etch_factor = 0.33
else:
etch_factor = 0.25
offset = thickness / etch_factor
else:
offset = self.offset_entry.get_value() / 1000 # in microns
try:
__ = iter(grb_obj.solid_geometry)
except TypeError:
grb_obj.solid_geometry = list(grb_obj.solid_geometry)
new_solid_geometry = []
for poly in grb_obj.solid_geometry:
new_solid_geometry.append(poly.buffer(offset, int(grb_circle_steps)))
new_solid_geometry = unary_union(new_solid_geometry)
new_options = {}
for opt in grb_obj.options:
new_options[opt] = deepcopy(grb_obj.options[opt])
new_apertures = deepcopy(grb_obj.apertures)
# update the apertures attributes (keys in the apertures dict)
for ap in new_apertures:
type = new_apertures[ap]['type']
for k in new_apertures[ap]:
if type == 'R' or type == 'O':
if k == 'width' or k == 'height':
new_apertures[ap][k] += offset
else:
if k == 'size' or k == 'width' or k == 'height':
new_apertures[ap][k] += offset
if k == 'geometry':
for geo_el in new_apertures[ap][k]:
if 'solid' in geo_el:
geo_el['solid'] = geo_el['solid'].buffer(offset, int(grb_circle_steps))
# in case of 'R' or 'O' aperture type we need to update the aperture 'size' after
# the 'width' and 'height' keys were updated
for ap in new_apertures:
type = new_apertures[ap]['type']
for k in new_apertures[ap]:
if type == 'R' or type == 'O':
if k == 'size':
new_apertures[ap][k] = math.sqrt(
new_apertures[ap]['width'] ** 2 + new_apertures[ap]['height'] ** 2)
def init_func(new_obj, app_obj):
"""
Init a new object in FlatCAM Object collection
:param new_obj: New object
:type new_obj: ObjectCollection
:param app_obj: App
:type app_obj: app_Main.App
:return: None
:rtype:
"""
new_obj.options.update(new_options)
new_obj.options['name'] = outname
new_obj.fill_color = deepcopy(grb_obj.fill_color)
new_obj.outline_color = deepcopy(grb_obj.outline_color)
new_obj.apertures = deepcopy(new_apertures)
new_obj.solid_geometry = deepcopy(new_solid_geometry)
new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None,
local_use=new_obj, use_thread=False)
self.app.app_obj.new_object('gerber', outname, init_func)
def reset_fields(self):
self.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
@staticmethod
def poly2rings(poly):
return [poly.exterior] + [interior for interior in poly.interiors]
# end of file

View File

@@ -0,0 +1,694 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 1/10/2020 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets, QtCore
from appTool import AppTool
from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, FCComboBox
from shapely.geometry import Point
import logging
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class ToolExtractDrills(AppTool):
toolName = _("Extract Drills")
def __init__(self, app):
AppTool.__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)
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 = FCComboBox()
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.is_last = True
self.gerber_object_combo.obj_type = "Gerber"
self.grb_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
self.grb_label.setToolTip('%s.' % _("Gerber from which to extract drill holes"))
# grid_lay.addRow("Bottom Layer:", self.object_combo)
grid_lay.addWidget(self.grb_label, 0, 0, 1, 2)
grid_lay.addWidget(self.gerber_object_combo, 1, 0, 1, 2)
self.padt_label = QtWidgets.QLabel("<b>%s</b>" % _("Processed Pads Type"))
self.padt_label.setToolTip(
_("The type of pads shape to be processed.\n"
"If the PCB has many SMD pads with rectangular pads,\n"
"disable the Rectangular aperture.")
)
grid_lay.addWidget(self.padt_label, 2, 0, 1, 2)
# Circular Aperture Selection
self.circular_cb = FCCheckBox('%s' % _("Circular"))
self.circular_cb.setToolTip(
_("Process Circular Pads.")
)
grid_lay.addWidget(self.circular_cb, 3, 0, 1, 2)
# Oblong Aperture Selection
self.oblong_cb = FCCheckBox('%s' % _("Oblong"))
self.oblong_cb.setToolTip(
_("Process Oblong Pads.")
)
grid_lay.addWidget(self.oblong_cb, 4, 0, 1, 2)
# Square Aperture Selection
self.square_cb = FCCheckBox('%s' % _("Square"))
self.square_cb.setToolTip(
_("Process Square Pads.")
)
grid_lay.addWidget(self.square_cb, 5, 0, 1, 2)
# Rectangular Aperture Selection
self.rectangular_cb = FCCheckBox('%s' % _("Rectangular"))
self.rectangular_cb.setToolTip(
_("Process Rectangular Pads.")
)
grid_lay.addWidget(self.rectangular_cb, 6, 0, 1, 2)
# Others type of Apertures Selection
self.other_cb = FCCheckBox('%s' % _("Others"))
self.other_cb.setToolTip(
_("Process pads not in the categories above.")
)
grid_lay.addWidget(self.other_cb, 7, 0, 1, 2)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid_lay.addWidget(separator_line, 8, 0, 1, 2)
# ## Grid Layout
grid1 = QtWidgets.QGridLayout()
self.layout.addLayout(grid1)
grid1.setColumnStretch(0, 0)
grid1.setColumnStretch(1, 1)
self.method_label = QtWidgets.QLabel('<b>%s</b>' % _("Method"))
self.method_label.setToolTip(
_("The method for processing pads. Can be:\n"
"- Fixed Diameter -> all holes will have a set size\n"
"- Fixed Annular Ring -> all holes will have a set annular ring\n"
"- Proportional -> each hole size will be a fraction of the pad size"))
grid1.addWidget(self.method_label, 2, 0, 1, 2)
# ## Holes Size
self.hole_size_radio = RadioSet(
[
{'label': _("Fixed Diameter"), 'value': 'fixed'},
{'label': _("Fixed Annular Ring"), 'value': 'ring'},
{'label': _("Proportional"), 'value': 'prop'}
],
orientation='vertical',
stretch=False)
grid1.addWidget(self.hole_size_radio, 3, 0, 1, 2)
# grid_lay1.addWidget(QtWidgets.QLabel(''))
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid1.addWidget(separator_line, 5, 0, 1, 2)
# Annular Ring
self.fixed_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Diameter"))
grid1.addWidget(self.fixed_label, 6, 0, 1, 2)
# Diameter value
self.dia_entry = FCDoubleSpinner(callback=self.confirmation_message)
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.")
)
grid1.addWidget(self.dia_label, 8, 0)
grid1.addWidget(self.dia_entry, 8, 1)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid1.addWidget(separator_line, 9, 0, 1, 2)
self.ring_frame = QtWidgets.QFrame()
self.ring_frame.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(self.ring_frame)
self.ring_box = QtWidgets.QVBoxLayout()
self.ring_box.setContentsMargins(0, 0, 0, 0)
self.ring_frame.setLayout(self.ring_box)
# ## Grid Layout
grid2 = QtWidgets.QGridLayout()
grid2.setColumnStretch(0, 0)
grid2.setColumnStretch(1, 1)
self.ring_box.addLayout(grid2)
# Annular Ring value
self.ring_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Annular Ring"))
self.ring_label.setToolTip(
_("The size of annular ring.\n"
"The copper sliver between the hole exterior\n"
"and the margin of the copper pad.")
)
grid2.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(callback=self.confirmation_message)
self.circular_ring_entry.set_precision(self.decimals)
self.circular_ring_entry.set_range(0.0000, 9999.9999)
grid2.addWidget(self.circular_ring_label, 1, 0)
grid2.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(callback=self.confirmation_message)
self.oblong_ring_entry.set_precision(self.decimals)
self.oblong_ring_entry.set_range(0.0000, 9999.9999)
grid2.addWidget(self.oblong_ring_label, 2, 0)
grid2.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(callback=self.confirmation_message)
self.square_ring_entry.set_precision(self.decimals)
self.square_ring_entry.set_range(0.0000, 9999.9999)
grid2.addWidget(self.square_ring_label, 3, 0)
grid2.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(callback=self.confirmation_message)
self.rectangular_ring_entry.set_precision(self.decimals)
self.rectangular_ring_entry.set_range(0.0000, 9999.9999)
grid2.addWidget(self.rectangular_ring_label, 4, 0)
grid2.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(callback=self.confirmation_message)
self.other_ring_entry.set_precision(self.decimals)
self.other_ring_entry.set_range(0.0000, 9999.9999)
grid2.addWidget(self.other_ring_label, 5, 0)
grid2.addWidget(self.other_ring_entry, 5, 1)
grid3 = QtWidgets.QGridLayout()
self.layout.addLayout(grid3)
grid3.setColumnStretch(0, 0)
grid3.setColumnStretch(1, 1)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid3.addWidget(separator_line, 1, 0, 1, 2)
# Annular Ring value
self.prop_label = QtWidgets.QLabel('<b>%s</b>' % _("Proportional Diameter"))
grid3.addWidget(self.prop_label, 2, 0, 1, 2)
# Diameter value
self.factor_entry = FCDoubleSpinner(callback=self.confirmation_message, 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 hole diameter will be a fraction of the pad size.")
)
grid3.addWidget(self.factor_label, 3, 0)
grid3.addWidget(self.factor_entry, 3, 1)
# Extract drills from Gerber apertures flashes (pads)
self.e_drills_button = QtWidgets.QPushButton(_("Extract Drills"))
self.e_drills_button.setToolTip(
_("Extract drills from a given Gerber file.")
)
self.e_drills_button.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
self.layout.addWidget(self.e_drills_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.circular_ring_entry.setEnabled(False)
self.oblong_ring_entry.setEnabled(False)
self.square_ring_entry.setEnabled(False)
self.rectangular_ring_entry.setEnabled(False)
self.other_ring_entry.setEnabled(False)
self.dia_entry.setDisabled(True)
self.dia_label.setDisabled(True)
self.factor_label.setDisabled(True)
self.factor_entry.setDisabled(True)
self.ring_frame.setDisabled(True)
# ## Signals
self.hole_size_radio.activated_custom.connect(self.on_hole_size_toggle)
self.e_drills_button.clicked.connect(self.on_extract_drills_click)
self.reset_button.clicked.connect(self.set_tool_ui)
self.circular_cb.stateChanged.connect(
lambda state:
self.circular_ring_entry.setDisabled(False) if state else self.circular_ring_entry.setDisabled(True)
)
self.oblong_cb.stateChanged.connect(
lambda state:
self.oblong_ring_entry.setDisabled(False) if state else self.oblong_ring_entry.setDisabled(True)
)
self.square_cb.stateChanged.connect(
lambda state:
self.square_ring_entry.setDisabled(False) if state else self.square_ring_entry.setDisabled(True)
)
self.rectangular_cb.stateChanged.connect(
lambda state:
self.rectangular_ring_entry.setDisabled(False) if state else self.rectangular_ring_entry.setDisabled(True)
)
self.other_cb.stateChanged.connect(
lambda state:
self.other_ring_entry.setDisabled(False) if state else self.other_ring_entry.setDisabled(True)
)
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='Alt+I', **kwargs)
def run(self, toggle=True):
self.app.defaults.report_usage("Extract Drills()")
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])
AppTool.run(self)
self.set_tool_ui()
self.app.ui.notebook.setTabText(2, _("Extract Drills Tool"))
def set_tool_ui(self):
self.reset_fields()
self.hole_size_radio.set_value(self.app.defaults["tools_edrills_hole_type"])
self.dia_entry.set_value(float(self.app.defaults["tools_edrills_hole_fixed_dia"]))
self.circular_ring_entry.set_value(float(self.app.defaults["tools_edrills_circular_ring"]))
self.oblong_ring_entry.set_value(float(self.app.defaults["tools_edrills_oblong_ring"]))
self.square_ring_entry.set_value(float(self.app.defaults["tools_edrills_square_ring"]))
self.rectangular_ring_entry.set_value(float(self.app.defaults["tools_edrills_rectangular_ring"]))
self.other_ring_entry.set_value(float(self.app.defaults["tools_edrills_others_ring"]))
self.circular_cb.set_value(self.app.defaults["tools_edrills_circular"])
self.oblong_cb.set_value(self.app.defaults["tools_edrills_oblong"])
self.square_cb.set_value(self.app.defaults["tools_edrills_square"])
self.rectangular_cb.set_value(self.app.defaults["tools_edrills_rectangular"])
self.other_cb.set_value(self.app.defaults["tools_edrills_others"])
self.factor_entry.set_value(float(self.app.defaults["tools_edrills_hole_prop_factor"]))
def on_extract_drills_click(self):
drill_dia = self.dia_entry.get_value()
circ_r_val = self.circular_ring_entry.get_value()
oblong_r_val = self.oblong_ring_entry.get_value()
square_r_val = self.square_ring_entry.get_value()
rect_r_val = self.rectangular_ring_entry.get_value()
other_r_val = self.other_ring_entry.get_value()
prop_factor = self.factor_entry.get_value() / 100.0
drills = []
tools = {}
selection_index = self.gerber_object_combo.currentIndex()
model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
try:
fcobj = model_index.internalPointer().obj
except Exception:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
return
outname = fcobj.options['name'].rpartition('.')[0]
mode = self.hole_size_radio.get_value()
if mode == 'fixed':
tools = {"1": {"C": drill_dia}}
for apid, apid_value in fcobj.apertures.items():
ap_type = apid_value['type']
if ap_type == 'C':
if self.circular_cb.get_value() is False:
continue
elif ap_type == 'O':
if self.oblong_cb.get_value() is False:
continue
elif ap_type == 'R':
width = float(apid_value['width'])
height = float(apid_value['height'])
# if the height == width (float numbers so the reason for the following)
if round(width, self.decimals) == round(height, self.decimals):
if self.square_cb.get_value() is False:
continue
else:
if self.rectangular_cb.get_value() is False:
continue
else:
if self.other_cb.get_value() is False:
continue
for geo_el in apid_value['geometry']:
if 'follow' in geo_el and isinstance(geo_el['follow'], Point):
drills.append({"point": geo_el['follow'], "tool": "1"})
if 'solid_geometry' not in tools["1"]:
tools["1"]['solid_geometry'] = []
else:
tools["1"]['solid_geometry'].append(geo_el['follow'])
if 'solid_geometry' not in tools["1"] or not tools["1"]['solid_geometry']:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("No drills extracted. Try different parameters."))
return
elif mode == 'ring':
drills_found = set()
for apid, apid_value in fcobj.apertures.items():
ap_type = apid_value['type']
dia = None
if ap_type == 'C':
if self.circular_cb.get_value():
dia = float(apid_value['size']) - (2 * circ_r_val)
elif ap_type == 'O':
width = float(apid_value['width'])
height = float(apid_value['height'])
if self.oblong_cb.get_value():
if width > height:
dia = float(apid_value['height']) - (2 * oblong_r_val)
else:
dia = float(apid_value['width']) - (2 * oblong_r_val)
elif ap_type == 'R':
width = float(apid_value['width'])
height = float(apid_value['height'])
# if the height == width (float numbers so the reason for the following)
if abs(float('%.*f' % (self.decimals, width)) - float('%.*f' % (self.decimals, height))) < \
(10 ** -self.decimals):
if self.square_cb.get_value():
dia = float(apid_value['height']) - (2 * square_r_val)
else:
if self.rectangular_cb.get_value():
if width > height:
dia = float(apid_value['height']) - (2 * rect_r_val)
else:
dia = float(apid_value['width']) - (2 * rect_r_val)
else:
if self.other_cb.get_value():
try:
dia = float(apid_value['size']) - (2 * other_r_val)
except KeyError:
if ap_type == 'AM':
pol = apid_value['geometry'][0]['solid']
x0, y0, x1, y1 = pol.bounds
dx = x1 - x0
dy = y1 - y0
if dx <= dy:
dia = dx - (2 * other_r_val)
else:
dia = dy - (2 * other_r_val)
# if dia is None then none of the above applied so we skip the following
if dia is None:
continue
tool_in_drills = False
for tool, tool_val in tools.items():
if abs(float('%.*f' % (self.decimals, tool_val["C"])) - float('%.*f' % (self.decimals, dia))) < \
(10 ** -self.decimals):
tool_in_drills = tool
if tool_in_drills is False:
if tools:
new_tool = max([int(t) for t in tools]) + 1
tool_in_drills = str(new_tool)
else:
tool_in_drills = "1"
for geo_el in apid_value['geometry']:
if 'follow' in geo_el and isinstance(geo_el['follow'], Point):
if tool_in_drills not in tools:
tools[tool_in_drills] = {"C": dia}
drills.append({"point": geo_el['follow'], "tool": tool_in_drills})
if 'solid_geometry' not in tools[tool_in_drills]:
tools[tool_in_drills]['solid_geometry'] = []
else:
tools[tool_in_drills]['solid_geometry'].append(geo_el['follow'])
if tool_in_drills in tools:
if 'solid_geometry' not in tools[tool_in_drills] or not tools[tool_in_drills]['solid_geometry']:
drills_found.add(False)
else:
drills_found.add(True)
if True not in drills_found:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("No drills extracted. Try different parameters."))
return
else:
drills_found = set()
for apid, apid_value in fcobj.apertures.items():
ap_type = apid_value['type']
dia = None
if ap_type == 'C':
if self.circular_cb.get_value():
dia = float(apid_value['size']) * prop_factor
elif ap_type == 'O':
width = float(apid_value['width'])
height = float(apid_value['height'])
if self.oblong_cb.get_value():
if width > height:
dia = float(apid_value['height']) * prop_factor
else:
dia = float(apid_value['width']) * prop_factor
elif ap_type == 'R':
width = float(apid_value['width'])
height = float(apid_value['height'])
# if the height == width (float numbers so the reason for the following)
if abs(float('%.*f' % (self.decimals, width)) - float('%.*f' % (self.decimals, height))) < \
(10 ** -self.decimals):
if self.square_cb.get_value():
dia = float(apid_value['height']) * prop_factor
else:
if self.rectangular_cb.get_value():
if width > height:
dia = float(apid_value['height']) * prop_factor
else:
dia = float(apid_value['width']) * prop_factor
else:
if self.other_cb.get_value():
try:
dia = float(apid_value['size']) * prop_factor
except KeyError:
if ap_type == 'AM':
pol = apid_value['geometry'][0]['solid']
x0, y0, x1, y1 = pol.bounds
dx = x1 - x0
dy = y1 - y0
if dx <= dy:
dia = dx * prop_factor
else:
dia = dy * prop_factor
# if dia is None then none of the above applied so we skip the following
if dia is None:
continue
tool_in_drills = False
for tool, tool_val in tools.items():
if abs(float('%.*f' % (self.decimals, tool_val["C"])) - float('%.*f' % (self.decimals, dia))) < \
(10 ** -self.decimals):
tool_in_drills = tool
if tool_in_drills is False:
if tools:
new_tool = max([int(t) for t in tools]) + 1
tool_in_drills = str(new_tool)
else:
tool_in_drills = "1"
for geo_el in apid_value['geometry']:
if 'follow' in geo_el and isinstance(geo_el['follow'], Point):
if tool_in_drills not in tools:
tools[tool_in_drills] = {"C": dia}
drills.append({"point": geo_el['follow'], "tool": tool_in_drills})
if 'solid_geometry' not in tools[tool_in_drills]:
tools[tool_in_drills]['solid_geometry'] = []
else:
tools[tool_in_drills]['solid_geometry'].append(geo_el['follow'])
if tool_in_drills in tools:
if 'solid_geometry' not in tools[tool_in_drills] or not tools[tool_in_drills]['solid_geometry']:
drills_found.add(False)
else:
drills_found.add(True)
if True not in drills_found:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("No drills extracted. Try different parameters."))
return
def obj_init(obj_inst, app_inst):
obj_inst.tools = tools
obj_inst.drills = drills
obj_inst.create_geometry()
obj_inst.source_file = self.app.export_excellon(obj_name=outname, local_use=obj_inst, filename=None,
use_thread=False)
self.app.app_obj.new_object("excellon", outname, obj_init)
def on_hole_size_toggle(self, val):
if val == "fixed":
self.fixed_label.setDisabled(False)
self.dia_entry.setDisabled(False)
self.dia_label.setDisabled(False)
self.ring_frame.setDisabled(True)
self.prop_label.setDisabled(True)
self.factor_label.setDisabled(True)
self.factor_entry.setDisabled(True)
elif val == "ring":
self.fixed_label.setDisabled(True)
self.dia_entry.setDisabled(True)
self.dia_label.setDisabled(True)
self.ring_frame.setDisabled(False)
self.prop_label.setDisabled(True)
self.factor_label.setDisabled(True)
self.factor_entry.setDisabled(True)
elif val == "prop":
self.fixed_label.setDisabled(True)
self.dia_entry.setDisabled(True)
self.dia_label.setDisabled(True)
self.ring_frame.setDisabled(True)
self.prop_label.setDisabled(False)
self.factor_label.setDisabled(False)
self.factor_entry.setDisabled(False)
def reset_fields(self):
self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.gerber_object_combo.setCurrentIndex(0)

926
appTools/ToolFiducials.py Normal file
View File

@@ -0,0 +1,926 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 11/21/2019 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets, QtCore
from appTool import AppTool
from appGUI.GUIElements import FCDoubleSpinner, RadioSet, EvalEntry, FCTable, FCComboBox
from shapely.geometry import Point, Polygon, MultiPolygon, LineString
from shapely.geometry import box as box
import math
import logging
from copy import deepcopy
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class ToolFiducials(AppTool):
toolName = _("Fiducials Tool")
def __init__(self, app):
AppTool.__init__(self, app)
self.app = app
self.canvas = self.app.plotcanvas
self.decimals = self.app.decimals
self.units = ''
# ## 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(''))
self.points_label = QtWidgets.QLabel('<b>%s:</b>' % _('Fiducials Coordinates'))
self.points_label.setToolTip(
_("A table with the fiducial points coordinates,\n"
"in the format (x, y).")
)
self.layout.addWidget(self.points_label)
self.points_table = FCTable()
self.points_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.points_table.setColumnCount(3)
self.points_table.setHorizontalHeaderLabels(
[
'#',
_("Name"),
_("Coordinates"),
]
)
self.points_table.setRowCount(3)
row = 0
flags = QtCore.Qt.ItemIsEnabled
# BOTTOM LEFT
id_item_1 = QtWidgets.QTableWidgetItem('%d' % 1)
id_item_1.setFlags(flags)
self.points_table.setItem(row, 0, id_item_1) # Tool name/id
self.bottom_left_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Bottom Left'))
self.bottom_left_coords_lbl.setFlags(flags)
self.points_table.setItem(row, 1, self.bottom_left_coords_lbl)
self.bottom_left_coords_entry = EvalEntry()
self.points_table.setCellWidget(row, 2, self.bottom_left_coords_entry)
row += 1
# TOP RIGHT
id_item_2 = QtWidgets.QTableWidgetItem('%d' % 2)
id_item_2.setFlags(flags)
self.points_table.setItem(row, 0, id_item_2) # Tool name/id
self.top_right_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Top Right'))
self.top_right_coords_lbl.setFlags(flags)
self.points_table.setItem(row, 1, self.top_right_coords_lbl)
self.top_right_coords_entry = EvalEntry()
self.points_table.setCellWidget(row, 2, self.top_right_coords_entry)
row += 1
# Second Point
self.id_item_3 = QtWidgets.QTableWidgetItem('%d' % 3)
self.id_item_3.setFlags(flags)
self.points_table.setItem(row, 0, self.id_item_3) # Tool name/id
self.sec_point_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Second Point'))
self.sec_point_coords_lbl.setFlags(flags)
self.points_table.setItem(row, 1, self.sec_point_coords_lbl)
self.sec_points_coords_entry = EvalEntry()
self.points_table.setCellWidget(row, 2, self.sec_points_coords_entry)
vertical_header = self.points_table.verticalHeader()
vertical_header.hide()
self.points_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
horizontal_header = self.points_table.horizontalHeader()
horizontal_header.setMinimumSectionSize(10)
horizontal_header.setDefaultSectionSize(70)
self.points_table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
# for x in range(4):
# self.points_table.resizeColumnToContents(x)
self.points_table.resizeColumnsToContents()
self.points_table.resizeRowsToContents()
horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
horizontal_header.resizeSection(0, 20)
horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
self.points_table.setMinimumHeight(self.points_table.getHeight() + 2)
self.points_table.setMaximumHeight(self.points_table.getHeight() + 2)
# remove the frame on the QLineEdit childrens of the table
for row in range(self.points_table.rowCount()):
self.points_table.cellWidget(row, 2).setFrame(False)
self.layout.addWidget(self.points_table)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
self.layout.addWidget(separator_line)
# ## Grid Layout
grid_lay = QtWidgets.QGridLayout()
self.layout.addLayout(grid_lay)
grid_lay.setColumnStretch(0, 0)
grid_lay.setColumnStretch(1, 1)
self.param_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
self.param_label.setToolTip(
_("Parameters used for this tool.")
)
grid_lay.addWidget(self.param_label, 0, 0, 1, 2)
# DIAMETER #
self.size_label = QtWidgets.QLabel('%s:' % _("Size"))
self.size_label.setToolTip(
_("This set the fiducial diameter if fiducial type is circular,\n"
"otherwise is the size of the fiducial.\n"
"The soldermask opening is double than that.")
)
self.fid_size_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.fid_size_entry.set_range(1.0000, 3.0000)
self.fid_size_entry.set_precision(self.decimals)
self.fid_size_entry.setWrapping(True)
self.fid_size_entry.setSingleStep(0.1)
grid_lay.addWidget(self.size_label, 1, 0)
grid_lay.addWidget(self.fid_size_entry, 1, 1)
# MARGIN #
self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
self.margin_label.setToolTip(
_("Bounding box margin.")
)
self.margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.margin_entry.set_range(-9999.9999, 9999.9999)
self.margin_entry.set_precision(self.decimals)
self.margin_entry.setSingleStep(0.1)
grid_lay.addWidget(self.margin_label, 2, 0)
grid_lay.addWidget(self.margin_entry, 2, 1)
# Mode #
self.mode_radio = RadioSet([
{'label': _('Auto'), 'value': 'auto'},
{"label": _("Manual"), "value": "manual"}
], stretch=False)
self.mode_label = QtWidgets.QLabel(_("Mode:"))
self.mode_label.setToolTip(
_("- 'Auto' - automatic placement of fiducials in the corners of the bounding box.\n "
"- 'Manual' - manual placement of fiducials.")
)
grid_lay.addWidget(self.mode_label, 3, 0)
grid_lay.addWidget(self.mode_radio, 3, 1)
# Position for second fiducial #
self.pos_radio = RadioSet([
{'label': _('Up'), 'value': 'up'},
{"label": _("Down"), "value": "down"},
{"label": _("None"), "value": "no"}
], stretch=False)
self.pos_label = QtWidgets.QLabel('%s:' % _("Second fiducial"))
self.pos_label.setToolTip(
_("The position for the second fiducial.\n"
"- 'Up' - the order is: bottom-left, top-left, top-right.\n"
"- 'Down' - the order is: bottom-left, bottom-right, top-right.\n"
"- 'None' - there is no second fiducial. The order is: bottom-left, top-right.")
)
grid_lay.addWidget(self.pos_label, 4, 0)
grid_lay.addWidget(self.pos_radio, 4, 1)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid_lay.addWidget(separator_line, 5, 0, 1, 2)
# Fiducial type #
self.fid_type_radio = RadioSet([
{'label': _('Circular'), 'value': 'circular'},
{"label": _("Cross"), "value": "cross"},
{"label": _("Chess"), "value": "chess"}
], stretch=False)
self.fid_type_label = QtWidgets.QLabel('%s:' % _("Fiducial Type"))
self.fid_type_label.setToolTip(
_("The type of fiducial.\n"
"- 'Circular' - this is the regular fiducial.\n"
"- 'Cross' - cross lines fiducial.\n"
"- 'Chess' - chess pattern fiducial.")
)
grid_lay.addWidget(self.fid_type_label, 6, 0)
grid_lay.addWidget(self.fid_type_radio, 6, 1)
# Line Thickness #
self.line_thickness_label = QtWidgets.QLabel('%s:' % _("Line thickness"))
self.line_thickness_label.setToolTip(
_("Thickness of the line that makes the fiducial.")
)
self.line_thickness_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.line_thickness_entry.set_range(0.00001, 9999.9999)
self.line_thickness_entry.set_precision(self.decimals)
self.line_thickness_entry.setSingleStep(0.1)
grid_lay.addWidget(self.line_thickness_label, 7, 0)
grid_lay.addWidget(self.line_thickness_entry, 7, 1)
separator_line_1 = QtWidgets.QFrame()
separator_line_1.setFrameShape(QtWidgets.QFrame.HLine)
separator_line_1.setFrameShadow(QtWidgets.QFrame.Sunken)
grid_lay.addWidget(separator_line_1, 8, 0, 1, 2)
# Copper Gerber object
self.grb_object_combo = FCComboBox()
self.grb_object_combo.setModel(self.app.collection)
self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.grb_object_combo.is_last = True
self.grb_object_combo.obj_type = "Gerber"
self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
self.grbobj_label.setToolTip(
_("Gerber Object to which will be added a copper thieving.")
)
grid_lay.addWidget(self.grbobj_label, 9, 0, 1, 2)
grid_lay.addWidget(self.grb_object_combo, 10, 0, 1, 2)
# ## Insert Copper Fiducial
self.add_cfid_button = QtWidgets.QPushButton(_("Add Fiducial"))
self.add_cfid_button.setToolTip(
_("Will add a polygon on the copper layer to serve as fiducial.")
)
self.add_cfid_button.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
grid_lay.addWidget(self.add_cfid_button, 11, 0, 1, 2)
separator_line_2 = QtWidgets.QFrame()
separator_line_2.setFrameShape(QtWidgets.QFrame.HLine)
separator_line_2.setFrameShadow(QtWidgets.QFrame.Sunken)
grid_lay.addWidget(separator_line_2, 12, 0, 1, 2)
# Soldermask Gerber object #
self.sm_object_label = QtWidgets.QLabel('<b>%s:</b>' % _("Soldermask Gerber"))
self.sm_object_label.setToolTip(
_("The Soldermask Gerber object.")
)
self.sm_object_combo = FCComboBox()
self.sm_object_combo.setModel(self.app.collection)
self.sm_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.sm_object_combo.is_last = True
self.sm_object_combo.obj_type = "Gerber"
grid_lay.addWidget(self.sm_object_label, 13, 0, 1, 2)
grid_lay.addWidget(self.sm_object_combo, 14, 0, 1, 2)
# ## Insert Soldermask opening for Fiducial
self.add_sm_opening_button = QtWidgets.QPushButton(_("Add Soldermask Opening"))
self.add_sm_opening_button.setToolTip(
_("Will add a polygon on the soldermask layer\n"
"to serve as fiducial opening.\n"
"The diameter is always double of the diameter\n"
"for the copper fiducial.")
)
self.add_sm_opening_button.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
grid_lay.addWidget(self.add_sm_opening_button, 15, 0, 1, 2)
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)
# Objects involved in Copper thieving
self.grb_object = None
self.sm_object = None
self.copper_obj_set = set()
self.sm_obj_set = set()
# store the flattened geometry here:
self.flat_geometry = []
# Events ID
self.mr = None
self.mm = None
# Mouse cursor positions
self.cursor_pos = (0, 0)
self.first_click = False
self.mode_method = False
# Tool properties
self.fid_dia = None
self.sm_opening_dia = None
self.margin_val = None
self.sec_position = None
self.grb_steps_per_circle = self.app.defaults["gerber_circle_steps"]
self.click_points = []
# SIGNALS
self.add_cfid_button.clicked.connect(self.add_fiducials)
self.add_sm_opening_button.clicked.connect(self.add_soldermask_opening)
self.fid_type_radio.activated_custom.connect(self.on_fiducial_type)
self.pos_radio.activated_custom.connect(self.on_second_point)
self.mode_radio.activated_custom.connect(self.on_method_change)
self.reset_button.clicked.connect(self.set_tool_ui)
def run(self, toggle=True):
self.app.defaults.report_usage("ToolFiducials()")
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])
AppTool.run(self)
self.set_tool_ui()
self.app.ui.notebook.setTabText(2, _("Fiducials Tool"))
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='Alt+F', **kwargs)
def set_tool_ui(self):
self.units = self.app.defaults['units']
self.fid_size_entry.set_value(self.app.defaults["tools_fiducials_dia"])
self.margin_entry.set_value(float(self.app.defaults["tools_fiducials_margin"]))
self.mode_radio.set_value(self.app.defaults["tools_fiducials_mode"])
self.pos_radio.set_value(self.app.defaults["tools_fiducials_second_pos"])
self.fid_type_radio.set_value(self.app.defaults["tools_fiducials_type"])
self.line_thickness_entry.set_value(float(self.app.defaults["tools_fiducials_line_thickness"]))
self.click_points = []
self.bottom_left_coords_entry.set_value('')
self.top_right_coords_entry.set_value('')
self.sec_points_coords_entry.set_value('')
self.copper_obj_set = set()
self.sm_obj_set = set()
def on_second_point(self, val):
if val == 'no':
self.id_item_3.setFlags(QtCore.Qt.NoItemFlags)
self.sec_point_coords_lbl.setFlags(QtCore.Qt.NoItemFlags)
self.sec_points_coords_entry.setDisabled(True)
else:
self.id_item_3.setFlags(QtCore.Qt.ItemIsEnabled)
self.sec_point_coords_lbl.setFlags(QtCore.Qt.ItemIsEnabled)
self.sec_points_coords_entry.setDisabled(False)
def on_method_change(self, val):
"""
Make sure that on method change we disconnect the event handlers and reset the points storage
:param val: value of the Radio button which trigger this method
:return: None
"""
if val == 'auto':
self.click_points = []
try:
self.disconnect_event_handlers()
except TypeError:
pass
def on_fiducial_type(self, val):
if val == 'cross':
self.line_thickness_label.setDisabled(False)
self.line_thickness_entry.setDisabled(False)
else:
self.line_thickness_label.setDisabled(True)
self.line_thickness_entry.setDisabled(True)
def add_fiducials(self):
self.app.call_source = "fiducials_tool"
self.mode_method = self.mode_radio.get_value()
self.margin_val = self.margin_entry.get_value()
self.sec_position = self.pos_radio.get_value()
fid_type = self.fid_type_radio.get_value()
self.click_points = []
# get the Gerber object on which the Fiducial will be inserted
selection_index = self.grb_object_combo.currentIndex()
model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
try:
self.grb_object = model_index.internalPointer().obj
except Exception as e:
log.debug("ToolFiducials.execute() --> %s" % str(e))
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
return 'fail'
self.copper_obj_set.add(self.grb_object.options['name'])
if self.mode_method == 'auto':
xmin, ymin, xmax, ymax = self.grb_object.bounds()
bbox = box(xmin, ymin, xmax, ymax)
buf_bbox = bbox.buffer(self.margin_val, self.grb_steps_per_circle, join_style=2)
x0, y0, x1, y1 = buf_bbox.bounds
self.click_points.append(
(
float('%.*f' % (self.decimals, x0)),
float('%.*f' % (self.decimals, y0))
)
)
self.bottom_left_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x0, self.decimals, y0))
self.click_points.append(
(
float('%.*f' % (self.decimals, x1)),
float('%.*f' % (self.decimals, y1))
)
)
self.top_right_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x1, self.decimals, y1))
if self.sec_position == 'up':
self.click_points.append(
(
float('%.*f' % (self.decimals, x0)),
float('%.*f' % (self.decimals, y1))
)
)
self.sec_points_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x0, self.decimals, y1))
elif self.sec_position == 'down':
self.click_points.append(
(
float('%.*f' % (self.decimals, x1)),
float('%.*f' % (self.decimals, y0))
)
)
self.sec_points_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x1, self.decimals, y0))
self.add_fiducials_geo(self.click_points, g_obj=self.grb_object, fid_type=fid_type)
self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'],
filename=None,
local_use=self.grb_object, use_thread=False)
self.on_exit()
else:
self.app.inform.emit(_("Click to add first Fiducial. Bottom Left..."))
self.bottom_left_coords_entry.set_value('')
self.top_right_coords_entry.set_value('')
self.sec_points_coords_entry.set_value('')
self.connect_event_handlers()
# To be called after clicking on the plot.
def add_fiducials_geo(self, points_list, g_obj, fid_size=None, fid_type=None, line_size=None):
"""
Add geometry to the solid_geometry of the copper Gerber object
:param points_list: list of coordinates for the fiducials
:param g_obj: the Gerber object where to add the geometry
:param fid_size: the overall size of the fiducial or fiducial opening depending on the g_obj type
:param fid_type: the type of fiducial: circular or cross
:param line_size: the line thickenss when the fiducial type is cross
:return:
"""
fid_size = self.fid_size_entry.get_value() if fid_size is None else fid_size
fid_type = 'circular' if fid_type is None else fid_type
line_thickness = self.line_thickness_entry.get_value() if line_size is None else line_size
radius = fid_size / 2.0
if fid_type == 'circular':
geo_list = [Point(pt).buffer(radius, self.grb_steps_per_circle) for pt in points_list]
aperture_found = None
for ap_id, ap_val in g_obj.apertures.items():
if ap_val['type'] == 'C' and ap_val['size'] == fid_size:
aperture_found = ap_id
break
if aperture_found:
for geo in geo_list:
dict_el = {}
dict_el['follow'] = geo.centroid
dict_el['solid'] = geo
g_obj.apertures[aperture_found]['geometry'].append(deepcopy(dict_el))
else:
ap_keys = list(g_obj.apertures.keys())
if ap_keys:
new_apid = str(int(max(ap_keys)) + 1)
else:
new_apid = '10'
g_obj.apertures[new_apid] = {}
g_obj.apertures[new_apid]['type'] = 'C'
g_obj.apertures[new_apid]['size'] = fid_size
g_obj.apertures[new_apid]['geometry'] = []
for geo in geo_list:
dict_el = {}
dict_el['follow'] = geo.centroid
dict_el['solid'] = geo
g_obj.apertures[new_apid]['geometry'].append(deepcopy(dict_el))
s_list = []
if g_obj.solid_geometry:
try:
for poly in g_obj.solid_geometry:
s_list.append(poly)
except TypeError:
s_list.append(g_obj.solid_geometry)
s_list += geo_list
g_obj.solid_geometry = MultiPolygon(s_list)
elif fid_type == 'cross':
geo_list = []
for pt in points_list:
x = pt[0]
y = pt[1]
line_geo_hor = LineString([
(x - radius + (line_thickness / 2.0), y), (x + radius - (line_thickness / 2.0), y)
])
line_geo_vert = LineString([
(x, y - radius + (line_thickness / 2.0)), (x, y + radius - (line_thickness / 2.0))
])
geo_list.append([line_geo_hor, line_geo_vert])
aperture_found = None
for ap_id, ap_val in g_obj.apertures.items():
if ap_val['type'] == 'C' and ap_val['size'] == line_thickness:
aperture_found = ap_id
break
geo_buff_list = []
if aperture_found:
for geo in geo_list:
geo_buff_h = geo[0].buffer(line_thickness / 2.0, self.grb_steps_per_circle)
geo_buff_v = geo[1].buffer(line_thickness / 2.0, self.grb_steps_per_circle)
geo_buff_list.append(geo_buff_h)
geo_buff_list.append(geo_buff_v)
dict_el = {}
dict_el['follow'] = geo_buff_h.centroid
dict_el['solid'] = geo_buff_h
g_obj.apertures[aperture_found]['geometry'].append(deepcopy(dict_el))
dict_el['follow'] = geo_buff_v.centroid
dict_el['solid'] = geo_buff_v
g_obj.apertures[aperture_found]['geometry'].append(deepcopy(dict_el))
else:
ap_keys = list(g_obj.apertures.keys())
if ap_keys:
new_apid = str(int(max(ap_keys)) + 1)
else:
new_apid = '10'
g_obj.apertures[new_apid] = {}
g_obj.apertures[new_apid]['type'] = 'C'
g_obj.apertures[new_apid]['size'] = line_thickness
g_obj.apertures[new_apid]['geometry'] = []
for geo in geo_list:
geo_buff_h = geo[0].buffer(line_thickness / 2.0, self.grb_steps_per_circle)
geo_buff_v = geo[1].buffer(line_thickness / 2.0, self.grb_steps_per_circle)
geo_buff_list.append(geo_buff_h)
geo_buff_list.append(geo_buff_v)
dict_el = {}
dict_el['follow'] = geo_buff_h.centroid
dict_el['solid'] = geo_buff_h
g_obj.apertures[new_apid]['geometry'].append(deepcopy(dict_el))
dict_el['follow'] = geo_buff_v.centroid
dict_el['solid'] = geo_buff_v
g_obj.apertures[new_apid]['geometry'].append(deepcopy(dict_el))
s_list = []
if g_obj.solid_geometry:
try:
for poly in g_obj.solid_geometry:
s_list.append(poly)
except TypeError:
s_list.append(g_obj.solid_geometry)
geo_buff_list = MultiPolygon(geo_buff_list)
geo_buff_list = geo_buff_list.buffer(0)
for poly in geo_buff_list:
s_list.append(poly)
g_obj.solid_geometry = MultiPolygon(s_list)
else:
# chess pattern fiducial type
geo_list = []
def make_square_poly(center_pt, side_size):
half_s = side_size / 2
x_center = center_pt[0]
y_center = center_pt[1]
pt1 = (x_center - half_s, y_center - half_s)
pt2 = (x_center + half_s, y_center - half_s)
pt3 = (x_center + half_s, y_center + half_s)
pt4 = (x_center - half_s, y_center + half_s)
return Polygon([pt1, pt2, pt3, pt4, pt1])
for pt in points_list:
x = pt[0]
y = pt[1]
first_square = make_square_poly(center_pt=(x-fid_size/4, y+fid_size/4), side_size=fid_size/2)
second_square = make_square_poly(center_pt=(x+fid_size/4, y-fid_size/4), side_size=fid_size/2)
geo_list += [first_square, second_square]
aperture_found = None
new_ap_size = math.sqrt(fid_size**2 + fid_size**2)
for ap_id, ap_val in g_obj.apertures.items():
if ap_val['type'] == 'R' and \
round(ap_val['size'], ndigits=self.decimals) == round(new_ap_size, ndigits=self.decimals):
aperture_found = ap_id
break
geo_buff_list = []
if aperture_found:
for geo in geo_list:
geo_buff_list.append(geo)
dict_el = {}
dict_el['follow'] = geo.centroid
dict_el['solid'] = geo
g_obj.apertures[aperture_found]['geometry'].append(deepcopy(dict_el))
else:
ap_keys = list(g_obj.apertures.keys())
if ap_keys:
new_apid = str(int(max(ap_keys)) + 1)
else:
new_apid = '10'
g_obj.apertures[new_apid] = {}
g_obj.apertures[new_apid]['type'] = 'R'
g_obj.apertures[new_apid]['size'] = new_ap_size
g_obj.apertures[new_apid]['width'] = fid_size
g_obj.apertures[new_apid]['height'] = fid_size
g_obj.apertures[new_apid]['geometry'] = []
for geo in geo_list:
geo_buff_list.append(geo)
dict_el = {}
dict_el['follow'] = geo.centroid
dict_el['solid'] = geo
g_obj.apertures[new_apid]['geometry'].append(deepcopy(dict_el))
s_list = []
if g_obj.solid_geometry:
try:
for poly in g_obj.solid_geometry:
s_list.append(poly)
except TypeError:
s_list.append(g_obj.solid_geometry)
for poly in geo_buff_list:
s_list.append(poly)
g_obj.solid_geometry = MultiPolygon(s_list)
def add_soldermask_opening(self):
sm_opening_dia = self.fid_size_entry.get_value() * 2.0
# get the Gerber object on which the Fiducial will be inserted
selection_index = self.sm_object_combo.currentIndex()
model_index = self.app.collection.index(selection_index, 0, self.sm_object_combo.rootModelIndex())
try:
self.sm_object = model_index.internalPointer().obj
except Exception as e:
log.debug("ToolFiducials.add_soldermask_opening() --> %s" % str(e))
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
return 'fail'
self.sm_obj_set.add(self.sm_object.options['name'])
self.add_fiducials_geo(self.click_points, g_obj=self.sm_object, fid_size=sm_opening_dia, fid_type='circular')
self.sm_object.source_file = self.app.export_gerber(obj_name=self.sm_object.options['name'], filename=None,
local_use=self.sm_object, use_thread=False)
self.on_exit()
def on_mouse_release(self, event):
if event.button == 1:
if self.app.is_legacy is False:
event_pos = event.pos
else:
event_pos = (event.xdata, event.ydata)
pos_canvas = self.canvas.translate_coords(event_pos)
if self.app.grid_status():
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
else:
pos = (pos_canvas[0], pos_canvas[1])
click_pt = Point([pos[0], pos[1]])
self.click_points.append(
(
float('%.*f' % (self.decimals, click_pt.x)),
float('%.*f' % (self.decimals, click_pt.y))
)
)
self.check_points()
def check_points(self):
fid_type = self.fid_type_radio.get_value()
if len(self.click_points) == 1:
self.bottom_left_coords_entry.set_value(self.click_points[0])
self.app.inform.emit(_("Click to add the last fiducial. Top Right..."))
if self.sec_position != 'no':
if len(self.click_points) == 2:
self.top_right_coords_entry.set_value(self.click_points[1])
self.app.inform.emit(_("Click to add the second fiducial. Top Left or Bottom Right..."))
elif len(self.click_points) == 3:
self.sec_points_coords_entry.set_value(self.click_points[2])
self.app.inform.emit('[success] %s' % _("Done. All fiducials have been added."))
self.add_fiducials_geo(self.click_points, g_obj=self.grb_object, fid_type=fid_type)
self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'],
filename=None,
local_use=self.grb_object, use_thread=False)
self.on_exit()
else:
if len(self.click_points) == 2:
self.top_right_coords_entry.set_value(self.click_points[1])
self.app.inform.emit('[success] %s' % _("Done. All fiducials have been added."))
self.add_fiducials_geo(self.click_points, g_obj=self.grb_object, fid_type=fid_type)
self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'],
filename=None,
local_use=self.grb_object, use_thread=False)
self.on_exit()
def on_mouse_move(self, event):
pass
def replot(self, obj, run_thread=True):
def worker_task():
with self.app.proc_container.new('%s...' % _("Plotting")):
obj.plot()
if run_thread:
self.app.worker_task.emit({'fcn': worker_task, 'params': []})
else:
worker_task()
def on_exit(self):
# plot the object
for ob_name in self.copper_obj_set:
try:
copper_obj = self.app.collection.get_by_name(name=ob_name)
if len(self.copper_obj_set) > 1:
self.replot(obj=copper_obj, run_thread=False)
else:
self.replot(obj=copper_obj)
except (AttributeError, TypeError):
continue
# update the bounding box values
try:
a, b, c, d = copper_obj.bounds()
copper_obj.options['xmin'] = a
copper_obj.options['ymin'] = b
copper_obj.options['xmax'] = c
copper_obj.options['ymax'] = d
except Exception as e:
log.debug("ToolFiducials.on_exit() copper_obj bounds error --> %s" % str(e))
for ob_name in self.sm_obj_set:
try:
sm_obj = self.app.collection.get_by_name(name=ob_name)
if len(self.sm_obj_set) > 1:
self.replot(obj=sm_obj, run_thread=False)
else:
self.replot(obj=sm_obj)
except (AttributeError, TypeError):
continue
# update the bounding box values
try:
a, b, c, d = sm_obj.bounds()
sm_obj.options['xmin'] = a
sm_obj.options['ymin'] = b
sm_obj.options['xmax'] = c
sm_obj.options['ymax'] = d
except Exception as e:
log.debug("ToolFiducials.on_exit() sm_obj bounds error --> %s" % str(e))
# reset the variables
self.grb_object = None
self.sm_object = None
# Events ID
self.mr = None
# self.mm = None
# Mouse cursor positions
self.cursor_pos = (0, 0)
self.first_click = False
self.disconnect_event_handlers()
self.app.call_source = "app"
self.app.inform.emit('[success] %s' % _("Fiducials Tool exit."))
def connect_event_handlers(self):
if self.app.is_legacy is False:
self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
# self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
else:
self.app.plotcanvas.graph_event_disconnect(self.app.mp)
# self.app.plotcanvas.graph_event_disconnect(self.app.mm)
self.app.plotcanvas.graph_event_disconnect(self.app.mr)
self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
# self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
def disconnect_event_handlers(self):
if self.app.is_legacy is False:
self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
# self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
else:
self.app.plotcanvas.graph_event_disconnect(self.mr)
# self.app.plotcanvas.graph_event_disconnect(self.mm)
self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
self.app.on_mouse_click_over_plot)
# self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
# self.app.on_mouse_move_over_plot)
self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
self.app.on_mouse_click_release_over_plot)
def flatten(self, geometry):
"""
Creates a list of non-iterable linear geometry objects.
:param geometry: Shapely type or list or list of list of such.
Results are placed in self.flat_geometry
"""
# ## If iterable, expand recursively.
try:
for geo in geometry:
if geo is not None:
self.flatten(geometry=geo)
# ## Not iterable, do the actual indexing and add.
except TypeError:
self.flat_geometry.append(geometry)
return self.flat_geometry

1258
appTools/ToolFilm.py Normal file

File diff suppressed because it is too large Load Diff

297
appTools/ToolImage.py Normal file
View File

@@ -0,0 +1,297 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 3/10/2019 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtGui, QtWidgets
from appTool import AppTool
from appGUI.GUIElements import RadioSet, FCComboBox, FCSpinner
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
class ToolImage(AppTool):
toolName = _("Image as Object")
def __init__(self, app):
AppTool.__init__(self, app)
self.app = app
self.decimals = self.app.decimals
# Title
title_label = QtWidgets.QLabel("%s" % _('Image to PCB'))
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.layout.addWidget(title_label)
# Form Layout
ti_form_layout = QtWidgets.QFormLayout()
self.layout.addLayout(ti_form_layout)
# Type of object to create for the image
self.tf_type_obj_combo = FCComboBox()
self.tf_type_obj_combo.addItems([_("Gerber"), _("Geometry")])
self.tf_type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
self.tf_type_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
self.tf_type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Object Type"))
self.tf_type_obj_combo_label.setToolTip(
_("Specify the type of object to create from the image.\n"
"It can be of type: Gerber or Geometry.")
)
ti_form_layout.addRow(self.tf_type_obj_combo_label, self.tf_type_obj_combo)
# DPI value of the imported image
self.dpi_entry = FCSpinner(callback=self.confirmation_message_int)
self.dpi_entry.set_range(0, 99999)
self.dpi_label = QtWidgets.QLabel('%s:' % _("DPI value"))
self.dpi_label.setToolTip(_("Specify a DPI value for the image.") )
ti_form_layout.addRow(self.dpi_label, self.dpi_entry)
self.emty_lbl = QtWidgets.QLabel("")
self.layout.addWidget(self.emty_lbl)
self.detail_label = QtWidgets.QLabel("<font size=4><b>%s:</b></font>" % _('Level of detail'))
self.layout.addWidget(self.detail_label)
ti2_form_layout = QtWidgets.QFormLayout()
self.layout.addLayout(ti2_form_layout)
# Type of image interpretation
self.image_type = RadioSet([{'label': 'B/W', 'value': 'black'},
{'label': 'Color', 'value': 'color'}])
self.image_type_label = QtWidgets.QLabel("<b>%s:</b>" % _('Image type'))
self.image_type_label.setToolTip(
_("Choose a method for the image interpretation.\n"
"B/W means a black & white image. Color means a colored image.")
)
ti2_form_layout.addRow(self.image_type_label, self.image_type)
# Mask value of the imported image when image monochrome
self.mask_bw_entry = FCSpinner(callback=self.confirmation_message_int)
self.mask_bw_entry.set_range(0, 255)
self.mask_bw_label = QtWidgets.QLabel("%s <b>B/W</b>:" % _('Mask value'))
self.mask_bw_label.setToolTip(
_("Mask for monochrome image.\n"
"Takes values between [0 ... 255].\n"
"Decides the level of details to include\n"
"in the resulting geometry.\n"
"0 means no detail and 255 means everything \n"
"(which is totally black).")
)
ti2_form_layout.addRow(self.mask_bw_label, self.mask_bw_entry)
# Mask value of the imported image for RED color when image color
self.mask_r_entry = FCSpinner(callback=self.confirmation_message_int)
self.mask_r_entry.set_range(0, 255)
self.mask_r_label = QtWidgets.QLabel("%s <b>R:</b>" % _('Mask value'))
self.mask_r_label.setToolTip(
_("Mask for RED color.\n"
"Takes values between [0 ... 255].\n"
"Decides the level of details to include\n"
"in the resulting geometry.")
)
ti2_form_layout.addRow(self.mask_r_label, self.mask_r_entry)
# Mask value of the imported image for GREEN color when image color
self.mask_g_entry = FCSpinner(callback=self.confirmation_message_int)
self.mask_g_entry.set_range(0, 255)
self.mask_g_label = QtWidgets.QLabel("%s <b>G:</b>" % _('Mask value'))
self.mask_g_label.setToolTip(
_("Mask for GREEN color.\n"
"Takes values between [0 ... 255].\n"
"Decides the level of details to include\n"
"in the resulting geometry.")
)
ti2_form_layout.addRow(self.mask_g_label, self.mask_g_entry)
# Mask value of the imported image for BLUE color when image color
self.mask_b_entry = FCSpinner(callback=self.confirmation_message_int)
self.mask_b_entry.set_range(0, 255)
self.mask_b_label = QtWidgets.QLabel("%s <b>B:</b>" % _('Mask value'))
self.mask_b_label.setToolTip(
_("Mask for BLUE color.\n"
"Takes values between [0 ... 255].\n"
"Decides the level of details to include\n"
"in the resulting geometry.")
)
ti2_form_layout.addRow(self.mask_b_label, self.mask_b_entry)
# Buttons
self.import_button = QtWidgets.QPushButton(_("Import image"))
self.import_button.setToolTip(
_("Open a image of raster type and then import it in FlatCAM.")
)
self.layout.addWidget(self.import_button)
self.layout.addStretch()
self.on_image_type(val=False)
# ## Signals
self.import_button.clicked.connect(self.on_file_importimage)
self.image_type.activated_custom.connect(self.on_image_type)
def run(self, toggle=True):
self.app.defaults.report_usage("ToolImage()")
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])
AppTool.run(self)
self.set_tool_ui()
self.app.ui.notebook.setTabText(2, _("Image Tool"))
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, **kwargs)
def set_tool_ui(self):
# ## Initialize form
self.dpi_entry.set_value(96)
self.image_type.set_value('black')
self.mask_bw_entry.set_value(250)
self.mask_r_entry.set_value(250)
self.mask_g_entry.set_value(250)
self.mask_b_entry.set_value(250)
def on_image_type(self, val):
if val == 'color':
self.mask_r_label.setDisabled(False)
self.mask_r_entry.setDisabled(False)
self.mask_g_label.setDisabled(False)
self.mask_g_entry.setDisabled(False)
self.mask_b_label.setDisabled(False)
self.mask_b_entry.setDisabled(False)
self.mask_bw_label.setDisabled(True)
self.mask_bw_entry.setDisabled(True)
else:
self.mask_r_label.setDisabled(True)
self.mask_r_entry.setDisabled(True)
self.mask_g_label.setDisabled(True)
self.mask_g_entry.setDisabled(True)
self.mask_b_label.setDisabled(True)
self.mask_b_entry.setDisabled(True)
self.mask_bw_label.setDisabled(False)
self.mask_bw_entry.setDisabled(False)
def on_file_importimage(self):
"""
Callback for menu item File->Import IMAGE.
:param type_of_obj: to import the IMAGE as Geometry or as Gerber
:type type_of_obj: str
:return: None
"""
mask = []
self.app.log.debug("on_file_importimage()")
_filter = "Image Files(*.BMP *.PNG *.JPG *.JPEG);;" \
"Bitmap File (*.BMP);;" \
"PNG File (*.PNG);;" \
"Jpeg File (*.JPG);;" \
"All Files (*.*)"
try:
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import IMAGE"),
directory=self.app.get_last_folder(), filter=_filter)
except TypeError:
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import IMAGE"), filter=filter)
filename = str(filename)
type_obj = self.tf_type_obj_combo.get_value()
dpi = self.dpi_entry.get_value()
mode = self.image_type.get_value()
mask = [self.mask_bw_entry.get_value(), self.mask_r_entry.get_value(), self.mask_g_entry.get_value(),
self.mask_b_entry.get_value()]
if filename == "":
self.app.inform.emit(_("Cancelled."))
else:
self.app.worker_task.emit({'fcn': self.import_image,
'params': [filename, type_obj, dpi, mode, mask]})
def import_image(self, filename, o_type=_("Gerber"), dpi=96, mode='black', mask=None, outname=None):
"""
Adds a new Geometry Object to the projects and populates
it with shapes extracted from the SVG file.
:param filename: Path to the SVG file.
:param o_type: type of FlatCAM objeect
:param dpi: dot per inch
:param mode: black or color
:param mask: dictate the level of detail
:param outname: name for the resulting file
:return:
"""
self.app.defaults.report_usage("import_image()")
if mask is None:
mask = [250, 250, 250, 250]
if o_type is None or o_type == _("Geometry"):
obj_type = "geometry"
elif o_type == _("Gerber"):
obj_type = "gerber"
else:
self.app.inform.emit('[ERROR_NOTCL] %s' %
_("Not supported type is picked as parameter. "
"Only Geometry and Gerber are supported"))
return
def obj_init(geo_obj, app_obj):
geo_obj.import_image(filename, units=units, dpi=dpi, mode=mode, mask=mask)
geo_obj.multigeo = False
with self.app.proc_container.new(_("Importing Image")) as proc:
# Object name
name = outname or filename.split('/')[-1].split('\\')[-1]
units = self.app.defaults['units']
self.app.app_obj.new_object(obj_type, name, obj_init)
# Register recent file
self.app.file_opened.emit("image", filename)
# GUI feedback
self.app.inform.emit('[success] %s: %s' % (_("Opened"), filename))

View File

@@ -0,0 +1,309 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 2/14/2020 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets, QtCore
from appTool import AppTool
from appGUI.GUIElements import FCButton, FCDoubleSpinner, RadioSet, FCComboBox
from shapely.geometry import box
from copy import deepcopy
import logging
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class ToolInvertGerber(AppTool):
toolName = _("Invert Gerber Tool")
def __init__(self, app):
self.app = app
self.decimals = self.app.decimals
AppTool.__init__(self, app)
self.tools_frame = QtWidgets.QFrame()
self.tools_frame.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(self.tools_frame)
self.tools_box = QtWidgets.QVBoxLayout()
self.tools_box.setContentsMargins(0, 0, 0, 0)
self.tools_frame.setLayout(self.tools_box)
# Title
title_label = QtWidgets.QLabel("%s" % self.toolName)
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.tools_box.addWidget(title_label)
# Grid Layout
grid0 = QtWidgets.QGridLayout()
grid0.setColumnStretch(0, 0)
grid0.setColumnStretch(1, 1)
self.tools_box.addLayout(grid0)
grid0.addWidget(QtWidgets.QLabel(''), 0, 0, 1, 2)
# Target Gerber Object
self.gerber_combo = FCComboBox()
self.gerber_combo.setModel(self.app.collection)
self.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.gerber_combo.is_last = True
self.gerber_combo.obj_type = "Gerber"
self.gerber_label = QtWidgets.QLabel('<b>%s:</b>' % _("GERBER"))
self.gerber_label.setToolTip(
_("Gerber object that will be inverted.")
)
grid0.addWidget(self.gerber_label, 1, 0, 1, 2)
grid0.addWidget(self.gerber_combo, 2, 0, 1, 2)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid0.addWidget(separator_line, 3, 0, 1, 2)
self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
self.param_label.setToolTip('%s.' % _("Parameters for this tool"))
grid0.addWidget(self.param_label, 4, 0, 1, 2)
# Margin
self.margin_label = QtWidgets.QLabel('%s:' % _('Margin'))
self.margin_label.setToolTip(
_("Distance by which to avoid\n"
"the edges of the Gerber object.")
)
self.margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.margin_entry.set_precision(self.decimals)
self.margin_entry.set_range(0.0000, 9999.9999)
self.margin_entry.setObjectName(_("Margin"))
grid0.addWidget(self.margin_label, 5, 0, 1, 2)
grid0.addWidget(self.margin_entry, 6, 0, 1, 2)
self.join_label = QtWidgets.QLabel('%s:' % _("Lines Join Style"))
self.join_label.setToolTip(
_("The way that the lines in the object outline will be joined.\n"
"Can be:\n"
"- rounded -> an arc is added between two joining lines\n"
"- square -> the lines meet in 90 degrees angle\n"
"- bevel -> the lines are joined by a third line")
)
self.join_radio = RadioSet([
{'label': 'Rounded', 'value': 'r'},
{'label': 'Square', 'value': 's'},
{'label': 'Bevel', 'value': 'b'}
], orientation='vertical', stretch=False)
grid0.addWidget(self.join_label, 7, 0, 1, 2)
grid0.addWidget(self.join_radio, 8, 0, 1, 2)
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.invert_btn = FCButton(_('Invert Gerber'))
self.invert_btn.setToolTip(
_("Will invert the Gerber object: areas that have copper\n"
"will be empty of copper and previous empty area will be\n"
"filled with copper.")
)
self.invert_btn.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
grid0.addWidget(self.invert_btn, 10, 0, 1, 2)
self.tools_box.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.tools_box.addWidget(self.reset_button)
self.invert_btn.clicked.connect(self.on_grb_invert)
self.reset_button.clicked.connect(self.set_tool_ui)
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='', **kwargs)
def run(self, toggle=True):
self.app.defaults.report_usage("ToolInvertGerber()")
log.debug("ToolInvertGerber() is running ...")
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])
AppTool.run(self)
self.set_tool_ui()
self.app.ui.notebook.setTabText(2, _("Invert Tool"))
def set_tool_ui(self):
self.margin_entry.set_value(float(self.app.defaults["tools_invert_margin"]))
self.join_radio.set_value(self.app.defaults["tools_invert_join_style"])
def on_grb_invert(self):
margin = self.margin_entry.get_value()
if round(margin, self.decimals) == 0.0:
margin = 1E-10
join_style = {'r': 1, 'b': 3, 's': 2}[self.join_radio.get_value()]
if join_style is None:
join_style = 'r'
grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
obj_name = self.gerber_combo.currentText()
outname = obj_name + "_inverted"
# Get source object.
try:
grb_obj = self.app.collection.get_by_name(obj_name)
except Exception as e:
self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(obj_name)))
return "Could not retrieve object: %s with error: %s" % (obj_name, str(e))
if grb_obj is None:
if obj_name == '':
obj_name = 'None'
self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(obj_name)))
return
xmin, ymin, xmax, ymax = grb_obj.bounds()
grb_box = box(xmin, ymin, xmax, ymax).buffer(margin, resolution=grb_circle_steps, join_style=join_style)
try:
__ = iter(grb_obj.solid_geometry)
except TypeError:
grb_obj.solid_geometry = list(grb_obj.solid_geometry)
new_solid_geometry = deepcopy(grb_box)
for poly in grb_obj.solid_geometry:
new_solid_geometry = new_solid_geometry.difference(poly)
try:
__ = iter(new_solid_geometry)
except TypeError:
new_solid_geometry = [new_solid_geometry]
new_options = {}
for opt in grb_obj.options:
new_options[opt] = deepcopy(grb_obj.options[opt])
new_apertures = {}
# for apid, val in grb_obj.apertures.items():
# new_apertures[apid] = {}
# for key in val:
# if key == 'geometry':
# new_apertures[apid]['geometry'] = []
# for elem in val['geometry']:
# geo_elem = {}
# if 'follow' in elem:
# try:
# geo_elem['clear'] = elem['follow'].buffer(val['size'] / 2.0).exterior
# except AttributeError:
# # TODO should test if width or height is bigger
# geo_elem['clear'] = elem['follow'].buffer(val['width'] / 2.0).exterior
# if 'clear' in elem:
# if isinstance(elem['clear'], Polygon):
# try:
# geo_elem['solid'] = elem['clear'].buffer(val['size'] / 2.0, grb_circle_steps)
# except AttributeError:
# # TODO should test if width or height is bigger
# geo_elem['solid'] = elem['clear'].buffer(val['width'] / 2.0, grb_circle_steps)
# else:
# geo_elem['follow'] = elem['clear']
# new_apertures[apid]['geometry'].append(deepcopy(geo_elem))
# else:
# new_apertures[apid][key] = deepcopy(val[key])
if '0' not in new_apertures:
new_apertures['0'] = {}
new_apertures['0']['type'] = 'C'
new_apertures['0']['size'] = 0.0
new_apertures['0']['geometry'] = []
try:
for poly in new_solid_geometry:
new_el = {}
new_el['solid'] = poly
new_el['follow'] = poly.exterior
new_apertures['0']['geometry'].append(new_el)
except TypeError:
new_el = {}
new_el['solid'] = new_solid_geometry
new_el['follow'] = new_solid_geometry.exterior
new_apertures['0']['geometry'].append(new_el)
def init_func(new_obj, app_obj):
new_obj.options.update(new_options)
new_obj.options['name'] = outname
new_obj.fill_color = deepcopy(grb_obj.fill_color)
new_obj.outline_color = deepcopy(grb_obj.outline_color)
new_obj.apertures = deepcopy(new_apertures)
new_obj.solid_geometry = deepcopy(new_solid_geometry)
new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None,
local_use=new_obj, use_thread=False)
self.app.app_obj.new_object('gerber', outname, init_func)
def reset_fields(self):
self.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
@staticmethod
def poly2rings(poly):
return [poly.exterior] + [interior for interior in poly.interiors]
# end of file

3018
appTools/ToolIsolation.py Normal file

File diff suppressed because it is too large Load Diff

341
appTools/ToolMove.py Normal file
View File

@@ -0,0 +1,341 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 3/10/2019 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets, QtCore
from appTool import AppTool
from appGUI.VisPyVisuals import *
from copy import copy
import logging
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class ToolMove(AppTool):
toolName = _("Move")
replot_signal = QtCore.pyqtSignal(list)
def __init__(self, app):
AppTool.__init__(self, app)
self.app = app
self.decimals = self.app.decimals
self.layout.setContentsMargins(0, 0, 3, 0)
self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Maximum)
self.clicked_move = 0
self.point1 = None
self.point2 = None
# the default state is disabled for the Move command
self.setVisible(False)
self.sel_rect = None
self.old_coords = []
# VisPy visuals
if self.app.is_legacy is False:
self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
else:
from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy
self.sel_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name="move")
self.mm = None
self.mp = None
self.kr = None
self.replot_signal[list].connect(self.replot)
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='M', **kwargs)
def run(self, toggle):
self.app.defaults.report_usage("ToolMove()")
if self.app.tool_tab_locked is True:
return
self.toggle()
def toggle(self, toggle=False):
if self.isVisible():
self.setVisible(False)
if self.app.is_legacy is False:
self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_move)
self.app.plotcanvas.graph_event_disconnect('mouse_press', self.on_left_click)
self.app.plotcanvas.graph_event_disconnect('key_release', self.on_key_press)
self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent)
else:
self.app.plotcanvas.graph_event_disconnect(self.mm)
self.app.plotcanvas.graph_event_disconnect(self.mp)
self.app.plotcanvas.graph_event_disconnect(self.kr)
self.app.kr = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent)
self.clicked_move = 0
# signal that there is no command active
self.app.command_active = None
# delete the selection box
self.delete_shape()
return
else:
self.setVisible(True)
# signal that there is a command active and it is 'Move'
self.app.command_active = "Move"
sel_obj_list = self.app.collection.get_selected()
if sel_obj_list:
self.app.inform.emit(_("MOVE: Click on the Start point ..."))
# if we have an object selected then we can safely activate the mouse events
self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_move)
self.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.on_left_click)
self.kr = self.app.plotcanvas.graph_event_connect('key_release', self.on_key_press)
# draw the selection box
self.draw_sel_bbox()
else:
self.toggle()
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. No object(s) to move."))
def on_left_click(self, event):
# mouse click will be accepted only if the left button is clicked
# this is necessary because right mouse click and middle mouse click
# are used for panning on the canvas
if self.app.is_legacy is False:
event_pos = event.pos
else:
event_pos = (event.xdata, event.ydata)
if event.button == 1:
if self.clicked_move == 0:
pos_canvas = self.app.plotcanvas.translate_coords(event_pos)
# if GRID is active we need to get the snapped positions
if self.app.grid_status():
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
else:
pos = pos_canvas
if self.point1 is None:
self.point1 = pos
else:
self.point2 = copy(self.point1)
self.point1 = pos
self.app.inform.emit(_("MOVE: Click on the Destination point ..."))
if self.clicked_move == 1:
try:
pos_canvas = self.app.plotcanvas.translate_coords(event_pos)
# delete the selection bounding box
self.delete_shape()
# if GRID is active we need to get the snapped positions
if self.app.grid_status():
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
else:
pos = pos_canvas
dx = pos[0] - self.point1[0]
dy = pos[1] - self.point1[1]
# move only the objects selected and plotted and visible
obj_list = [obj for obj in self.app.collection.get_selected()
if obj.options['plot'] and obj.visible is True]
def job_move(app_obj):
with self.app.proc_container.new(_("Moving...")) as proc:
if not obj_list:
app_obj.app.inform.emit('[WARNING_NOTCL] %s' % _("No object(s) selected."))
return "fail"
try:
# remove any mark aperture shape that may be displayed
for sel_obj in obj_list:
# if the Gerber mark shapes are enabled they need to be disabled before move
if sel_obj.kind == 'gerber':
sel_obj.ui.aperture_table_visibility_cb.setChecked(False)
try:
sel_obj.replotApertures.emit()
except Exception:
pass
# offset solid_geometry
sel_obj.offset((dx, dy))
# Update the object bounding box options
a, b, c, d = sel_obj.bounds()
sel_obj.options['xmin'] = a
sel_obj.options['ymin'] = b
sel_obj.options['xmax'] = c
sel_obj.options['ymax'] = d
# update the source_file with the new positions
for sel_obj in obj_list:
out_name = sel_obj.options["name"]
if sel_obj.kind == 'gerber':
sel_obj.source_file = self.app.export_gerber(
obj_name=out_name, filename=None, local_use=sel_obj, use_thread=False)
elif sel_obj.kind == 'excellon':
sel_obj.source_file = self.app.export_excellon(
obj_name=out_name, filename=None, local_use=sel_obj, use_thread=False)
except Exception as err:
log.debug('[ERROR_NOTCL] %s --> %s' % ('ToolMove.on_left_click()', str(err)))
return "fail"
# time to plot the moved objects
app_obj.replot_signal.emit(obj_list)
# delete the selection bounding box
self.delete_shape()
self.app.inform.emit('[success] %s %s' %
(str(sel_obj.kind).capitalize(), 'object was moved ...'))
self.app.worker_task.emit({'fcn': job_move, 'params': [self]})
self.clicked_move = 0
self.toggle()
return
except TypeError as e:
log.debug("ToolMove.on_left_click() --> %s" % str(e))
self.app.inform.emit('[ERROR_NOTCL] ToolMove.on_left_click() --> %s' %
_('Error when mouse left click.'))
return
self.clicked_move = 1
def replot(self, obj_list):
def worker_task():
with self.app.proc_container.new('%s...' % _("Plotting")):
for sel_obj in obj_list:
sel_obj.plot()
self.app.worker_task.emit({'fcn': worker_task, 'params': []})
def on_move(self, event):
if self.app.is_legacy is False:
event_pos = event.pos
else:
event_pos = (event.xdata, event.ydata)
try:
x = float(event_pos[0])
y = float(event_pos[1])
except TypeError:
return
pos_canvas = self.app.plotcanvas.translate_coords((x, y))
# if GRID is active we need to get the snapped positions
if self.app.grid_status():
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
else:
pos = pos_canvas
if self.point1 is None:
dx = pos[0]
dy = pos[1]
else:
dx = pos[0] - self.point1[0]
dy = pos[1] - self.point1[1]
if self.clicked_move == 1:
self.update_sel_bbox((dx, dy))
def on_key_press(self, event):
if event.key == 'escape':
# abort the move action
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
self.toggle()
return
def draw_sel_bbox(self):
xminlist = []
yminlist = []
xmaxlist = []
ymaxlist = []
obj_list = self.app.collection.get_selected()
# first get a bounding box to fit all
for obj in obj_list:
# don't move disabled objects, move only plotted objects
if obj.options['plot']:
xmin, ymin, xmax, ymax = obj.bounds()
xminlist.append(xmin)
yminlist.append(ymin)
xmaxlist.append(xmax)
ymaxlist.append(ymax)
# get the minimum x,y and maximum x,y for all objects selected
xminimal = min(xminlist)
yminimal = min(yminlist)
xmaximal = max(xmaxlist)
ymaximal = max(ymaxlist)
p1 = (xminimal, yminimal)
p2 = (xmaximal, yminimal)
p3 = (xmaximal, ymaximal)
p4 = (xminimal, ymaximal)
self.old_coords = [p1, p2, p3, p4]
self.draw_shape(Polygon(self.old_coords))
if self.app.is_legacy is True:
self.sel_shapes.redraw()
def update_sel_bbox(self, pos):
self.delete_shape()
pt1 = (self.old_coords[0][0] + pos[0], self.old_coords[0][1] + pos[1])
pt2 = (self.old_coords[1][0] + pos[0], self.old_coords[1][1] + pos[1])
pt3 = (self.old_coords[2][0] + pos[0], self.old_coords[2][1] + pos[1])
pt4 = (self.old_coords[3][0] + pos[0], self.old_coords[3][1] + pos[1])
self.draw_shape(Polygon([pt1, pt2, pt3, pt4]))
if self.app.is_legacy is True:
self.sel_shapes.redraw()
def delete_shape(self):
self.sel_shapes.clear()
self.sel_shapes.redraw()
def draw_shape(self, shape):
if self.app.defaults['units'].upper() == 'MM':
proc_shape = shape.buffer(-0.1)
proc_shape = proc_shape.buffer(0.2)
else:
proc_shape = shape.buffer(-0.00393)
proc_shape = proc_shape.buffer(0.00787)
# face = Color('blue')
# face.alpha = 0.2
face = '#0000FF' + str(hex(int(0.2 * 255)))[2:]
outline = '#0000FFAF'
self.sel_shapes.add(proc_shape, color=outline, face_color=face, update=True, layer=0, tolerance=None)
# end of file

4138
appTools/ToolNCC.py Normal file

File diff suppressed because it is too large Load Diff

596
appTools/ToolOptimal.py Normal file
View File

@@ -0,0 +1,596 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 09/27/2019 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets, QtCore, QtGui
from appTool import AppTool
from appGUI.GUIElements import OptionalHideInputSection, FCTextArea, FCEntry, FCSpinner, FCCheckBox, FCComboBox
from camlib import grace
from shapely.geometry import MultiPolygon
from shapely.ops import nearest_points
import numpy as np
import logging
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class ToolOptimal(AppTool):
toolName = _("Optimal Tool")
update_text = QtCore.pyqtSignal(list)
update_sec_distances = QtCore.pyqtSignal(dict)
def __init__(self, app):
AppTool.__init__(self, app)
self.units = self.app.defaults['units'].upper()
self.decimals = self.app.decimals
# ############################################################################
# ############################ GUI creation ##################################
# ## Title
title_label = QtWidgets.QLabel("%s" % self.toolName)
title_label.setStyleSheet(
"""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.layout.addWidget(title_label)
# ## Form Layout
form_lay = QtWidgets.QFormLayout()
self.layout.addLayout(form_lay)
form_lay.addRow(QtWidgets.QLabel(""))
# ## Gerber Object to mirror
self.gerber_object_combo = FCComboBox()
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.is_last = True
self.gerber_object_combo.obj_type = "Gerber"
self.gerber_object_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
self.gerber_object_label.setToolTip(
"Gerber object for which to find the minimum distance between copper features."
)
form_lay.addRow(self.gerber_object_label)
form_lay.addRow(self.gerber_object_combo)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
form_lay.addRow(separator_line)
# Precision = nr of decimals
self.precision_label = QtWidgets.QLabel('%s:' % _("Precision"))
self.precision_label.setToolTip(_("Number of decimals kept for found distances."))
self.precision_spinner = FCSpinner(callback=self.confirmation_message_int)
self.precision_spinner.set_range(2, 10)
self.precision_spinner.setWrapping(True)
form_lay.addRow(self.precision_label, self.precision_spinner)
# Results Title
self.title_res_label = QtWidgets.QLabel('<b>%s:</b>' % _("Minimum distance"))
self.title_res_label.setToolTip(_("Display minimum distance between copper features."))
form_lay.addRow(self.title_res_label)
# Result value
self.result_label = QtWidgets.QLabel('%s:' % _("Determined"))
self.result_entry = FCEntry()
self.result_entry.setReadOnly(True)
self.units_lbl = QtWidgets.QLabel(self.units.lower())
self.units_lbl.setDisabled(True)
hlay = QtWidgets.QHBoxLayout()
hlay.addWidget(self.result_entry)
hlay.addWidget(self.units_lbl)
form_lay.addRow(self.result_label, hlay)
# Frequency of minimum encounter
self.freq_label = QtWidgets.QLabel('%s:' % _("Occurring"))
self.freq_label.setToolTip(_("How many times this minimum is found."))
self.freq_entry = FCEntry()
self.freq_entry.setReadOnly(True)
form_lay.addRow(self.freq_label, self.freq_entry)
# Control if to display the locations of where the minimum was found
self.locations_cb = FCCheckBox(_("Minimum points coordinates"))
self.locations_cb.setToolTip(_("Coordinates for points where minimum distance was found."))
form_lay.addRow(self.locations_cb)
# Locations where minimum was found
self.locations_textb = FCTextArea(parent=self)
self.locations_textb.setPlaceholderText(
_("Coordinates for points where minimum distance was found.")
)
self.locations_textb.setReadOnly(True)
stylesheet = """
QTextEdit { selection-background-color:blue;
selection-color:white;
}
"""
self.locations_textb.setStyleSheet(stylesheet)
form_lay.addRow(self.locations_textb)
# Jump button
self.locate_button = QtWidgets.QPushButton(_("Jump to selected position"))
self.locate_button.setToolTip(
_("Select a position in the Locations text box and then\n"
"click this button.")
)
self.locate_button.setMinimumWidth(60)
self.locate_button.setDisabled(True)
form_lay.addRow(self.locate_button)
# Other distances in Gerber
self.title_second_res_label = QtWidgets.QLabel('<b>%s:</b>' % _("Other distances"))
self.title_second_res_label.setToolTip(_("Will display other distances in the Gerber file ordered from\n"
"the minimum to the maximum, not including the absolute minimum."))
form_lay.addRow(self.title_second_res_label)
# Control if to display the locations of where the minimum was found
self.sec_locations_cb = FCCheckBox(_("Other distances points coordinates"))
self.sec_locations_cb.setToolTip(_("Other distances and the coordinates for points\n"
"where the distance was found."))
form_lay.addRow(self.sec_locations_cb)
# this way I can hide/show the frame
self.sec_locations_frame = QtWidgets.QFrame()
self.sec_locations_frame.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(self.sec_locations_frame)
self.distances_box = QtWidgets.QVBoxLayout()
self.distances_box.setContentsMargins(0, 0, 0, 0)
self.sec_locations_frame.setLayout(self.distances_box)
# Other Distances label
self.distances_label = QtWidgets.QLabel('%s' % _("Gerber distances"))
self.distances_label.setToolTip(_("Other distances and the coordinates for points\n"
"where the distance was found."))
self.distances_box.addWidget(self.distances_label)
# Other distances
self.distances_textb = FCTextArea(parent=self)
self.distances_textb.setPlaceholderText(
_("Other distances and the coordinates for points\n"
"where the distance was found.")
)
self.distances_textb.setReadOnly(True)
stylesheet = """
QTextEdit { selection-background-color:blue;
selection-color:white;
}
"""
self.distances_textb.setStyleSheet(stylesheet)
self.distances_box.addWidget(self.distances_textb)
self.distances_box.addWidget(QtWidgets.QLabel(''))
# Other Locations label
self.locations_label = QtWidgets.QLabel('%s' % _("Points coordinates"))
self.locations_label.setToolTip(_("Other distances and the coordinates for points\n"
"where the distance was found."))
self.distances_box.addWidget(self.locations_label)
# Locations where minimum was found
self.locations_sec_textb = FCTextArea(parent=self)
self.locations_sec_textb.setPlaceholderText(
_("Other distances and the coordinates for points\n"
"where the distance was found.")
)
self.locations_sec_textb.setReadOnly(True)
stylesheet = """
QTextEdit { selection-background-color:blue;
selection-color:white;
}
"""
self.locations_sec_textb.setStyleSheet(stylesheet)
self.distances_box.addWidget(self.locations_sec_textb)
# Jump button
self.locate_sec_button = QtWidgets.QPushButton(_("Jump to selected position"))
self.locate_sec_button.setToolTip(
_("Select a position in the Locations text box and then\n"
"click this button.")
)
self.locate_sec_button.setMinimumWidth(60)
self.locate_sec_button.setDisabled(True)
self.distances_box.addWidget(self.locate_sec_button)
# GO button
self.calculate_button = QtWidgets.QPushButton(_("Find Minimum"))
self.calculate_button.setToolTip(
_("Calculate the minimum distance between copper features,\n"
"this will allow the determination of the right tool to\n"
"use for isolation or copper clearing.")
)
self.calculate_button.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
self.calculate_button.setMinimumWidth(60)
self.layout.addWidget(self.calculate_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.loc_ois = OptionalHideInputSection(self.locations_cb, [self.locations_textb, self.locate_button])
self.sec_loc_ois = OptionalHideInputSection(self.sec_locations_cb, [self.sec_locations_frame])
# ################## Finished GUI creation ###################################
# ############################################################################
# this is the line selected in the textbox with the locations of the minimum
self.selected_text = ''
# this is the line selected in the textbox with the locations of the other distances found in the Gerber object
self.selected_locations_text = ''
# dict to hold the distances between every two elements in Gerber as keys and the actual locations where that
# distances happen as values
self.min_dict = {}
# ############################################################################
# ############################ Signals #######################################
# ############################################################################
self.calculate_button.clicked.connect(self.find_minimum_distance)
self.locate_button.clicked.connect(self.on_locate_position)
self.update_text.connect(self.on_update_text)
self.locations_textb.cursorPositionChanged.connect(self.on_textbox_clicked)
self.locate_sec_button.clicked.connect(self.on_locate_sec_position)
self.update_sec_distances.connect(self.on_update_sec_distances_txt)
self.distances_textb.cursorPositionChanged.connect(self.on_distances_textb_clicked)
self.locations_sec_textb.cursorPositionChanged.connect(self.on_locations_sec_clicked)
self.reset_button.clicked.connect(self.set_tool_ui)
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='Alt+O', **kwargs)
def run(self, toggle=True):
self.app.defaults.report_usage("ToolOptimal()")
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])
AppTool.run(self)
self.set_tool_ui()
self.app.ui.notebook.setTabText(2, _("Optimal Tool"))
def set_tool_ui(self):
self.result_entry.set_value(0.0)
self.freq_entry.set_value('0')
self.precision_spinner.set_value(int(self.app.defaults["tools_opt_precision"]))
self.locations_textb.clear()
# new cursor - select all document
cursor = self.locations_textb.textCursor()
cursor.select(QtGui.QTextCursor.Document)
# clear previous selection highlight
tmp = cursor.blockFormat()
tmp.clearBackground()
cursor.setBlockFormat(tmp)
self.locations_textb.setVisible(False)
self.locate_button.setVisible(False)
self.result_entry.set_value(0.0)
self.freq_entry.set_value('0')
self.reset_fields()
def find_minimum_distance(self):
self.units = self.app.defaults['units'].upper()
self.decimals = int(self.precision_spinner.get_value())
selection_index = self.gerber_object_combo.currentIndex()
model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
try:
fcobj = model_index.internalPointer().obj
except Exception as e:
log.debug("ToolOptimal.find_minimum_distance() --> %s" % str(e))
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
return
if fcobj.kind != 'gerber':
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber objects can be evaluated."))
return
proc = self.app.proc_container.new(_("Working..."))
def job_thread(app_obj):
app_obj.inform.emit(_("Optimal Tool. Started to search for the minimum distance between copper features."))
try:
old_disp_number = 0
pol_nr = 0
app_obj.proc_container.update_view_text(' %d%%' % 0)
total_geo = []
for ap in list(fcobj.apertures.keys()):
if 'geometry' in fcobj.apertures[ap]:
app_obj.inform.emit(
'%s: %s' % (_("Optimal Tool. Parsing geometry for aperture"), str(ap)))
for geo_el in fcobj.apertures[ap]['geometry']:
if self.app.abort_flag:
# graceful abort requested by the user
raise grace
if 'solid' in geo_el and geo_el['solid'] is not None and geo_el['solid'].is_valid:
total_geo.append(geo_el['solid'])
app_obj.inform.emit(
_("Optimal Tool. Creating a buffer for the object geometry."))
total_geo = MultiPolygon(total_geo)
total_geo = total_geo.buffer(0)
try:
__ = iter(total_geo)
geo_len = len(total_geo)
geo_len = (geo_len * (geo_len - 1)) / 2
except TypeError:
app_obj.inform.emit('[ERROR_NOTCL] %s' %
_("The Gerber object has one Polygon as geometry.\n"
"There are no distances between geometry elements to be found."))
return 'fail'
app_obj.inform.emit(
'%s: %s' % (_("Optimal Tool. Finding the distances between each two elements. Iterations"),
str(geo_len)))
self.min_dict = {}
idx = 1
for geo in total_geo:
for s_geo in total_geo[idx:]:
if self.app.abort_flag:
# graceful abort requested by the user
raise grace
# minimize the number of distances by not taking into considerations those that are too small
dist = geo.distance(s_geo)
dist = float('%.*f' % (self.decimals, dist))
loc_1, loc_2 = nearest_points(geo, s_geo)
proc_loc = (
(float('%.*f' % (self.decimals, loc_1.x)), float('%.*f' % (self.decimals, loc_1.y))),
(float('%.*f' % (self.decimals, loc_2.x)), float('%.*f' % (self.decimals, loc_2.y)))
)
if dist in self.min_dict:
self.min_dict[dist].append(proc_loc)
else:
self.min_dict[dist] = [proc_loc]
pol_nr += 1
disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
if old_disp_number < disp_number <= 100:
app_obj.proc_container.update_view_text(' %d%%' % disp_number)
old_disp_number = disp_number
idx += 1
app_obj.inform.emit(
_("Optimal Tool. Finding the minimum distance."))
min_list = list(self.min_dict.keys())
min_dist = min(min_list)
min_dist_string = '%.*f' % (self.decimals, float(min_dist))
self.result_entry.set_value(min_dist_string)
freq = len(self.min_dict[min_dist])
freq = '%d' % int(freq)
self.freq_entry.set_value(freq)
min_locations = self.min_dict.pop(min_dist)
self.update_text.emit(min_locations)
self.update_sec_distances.emit(self.min_dict)
app_obj.inform.emit('[success] %s' % _("Optimal Tool. Finished successfully."))
except Exception as ee:
proc.done()
log.debug(str(ee))
return
proc.done()
self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
def on_locate_position(self):
# cursor = self.locations_textb.textCursor()
# self.selected_text = cursor.selectedText()
try:
if self.selected_text != '':
loc = eval(self.selected_text)
else:
return 'fail'
except Exception as e:
log.debug("ToolOptimal.on_locate_position() --> first try %s" % str(e))
self.app.inform.emit("[ERROR_NOTCL] The selected text is no valid location in the format "
"((x0, y0), (x1, y1)).")
return
try:
loc_1 = loc[0]
loc_2 = loc[1]
dx = loc_1[0] - loc_2[0]
dy = loc_1[1] - loc_2[1]
loc = (float('%.*f' % (self.decimals, (min(loc_1[0], loc_2[0]) + (abs(dx) / 2)))),
float('%.*f' % (self.decimals, (min(loc_1[1], loc_2[1]) + (abs(dy) / 2)))))
self.app.on_jump_to(custom_location=loc)
except Exception as e:
log.debug("ToolOptimal.on_locate_position() --> sec try %s" % str(e))
return
def on_update_text(self, data):
txt = ''
for loc in data:
if loc:
txt += '%s, %s\n' % (str(loc[0]), str(loc[1]))
self.locations_textb.setPlainText(txt)
self.locate_button.setDisabled(False)
def on_textbox_clicked(self):
# new cursor - select all document
cursor = self.locations_textb.textCursor()
cursor.select(QtGui.QTextCursor.Document)
# clear previous selection highlight
tmp = cursor.blockFormat()
tmp.clearBackground()
cursor.setBlockFormat(tmp)
# new cursor - select the current line
cursor = self.locations_textb.textCursor()
cursor.select(QtGui.QTextCursor.LineUnderCursor)
# highlight the current selected line
tmp = cursor.blockFormat()
tmp.setBackground(QtGui.QBrush(QtCore.Qt.yellow))
cursor.setBlockFormat(tmp)
self.selected_text = cursor.selectedText()
def on_update_sec_distances_txt(self, data):
distance_list = sorted(list(data.keys()))
txt = ''
for loc in distance_list:
txt += '%s\n' % str(loc)
self.distances_textb.setPlainText(txt)
self.locate_sec_button.setDisabled(False)
def on_distances_textb_clicked(self):
# new cursor - select all document
cursor = self.distances_textb.textCursor()
cursor.select(QtGui.QTextCursor.Document)
# clear previous selection highlight
tmp = cursor.blockFormat()
tmp.clearBackground()
cursor.setBlockFormat(tmp)
# new cursor - select the current line
cursor = self.distances_textb.textCursor()
cursor.select(QtGui.QTextCursor.LineUnderCursor)
# highlight the current selected line
tmp = cursor.blockFormat()
tmp.setBackground(QtGui.QBrush(QtCore.Qt.yellow))
cursor.setBlockFormat(tmp)
distance_text = cursor.selectedText()
key_in_min_dict = eval(distance_text)
self.on_update_locations_text(dist=key_in_min_dict)
def on_update_locations_text(self, dist):
distance_list = self.min_dict[dist]
txt = ''
for loc in distance_list:
if loc:
txt += '%s, %s\n' % (str(loc[0]), str(loc[1]))
self.locations_sec_textb.setPlainText(txt)
def on_locations_sec_clicked(self):
# new cursor - select all document
cursor = self.locations_sec_textb.textCursor()
cursor.select(QtGui.QTextCursor.Document)
# clear previous selection highlight
tmp = cursor.blockFormat()
tmp.clearBackground()
cursor.setBlockFormat(tmp)
# new cursor - select the current line
cursor = self.locations_sec_textb.textCursor()
cursor.select(QtGui.QTextCursor.LineUnderCursor)
# highlight the current selected line
tmp = cursor.blockFormat()
tmp.setBackground(QtGui.QBrush(QtCore.Qt.yellow))
cursor.setBlockFormat(tmp)
self.selected_locations_text = cursor.selectedText()
def on_locate_sec_position(self):
try:
if self.selected_locations_text != '':
loc = eval(self.selected_locations_text)
else:
return
except Exception as e:
log.debug("ToolOptimal.on_locate_sec_position() --> first try %s" % str(e))
self.app.inform.emit("[ERROR_NOTCL] The selected text is no valid location in the format "
"((x0, y0), (x1, y1)).")
return
try:
loc_1 = loc[0]
loc_2 = loc[1]
dx = loc_1[0] - loc_2[0]
dy = loc_1[1] - loc_2[1]
loc = (float('%.*f' % (self.decimals, (min(loc_1[0], loc_2[0]) + (abs(dx) / 2)))),
float('%.*f' % (self.decimals, (min(loc_1[1], loc_2[1]) + (abs(dy) / 2)))))
self.app.on_jump_to(custom_location=loc)
except Exception as e:
log.debug("ToolOptimal.on_locate_sec_position() --> sec try %s" % str(e))
return
def reset_fields(self):
self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.gerber_object_combo.setCurrentIndex(0)

361
appTools/ToolPDF.py Normal file
View File

@@ -0,0 +1,361 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 4/23/2019 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets, QtCore
from appTool import AppTool
from appParsers.ParsePDF import PdfParser, grace
from shapely.geometry import Point, MultiPolygon
from shapely.ops import unary_union
from copy import deepcopy
import zlib
import re
import time
import logging
import traceback
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class ToolPDF(AppTool):
"""
Parse a PDF file.
Reference here: https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf
Return a list of geometries
"""
toolName = _("PDF Import Tool")
def __init__(self, app):
AppTool.__init__(self, app)
self.app = app
self.decimals = self.app.decimals
self.stream_re = re.compile(b'.*?FlateDecode.*?stream(.*?)endstream', re.S)
self.pdf_decompressed = {}
# key = file name and extension
# value is a dict to store the parsed content of the PDF
self.pdf_parsed = {}
# QTimer for periodic check
self.check_thread = QtCore.QTimer()
# Every time a parser is started we add a promise; every time a parser finished we remove a promise
# when empty we start the layer rendering
self.parsing_promises = []
self.parser = PdfParser(app=self.app)
def run(self, toggle=True):
self.app.defaults.report_usage("ToolPDF()")
self.set_tool_ui()
self.on_open_pdf_click()
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='Ctrl+Q', **kwargs)
def set_tool_ui(self):
pass
def on_open_pdf_click(self):
"""
File menu callback for opening an PDF file.
:return: None
"""
self.app.defaults.report_usage("ToolPDF.on_open_pdf_click()")
self.app.log.debug("ToolPDF.on_open_pdf_click()")
_filter_ = "Adobe PDF Files (*.pdf);;" \
"All Files (*.*)"
try:
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"),
directory=self.app.get_last_folder(),
filter=_filter_)
except TypeError:
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"), filter=_filter_)
if len(filenames) == 0:
self.app.inform.emit('[WARNING_NOTCL] %s.' % _("Open PDF cancelled"))
else:
# start the parsing timer with a period of 1 second
self.periodic_check(1000)
for filename in filenames:
if filename != '':
self.app.worker_task.emit({'fcn': self.open_pdf,
'params': [filename]})
def open_pdf(self, filename):
short_name = filename.split('/')[-1].split('\\')[-1]
self.parsing_promises.append(short_name)
self.pdf_parsed[short_name] = {}
self.pdf_parsed[short_name]['pdf'] = {}
self.pdf_parsed[short_name]['filename'] = filename
self.pdf_decompressed[short_name] = ''
if self.app.abort_flag:
# graceful abort requested by the user
raise grace
with self.app.proc_container.new(_("Parsing PDF file ...")):
with open(filename, "rb") as f:
pdf = f.read()
stream_nr = 0
for s in re.findall(self.stream_re, pdf):
if self.app.abort_flag:
# graceful abort requested by the user
raise grace
stream_nr += 1
log.debug("PDF STREAM: %d\n" % stream_nr)
s = s.strip(b'\r\n')
try:
self.pdf_decompressed[short_name] += (zlib.decompress(s).decode('UTF-8') + '\r\n')
except Exception as e:
self.app.inform.emit('[ERROR_NOTCL] %s: %s\n%s' % (_("Failed to open"), str(filename), str(e)))
log.debug("ToolPDF.open_pdf().obj_init() --> %s" % str(e))
return
self.pdf_parsed[short_name]['pdf'] = self.parser.parse_pdf(pdf_content=self.pdf_decompressed[short_name])
# we used it, now we delete it
self.pdf_decompressed[short_name] = ''
# removal from list is done in a multithreaded way therefore not always the removal can be done
# try to remove until it's done
try:
while True:
self.parsing_promises.remove(short_name)
time.sleep(0.1)
except Exception as e:
log.debug("ToolPDF.open_pdf() --> %s" % str(e))
self.app.inform.emit('[success] %s: %s' % (_("Opened"), str(filename)))
def layer_rendering_as_excellon(self, filename, ap_dict, layer_nr):
outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr)
# store the points here until reconstitution:
# keys are diameters and values are list of (x,y) coords
points = {}
def obj_init(exc_obj, app_obj):
clear_geo = [geo_el['clear'] for geo_el in ap_dict['0']['geometry']]
for geo in clear_geo:
xmin, ymin, xmax, ymax = geo.bounds
center = (((xmax - xmin) / 2) + xmin, ((ymax - ymin) / 2) + ymin)
# for drill bits, even in INCH, it's enough 3 decimals
correction_factor = 0.974
dia = (xmax - xmin) * correction_factor
dia = round(dia, 3)
if dia in points:
points[dia].append(center)
else:
points[dia] = [center]
sorted_dia = sorted(points.keys())
name_tool = 0
for dia in sorted_dia:
name_tool += 1
# create tools dictionary
spec = {"C": dia, 'solid_geometry': []}
exc_obj.tools[str(name_tool)] = spec
# create drill list of dictionaries
for dia_points in points:
if dia == dia_points:
for pt in points[dia_points]:
exc_obj.drills.append({'point': Point(pt), 'tool': str(name_tool)})
break
ret = exc_obj.create_geometry()
if ret == 'fail':
log.debug("Could not create geometry for Excellon object.")
return "fail"
for tool in exc_obj.tools:
if exc_obj.tools[tool]['solid_geometry']:
return
app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("No geometry found in file"), outname))
return "fail"
with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
ret_val = self.app.app_obj.new_object("excellon", outname, obj_init, autoselected=False)
if ret_val == 'fail':
self.app.inform.emit('[ERROR_NOTCL] %s' % _('Open PDF file failed.'))
return
# Register recent file
self.app.file_opened.emit("excellon", filename)
# GUI feedback
self.app.inform.emit('[success] %s: %s' % (_("Rendered"), outname))
def layer_rendering_as_gerber(self, filename, ap_dict, layer_nr):
outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr)
def obj_init(grb_obj, app_obj):
grb_obj.apertures = ap_dict
poly_buff = []
follow_buf = []
for ap in grb_obj.apertures:
for k in grb_obj.apertures[ap]:
if k == 'geometry':
for geo_el in ap_dict[ap][k]:
if 'solid' in geo_el:
poly_buff.append(geo_el['solid'])
if 'follow' in geo_el:
follow_buf.append(geo_el['follow'])
poly_buff = unary_union(poly_buff)
if '0' in grb_obj.apertures:
global_clear_geo = []
if 'geometry' in grb_obj.apertures['0']:
for geo_el in ap_dict['0']['geometry']:
if 'clear' in geo_el:
global_clear_geo.append(geo_el['clear'])
if global_clear_geo:
solid = []
for apid in grb_obj.apertures:
if 'geometry' in grb_obj.apertures[apid]:
for elem in grb_obj.apertures[apid]['geometry']:
if 'solid' in elem:
solid_geo = deepcopy(elem['solid'])
for clear_geo in global_clear_geo:
# Make sure that the clear_geo is within the solid_geo otherwise we loose
# the solid_geometry. We want for clear_geometry just to cut into solid_geometry
# not to delete it
if clear_geo.within(solid_geo):
solid_geo = solid_geo.difference(clear_geo)
if solid_geo.is_empty:
solid_geo = elem['solid']
try:
for poly in solid_geo:
solid.append(poly)
except TypeError:
solid.append(solid_geo)
poly_buff = deepcopy(MultiPolygon(solid))
follow_buf = unary_union(follow_buf)
try:
poly_buff = poly_buff.buffer(0.0000001)
except ValueError:
pass
try:
poly_buff = poly_buff.buffer(-0.0000001)
except ValueError:
pass
grb_obj.solid_geometry = deepcopy(poly_buff)
grb_obj.follow_geometry = deepcopy(follow_buf)
with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
ret = self.app.app_obj.new_object('gerber', outname, obj_init, autoselected=False)
if ret == 'fail':
self.app.inform.emit('[ERROR_NOTCL] %s' % _('Open PDF file failed.'))
return
# Register recent file
self.app.file_opened.emit('gerber', filename)
# GUI feedback
self.app.inform.emit('[success] %s: %s' % (_("Rendered"), outname))
def periodic_check(self, check_period):
"""
This function starts an QTimer and it will periodically check if parsing was done
:param check_period: time at which to check periodically if all plots finished to be plotted
:return:
"""
# self.plot_thread = threading.Thread(target=lambda: self.check_plot_finished(check_period))
# self.plot_thread.start()
log.debug("ToolPDF --> Periodic Check started.")
try:
self.check_thread.stop()
except TypeError:
pass
self.check_thread.setInterval(check_period)
try:
self.check_thread.timeout.disconnect(self.periodic_check_handler)
except (TypeError, AttributeError):
pass
self.check_thread.timeout.connect(self.periodic_check_handler)
self.check_thread.start(QtCore.QThread.HighPriority)
def periodic_check_handler(self):
"""
If the parsing worker finished then start multithreaded rendering
:return:
"""
# log.debug("checking parsing --> %s" % str(self.parsing_promises))
try:
if not self.parsing_promises:
self.check_thread.stop()
log.debug("PDF --> start rendering")
# parsing finished start the layer rendering
if self.pdf_parsed:
obj_to_delete = []
for object_name in self.pdf_parsed:
if self.app.abort_flag:
# graceful abort requested by the user
raise grace
filename = deepcopy(self.pdf_parsed[object_name]['filename'])
pdf_content = deepcopy(self.pdf_parsed[object_name]['pdf'])
obj_to_delete.append(object_name)
for k in pdf_content:
if self.app.abort_flag:
# graceful abort requested by the user
raise grace
ap_dict = pdf_content[k]
print(k, ap_dict)
if ap_dict:
layer_nr = k
if k == 0:
self.app.worker_task.emit({'fcn': self.layer_rendering_as_excellon,
'params': [filename, ap_dict, layer_nr]})
else:
self.app.worker_task.emit({'fcn': self.layer_rendering_as_gerber,
'params': [filename, ap_dict, layer_nr]})
# delete the object already processed so it will not be processed again for other objects
# that were opened at the same time; like in drag & drop on appGUI
for obj_name in obj_to_delete:
if obj_name in self.pdf_parsed:
self.pdf_parsed.pop(obj_name)
log.debug("ToolPDF --> Periodic check finished.")
except Exception:
traceback.print_exc()

3673
appTools/ToolPaint.py Normal file

File diff suppressed because it is too large Load Diff

827
appTools/ToolPanelize.py Normal file
View File

@@ -0,0 +1,827 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 3/10/2019 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets, QtGui, QtCore
from appTool import AppTool
from appGUI.GUIElements import FCSpinner, FCDoubleSpinner, RadioSet, FCCheckBox, OptionalInputSection, FCComboBox
from camlib import grace
from copy import deepcopy
import numpy as np
import shapely.affinity as affinity
from shapely.ops import unary_union
from shapely.geometry import LineString
import gettext
import appTranslation as fcTranslate
import builtins
import logging
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class Panelize(AppTool):
toolName = _("Panelize PCB")
def __init__(self, app):
self.decimals = app.decimals
AppTool.__init__(self, app)
# ## Title
title_label = QtWidgets.QLabel("%s" % self.toolName)
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.layout.addWidget(title_label)
self.object_label = QtWidgets.QLabel('<b>%s:</b>' % _("Source Object"))
self.object_label.setToolTip(
_("Specify the type of object to be panelized\n"
"It can be of type: Gerber, Excellon or Geometry.\n"
"The selection here decide the type of objects that will be\n"
"in the Object combobox.")
)
self.layout.addWidget(self.object_label)
# Form Layout
form_layout_0 = QtWidgets.QFormLayout()
self.layout.addLayout(form_layout_0)
# Type of object to be panelized
self.type_obj_combo = FCComboBox()
self.type_obj_combo.addItem("Gerber")
self.type_obj_combo.addItem("Excellon")
self.type_obj_combo.addItem("Geometry")
self.type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
self.type_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
self.type_obj_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
self.type_object_label = QtWidgets.QLabel('%s:' % _("Object Type"))
form_layout_0.addRow(self.type_object_label, self.type_obj_combo)
# Object to be panelized
self.object_combo = FCComboBox()
self.object_combo.setModel(self.app.collection)
self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.object_combo.is_last = True
self.object_combo.setToolTip(
_("Object to be panelized. This means that it will\n"
"be duplicated in an array of rows and columns.")
)
form_layout_0.addRow(self.object_combo)
# Form Layout
form_layout = QtWidgets.QFormLayout()
self.layout.addLayout(form_layout)
# Type of box Panel object
self.reference_radio = RadioSet([{'label': _('Object'), 'value': 'object'},
{'label': _('Bounding Box'), 'value': 'bbox'}])
self.box_label = QtWidgets.QLabel("<b>%s:</b>" % _("Penelization Reference"))
self.box_label.setToolTip(
_("Choose the reference for panelization:\n"
"- Object = the bounding box of a different object\n"
"- Bounding Box = the bounding box of the object to be panelized\n"
"\n"
"The reference is useful when doing panelization for more than one\n"
"object. The spacings (really offsets) will be applied in reference\n"
"to this reference object therefore maintaining the panelized\n"
"objects in sync.")
)
form_layout.addRow(self.box_label)
form_layout.addRow(self.reference_radio)
# Type of Box Object to be used as an envelope for panelization
self.type_box_combo = FCComboBox()
self.type_box_combo.addItems([_("Gerber"), _("Geometry")])
# we get rid of item1 ("Excellon") as it is not suitable for use as a "box" for panelizing
# self.type_box_combo.view().setRowHidden(1, True)
self.type_box_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
self.type_box_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
self.type_box_combo_label = QtWidgets.QLabel('%s:' % _("Box Type"))
self.type_box_combo_label.setToolTip(
_("Specify the type of object to be used as an container for\n"
"panelization. It can be: Gerber or Geometry type.\n"
"The selection here decide the type of objects that will be\n"
"in the Box Object combobox.")
)
form_layout.addRow(self.type_box_combo_label, self.type_box_combo)
# Box
self.box_combo = FCComboBox()
self.box_combo.setModel(self.app.collection)
self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.box_combo.is_last = True
self.box_combo.setToolTip(
_("The actual object that is used as container for the\n "
"selected object that is to be panelized.")
)
form_layout.addRow(self.box_combo)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
form_layout.addRow(separator_line)
panel_data_label = QtWidgets.QLabel("<b>%s:</b>" % _("Panel Data"))
panel_data_label.setToolTip(
_("This informations will shape the resulting panel.\n"
"The number of rows and columns will set how many\n"
"duplicates of the original geometry will be generated.\n"
"\n"
"The spacings will set the distance between any two\n"
"elements of the panel array.")
)
form_layout.addRow(panel_data_label)
# Spacing Columns
self.spacing_columns = FCDoubleSpinner(callback=self.confirmation_message)
self.spacing_columns.set_range(0, 9999)
self.spacing_columns.set_precision(4)
self.spacing_columns_label = QtWidgets.QLabel('%s:' % _("Spacing cols"))
self.spacing_columns_label.setToolTip(
_("Spacing between columns of the desired panel.\n"
"In current units.")
)
form_layout.addRow(self.spacing_columns_label, self.spacing_columns)
# Spacing Rows
self.spacing_rows = FCDoubleSpinner(callback=self.confirmation_message)
self.spacing_rows.set_range(0, 9999)
self.spacing_rows.set_precision(4)
self.spacing_rows_label = QtWidgets.QLabel('%s:' % _("Spacing rows"))
self.spacing_rows_label.setToolTip(
_("Spacing between rows of the desired panel.\n"
"In current units.")
)
form_layout.addRow(self.spacing_rows_label, self.spacing_rows)
# Columns
self.columns = FCSpinner(callback=self.confirmation_message_int)
self.columns.set_range(0, 9999)
self.columns_label = QtWidgets.QLabel('%s:' % _("Columns"))
self.columns_label.setToolTip(
_("Number of columns of the desired panel")
)
form_layout.addRow(self.columns_label, self.columns)
# Rows
self.rows = FCSpinner(callback=self.confirmation_message_int)
self.rows.set_range(0, 9999)
self.rows_label = QtWidgets.QLabel('%s:' % _("Rows"))
self.rows_label.setToolTip(
_("Number of rows of the desired panel")
)
form_layout.addRow(self.rows_label, self.rows)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
form_layout.addRow(separator_line)
# Type of resulting Panel object
self.panel_type_radio = RadioSet([{'label': _('Gerber'), 'value': 'gerber'},
{'label': _('Geo'), 'value': 'geometry'}])
self.panel_type_label = QtWidgets.QLabel("<b>%s:</b>" % _("Panel Type"))
self.panel_type_label.setToolTip(
_("Choose the type of object for the panel object:\n"
"- Geometry\n"
"- Gerber")
)
form_layout.addRow(self.panel_type_label)
form_layout.addRow(self.panel_type_radio)
# Constrains
self.constrain_cb = FCCheckBox('%s:' % _("Constrain panel within"))
self.constrain_cb.setToolTip(
_("Area define by DX and DY within to constrain the panel.\n"
"DX and DY values are in current units.\n"
"Regardless of how many columns and rows are desired,\n"
"the final panel will have as many columns and rows as\n"
"they fit completely within selected area.")
)
form_layout.addRow(self.constrain_cb)
self.x_width_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.x_width_entry.set_precision(4)
self.x_width_entry.set_range(0, 9999)
self.x_width_lbl = QtWidgets.QLabel('%s:' % _("Width (DX)"))
self.x_width_lbl.setToolTip(
_("The width (DX) within which the panel must fit.\n"
"In current units.")
)
form_layout.addRow(self.x_width_lbl, self.x_width_entry)
self.y_height_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.y_height_entry.set_range(0, 9999)
self.y_height_entry.set_precision(4)
self.y_height_lbl = QtWidgets.QLabel('%s:' % _("Height (DY)"))
self.y_height_lbl.setToolTip(
_("The height (DY)within which the panel must fit.\n"
"In current units.")
)
form_layout.addRow(self.y_height_lbl, self.y_height_entry)
self.constrain_sel = OptionalInputSection(
self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry])
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
form_layout.addRow(separator_line)
# Buttons
self.panelize_object_button = QtWidgets.QPushButton(_("Panelize Object"))
self.panelize_object_button.setToolTip(
_("Panelize the specified object around the specified box.\n"
"In other words it creates multiple copies of the source object,\n"
"arranged in a 2D array of rows and columns.")
)
self.panelize_object_button.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
self.layout.addWidget(self.panelize_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)
# Signals
self.reference_radio.activated_custom.connect(self.on_reference_radio_changed)
self.panelize_object_button.clicked.connect(self.on_panelize)
self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
self.type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
self.reset_button.clicked.connect(self.set_tool_ui)
# list to hold the temporary objects
self.objs = []
# final name for the panel object
self.outname = ""
# flag to signal the constrain was activated
self.constrain_flag = False
def run(self, toggle=True):
self.app.defaults.report_usage("ToolPanelize()")
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])
AppTool.run(self)
self.set_tool_ui()
self.app.ui.notebook.setTabText(2, _("Panel. Tool"))
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='Alt+Z', **kwargs)
def set_tool_ui(self):
self.reset_fields()
self.reference_radio.set_value('bbox')
sp_c = self.app.defaults["tools_panelize_spacing_columns"] if \
self.app.defaults["tools_panelize_spacing_columns"] else 0.0
self.spacing_columns.set_value(float(sp_c))
sp_r = self.app.defaults["tools_panelize_spacing_rows"] if \
self.app.defaults["tools_panelize_spacing_rows"] else 0.0
self.spacing_rows.set_value(float(sp_r))
rr = self.app.defaults["tools_panelize_rows"] if \
self.app.defaults["tools_panelize_rows"] else 0.0
self.rows.set_value(int(rr))
cc = self.app.defaults["tools_panelize_columns"] if \
self.app.defaults["tools_panelize_columns"] else 0.0
self.columns.set_value(int(cc))
c_cb = self.app.defaults["tools_panelize_constrain"] if \
self.app.defaults["tools_panelize_constrain"] else False
self.constrain_cb.set_value(c_cb)
x_w = self.app.defaults["tools_panelize_constrainx"] if \
self.app.defaults["tools_panelize_constrainx"] else 0.0
self.x_width_entry.set_value(float(x_w))
y_w = self.app.defaults["tools_panelize_constrainy"] if \
self.app.defaults["tools_panelize_constrainy"] else 0.0
self.y_height_entry.set_value(float(y_w))
panel_type = self.app.defaults["tools_panelize_panel_type"] if \
self.app.defaults["tools_panelize_panel_type"] else 'gerber'
self.panel_type_radio.set_value(panel_type)
# run once the following so the obj_type attribute is updated in the FCComboBoxes
# such that the last loaded object is populated in the combo boxes
self.on_type_obj_index_changed()
self.on_type_box_index_changed()
def on_type_obj_index_changed(self):
obj_type = self.type_obj_combo.currentIndex()
self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
self.object_combo.setCurrentIndex(0)
self.object_combo.obj_type = {
_("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
}[self.type_obj_combo.get_value()]
# hide the panel type for Excellons, the panel can be only of type Geometry
if self.type_obj_combo.currentText() != 'Excellon':
self.panel_type_label.setDisabled(False)
self.panel_type_radio.setDisabled(False)
else:
self.panel_type_label.setDisabled(True)
self.panel_type_radio.setDisabled(True)
self.panel_type_radio.set_value('geometry')
def on_type_box_index_changed(self):
obj_type = self.type_box_combo.currentIndex()
self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
self.box_combo.setCurrentIndex(0)
self.box_combo.obj_type = {
_("Gerber"): "Gerber", _("Geometry"): "Geometry"
}[self.type_box_combo.get_value()]
def on_reference_radio_changed(self, current_val):
if current_val == 'object':
self.type_box_combo.setDisabled(False)
self.type_box_combo_label.setDisabled(False)
self.box_combo.setDisabled(False)
else:
self.type_box_combo.setDisabled(True)
self.type_box_combo_label.setDisabled(True)
self.box_combo.setDisabled(True)
def on_panelize(self):
name = self.object_combo.currentText()
# Get source object to be panelized.
try:
panel_source_obj = self.app.collection.get_by_name(str(name))
except Exception as e:
log.debug("Panelize.on_panelize() --> %s" % str(e))
self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), name))
return "Could not retrieve object: %s" % name
if panel_source_obj is None:
self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
(_("Object not found"), panel_source_obj))
return "Object not found: %s" % panel_source_obj
boxname = self.box_combo.currentText()
try:
box = self.app.collection.get_by_name(boxname)
except Exception as e:
log.debug("Panelize.on_panelize() --> %s" % str(e))
self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), boxname))
return "Could not retrieve object: %s" % boxname
if box is None:
self.app.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), panel_source_obj))
self.reference_radio.set_value('bbox')
if self.reference_radio.get_value() == 'bbox':
box = panel_source_obj
self.outname = name + '_panelized'
spacing_columns = float(self.spacing_columns.get_value())
spacing_columns = spacing_columns if spacing_columns is not None else 0
spacing_rows = float(self.spacing_rows.get_value())
spacing_rows = spacing_rows if spacing_rows is not None else 0
rows = int(self.rows.get_value())
rows = rows if rows is not None else 1
columns = int(self.columns.get_value())
columns = columns if columns is not None else 1
constrain_dx = float(self.x_width_entry.get_value())
constrain_dy = float(self.y_height_entry.get_value())
panel_type = str(self.panel_type_radio.get_value())
if 0 in {columns, rows}:
self.app.inform.emit('[ERROR_NOTCL] %s' %
_("Columns or Rows are zero value. Change them to a positive integer."))
return "Columns or Rows are zero value. Change them to a positive integer."
xmin, ymin, xmax, ymax = box.bounds()
lenghtx = xmax - xmin + spacing_columns
lenghty = ymax - ymin + spacing_rows
# check if constrain within an area is desired
if self.constrain_cb.isChecked():
panel_lengthx = ((xmax - xmin) * columns) + (spacing_columns * (columns - 1))
panel_lengthy = ((ymax - ymin) * rows) + (spacing_rows * (rows - 1))
# adjust the number of columns and/or rows so the panel will fit within the panel constraint area
if (panel_lengthx > constrain_dx) or (panel_lengthy > constrain_dy):
self.constrain_flag = True
while panel_lengthx > constrain_dx:
columns -= 1
panel_lengthx = ((xmax - xmin) * columns) + (spacing_columns * (columns - 1))
while panel_lengthy > constrain_dy:
rows -= 1
panel_lengthy = ((ymax - ymin) * rows) + (spacing_rows * (rows - 1))
if panel_source_obj.kind == 'excellon' or panel_source_obj.kind == 'geometry':
# make a copy of the panelized Excellon or Geometry tools
copied_tools = {}
for tt, tt_val in list(panel_source_obj.tools.items()):
copied_tools[tt] = deepcopy(tt_val)
if panel_source_obj.kind == 'gerber':
# make a copy of the panelized Gerber apertures
copied_apertures = {}
for tt, tt_val in list(panel_source_obj.apertures.items()):
copied_apertures[tt] = deepcopy(tt_val)
def panelize_worker():
if panel_source_obj is not None:
self.app.inform.emit(_("Generating panel ... "))
def job_init_excellon(obj_fin, app_obj):
currenty = 0.0
obj_fin.tools = copied_tools
obj_fin.drills = []
obj_fin.slots = []
obj_fin.solid_geometry = []
for option in panel_source_obj.options:
if option != 'name':
try:
obj_fin.options[option] = panel_source_obj.options[option]
except KeyError:
log.warning("Failed to copy option. %s" % str(option))
geo_len_drills = len(panel_source_obj.drills) if panel_source_obj.drills else 0
geo_len_slots = len(panel_source_obj.slots) if panel_source_obj.slots else 0
element = 0
for row in range(rows):
currentx = 0.0
for col in range(columns):
element += 1
old_disp_number = 0
if panel_source_obj.drills:
drill_nr = 0
for tool_dict in panel_source_obj.drills:
if self.app.abort_flag:
# graceful abort requested by the user
raise grace
point_offseted = affinity.translate(tool_dict['point'], currentx, currenty)
obj_fin.drills.append(
{
"point": point_offseted,
"tool": tool_dict['tool']
}
)
drill_nr += 1
disp_number = int(np.interp(drill_nr, [0, geo_len_drills], [0, 100]))
if old_disp_number < disp_number <= 100:
self.app.proc_container.update_view_text(' %s: %d D:%d%%' %
(_("Copy"),
int(element),
disp_number))
old_disp_number = disp_number
if panel_source_obj.slots:
slot_nr = 0
for tool_dict in panel_source_obj.slots:
if self.app.abort_flag:
# graceful abort requested by the user
raise grace
start_offseted = affinity.translate(tool_dict['start'], currentx, currenty)
stop_offseted = affinity.translate(tool_dict['stop'], currentx, currenty)
obj_fin.slots.append(
{
"start": start_offseted,
"stop": stop_offseted,
"tool": tool_dict['tool']
}
)
slot_nr += 1
disp_number = int(np.interp(slot_nr, [0, geo_len_slots], [0, 100]))
if old_disp_number < disp_number <= 100:
self.app.proc_container.update_view_text(' %s: %d S:%d%%' %
(_("Copy"),
int(element),
disp_number))
old_disp_number = disp_number
currentx += lenghtx
currenty += lenghty
obj_fin.create_geometry()
obj_fin.zeros = panel_source_obj.zeros
obj_fin.units = panel_source_obj.units
self.app.proc_container.update_view_text('')
def job_init_geometry(obj_fin, app_obj):
currentx = 0.0
currenty = 0.0
def translate_recursion(geom):
if type(geom) == list:
geoms = []
for local_geom in geom:
res_geo = translate_recursion(local_geom)
try:
geoms += res_geo
except TypeError:
geoms.append(res_geo)
return geoms
else:
return affinity.translate(geom, xoff=currentx, yoff=currenty)
obj_fin.solid_geometry = []
# create the initial structure on which to create the panel
if panel_source_obj.kind == 'geometry':
obj_fin.multigeo = panel_source_obj.multigeo
obj_fin.tools = copied_tools
if panel_source_obj.multigeo is True:
for tool in panel_source_obj.tools:
obj_fin.tools[tool]['solid_geometry'][:] = []
elif panel_source_obj.kind == 'gerber':
obj_fin.apertures = copied_apertures
for ap in obj_fin.apertures:
obj_fin.apertures[ap]['geometry'] = []
# find the number of polygons in the source solid_geometry
geo_len = 0
if panel_source_obj.kind == 'geometry':
if panel_source_obj.multigeo is True:
for tool in panel_source_obj.tools:
try:
geo_len += len(panel_source_obj.tools[tool]['solid_geometry'])
except TypeError:
geo_len += 1
else:
try:
geo_len = len(panel_source_obj.solid_geometry)
except TypeError:
geo_len = 1
elif panel_source_obj.kind == 'gerber':
for ap in panel_source_obj.apertures:
if 'geometry' in panel_source_obj.apertures[ap]:
try:
geo_len += len(panel_source_obj.apertures[ap]['geometry'])
except TypeError:
geo_len += 1
element = 0
for row in range(rows):
currentx = 0.0
for col in range(columns):
element += 1
old_disp_number = 0
# Will panelize a Geometry Object
if panel_source_obj.kind == 'geometry':
if panel_source_obj.multigeo is True:
for tool in panel_source_obj.tools:
if self.app.abort_flag:
# graceful abort requested by the user
raise grace
# calculate the number of polygons
geo_len = len(panel_source_obj.tools[tool]['solid_geometry'])
pol_nr = 0
for geo_el in panel_source_obj.tools[tool]['solid_geometry']:
trans_geo = translate_recursion(geo_el)
obj_fin.tools[tool]['solid_geometry'].append(trans_geo)
pol_nr += 1
disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
if old_disp_number < disp_number <= 100:
self.app.proc_container.update_view_text(' %s: %d %d%%' %
(_("Copy"),
int(element),
disp_number))
old_disp_number = disp_number
else:
if self.app.abort_flag:
# graceful abort requested by the user
raise grace
try:
# calculate the number of polygons
geo_len = len(panel_source_obj.solid_geometry)
except TypeError:
geo_len = 1
pol_nr = 0
try:
for geo_el in panel_source_obj.solid_geometry:
if self.app.abort_flag:
# graceful abort requested by the user
raise grace
trans_geo = translate_recursion(geo_el)
obj_fin.solid_geometry.append(trans_geo)
pol_nr += 1
disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
if old_disp_number < disp_number <= 100:
self.app.proc_container.update_view_text(' %s: %d %d%%' %
(_("Copy"),
int(element),
disp_number))
old_disp_number = disp_number
except TypeError:
trans_geo = translate_recursion(panel_source_obj.solid_geometry)
obj_fin.solid_geometry.append(trans_geo)
# Will panelize a Gerber Object
else:
if self.app.abort_flag:
# graceful abort requested by the user
raise grace
try:
for geo_el in panel_source_obj.solid_geometry:
if self.app.abort_flag:
# graceful abort requested by the user
raise grace
trans_geo = translate_recursion(geo_el)
obj_fin.solid_geometry.append(trans_geo)
except TypeError:
trans_geo = translate_recursion(panel_source_obj.solid_geometry)
obj_fin.solid_geometry.append(trans_geo)
for apid in panel_source_obj.apertures:
if self.app.abort_flag:
# graceful abort requested by the user
raise grace
if 'geometry' in panel_source_obj.apertures[apid]:
try:
# calculate the number of polygons
geo_len = len(panel_source_obj.apertures[apid]['geometry'])
except TypeError:
geo_len = 1
pol_nr = 0
for el in panel_source_obj.apertures[apid]['geometry']:
if self.app.abort_flag:
# graceful abort requested by the user
raise grace
new_el = {}
if 'solid' in el:
geo_aper = translate_recursion(el['solid'])
new_el['solid'] = geo_aper
if 'clear' in el:
geo_aper = translate_recursion(el['clear'])
new_el['clear'] = geo_aper
if 'follow' in el:
geo_aper = translate_recursion(el['follow'])
new_el['follow'] = geo_aper
obj_fin.apertures[apid]['geometry'].append(deepcopy(new_el))
pol_nr += 1
disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
if old_disp_number < disp_number <= 100:
self.app.proc_container.update_view_text(' %s: %d %d%%' %
(_("Copy"),
int(element),
disp_number))
old_disp_number = disp_number
currentx += lenghtx
currenty += lenghty
print("before", obj_fin.tools)
if panel_source_obj.kind == 'geometry' and panel_source_obj.multigeo is True:
# I'm going to do this only here as a fix for panelizing cutouts
# I'm going to separate linestrings out of the solid geometry from other
# possible type of elements and apply unary_union on them to fuse them
for tool in obj_fin.tools:
lines = []
other_geo = []
for geo in obj_fin.tools[tool]['solid_geometry']:
if isinstance(geo, LineString):
lines.append(geo)
else:
other_geo.append(geo)
fused_lines = list(unary_union(lines))
obj_fin.tools[tool]['solid_geometry'] = fused_lines + other_geo
print("after", obj_fin.tools)
if panel_type == 'gerber':
self.app.inform.emit('%s' % _("Generating panel ... Adding the Gerber code."))
obj_fin.source_file = self.app.export_gerber(obj_name=self.outname, filename=None,
local_use=obj_fin, use_thread=False)
# obj_fin.solid_geometry = cascaded_union(obj_fin.solid_geometry)
# app_obj.log.debug("Finished creating a cascaded union for the panel.")
self.app.proc_container.update_view_text('')
self.app.inform.emit('%s: %d' % (_("Generating panel... Spawning copies"), (int(rows * columns))))
if panel_source_obj.kind == 'excellon':
self.app.app_obj.new_object("excellon", self.outname, job_init_excellon, plot=True, autoselected=True)
else:
self.app.app_obj.new_object(panel_type, self.outname, job_init_geometry, plot=True, autoselected=True)
if self.constrain_flag is False:
self.app.inform.emit('[success] %s' % _("Panel done..."))
else:
self.constrain_flag = False
self.app.inform.emit(_("{text} Too big for the constrain area. "
"Final panel has {col} columns and {row} rows").format(
text='[WARNING] ', col=columns, row=rows))
proc = self.app.proc_container.new(_("Working..."))
def job_thread(app_obj):
try:
panelize_worker()
self.app.inform.emit('[success] %s' % _("Panel created successfully."))
except Exception as ee:
proc.done()
log.debug(str(ee))
return
proc.done()
self.app.collection.promise(self.outname)
self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
def reset_fields(self):
self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))

469
appTools/ToolPcbWizard.py Normal file
View File

@@ -0,0 +1,469 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 4/15/2019 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets, QtCore
from appTool import AppTool
from appGUI.GUIElements import RadioSet, FCSpinner, FCButton, FCTable
import re
import os
from datetime import datetime
from io import StringIO
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
class PcbWizard(AppTool):
file_loaded = QtCore.pyqtSignal(str, str)
toolName = _("PcbWizard Import Tool")
def __init__(self, app):
AppTool.__init__(self, app)
self.app = app
self.decimals = self.app.decimals
# Title
title_label = QtWidgets.QLabel("%s" % _('Import 2-file Excellon'))
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.layout.addWidget(title_label)
self.layout.addWidget(QtWidgets.QLabel(""))
self.layout.addWidget(QtWidgets.QLabel("<b>%s:</b>" % _("Load files")))
# Form Layout
form_layout = QtWidgets.QFormLayout()
self.layout.addLayout(form_layout)
self.excellon_label = QtWidgets.QLabel('%s:' % _("Excellon file"))
self.excellon_label.setToolTip(
_("Load the Excellon file.\n"
"Usually it has a .DRL extension")
)
self.excellon_brn = FCButton(_("Open"))
form_layout.addRow(self.excellon_label, self.excellon_brn)
self.inf_label = QtWidgets.QLabel('%s:' % _("INF file"))
self.inf_label.setToolTip(
_("Load the INF file.")
)
self.inf_btn = FCButton(_("Open"))
form_layout.addRow(self.inf_label, self.inf_btn)
self.tools_table = FCTable()
self.layout.addWidget(self.tools_table)
self.tools_table.setColumnCount(2)
self.tools_table.setHorizontalHeaderLabels(['#Tool', _('Diameter')])
self.tools_table.horizontalHeaderItem(0).setToolTip(
_("Tool Number"))
self.tools_table.horizontalHeaderItem(1).setToolTip(
_("Tool diameter in file units."))
# start with apertures table hidden
self.tools_table.setVisible(False)
self.layout.addWidget(QtWidgets.QLabel(""))
self.layout.addWidget(QtWidgets.QLabel("<b>%s:</b>" % _("Excellon format")))
# Form Layout
form_layout1 = QtWidgets.QFormLayout()
self.layout.addLayout(form_layout1)
# Integral part of the coordinates
self.int_entry = FCSpinner(callback=self.confirmation_message_int)
self.int_entry.set_range(1, 10)
self.int_label = QtWidgets.QLabel('%s:' % _("Int. digits"))
self.int_label.setToolTip(
_("The number of digits for the integral part of the coordinates.")
)
form_layout1.addRow(self.int_label, self.int_entry)
# Fractional part of the coordinates
self.frac_entry = FCSpinner(callback=self.confirmation_message_int)
self.frac_entry.set_range(1, 10)
self.frac_label = QtWidgets.QLabel('%s:' % _("Frac. digits"))
self.frac_label.setToolTip(
_("The number of digits for the fractional part of the coordinates.")
)
form_layout1.addRow(self.frac_label, self.frac_entry)
# Zeros suppression for coordinates
self.zeros_radio = RadioSet([{'label': _('LZ'), 'value': 'LZ'},
{'label': _('TZ'), 'value': 'TZ'},
{'label': _('No Suppression'), 'value': 'D'}])
self.zeros_label = QtWidgets.QLabel('%s:' % _("Zeros supp."))
self.zeros_label.setToolTip(
_("The type of zeros suppression used.\n"
"Can be of type:\n"
"- LZ = leading zeros are kept\n"
"- TZ = trailing zeros are kept\n"
"- No Suppression = no zero suppression")
)
form_layout1.addRow(self.zeros_label, self.zeros_radio)
# Units type
self.units_radio = RadioSet([{'label': _('INCH'), 'value': 'INCH'},
{'label': _('MM'), 'value': 'METRIC'}])
self.units_label = QtWidgets.QLabel("<b>%s:</b>" % _('Units'))
self.units_label.setToolTip(
_("The type of units that the coordinates and tool\n"
"diameters are using. Can be INCH or MM.")
)
form_layout1.addRow(self.units_label, self.units_radio)
# Buttons
self.import_button = QtWidgets.QPushButton(_("Import Excellon"))
self.import_button.setToolTip(
_("Import in FlatCAM an Excellon file\n"
"that store it's information's in 2 files.\n"
"One usually has .DRL extension while\n"
"the other has .INF extension.")
)
self.layout.addWidget(self.import_button)
self.layout.addStretch()
self.excellon_loaded = False
self.inf_loaded = False
self.process_finished = False
self.modified_excellon_file = ''
# ## Signals
self.excellon_brn.clicked.connect(self.on_load_excellon_click)
self.inf_btn.clicked.connect(self.on_load_inf_click)
self.import_button.clicked.connect(lambda: self.on_import_excellon(
excellon_fileobj=self.modified_excellon_file))
self.file_loaded.connect(self.on_file_loaded)
self.units_radio.activated_custom.connect(self.on_units_change)
self.units = 'INCH'
self.zeros = 'LZ'
self.integral = 2
self.fractional = 4
self.outname = 'file'
self.exc_file_content = None
self.tools_from_inf = {}
def run(self, toggle=False):
self.app.defaults.report_usage("PcbWizard Tool()")
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])
AppTool.run(self)
self.set_tool_ui()
self.app.ui.notebook.setTabText(2, _("PCBWizard Tool"))
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, **kwargs)
def set_tool_ui(self):
self.units = 'INCH'
self.zeros = 'LZ'
self.integral = 2
self.fractional = 4
self.outname = 'file'
self.exc_file_content = None
self.tools_from_inf = {}
# ## Initialize form
self.int_entry.set_value(self.integral)
self.frac_entry.set_value(self.fractional)
self.zeros_radio.set_value(self.zeros)
self.units_radio.set_value(self.units)
self.excellon_loaded = False
self.inf_loaded = False
self.process_finished = False
self.modified_excellon_file = ''
self.build_ui()
def build_ui(self):
sorted_tools = []
if not self.tools_from_inf:
self.tools_table.setVisible(False)
else:
sort = []
for k, v in list(self.tools_from_inf.items()):
sort.append(int(k))
sorted_tools = sorted(sort)
n = len(sorted_tools)
self.tools_table.setRowCount(n)
tool_row = 0
for tool in sorted_tools:
tool_id_item = QtWidgets.QTableWidgetItem('%d' % int(tool))
tool_id_item.setFlags(QtCore.Qt.ItemIsEnabled)
self.tools_table.setItem(tool_row, 0, tool_id_item) # Tool name/id
tool_dia_item = QtWidgets.QTableWidgetItem(str(self.tools_from_inf[tool]))
tool_dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
self.tools_table.setItem(tool_row, 1, tool_dia_item)
tool_row += 1
self.tools_table.resizeColumnsToContents()
self.tools_table.resizeRowsToContents()
vertical_header = self.tools_table.verticalHeader()
vertical_header.hide()
self.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
horizontal_header = self.tools_table.horizontalHeader()
# horizontal_header.setMinimumSectionSize(10)
# horizontal_header.setDefaultSectionSize(70)
horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
self.tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.tools_table.setSortingEnabled(False)
self.tools_table.setMinimumHeight(self.tools_table.getHeight())
self.tools_table.setMaximumHeight(self.tools_table.getHeight())
def update_params(self):
self.units = self.units_radio.get_value()
self.zeros = self.zeros_radio.get_value()
self.integral = self.int_entry.get_value()
self.fractional = self.frac_entry.get_value()
def on_units_change(self, val):
if val == 'INCH':
self.int_entry.set_value(2)
self.frac_entry.set_value(4)
else:
self.int_entry.set_value(3)
self.frac_entry.set_value(3)
def on_load_excellon_click(self):
"""
:return: None
"""
self.app.log.debug("on_load_excellon_click()")
_filter = "Excellon Files(*.DRL *.DRD *.TXT);;All Files (*.*)"
try:
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Load PcbWizard Excellon file"),
directory=self.app.get_last_folder(),
filter=_filter)
except TypeError:
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Load PcbWizard Excellon file"),
filter=_filter)
filename = str(filename)
if filename == "":
self.app.inform.emit(_("Cancelled."))
else:
self.app.worker_task.emit({'fcn': self.load_excellon, 'params': [filename]})
def on_load_inf_click(self):
"""
:return: None
"""
self.app.log.debug("on_load_inf_click()")
_filter = "INF Files(*.INF);;All Files (*.*)"
try:
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Load PcbWizard INF file"),
directory=self.app.get_last_folder(),
filter=_filter)
except TypeError:
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Load PcbWizard INF file"),
filter=_filter)
filename = str(filename)
if filename == "":
self.app.inform.emit(_("Cancelled."))
else:
self.app.worker_task.emit({'fcn': self.load_inf, 'params': [filename]})
def load_inf(self, filename):
self.app.log.debug("ToolPcbWizard.load_inf()")
with open(filename, 'r') as inf_f:
inf_file_content = inf_f.readlines()
tool_re = re.compile(r'^T(\d+)\s+(\d*\.?\d+)$')
format_re = re.compile(r'^(\d+)\.?(\d+)\s*format,\s*(inches|metric)?,\s*(absolute|incremental)?.*$')
for eline in inf_file_content:
# Cleanup lines
eline = eline.strip(' \r\n')
match = tool_re.search(eline)
if match:
tool = int(match.group(1))
dia = float(match.group(2))
# if dia < 0.1:
# # most likely the file is in INCH
# self.units_radio.set_value('INCH')
self.tools_from_inf[tool] = dia
continue
match = format_re.search(eline)
if match:
self.integral = int(match.group(1))
self.fractional = int(match.group(2))
units = match.group(3)
if units == 'inches':
self.units = 'INCH'
else:
self.units = 'METRIC'
self.units_radio.set_value(self.units)
self.int_entry.set_value(self.integral)
self.frac_entry.set_value(self.fractional)
if not self.tools_from_inf:
self.app.inform.emit('[ERROR] %s' %
_("The INF file does not contain the tool table.\n"
"Try to open the Excellon file from File -> Open -> Excellon\n"
"and edit the drill diameters manually."))
return "fail"
self.file_loaded.emit('inf', filename)
def load_excellon(self, filename):
with open(filename, 'r') as exc_f:
self.exc_file_content = exc_f.readlines()
self.file_loaded.emit("excellon", filename)
def on_file_loaded(self, signal, filename):
self.build_ui()
time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
if signal == 'inf':
self.inf_loaded = True
self.tools_table.setVisible(True)
self.app.inform.emit('[success] %s' %
_("PcbWizard .INF file loaded."))
elif signal == 'excellon':
self.excellon_loaded = True
self.outname = os.path.split(str(filename))[1]
self.app.inform.emit('[success] %s' %
_("Main PcbWizard Excellon file loaded."))
if self.excellon_loaded and self.inf_loaded:
self.update_params()
excellon_string = ''
for line in self.exc_file_content:
excellon_string += line
if 'M48' in line:
header = ';EXCELLON RE-GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s\n' % \
(str(self.app.version), str(self.app.version_date))
header += ';Created on : %s' % time_str + '\n'
header += ';FILE_FORMAT={integral}:{fractional}\n'.format(integral=self.integral,
fractional=self.fractional)
header += '{units},{zeros}\n'.format(units=self.units, zeros=self.zeros)
for k, v in self.tools_from_inf.items():
header += 'T{tool}C{dia}\n'.format(tool=int(k), dia=float(v))
excellon_string += header
self.modified_excellon_file = StringIO(excellon_string)
self.process_finished = True
# Register recent file
self.app.defaults["global_last_folder"] = os.path.split(str(filename))[0]
def on_import_excellon(self, signal=None, excellon_fileobj=None):
self.app.log.debug("import_2files_excellon()")
# How the object should be initialized
def obj_init(excellon_obj, app_obj):
try:
ret = excellon_obj.parse_file(file_obj=excellon_fileobj)
if ret == "fail":
app_obj.log.debug("Excellon parsing failed.")
app_obj.inform.emit('[ERROR_NOTCL] %s' % _("This is not Excellon file."))
return "fail"
except IOError:
app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Cannot parse file"), self.outname))
app_obj.log.debug("Could not import Excellon object.")
return "fail"
except Exception as e:
app_obj.log.debug("PcbWizard.on_import_excellon().obj_init() %s" % str(e))
msg = '[ERROR_NOTCL] %s' % _("An internal error has occurred. See shell.\n")
msg += app_obj.traceback.format_exc()
app_obj.inform.emit(msg)
return "fail"
ret = excellon_obj.create_geometry()
if ret == 'fail':
app_obj.log.debug("Could not create geometry for Excellon object.")
return "fail"
for tool in excellon_obj.tools:
if excellon_obj.tools[tool]['solid_geometry']:
return
app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("No geometry found in file"), name))
return "fail"
if excellon_fileobj is not None and excellon_fileobj != '':
if self.process_finished:
with self.app.proc_container.new(_("Importing Excellon.")):
# Object name
name = self.outname
ret_val = self.app.app_obj.new_object("excellon", name, obj_init, autoselected=False)
if ret_val == 'fail':
self.app.inform.emit('[ERROR_NOTCL] %s' % _('Import Excellon file failed.'))
return
# Register recent file
self.app.file_opened.emit("excellon", name)
# GUI feedback
self.app.inform.emit('[success] %s: %s' % (_("Imported"), name))
self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
else:
self.app.inform.emit('[WARNING_NOTCL] %s' % _('Excellon merging is in progress. Please wait...'))
else:
self.app.inform.emit('[ERROR_NOTCL] %s' % _('The imported Excellon file is empty.'))

593
appTools/ToolProperties.py Normal file
View File

@@ -0,0 +1,593 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 3/10/2019 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtGui, QtCore, QtWidgets
from appTool import AppTool
from appGUI.GUIElements import FCTree
from shapely.geometry import MultiPolygon, Polygon
from shapely.ops import cascaded_union
from copy import deepcopy
import math
import logging
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class Properties(AppTool):
toolName = _("Properties")
calculations_finished = QtCore.pyqtSignal(float, float, float, float, float, object)
def __init__(self, app):
AppTool.__init__(self, app)
self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
self.decimals = self.app.decimals
# this way I can hide/show the frame
self.properties_frame = QtWidgets.QFrame()
self.properties_frame.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(self.properties_frame)
self.properties_box = QtWidgets.QVBoxLayout()
self.properties_box.setContentsMargins(0, 0, 0, 0)
self.properties_frame.setLayout(self.properties_box)
# ## Title
title_label = QtWidgets.QLabel("%s" % self.toolName)
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.properties_box.addWidget(title_label)
# self.layout.setMargin(0) # PyQt4
self.properties_box.setContentsMargins(0, 0, 0, 0) # PyQt5
self.vlay = QtWidgets.QVBoxLayout()
self.properties_box.addLayout(self.vlay)
self.treeWidget = FCTree(columns=2)
self.vlay.addWidget(self.treeWidget)
self.vlay.setStretch(0, 0)
self.calculations_finished.connect(self.show_area_chull)
def run(self, toggle=True):
self.app.defaults.report_usage("ToolProperties()")
if self.app.tool_tab_locked is True:
return
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])
AppTool.run(self)
self.set_tool_ui()
self.properties()
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='P', **kwargs)
def set_tool_ui(self):
# this reset the TreeWidget
self.treeWidget.clear()
self.properties_frame.show()
def properties(self):
obj_list = self.app.collection.get_selected()
if not obj_list:
self.app.inform.emit('[ERROR_NOTCL] %s' % _("No object selected."))
self.app.ui.notebook.setTabText(2, _("Tools"))
self.properties_frame.hide()
self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
return
# delete the selection shape, if any
try:
self.app.delete_selection_shape()
except Exception as e:
log.debug("ToolProperties.Properties.properties() --> %s" % str(e))
# populate the properties items
for obj in obj_list:
self.addItems(obj)
self.app.inform.emit('[success] %s' % _("Object Properties are displayed."))
# make sure that the FCTree widget columns are resized to content
self.treeWidget.resize_sig.emit()
self.app.ui.notebook.setTabText(2, _("Properties Tool"))
def addItems(self, obj):
parent = self.treeWidget.invisibleRootItem()
apertures = ''
tools = ''
drills = ''
slots = ''
others = ''
font = QtGui.QFont()
font.setBold(True)
# main Items categories
obj_type = self.treeWidget.addParent(parent, _('TYPE'), expanded=True, color=QtGui.QColor("#000000"), font=font)
obj_name = self.treeWidget.addParent(parent, _('NAME'), expanded=True, color=QtGui.QColor("#000000"), font=font)
dims = self.treeWidget.addParent(
parent, _('Dimensions'), expanded=True, color=QtGui.QColor("#000000"), font=font)
units = self.treeWidget.addParent(parent, _('Units'), expanded=True, color=QtGui.QColor("#000000"), font=font)
options = self.treeWidget.addParent(parent, _('Options'), color=QtGui.QColor("#000000"), font=font)
if obj.kind.lower() == 'gerber':
apertures = self.treeWidget.addParent(
parent, _('Apertures'), expanded=True, color=QtGui.QColor("#000000"), font=font)
else:
tools = self.treeWidget.addParent(
parent, _('Tools'), expanded=True, color=QtGui.QColor("#000000"), font=font)
if obj.kind.lower() == 'excellon':
drills = self.treeWidget.addParent(
parent, _('Drills'), expanded=True, color=QtGui.QColor("#000000"), font=font)
slots = self.treeWidget.addParent(
parent, _('Slots'), expanded=True, color=QtGui.QColor("#000000"), font=font)
if obj.kind.lower() == 'cncjob':
others = self.treeWidget.addParent(
parent, _('Others'), expanded=True, color=QtGui.QColor("#000000"), font=font)
separator = self.treeWidget.addParent(parent, '')
self.treeWidget.addChild(
obj_type, ['%s:' % _('Object Type'), ('%s' % (obj.kind.upper()))], True, font=font, font_items=1)
try:
self.treeWidget.addChild(obj_type,
[
'%s:' % _('Geo Type'),
('%s' % (
{
False: _("Single-Geo"),
True: _("Multi-Geo")
}[obj.multigeo])
)
],
True)
except Exception as e:
log.debug("Properties.addItems() --> %s" % str(e))
self.treeWidget.addChild(obj_name, [obj.options['name']])
def job_thread(obj_prop):
proc = self.app.proc_container.new(_("Calculating dimensions ... Please wait."))
length = 0.0
width = 0.0
area = 0.0
copper_area = 0.0
geo = obj_prop.solid_geometry
if geo:
# calculate physical dimensions
try:
xmin, ymin, xmax, ymax = obj_prop.bounds()
length = abs(xmax - xmin)
width = abs(ymax - ymin)
except Exception as ee:
log.debug("PropertiesTool.addItems() -> calculate dimensions --> %s" % str(ee))
# calculate box area
if self.app.defaults['units'].lower() == 'mm':
area = (length * width) / 100
else:
area = length * width
if obj_prop.kind.lower() == 'gerber':
# calculate copper area
try:
for geo_el in geo:
copper_area += geo_el.area
except TypeError:
copper_area += geo.area
copper_area /= 100
else:
xmin = []
ymin = []
xmax = []
ymax = []
if obj_prop.kind.lower() == 'cncjob':
try:
for tool_k in obj_prop.exc_cnc_tools:
x0, y0, x1, y1 = cascaded_union(obj_prop.exc_cnc_tools[tool_k]['solid_geometry']).bounds
xmin.append(x0)
ymin.append(y0)
xmax.append(x1)
ymax.append(y1)
except Exception as ee:
log.debug("PropertiesTool.addItems() --> %s" % str(ee))
try:
for tool_k in obj_prop.cnc_tools:
x0, y0, x1, y1 = cascaded_union(obj_prop.cnc_tools[tool_k]['solid_geometry']).bounds
xmin.append(x0)
ymin.append(y0)
xmax.append(x1)
ymax.append(y1)
except Exception as ee:
log.debug("PropertiesTool.addItems() --> %s" % str(ee))
else:
try:
for tool_k in obj_prop.tools:
x0, y0, x1, y1 = cascaded_union(obj_prop.tools[tool_k]['solid_geometry']).bounds
xmin.append(x0)
ymin.append(y0)
xmax.append(x1)
ymax.append(y1)
except Exception as ee:
log.debug("PropertiesTool.addItems() --> %s" % str(ee))
try:
xmin = min(xmin)
ymin = min(ymin)
xmax = max(xmax)
ymax = max(ymax)
length = abs(xmax - xmin)
width = abs(ymax - ymin)
# calculate box area
if self.app.defaults['units'].lower() == 'mm':
area = (length * width) / 100
else:
area = length * width
if obj_prop.kind.lower() == 'gerber':
# calculate copper area
# create a complete solid_geometry from the tools
geo_tools = []
for tool_k in obj_prop.tools:
if 'solid_geometry' in obj_prop.tools[tool_k]:
for geo_el in obj_prop.tools[tool_k]['solid_geometry']:
geo_tools.append(geo_el)
try:
for geo_el in geo_tools:
copper_area += geo_el.area
except TypeError:
copper_area += geo_tools.area
copper_area /= 100
except Exception as err:
log.debug("Properties.addItems() --> %s" % str(err))
area_chull = 0.0
if obj_prop.kind.lower() != 'cncjob':
# calculate and add convex hull area
if geo:
if isinstance(geo, list) and geo[0] is not None:
if isinstance(geo, MultiPolygon):
env_obj = geo.convex_hull
elif (isinstance(geo, MultiPolygon) and len(geo) == 1) or \
(isinstance(geo, list) and len(geo) == 1) and isinstance(geo[0], Polygon):
env_obj = cascaded_union(geo)
env_obj = env_obj.convex_hull
else:
env_obj = cascaded_union(geo)
env_obj = env_obj.convex_hull
area_chull = env_obj.area
else:
area_chull = 0
else:
try:
area_chull = []
for tool_k in obj_prop.tools:
area_el = cascaded_union(obj_prop.tools[tool_k]['solid_geometry']).convex_hull
area_chull.append(area_el.area)
area_chull = max(area_chull)
except Exception as er:
area_chull = None
log.debug("Properties.addItems() --> %s" % str(er))
if self.app.defaults['units'].lower() == 'mm' and area_chull:
area_chull = area_chull / 100
if area_chull is None:
area_chull = 0
self.calculations_finished.emit(area, length, width, area_chull, copper_area, dims)
self.app.worker_task.emit({'fcn': job_thread, 'params': [obj]})
# Units items
f_unit = {'in': _('Inch'), 'mm': _('Metric')}[str(self.app.defaults['units'].lower())]
self.treeWidget.addChild(units, ['FlatCAM units:', f_unit], True)
o_unit = {
'in': _('Inch'),
'mm': _('Metric'),
'inch': _('Inch'),
'metric': _('Metric')
}[str(obj.units_found.lower())]
self.treeWidget.addChild(units, ['Object units:', o_unit], True)
# Options items
for option in obj.options:
if option == 'name':
continue
self.treeWidget.addChild(options, [str(option), str(obj.options[option])], True)
# Items that depend on the object type
if obj.kind.lower() == 'gerber':
temp_ap = {}
for ap in obj.apertures:
temp_ap.clear()
temp_ap = deepcopy(obj.apertures[ap])
temp_ap.pop('geometry', None)
solid_nr = 0
follow_nr = 0
clear_nr = 0
if 'geometry' in obj.apertures[ap]:
if obj.apertures[ap]['geometry']:
font.setBold(True)
for el in obj.apertures[ap]['geometry']:
if 'solid' in el:
solid_nr += 1
if 'follow' in el:
follow_nr += 1
if 'clear' in el:
clear_nr += 1
else:
font.setBold(False)
temp_ap['Solid_Geo'] = '%s Polygons' % str(solid_nr)
temp_ap['Follow_Geo'] = '%s LineStrings' % str(follow_nr)
temp_ap['Clear_Geo'] = '%s Polygons' % str(clear_nr)
apid = self.treeWidget.addParent(
apertures, str(ap), expanded=False, color=QtGui.QColor("#000000"), font=font)
for key in temp_ap:
self.treeWidget.addChild(apid, [str(key), str(temp_ap[key])], True)
elif obj.kind.lower() == 'excellon':
tot_drill_cnt = 0
tot_slot_cnt = 0
for tool, value in obj.tools.items():
toolid = self.treeWidget.addParent(
tools, str(tool), expanded=False, color=QtGui.QColor("#000000"), font=font)
drill_cnt = 0 # variable to store the nr of drills per tool
slot_cnt = 0 # variable to store the nr of slots per tool
# Find no of drills for the current tool
for drill in obj.drills:
if drill['tool'] == tool:
drill_cnt += 1
tot_drill_cnt += drill_cnt
# Find no of slots for the current tool
for slot in obj.slots:
if slot['tool'] == tool:
slot_cnt += 1
tot_slot_cnt += slot_cnt
self.treeWidget.addChild(
toolid,
[
_('Diameter'),
'%.*f %s' % (self.decimals, value['C'], self.app.defaults['units'].lower())
],
True
)
self.treeWidget.addChild(toolid, [_('Drills number'), str(drill_cnt)], True)
self.treeWidget.addChild(toolid, [_('Slots number'), str(slot_cnt)], True)
self.treeWidget.addChild(drills, [_('Drills total number:'), str(tot_drill_cnt)], True)
self.treeWidget.addChild(slots, [_('Slots total number:'), str(tot_slot_cnt)], True)
elif obj.kind.lower() == 'geometry':
for tool, value in obj.tools.items():
geo_tool = self.treeWidget.addParent(
tools, str(tool), expanded=True, color=QtGui.QColor("#000000"), font=font)
for k, v in value.items():
if k == 'solid_geometry':
# printed_value = _('Present') if v else _('None')
try:
printed_value = str(len(v))
except (TypeError, AttributeError):
printed_value = '1'
self.treeWidget.addChild(geo_tool, [str(k), printed_value], True)
elif k == 'data':
tool_data = self.treeWidget.addParent(
geo_tool, str(k).capitalize(), color=QtGui.QColor("#000000"), font=font)
for data_k, data_v in v.items():
self.treeWidget.addChild(tool_data, [str(data_k), str(data_v)], True)
else:
self.treeWidget.addChild(geo_tool, [str(k), str(v)], True)
elif obj.kind.lower() == 'cncjob':
# for cncjob objects made from gerber or geometry
for tool, value in obj.cnc_tools.items():
geo_tool = self.treeWidget.addParent(
tools, str(tool), expanded=True, color=QtGui.QColor("#000000"), font=font)
for k, v in value.items():
if k == 'solid_geometry':
printed_value = _('Present') if v else _('None')
self.treeWidget.addChild(geo_tool, [_("Solid Geometry"), printed_value], True)
elif k == 'gcode':
printed_value = _('Present') if v != '' else _('None')
self.treeWidget.addChild(geo_tool, [_("GCode Text"), printed_value], True)
elif k == 'gcode_parsed':
printed_value = _('Present') if v else _('None')
self.treeWidget.addChild(geo_tool, [_("GCode Geometry"), printed_value], True)
elif k == 'data':
tool_data = self.treeWidget.addParent(
geo_tool, _("Data"), color=QtGui.QColor("#000000"), font=font)
for data_k, data_v in v.items():
self.treeWidget.addChild(tool_data, [str(data_k).capitalize(), str(data_v)], True)
else:
self.treeWidget.addChild(geo_tool, [str(k), str(v)], True)
# for cncjob objects made from excellon
for tool_dia, value in obj.exc_cnc_tools.items():
exc_tool = self.treeWidget.addParent(
tools, str(value['tool']), expanded=False, color=QtGui.QColor("#000000"), font=font
)
self.treeWidget.addChild(
exc_tool,
[
_('Diameter'),
'%.*f %s' % (self.decimals, tool_dia, self.app.defaults['units'].lower())
],
True
)
for k, v in value.items():
if k == 'solid_geometry':
printed_value = _('Present') if v else _('None')
self.treeWidget.addChild(exc_tool, [_("Solid Geometry"), printed_value], True)
elif k == 'nr_drills':
self.treeWidget.addChild(exc_tool, [_("Drills number"), str(v)], True)
elif k == 'nr_slots':
self.treeWidget.addChild(exc_tool, [_("Slots number"), str(v)], True)
else:
pass
self.treeWidget.addChild(
exc_tool,
[
_("Depth of Cut"),
'%.*f %s' % (
self.decimals,
(obj.z_cut - abs(obj.tool_offset[tool_dia])),
self.app.defaults['units'].lower()
)
],
True
)
self.treeWidget.addChild(
exc_tool,
[
_("Clearance Height"),
'%.*f %s' % (
self.decimals,
obj.z_move,
self.app.defaults['units'].lower()
)
],
True
)
self.treeWidget.addChild(
exc_tool,
[
_("Feedrate"),
'%.*f %s/min' % (
self.decimals,
obj.feedrate,
self.app.defaults['units'].lower()
)
],
True
)
r_time = obj.routing_time
if r_time > 1:
units_lbl = 'min'
else:
r_time *= 60
units_lbl = 'sec'
r_time = math.ceil(float(r_time))
self.treeWidget.addChild(
others,
[
'%s:' % _('Routing time'),
'%.*f %s' % (self.decimals, r_time, units_lbl)],
True
)
self.treeWidget.addChild(
others,
[
'%s:' % _('Travelled distance'),
'%.*f %s' % (self.decimals, obj.travel_distance, self.app.defaults['units'].lower())
],
True
)
self.treeWidget.addChild(separator, [''])
def show_area_chull(self, area, length, width, chull_area, copper_area, location):
# add dimensions
self.treeWidget.addChild(
location,
['%s:' % _('Length'), '%.*f %s' % (self.decimals, length, self.app.defaults['units'].lower())],
True
)
self.treeWidget.addChild(
location,
['%s:' % _('Width'), '%.*f %s' % (self.decimals, width, self.app.defaults['units'].lower())],
True
)
# add box area
if self.app.defaults['units'].lower() == 'mm':
self.treeWidget.addChild(location, ['%s:' % _('Box Area'), '%.*f %s' % (self.decimals, area, 'cm2')], True)
self.treeWidget.addChild(
location,
['%s:' % _('Convex_Hull Area'), '%.*f %s' % (self.decimals, chull_area, 'cm2')],
True
)
else:
self.treeWidget.addChild(location, ['%s:' % _('Box Area'), '%.*f %s' % (self.decimals, area, 'in2')], True)
self.treeWidget.addChild(
location,
['%s:' % _('Convex_Hull Area'), '%.*f %s' % (self.decimals, chull_area, 'in2')],
True
)
# add copper area
if self.app.defaults['units'].lower() == 'mm':
self.treeWidget.addChild(
location, ['%s:' % _('Copper Area'), '%.*f %s' % (self.decimals, copper_area, 'cm2')], True)
else:
self.treeWidget.addChild(
location, ['%s:' % _('Copper Area'), '%.*f %s' % (self.decimals, copper_area, 'in2')], True)
# end of file

1003
appTools/ToolPunchGerber.py Normal file

File diff suppressed because it is too large Load Diff

897
appTools/ToolQRCode.py Normal file
View File

@@ -0,0 +1,897 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 10/24/2019 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.QtCore import Qt
from appTool import AppTool
from appGUI.GUIElements import RadioSet, FCTextArea, FCSpinner, FCEntry, FCCheckBox, FCComboBox, FCFileSaveDialog
from appParsers.ParseSVG import *
from shapely.geometry.base import *
from shapely.ops import unary_union
from shapely.affinity import translate
from shapely.geometry import box
from io import StringIO, BytesIO
from collections import Iterable
import logging
from copy import deepcopy
import qrcode
import qrcode.image.svg
import qrcode.image.pil
from lxml import etree as ET
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class QRCode(AppTool):
toolName = _("QRCode Tool")
def __init__(self, app):
AppTool.__init__(self, app)
self.app = app
self.canvas = self.app.plotcanvas
self.decimals = self.app.decimals
self.units = ''
# ## 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(''))
# ## Grid Layout
i_grid_lay = QtWidgets.QGridLayout()
self.layout.addLayout(i_grid_lay)
i_grid_lay.setColumnStretch(0, 0)
i_grid_lay.setColumnStretch(1, 1)
self.grb_object_combo = FCComboBox()
self.grb_object_combo.setModel(self.app.collection)
self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.grb_object_combo.is_last = True
self.grb_object_combo.obj_type = "Gerber"
self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
self.grbobj_label.setToolTip(
_("Gerber Object to which the QRCode will be added.")
)
i_grid_lay.addWidget(self.grbobj_label, 0, 0)
i_grid_lay.addWidget(self.grb_object_combo, 1, 0, 1, 2)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
i_grid_lay.addWidget(separator_line, 2, 0, 1, 2)
# Text box
self.text_label = QtWidgets.QLabel('<b>%s</b>:' % _("QRCode Data"))
self.text_label.setToolTip(
_("QRCode Data. Alphanumeric text to be encoded in the QRCode.")
)
self.text_data = FCTextArea()
self.text_data.setPlaceholderText(
_("Add here the text to be included in the QRCode...")
)
i_grid_lay.addWidget(self.text_label, 5, 0)
i_grid_lay.addWidget(self.text_data, 6, 0, 1, 2)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
i_grid_lay.addWidget(separator_line, 7, 0, 1, 2)
# ## Grid Layout
grid_lay = QtWidgets.QGridLayout()
self.layout.addLayout(grid_lay)
grid_lay.setColumnStretch(0, 0)
grid_lay.setColumnStretch(1, 1)
self.qrcode_label = QtWidgets.QLabel('<b>%s</b>' % _('Parameters'))
self.qrcode_label.setToolTip(
_("The parameters used to shape the QRCode.")
)
grid_lay.addWidget(self.qrcode_label, 0, 0, 1, 2)
# VERSION #
self.version_label = QtWidgets.QLabel('%s:' % _("Version"))
self.version_label.setToolTip(
_("QRCode version can have values from 1 (21x21 boxes)\n"
"to 40 (177x177 boxes).")
)
self.version_entry = FCSpinner(callback=self.confirmation_message_int)
self.version_entry.set_range(1, 40)
self.version_entry.setWrapping(True)
grid_lay.addWidget(self.version_label, 1, 0)
grid_lay.addWidget(self.version_entry, 1, 1)
# ERROR CORRECTION #
self.error_label = QtWidgets.QLabel('%s:' % _("Error correction"))
self.error_label.setToolTip(
_("Parameter that controls the error correction used for the QR Code.\n"
"L = maximum 7%% errors can be corrected\n"
"M = maximum 15%% errors can be corrected\n"
"Q = maximum 25%% errors can be corrected\n"
"H = maximum 30%% errors can be corrected.")
)
self.error_radio = RadioSet([{'label': 'L', 'value': 'L'},
{'label': 'M', 'value': 'M'},
{'label': 'Q', 'value': 'Q'},
{'label': 'H', 'value': 'H'}])
self.error_radio.setToolTip(
_("Parameter that controls the error correction used for the QR Code.\n"
"L = maximum 7%% errors can be corrected\n"
"M = maximum 15%% errors can be corrected\n"
"Q = maximum 25%% errors can be corrected\n"
"H = maximum 30%% errors can be corrected.")
)
grid_lay.addWidget(self.error_label, 2, 0)
grid_lay.addWidget(self.error_radio, 2, 1)
# BOX SIZE #
self.bsize_label = QtWidgets.QLabel('%s:' % _("Box Size"))
self.bsize_label.setToolTip(
_("Box size control the overall size of the QRcode\n"
"by adjusting the size of each box in the code.")
)
self.bsize_entry = FCSpinner(callback=self.confirmation_message_int)
self.bsize_entry.set_range(1, 9999)
self.bsize_entry.setWrapping(True)
grid_lay.addWidget(self.bsize_label, 3, 0)
grid_lay.addWidget(self.bsize_entry, 3, 1)
# BORDER SIZE #
self.border_size_label = QtWidgets.QLabel('%s:' % _("Border Size"))
self.border_size_label.setToolTip(
_("Size of the QRCode border. How many boxes thick is the border.\n"
"Default value is 4. The width of the clearance around the QRCode.")
)
self.border_size_entry = FCSpinner(callback=self.confirmation_message_int)
self.border_size_entry.set_range(1, 9999)
self.border_size_entry.setWrapping(True)
grid_lay.addWidget(self.border_size_label, 4, 0)
grid_lay.addWidget(self.border_size_entry, 4, 1)
# POLARITY CHOICE #
self.pol_label = QtWidgets.QLabel('%s:' % _("Polarity"))
self.pol_label.setToolTip(
_("Choose the polarity of the QRCode.\n"
"It can be drawn in a negative way (squares are clear)\n"
"or in a positive way (squares are opaque).")
)
self.pol_radio = RadioSet([{'label': _('Negative'), 'value': 'neg'},
{'label': _('Positive'), 'value': 'pos'}])
self.pol_radio.setToolTip(
_("Choose the type of QRCode to be created.\n"
"If added on a Silkscreen Gerber file the QRCode may\n"
"be added as positive. If it is added to a Copper Gerber\n"
"file then perhaps the QRCode can be added as negative.")
)
grid_lay.addWidget(self.pol_label, 7, 0)
grid_lay.addWidget(self.pol_radio, 7, 1)
# BOUNDING BOX TYPE #
self.bb_label = QtWidgets.QLabel('%s:' % _("Bounding Box"))
self.bb_label.setToolTip(
_("The bounding box, meaning the empty space that surrounds\n"
"the QRCode geometry, can have a rounded or a square shape.")
)
self.bb_radio = RadioSet([{'label': _('Rounded'), 'value': 'r'},
{'label': _('Square'), 'value': 's'}])
self.bb_radio.setToolTip(
_("The bounding box, meaning the empty space that surrounds\n"
"the QRCode geometry, can have a rounded or a square shape.")
)
grid_lay.addWidget(self.bb_label, 8, 0)
grid_lay.addWidget(self.bb_radio, 8, 1)
# Export QRCode
self.export_cb = FCCheckBox(_("Export QRCode"))
self.export_cb.setToolTip(
_("Show a set of controls allowing to export the QRCode\n"
"to a SVG file or an PNG file.")
)
grid_lay.addWidget(self.export_cb, 9, 0, 1, 2)
# this way I can hide/show the frame
self.export_frame = QtWidgets.QFrame()
self.export_frame.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(self.export_frame)
self.export_lay = QtWidgets.QGridLayout()
self.export_lay.setContentsMargins(0, 0, 0, 0)
self.export_frame.setLayout(self.export_lay)
self.export_lay.setColumnStretch(0, 0)
self.export_lay.setColumnStretch(1, 1)
# default is hidden
self.export_frame.hide()
# FILL COLOR #
self.fill_color_label = QtWidgets.QLabel('%s:' % _('Fill Color'))
self.fill_color_label.setToolTip(
_("Set the QRCode fill color (squares color).")
)
self.fill_color_entry = FCEntry()
self.fill_color_button = QtWidgets.QPushButton()
self.fill_color_button.setFixedSize(15, 15)
fill_lay_child = QtWidgets.QHBoxLayout()
fill_lay_child.setContentsMargins(0, 0, 0, 0)
fill_lay_child.addWidget(self.fill_color_entry)
fill_lay_child.addWidget(self.fill_color_button, alignment=Qt.AlignRight)
fill_lay_child.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
fill_color_widget = QtWidgets.QWidget()
fill_color_widget.setLayout(fill_lay_child)
self.export_lay.addWidget(self.fill_color_label, 0, 0)
self.export_lay.addWidget(fill_color_widget, 0, 1)
self.transparent_cb = FCCheckBox(_("Transparent back color"))
self.export_lay.addWidget(self.transparent_cb, 1, 0, 1, 2)
# BACK COLOR #
self.back_color_label = QtWidgets.QLabel('%s:' % _('Back Color'))
self.back_color_label.setToolTip(
_("Set the QRCode background color.")
)
self.back_color_entry = FCEntry()
self.back_color_button = QtWidgets.QPushButton()
self.back_color_button.setFixedSize(15, 15)
back_lay_child = QtWidgets.QHBoxLayout()
back_lay_child.setContentsMargins(0, 0, 0, 0)
back_lay_child.addWidget(self.back_color_entry)
back_lay_child.addWidget(self.back_color_button, alignment=Qt.AlignRight)
back_lay_child.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
back_color_widget = QtWidgets.QWidget()
back_color_widget.setLayout(back_lay_child)
self.export_lay.addWidget(self.back_color_label, 2, 0)
self.export_lay.addWidget(back_color_widget, 2, 1)
# ## Export QRCode as SVG image
self.export_svg_button = QtWidgets.QPushButton(_("Export QRCode SVG"))
self.export_svg_button.setToolTip(
_("Export a SVG file with the QRCode content.")
)
self.export_svg_button.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
self.export_lay.addWidget(self.export_svg_button, 3, 0, 1, 2)
# ## Export QRCode as PNG image
self.export_png_button = QtWidgets.QPushButton(_("Export QRCode PNG"))
self.export_png_button.setToolTip(
_("Export a PNG image file with the QRCode content.")
)
self.export_png_button.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
self.export_lay.addWidget(self.export_png_button, 4, 0, 1, 2)
# ## Insert QRCode
self.qrcode_button = QtWidgets.QPushButton(_("Insert QRCode"))
self.qrcode_button.setToolTip(
_("Create the QRCode object.")
)
self.qrcode_button.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
self.layout.addWidget(self.qrcode_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.grb_object = None
self.box_poly = None
self.proc = None
self.origin = (0, 0)
self.mm = None
self.mr = None
self.kr = None
self.shapes = self.app.move_tool.sel_shapes
self.qrcode_geometry = MultiPolygon()
self.qrcode_utility_geometry = MultiPolygon()
self.old_back_color = ''
# Signals #
self.qrcode_button.clicked.connect(self.execute)
self.export_cb.stateChanged.connect(self.on_export_frame)
self.export_png_button.clicked.connect(self.export_png_file)
self.export_svg_button.clicked.connect(self.export_svg_file)
self.fill_color_entry.editingFinished.connect(self.on_qrcode_fill_color_entry)
self.fill_color_button.clicked.connect(self.on_qrcode_fill_color_button)
self.back_color_entry.editingFinished.connect(self.on_qrcode_back_color_entry)
self.back_color_button.clicked.connect(self.on_qrcode_back_color_button)
self.transparent_cb.stateChanged.connect(self.on_transparent_back_color)
self.reset_button.clicked.connect(self.set_tool_ui)
def run(self, toggle=True):
self.app.defaults.report_usage("QRCode()")
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])
AppTool.run(self)
self.set_tool_ui()
self.app.ui.notebook.setTabText(2, _("QRCode Tool"))
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='Alt+Q', **kwargs)
def set_tool_ui(self):
self.units = self.app.defaults['units']
self.border_size_entry.set_value(4)
self.version_entry.set_value(int(self.app.defaults["tools_qrcode_version"]))
self.error_radio.set_value(self.app.defaults["tools_qrcode_error"])
self.bsize_entry.set_value(int(self.app.defaults["tools_qrcode_box_size"]))
self.border_size_entry.set_value(int(self.app.defaults["tools_qrcode_border_size"]))
self.pol_radio.set_value(self.app.defaults["tools_qrcode_polarity"])
self.bb_radio.set_value(self.app.defaults["tools_qrcode_rounded"])
self.text_data.set_value(self.app.defaults["tools_qrcode_qrdata"])
self.fill_color_entry.set_value(self.app.defaults['tools_qrcode_fill_color'])
self.fill_color_button.setStyleSheet("background-color:%s" %
str(self.app.defaults['tools_qrcode_fill_color'])[:7])
self.back_color_entry.set_value(self.app.defaults['tools_qrcode_back_color'])
self.back_color_button.setStyleSheet("background-color:%s" %
str(self.app.defaults['tools_qrcode_back_color'])[:7])
def on_export_frame(self, state):
self.export_frame.setVisible(state)
self.qrcode_button.setVisible(not state)
def execute(self):
text_data = self.text_data.get_value()
if text_data == '':
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box."))
return 'fail'
# get the Gerber object on which the QRCode will be inserted
selection_index = self.grb_object_combo.currentIndex()
model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
try:
self.grb_object = model_index.internalPointer().obj
except Exception as e:
log.debug("QRCode.execute() --> %s" % str(e))
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
return 'fail'
# we can safely activate the mouse events
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_release)
self.kr = self.app.plotcanvas.graph_event_connect('key_release', self.on_key_release)
self.proc = self.app.proc_container.new('%s...' % _("Generating QRCode geometry"))
def job_thread_qr(app_obj):
error_code = {
'L': qrcode.constants.ERROR_CORRECT_L,
'M': qrcode.constants.ERROR_CORRECT_M,
'Q': qrcode.constants.ERROR_CORRECT_Q,
'H': qrcode.constants.ERROR_CORRECT_H
}[self.error_radio.get_value()]
qr = qrcode.QRCode(
version=self.version_entry.get_value(),
error_correction=error_code,
box_size=self.bsize_entry.get_value(),
border=self.border_size_entry.get_value(),
image_factory=qrcode.image.svg.SvgFragmentImage
)
qr.add_data(text_data)
qr.make()
svg_file = BytesIO()
img = qr.make_image()
img.save(svg_file)
svg_text = StringIO(svg_file.getvalue().decode('UTF-8'))
svg_geometry = self.convert_svg_to_geo(svg_text, units=self.units)
self.qrcode_geometry = deepcopy(svg_geometry)
svg_geometry = unary_union(svg_geometry).buffer(0.0000001).buffer(-0.0000001)
self.qrcode_utility_geometry = svg_geometry
# make a bounding box of the QRCode geometry to help drawing the utility geometry in case it is too
# complicated
try:
a, b, c, d = self.qrcode_utility_geometry.bounds
self.box_poly = box(minx=a, miny=b, maxx=c, maxy=d)
except Exception as ee:
log.debug("QRCode.make() bounds error --> %s" % str(ee))
app_obj.call_source = 'qrcode_tool'
app_obj.inform.emit(_("Click on the Destination point ..."))
self.app.worker_task.emit({'fcn': job_thread_qr, 'params': [self.app]})
def make(self, pos):
self.on_exit()
# make sure that the source object solid geometry is an Iterable
if not isinstance(self.grb_object.solid_geometry, Iterable):
self.grb_object.solid_geometry = [self.grb_object.solid_geometry]
# I use the utility geometry (self.qrcode_utility_geometry) because it is already buffered
geo_list = self.grb_object.solid_geometry
if isinstance(self.grb_object.solid_geometry, MultiPolygon):
geo_list = list(self.grb_object.solid_geometry.geoms)
# this is the bounding box of the QRCode geometry
a, b, c, d = self.qrcode_utility_geometry.bounds
buff_val = self.border_size_entry.get_value() * (self.bsize_entry.get_value() / 10)
if self.bb_radio.get_value() == 'r':
mask_geo = box(a, b, c, d).buffer(buff_val)
else:
mask_geo = box(a, b, c, d).buffer(buff_val, join_style=2)
# update the solid geometry with the cutout (if it is the case)
new_solid_geometry = []
offset_mask_geo = translate(mask_geo, xoff=pos[0], yoff=pos[1])
for poly in geo_list:
if poly.contains(offset_mask_geo):
new_solid_geometry.append(poly.difference(offset_mask_geo))
else:
if poly not in new_solid_geometry:
new_solid_geometry.append(poly)
geo_list = deepcopy(list(new_solid_geometry))
# Polarity
if self.pol_radio.get_value() == 'pos':
working_geo = self.qrcode_utility_geometry
else:
working_geo = mask_geo.difference(self.qrcode_utility_geometry)
try:
for geo in working_geo:
geo_list.append(translate(geo, xoff=pos[0], yoff=pos[1]))
except TypeError:
geo_list.append(translate(working_geo, xoff=pos[0], yoff=pos[1]))
self.grb_object.solid_geometry = deepcopy(geo_list)
box_size = float(self.bsize_entry.get_value()) / 10.0
sort_apid = []
new_apid = '10'
if self.grb_object.apertures:
for k, v in list(self.grb_object.apertures.items()):
sort_apid.append(int(k))
sorted_apertures = sorted(sort_apid)
max_apid = max(sorted_apertures)
if max_apid >= 10:
new_apid = str(max_apid + 1)
else:
new_apid = '10'
# don't know if the condition is required since I already made sure above that the new_apid is a new one
if new_apid not in self.grb_object.apertures:
self.grb_object.apertures[new_apid] = {}
self.grb_object.apertures[new_apid]['geometry'] = []
self.grb_object.apertures[new_apid]['type'] = 'R'
# TODO: HACK
# I've artificially added 1% to the height and width because otherwise after loading the
# exported file, it will not be correctly reconstructed (it will be made from multiple shapes instead of
# one shape which show that the buffering didn't worked well). It may be the MM to INCH conversion.
self.grb_object.apertures[new_apid]['height'] = deepcopy(box_size * 1.01)
self.grb_object.apertures[new_apid]['width'] = deepcopy(box_size * 1.01)
self.grb_object.apertures[new_apid]['size'] = deepcopy(math.sqrt(box_size ** 2 + box_size ** 2))
if '0' not in self.grb_object.apertures:
self.grb_object.apertures['0'] = {}
self.grb_object.apertures['0']['geometry'] = []
self.grb_object.apertures['0']['type'] = 'REG'
self.grb_object.apertures['0']['size'] = 0.0
# in case that the QRCode geometry is dropped onto a copper region (found in the '0' aperture)
# make sure that I place a cutout there
zero_elem = {}
zero_elem['clear'] = offset_mask_geo
self.grb_object.apertures['0']['geometry'].append(deepcopy(zero_elem))
try:
a, b, c, d = self.grb_object.bounds()
self.grb_object.options['xmin'] = a
self.grb_object.options['ymin'] = b
self.grb_object.options['xmax'] = c
self.grb_object.options['ymax'] = d
except Exception as e:
log.debug("QRCode.make() bounds error --> %s" % str(e))
try:
for geo in self.qrcode_geometry:
geo_elem = {}
geo_elem['solid'] = translate(geo, xoff=pos[0], yoff=pos[1])
geo_elem['follow'] = translate(geo.centroid, xoff=pos[0], yoff=pos[1])
self.grb_object.apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
except TypeError:
geo_elem = {}
geo_elem['solid'] = self.qrcode_geometry
self.grb_object.apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
# update the source file with the new geometry:
self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'], filename=None,
local_use=self.grb_object, use_thread=False)
self.replot(obj=self.grb_object)
self.app.inform.emit('[success] %s' % _("QRCode Tool done."))
def draw_utility_geo(self, pos):
# face = '#0000FF' + str(hex(int(0.2 * 255)))[2:]
outline = '#0000FFAF'
offset_geo = []
# I use the len of self.qrcode_geometry instead of the utility one because the complexity of the polygons is
# better seen in this (bit what if the sel.qrcode_geometry is just one geo element? len will fail ...
if len(self.qrcode_geometry) <= self.app.defaults["tools_qrcode_sel_limit"]:
try:
for poly in self.qrcode_utility_geometry:
offset_geo.append(translate(poly.exterior, xoff=pos[0], yoff=pos[1]))
for geo_int in poly.interiors:
offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1]))
except TypeError:
offset_geo.append(translate(self.qrcode_utility_geometry.exterior, xoff=pos[0], yoff=pos[1]))
for geo_int in self.qrcode_utility_geometry.interiors:
offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1]))
else:
offset_geo = [translate(self.box_poly, xoff=pos[0], yoff=pos[1])]
for shape in offset_geo:
self.shapes.add(shape, color=outline, update=True, layer=0, tolerance=None)
if self.app.is_legacy is True:
self.shapes.redraw()
def delete_utility_geo(self):
self.shapes.clear(update=True)
self.shapes.redraw()
def on_mouse_move(self, event):
if self.app.is_legacy is False:
event_pos = event.pos
else:
event_pos = (event.xdata, event.ydata)
try:
x = float(event_pos[0])
y = float(event_pos[1])
except TypeError:
return
pos_canvas = self.app.plotcanvas.translate_coords((x, y))
# if GRID is active we need to get the snapped positions
if self.app.grid_status() == True:
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
else:
pos = pos_canvas
dx = pos[0] - self.origin[0]
dy = pos[1] - self.origin[1]
# delete the utility geometry
self.delete_utility_geo()
self.draw_utility_geo((dx, dy))
def on_mouse_release(self, event):
# mouse click will be accepted only if the left button is clicked
# this is necessary because right mouse click and middle mouse click
# are used for panning on the canvas
if self.app.is_legacy is False:
event_pos = event.pos
else:
event_pos = (event.xdata, event.ydata)
if event.button == 1:
pos_canvas = self.app.plotcanvas.translate_coords(event_pos)
self.delete_utility_geo()
# if GRID is active we need to get the snapped positions
if self.app.grid_status() == True:
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
else:
pos = pos_canvas
dx = pos[0] - self.origin[0]
dy = pos[1] - self.origin[1]
self.make(pos=(dx, dy))
def on_key_release(self, event):
pass
def convert_svg_to_geo(self, filename, object_type=None, flip=True, units='MM'):
"""
Convert shapes from an SVG file into a geometry list.
:param filename: A String Stream file.
:param object_type: parameter passed further along. What kind the object will receive the SVG geometry
:param flip: Flip the vertically.
:type flip: bool
:param units: FlatCAM units
:return: None
"""
# Parse into list of shapely objects
svg_tree = ET.parse(filename)
svg_root = svg_tree.getroot()
# Change origin to bottom left
# h = float(svg_root.get('height'))
# w = float(svg_root.get('width'))
h = svgparselength(svg_root.get('height'))[0] # TODO: No units support yet
geos = getsvggeo(svg_root, object_type)
if flip:
geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]
# flatten the svg geometry for the case when the QRCode SVG is added into a Gerber object
solid_geometry = list(self.flatten_list(geos))
geos_text = getsvgtext(svg_root, object_type, units=units)
if geos_text is not None:
geos_text_f = []
if flip:
# Change origin to bottom left
for i in geos_text:
_, minimy, _, maximy = i.bounds
h2 = (maximy - minimy) * 0.5
geos_text_f.append(translate(scale(i, 1.0, -1.0, origin=(0, 0)), yoff=(h + h2)))
if geos_text_f:
solid_geometry += geos_text_f
return solid_geometry
def flatten_list(self, geo_list):
for item in geo_list:
if isinstance(item, Iterable) and not isinstance(item, (str, bytes)):
yield from self.flatten_list(item)
else:
yield item
def replot(self, obj):
def worker_task():
with self.app.proc_container.new('%s...' % _("Plotting")):
obj.plot()
self.app.worker_task.emit({'fcn': worker_task, 'params': []})
def on_exit(self):
if self.app.is_legacy is False:
self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
self.app.plotcanvas.graph_event_disconnect('key_release', self.on_key_release)
else:
self.app.plotcanvas.graph_event_disconnect(self.mm)
self.app.plotcanvas.graph_event_disconnect(self.mr)
self.app.plotcanvas.graph_event_disconnect(self.kr)
# delete the utility geometry
self.delete_utility_geo()
self.app.call_source = 'app'
def export_png_file(self):
text_data = self.text_data.get_value()
if text_data == '':
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box."))
return 'fail'
def job_thread_qr_png(app_obj, fname):
error_code = {
'L': qrcode.constants.ERROR_CORRECT_L,
'M': qrcode.constants.ERROR_CORRECT_M,
'Q': qrcode.constants.ERROR_CORRECT_Q,
'H': qrcode.constants.ERROR_CORRECT_H
}[self.error_radio.get_value()]
qr = qrcode.QRCode(
version=self.version_entry.get_value(),
error_correction=error_code,
box_size=self.bsize_entry.get_value(),
border=self.border_size_entry.get_value(),
image_factory=qrcode.image.pil.PilImage
)
qr.add_data(text_data)
qr.make(fit=True)
img = qr.make_image(fill_color=self.fill_color_entry.get_value(),
back_color=self.back_color_entry.get_value())
img.save(fname)
app_obj.call_source = 'qrcode_tool'
name = 'qr_code'
_filter = "PNG File (*.png);;All Files (*.*)"
try:
filename, _f = FCFileSaveDialog.get_saved_filename(
caption=_("Export PNG"),
directory=self.app.get_last_save_folder() + '/' + str(name) + '_png',
ext_filter=_filter)
except TypeError:
filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export PNG"), ext_filter=_filter)
filename = str(filename)
if filename == "":
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
return
else:
self.app.worker_task.emit({'fcn': job_thread_qr_png, 'params': [self.app, filename]})
def export_svg_file(self):
text_data = self.text_data.get_value()
if text_data == '':
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box."))
return 'fail'
def job_thread_qr_svg(app_obj, fname):
error_code = {
'L': qrcode.constants.ERROR_CORRECT_L,
'M': qrcode.constants.ERROR_CORRECT_M,
'Q': qrcode.constants.ERROR_CORRECT_Q,
'H': qrcode.constants.ERROR_CORRECT_H
}[self.error_radio.get_value()]
qr = qrcode.QRCode(
version=self.version_entry.get_value(),
error_correction=error_code,
box_size=self.bsize_entry.get_value(),
border=self.border_size_entry.get_value(),
image_factory=qrcode.image.svg.SvgPathImage
)
qr.add_data(text_data)
img = qr.make_image(fill_color=self.fill_color_entry.get_value(),
back_color=self.back_color_entry.get_value())
img.save(fname)
app_obj.call_source = 'qrcode_tool'
name = 'qr_code'
_filter = "SVG File (*.svg);;All Files (*.*)"
try:
filename, _f = FCFileSaveDialog.get_saved_filename(
caption=_("Export SVG"),
directory=self.app.get_last_save_folder() + '/' + str(name) + '_svg',
ext_filter=_filter)
except TypeError:
filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export SVG"), ext_filter=_filter)
filename = str(filename)
if filename == "":
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
return
else:
self.app.worker_task.emit({'fcn': job_thread_qr_svg, 'params': [self.app, filename]})
def on_qrcode_fill_color_entry(self):
color = self.fill_color_entry.get_value()
self.fill_color_button.setStyleSheet("background-color:%s" % str(color))
def on_qrcode_fill_color_button(self):
current_color = QtGui.QColor(self.fill_color_entry.get_value())
c_dialog = QtWidgets.QColorDialog()
fill_color = c_dialog.getColor(initial=current_color)
if fill_color.isValid() is False:
return
self.fill_color_button.setStyleSheet("background-color:%s" % str(fill_color.name()))
new_val_sel = str(fill_color.name())
self.fill_color_entry.set_value(new_val_sel)
def on_qrcode_back_color_entry(self):
color = self.back_color_entry.get_value()
self.back_color_button.setStyleSheet("background-color:%s" % str(color))
def on_qrcode_back_color_button(self):
current_color = QtGui.QColor(self.back_color_entry.get_value())
c_dialog = QtWidgets.QColorDialog()
back_color = c_dialog.getColor(initial=current_color)
if back_color.isValid() is False:
return
self.back_color_button.setStyleSheet("background-color:%s" % str(back_color.name()))
new_val_sel = str(back_color.name())
self.back_color_entry.set_value(new_val_sel)
def on_transparent_back_color(self, state):
if state:
self.back_color_entry.setDisabled(True)
self.back_color_button.setDisabled(True)
self.old_back_color = self.back_color_entry.get_value()
self.back_color_entry.set_value('transparent')
else:
self.back_color_entry.setDisabled(False)
self.back_color_button.setDisabled(False)
self.back_color_entry.set_value(self.old_back_color)

1633
appTools/ToolRulesCheck.py Normal file

File diff suppressed because it is too large Load Diff

536
appTools/ToolShell.py Normal file
View File

@@ -0,0 +1,536 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://flatcam.org #
# Author: Juan Pablo Caram (c) #
# Date: 2/5/2014 #
# MIT Licence #
# ##########################################################
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QTextCursor, QPixmap
from PyQt5.QtWidgets import QVBoxLayout, QWidget, QHBoxLayout, QLabel
from appGUI.GUIElements import _BrowserTextEdit, _ExpandableTextEdit, FCLabel
import html
import sys
import traceback
import tkinter as tk
import tclCommands
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
class TermWidget(QWidget):
"""
Widget which represents terminal. It only displays text and allows to enter text.
All high level logic should be implemented by client classes
User pressed Enter. Client class should decide, if command must be executed or user may continue edit it
"""
def __init__(self, version, app, *args):
QWidget.__init__(self, *args)
self.app = app
self._browser = _BrowserTextEdit(version=version, app=app)
self._browser.setStyleSheet("font: 9pt \"Courier\";")
self._browser.setReadOnly(True)
self._browser.document().setDefaultStyleSheet(
self._browser.document().defaultStyleSheet() +
"span {white-space:pre;}")
self._edit = _ExpandableTextEdit(self, self)
self._edit.historyNext.connect(self._on_history_next)
self._edit.historyPrev.connect(self._on_history_prev)
self._edit.setFocus()
self.setFocusProxy(self._edit)
self._delete_line = FCLabel()
self._delete_line.setPixmap(QPixmap(self.app.resource_location + '/clear_line16.png'))
self._delete_line.setMargin(3)
self._delete_line.setToolTip(_("Clear the text."))
layout = QVBoxLayout(self)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._browser)
hlay = QHBoxLayout()
hlay.addWidget(self._delete_line)
hlay.addWidget(QLabel(" "))
hlay.addWidget(self._edit)
layout.addLayout(hlay)
self._history = [''] # current empty line
self._historyIndex = 0
self._delete_line.clicked.connect(self.on_delete_line_clicked)
def on_delete_line_clicked(self):
self._edit.clear()
def open_processing(self, detail=None):
"""
Open processing and disable using shell commands again until all commands are finished
:param detail: text detail about what is currently called from TCL to python
:return: None
"""
self._edit.setTextColor(Qt.white)
self._edit.setTextBackgroundColor(Qt.darkGreen)
if detail is None:
self._edit.setPlainText(_("...processing..."))
else:
self._edit.setPlainText('%s [%s]' % (_("...processing..."), detail))
self._edit.setDisabled(True)
self._edit.setFocus()
def close_processing(self):
"""
Close processing and enable using shell commands again
:return:
"""
self._edit.setTextColor(Qt.black)
self._edit.setTextBackgroundColor(Qt.white)
self._edit.setPlainText('')
self._edit.setDisabled(False)
self._edit.setFocus()
def _append_to_browser(self, style, text):
"""
Convert text to HTML for inserting it to browser
"""
assert style in ('in', 'out', 'err', 'warning', 'success', 'selected', 'raw')
if style != 'raw':
text = html.escape(text)
text = text.replace('\n', '<br/>')
else:
text = text.replace('\n', '<br>')
text = text.replace('\t', '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;')
idx = text.find(']')
mtype = text[:idx+1].upper()
mtype = mtype.replace('_NOTCL', '')
body = text[idx+1:]
if style.lower() == 'in':
text = '<span style="font-weight: bold;">%s</span>' % text
elif style.lower() == 'err':
text = '<span style="font-weight: bold; color: red;">%s</span>'\
'<span style="font-weight: bold;">%s</span>'\
% (mtype, body)
elif style.lower() == 'warning':
# text = '<span style="font-weight: bold; color: #f4b642;">%s</span>' % text
text = '<span style="font-weight: bold; color: #f4b642;">%s</span>' \
'<span style="font-weight: bold;">%s</span>' \
% (mtype, body)
elif style.lower() == 'success':
# text = '<span style="font-weight: bold; color: #15b300;">%s</span>' % text
text = '<span style="font-weight: bold; color: #15b300;">%s</span>' \
'<span style="font-weight: bold;">%s</span>' \
% (mtype, body)
elif style.lower() == 'selected':
text = ''
elif style.lower() == 'raw':
text = text
else:
# without span <br/> is ignored!!!
text = '<span>%s</span>' % text
scrollbar = self._browser.verticalScrollBar()
old_value = scrollbar.value()
# scrollattheend = old_value == scrollbar.maximum()
self._browser.moveCursor(QTextCursor.End)
self._browser.insertHtml(text)
"""TODO When user enters second line to the input, and input is resized, scrollbar changes its position
and stops moving. As quick fix of this problem, now we always scroll down when add new text.
To fix it correctly, scroll to the bottom, if before input has been resized,
scrollbar was in the bottom, and remove next line
"""
scrollattheend = True
if scrollattheend:
scrollbar.setValue(scrollbar.maximum())
else:
scrollbar.setValue(old_value)
def exec_current_command(self):
"""
Save current command in the history. Append it to the log. Clear edit line
Re-implement in the child classes to actually execute command
"""
text = str(self._edit.toPlainText())
# in Windows replace all backslash symbols '\' with '\\' slash because Windows paths are made with backslash
# and in Python single slash is the escape symbol
if sys.platform == 'win32':
text = text.replace('\\', '\\\\')
self._append_to_browser('in', '> ' + text + '\n')
if len(self._history) < 2 or self._history[-2] != text: # don't insert duplicating items
try:
if text[-1] == '\n':
self._history.insert(-1, text[:-1])
else:
self._history.insert(-1, text)
except IndexError:
return
self._historyIndex = len(self._history) - 1
self._history[-1] = ''
self._edit.clear()
if not text[-1] == '\n':
text += '\n'
self.child_exec_command(text)
def child_exec_command(self, text):
"""
Re-implement in the child classes
"""
pass
def add_line_break_to_input(self):
self._edit.textCursor().insertText('\n')
def append_output(self, text):
"""
Append text to output widget
"""
self._append_to_browser('out', text)
def append_raw(self, text):
"""
Append text to output widget as it is
"""
self._append_to_browser('raw', text)
def append_success(self, text):
"""Append text to output widget
"""
self._append_to_browser('success', text)
def append_selected(self, text):
"""Append text to output widget
"""
self._append_to_browser('selected', text)
def append_warning(self, text):
"""Append text to output widget
"""
self._append_to_browser('warning', text)
def append_error(self, text):
"""Append error text to output widget. Text is drawn with red background
"""
self._append_to_browser('err', text)
def is_command_complete(self, text):
"""
Executed by _ExpandableTextEdit. Re-implement this function in the child classes.
"""
return True
def browser(self):
return self._browser
def _on_history_next(self):
"""
Down pressed, show next item from the history
"""
if (self._historyIndex + 1) < len(self._history):
self._historyIndex += 1
self._edit.setPlainText(self._history[self._historyIndex])
self._edit.moveCursor(QTextCursor.End)
def _on_history_prev(self):
"""
Up pressed, show previous item from the history
"""
if self._historyIndex > 0:
if self._historyIndex == (len(self._history) - 1):
self._history[-1] = self._edit.toPlainText()
self._historyIndex -= 1
self._edit.setPlainText(self._history[self._historyIndex])
self._edit.moveCursor(QTextCursor.End)
class FCShell(TermWidget):
def __init__(self, app, version, *args):
"""
Initialize the TCL Shell. A dock widget that holds the GUI interface to the FlatCAM command line.
:param app: When instantiated the sysShell will be actually the FlatCAMApp.App() class
:param version: FlatCAM version string
:param args: Parameters passed to the TermWidget parent class
"""
TermWidget.__init__(self, version, *args, app=app)
self.app = app
self.tcl_commands_storage = {}
self.tcl = None
self.init_tcl()
self._edit.set_model_data(self.app.myKeywords)
self.setWindowIcon(self.app.ui.app_icon)
self.setWindowTitle("FlatCAM Shell")
self.resize(*self.app.defaults["global_shell_shape"])
self._append_to_browser('in', "FlatCAM %s - " % version)
self.append_output('%s\n\n' % _("Type >help< to get started"))
def init_tcl(self):
if hasattr(self, 'tcl') and self.tcl is not None:
# self.tcl = None
# new object cannot be used here as it will not remember values created for next passes,
# because tcl was executed in old instance of TCL
pass
else:
self.tcl = tk.Tcl()
self.setup_shell()
def setup_shell(self):
"""
Creates shell functions. Runs once at startup.
:return: None
"""
'''
How to implement TCL shell commands:
All parameters passed to command should be possible to set as None and test it afterwards.
This is because we need to see error caused in tcl,
if None value as default parameter is not allowed TCL will return empty error.
Use:
def mycommand(name=None,...):
Test it like this:
if name is None:
self.raise_tcl_error('Argument name is missing.')
When error occurred, always use raise_tcl_error, never return "some text" on error,
otherwise we will miss it and processing will silently continue.
Method raise_tcl_error pass error into TCL interpreter, then raise python exception,
which is caught in exec_command and displayed in TCL shell console with red background.
Error in console is displayed with TCL trace.
This behavior works only within main thread,
errors with promissed tasks can be catched and detected only with log.
TODO: this problem have to be addressed somehow, maybe rewrite promissing to be blocking somehow for
TCL shell.
Kamil's comment: I will rewrite existing TCL commands from time to time to follow this rules.
'''
# Import/overwrite tcl commands as objects of TclCommand descendants
# This modifies the variable 'self.tcl_commands_storage'.
tclCommands.register_all_commands(self.app, self.tcl_commands_storage)
# Add commands to the tcl interpreter
for cmd in self.tcl_commands_storage:
self.tcl.createcommand(cmd, self.tcl_commands_storage[cmd]['fcn'])
# Make the tcl puts function return instead of print to stdout
self.tcl.eval('''
rename puts original_puts
proc puts {args} {
if {[llength $args] == 1} {
return "[lindex $args 0]"
} else {
eval original_puts $args
}
}
''')
def is_command_complete(self, text):
# def skipQuotes(txt):
# quote = txt[0]
# text_val = txt[1:]
# endIndex = str(text_val).index(quote)
# return text[endIndex:]
# I'm disabling this because I need to be able to load paths that have spaces by
# enclosing them in quotes --- Marius Stanciu
# while text:
# if text[0] in ('"', "'"):
# try:
# text = skipQuotes(text)
# except ValueError:
# return False
# text = text[1:]
return True
def child_exec_command(self, text):
self.exec_command(text)
def exec_command(self, text, no_echo=False):
"""
Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
Also handles execution in separated threads
:param text: FlatCAM TclCommand with parameters
:param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it
will create crashes of the _Expandable_Edit widget
:return: output if there was any
"""
self.app.defaults.report_usage('exec_command')
return self.exec_command_test(text, False, no_echo=no_echo)
def exec_command_test(self, text, reraise=True, no_echo=False):
"""
Same as exec_command(...) with additional control over exceptions.
Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
:param text: Input command
:param reraise: Re-raise TclError exceptions in Python (mostly for unittests).
:param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it
will create crashes of the _Expandable_Edit widget
:return: Output from the command
"""
tcl_command_string = str(text)
try:
if no_echo is False:
self.open_processing() # Disables input box.
result = self.tcl.eval(str(tcl_command_string))
if result != 'None' and no_echo is False:
self.append_output(result + '\n')
except tk.TclError as e:
# This will display more precise answer if something in TCL shell fails
result = self.tcl.eval("set errorInfo")
self.app.log.error("Exception on Tcl Command execution: %s" % (result + '\n'))
if no_echo is False:
self.append_error('ERROR Report: ' + result + '\n')
# Show error in console and just return or in test raise exception
if reraise:
raise e
finally:
if no_echo is False:
self.close_processing()
pass
return result
def raise_tcl_unknown_error(self, unknownException):
"""
Raise exception if is different type than TclErrorException
this is here mainly to show unknown errors inside TCL shell console.
:param unknownException:
:return:
"""
if not isinstance(unknownException, self.TclErrorException):
self.raise_tcl_error("Unknown error: %s" % str(unknownException))
else:
raise unknownException
def display_tcl_error(self, error, error_info=None):
"""
Escape bracket [ with '\' otherwise there is error
"ERROR: missing close-bracket" instead of real error
:param error: it may be text or exception
:param error_info: Some informations about the error
:return: None
"""
if isinstance(error, Exception):
exc_type, exc_value, exc_traceback = error_info
if not isinstance(error, self.TclErrorException):
show_trace = 1
else:
show_trace = int(self.app.defaults['global_verbose_error_level'])
if show_trace > 0:
trc = traceback.format_list(traceback.extract_tb(exc_traceback))
trc_formated = []
for a in reversed(trc):
trc_formated.append(a.replace(" ", " > ").replace("\n", ""))
text = "%s\nPython traceback: %s\n%s" % (exc_value, exc_type, "\n".join(trc_formated))
else:
text = "%s" % error
else:
text = error
text = text.replace('[', '\\[').replace('"', '\\"')
self.tcl.eval('return -code error "%s"' % text)
def raise_tcl_error(self, text):
"""
This method pass exception from python into TCL as error, so we get stacktrace and reason
:param text: text of error
:return: raise exception
"""
self.display_tcl_error(text)
raise self.TclErrorException(text)
class TclErrorException(Exception):
"""
this exception is defined here, to be able catch it if we successfully handle all errors from shell command
"""
pass
# """
# Code below is unsused. Saved for later.
# """
# parts = re.findall(r'([\w\\:\.]+|".*?")+', text)
# parts = [p.replace('\n', '').replace('"', '') for p in parts]
# self.log.debug(parts)
# try:
# if parts[0] not in commands:
# self.shell.append_error("Unknown command\n")
# return
#
# #import inspect
# #inspect.getargspec(someMethod)
# if (type(commands[parts[0]]["params"]) is not list and len(parts)-1 != commands[parts[0]]["params"]) or \
# (type(commands[parts[0]]["params"]) is list and len(parts)-1 not in commands[parts[0]]["params"]):
# self.shell.append_error(
# "Command %s takes %d arguments. %d given.\n" %
# (parts[0], commands[parts[0]]["params"], len(parts)-1)
# )
# return
#
# cmdfcn = commands[parts[0]]["fcn"]
# cmdconv = commands[parts[0]]["converters"]
# if len(parts) - 1 > 0:
# retval = cmdfcn(*[cmdconv[i](parts[i + 1]) for i in range(len(parts)-1)])
# else:
# retval = cmdfcn()
# retfcn = commands[parts[0]]["retfcn"]
# if retval and retfcn(retval):
# self.shell.append_output(retfcn(retval) + "\n")
#
# except Exception as e:
# #self.shell.append_error(''.join(traceback.format_exc()))
# #self.shell.append_error("?\n")
# self.shell.append_error(str(e) + "\n")

1555
appTools/ToolSolderPaste.py Normal file

File diff suppressed because it is too large Load Diff

757
appTools/ToolSub.py Normal file
View File

@@ -0,0 +1,757 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 4/24/2019 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets, QtCore
from appTool import AppTool
from appGUI.GUIElements import FCCheckBox, FCButton, FCComboBox
from shapely.geometry import Polygon, MultiPolygon, MultiLineString, LineString
from shapely.ops import cascaded_union
import traceback
from copy import deepcopy
import time
import logging
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class ToolSub(AppTool):
job_finished = QtCore.pyqtSignal(bool)
# the string param is the outname and the list is a list of tuples each being formed from the new_aperture_geometry
# list and the second element is also a list with possible geometry that needs to be added to the '0' aperture
# meaning geometry that was deformed
aperture_processing_finished = QtCore.pyqtSignal(str, list)
toolName = _("Subtract Tool")
def __init__(self, app):
self.app = app
self.decimals = self.app.decimals
AppTool.__init__(self, app)
self.tools_frame = QtWidgets.QFrame()
self.tools_frame.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(self.tools_frame)
self.tools_box = QtWidgets.QVBoxLayout()
self.tools_box.setContentsMargins(0, 0, 0, 0)
self.tools_frame.setLayout(self.tools_box)
# Title
title_label = QtWidgets.QLabel("%s" % self.toolName)
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.tools_box.addWidget(title_label)
# Form Layout
form_layout = QtWidgets.QFormLayout()
self.tools_box.addLayout(form_layout)
self.gerber_title = QtWidgets.QLabel("<b>%s</b>" % _("GERBER"))
form_layout.addRow(self.gerber_title)
# Target Gerber Object
self.target_gerber_combo = FCComboBox()
self.target_gerber_combo.setModel(self.app.collection)
self.target_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
# self.target_gerber_combo.setCurrentIndex(1)
self.target_gerber_combo.is_last = True
self.target_gerber_combo.obj_type = "Gerber"
self.target_gerber_label = QtWidgets.QLabel('%s:' % _("Target"))
self.target_gerber_label.setToolTip(
_("Gerber object from which to subtract\n"
"the subtractor Gerber object.")
)
form_layout.addRow(self.target_gerber_label, self.target_gerber_combo)
# Substractor Gerber Object
self.sub_gerber_combo = FCComboBox()
self.sub_gerber_combo.setModel(self.app.collection)
self.sub_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.sub_gerber_combo.is_last = True
self.sub_gerber_combo.obj_type = "Gerber"
self.sub_gerber_label = QtWidgets.QLabel('%s:' % _("Subtractor"))
self.sub_gerber_label.setToolTip(
_("Gerber object that will be subtracted\n"
"from the target Gerber object.")
)
e_lab_1 = QtWidgets.QLabel('')
form_layout.addRow(self.sub_gerber_label, self.sub_gerber_combo)
self.intersect_btn = FCButton(_('Subtract Gerber'))
self.intersect_btn.setToolTip(
_("Will remove the area occupied by the subtractor\n"
"Gerber from the Target Gerber.\n"
"Can be used to remove the overlapping silkscreen\n"
"over the soldermask.")
)
self.intersect_btn.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
self.tools_box.addWidget(self.intersect_btn)
self.tools_box.addWidget(e_lab_1)
# Form Layout
form_geo_layout = QtWidgets.QFormLayout()
self.tools_box.addLayout(form_geo_layout)
self.geo_title = QtWidgets.QLabel("<b>%s</b>" % _("GEOMETRY"))
form_geo_layout.addRow(self.geo_title)
# Target Geometry Object
self.target_geo_combo = FCComboBox()
self.target_geo_combo.setModel(self.app.collection)
self.target_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
# self.target_geo_combo.setCurrentIndex(1)
self.target_geo_combo.is_last = True
self.target_geo_combo.obj_type = "Geometry"
self.target_geo_label = QtWidgets.QLabel('%s:' % _("Target"))
self.target_geo_label.setToolTip(
_("Geometry object from which to subtract\n"
"the subtractor Geometry object.")
)
form_geo_layout.addRow(self.target_geo_label, self.target_geo_combo)
# Substractor Geometry Object
self.sub_geo_combo = FCComboBox()
self.sub_geo_combo.setModel(self.app.collection)
self.sub_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
self.sub_geo_combo.is_last = True
self.sub_geo_combo.obj_type = "Geometry"
self.sub_geo_label = QtWidgets.QLabel('%s:' % _("Subtractor"))
self.sub_geo_label.setToolTip(
_("Geometry object that will be subtracted\n"
"from the target Geometry object.")
)
e_lab_1 = QtWidgets.QLabel('')
form_geo_layout.addRow(self.sub_geo_label, self.sub_geo_combo)
self.close_paths_cb = FCCheckBox(_("Close paths"))
self.close_paths_cb.setToolTip(_("Checking this will close the paths cut by the Geometry subtractor object."))
self.tools_box.addWidget(self.close_paths_cb)
self.intersect_geo_btn = FCButton(_('Subtract Geometry'))
self.intersect_geo_btn.setToolTip(
_("Will remove the area occupied by the subtractor\n"
"Geometry from the Target Geometry.")
)
self.intersect_geo_btn.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
self.tools_box.addWidget(self.intersect_geo_btn)
self.tools_box.addWidget(e_lab_1)
self.tools_box.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.tools_box.addWidget(self.reset_button)
# QTimer for periodic check
self.check_thread = QtCore.QTimer()
# Every time an intersection job is started we add a promise; every time an intersection job is finished
# we remove a promise.
# When empty we start the layer rendering
self.promises = []
self.new_apertures = {}
self.new_tools = {}
self.new_solid_geometry = []
self.sub_solid_union = None
self.sub_follow_union = None
self.sub_clear_union = None
self.sub_grb_obj = None
self.sub_grb_obj_name = None
self.target_grb_obj = None
self.target_grb_obj_name = None
self.sub_geo_obj = None
self.sub_geo_obj_name = None
self.target_geo_obj = None
self.target_geo_obj_name = None
# signal which type of substraction to do: "geo" or "gerber"
self.sub_type = None
# store here the options from target_obj
self.target_options = {}
self.sub_union = []
# multiprocessing
self.pool = self.app.pool
self.results = []
self.intersect_btn.clicked.connect(self.on_grb_intersection_click)
self.intersect_geo_btn.clicked.connect(self.on_geo_intersection_click)
self.job_finished.connect(self.on_job_finished)
self.aperture_processing_finished.connect(self.new_gerber_object)
self.reset_button.clicked.connect(self.set_tool_ui)
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='Alt+W', **kwargs)
def run(self, toggle=True):
self.app.defaults.report_usage("ToolSub()")
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])
AppTool.run(self)
self.set_tool_ui()
self.app.ui.notebook.setTabText(2, _("Sub Tool"))
def set_tool_ui(self):
self.new_apertures.clear()
self.new_tools.clear()
self.new_solid_geometry = []
self.target_options.clear()
self.tools_frame.show()
self.close_paths_cb.setChecked(self.app.defaults["tools_sub_close_paths"])
def on_grb_intersection_click(self):
# reset previous values
self.new_apertures.clear()
self.new_solid_geometry = []
self.sub_union = []
self.sub_type = "gerber"
self.target_grb_obj_name = self.target_gerber_combo.currentText()
if self.target_grb_obj_name == '':
self.app.inform.emit('[ERROR_NOTCL] %s' % _("No Target object loaded."))
return
self.app.inform.emit('%s' % _("Loading geometry from Gerber objects."))
# Get target object.
try:
self.target_grb_obj = self.app.collection.get_by_name(self.target_grb_obj_name)
except Exception as e:
log.debug("ToolSub.on_grb_intersection_click() --> %s" % str(e))
self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), self.obj_name))
return "Could not retrieve object: %s" % self.target_grb_obj_name
self.sub_grb_obj_name = self.sub_gerber_combo.currentText()
if self.sub_grb_obj_name == '':
self.app.inform.emit('[ERROR_NOTCL] %s' % _("No Subtractor object loaded."))
return
# Get substractor object.
try:
self.sub_grb_obj = self.app.collection.get_by_name(self.sub_grb_obj_name)
except Exception as e:
log.debug("ToolSub.on_grb_intersection_click() --> %s" % str(e))
self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), self.obj_name))
return "Could not retrieve object: %s" % self.sub_grb_obj_name
# crate the new_apertures dict structure
for apid in self.target_grb_obj.apertures:
self.new_apertures[apid] = {}
for key in self.target_grb_obj.apertures[apid]:
if key == 'geometry':
self.new_apertures[apid]['geometry'] = []
else:
self.new_apertures[apid][key] = self.target_grb_obj.apertures[apid][key]
def worker_job(app_obj):
for apid in self.target_grb_obj.apertures:
target_geo = self.target_grb_obj.apertures[apid]['geometry']
sub_geometry = {}
sub_geometry['solid'] = []
sub_geometry['clear'] = []
for s_apid in self.sub_grb_obj.apertures:
for s_el in self.sub_grb_obj.apertures[s_apid]['geometry']:
if "solid" in s_el:
sub_geometry['solid'].append(s_el["solid"])
if "clear" in s_el:
sub_geometry['clear'].append(s_el["clear"])
self.results.append(
self.pool.apply_async(self.aperture_intersection, args=(apid, target_geo, sub_geometry))
)
output = []
for p in self.results:
res = p.get()
output.append(res)
app_obj.inform.emit('%s: %s...' % (_("Finished parsing geometry for aperture"), str(res[0])))
app_obj.inform.emit("%s" % _("Subtraction aperture processing finished."))
outname = self.target_gerber_combo.currentText() + '_sub'
self.aperture_processing_finished.emit(outname, output)
self.app.worker_task.emit({'fcn': worker_job, 'params': [self.app]})
@staticmethod
def aperture_intersection(apid, target_geo, sub_geometry):
"""
:param apid: the aperture id for which we process geometry
:type apid: str
:param target_geo: the geometry list that holds the geometry from which we subtract
:type target_geo: list
:param sub_geometry: the apertures dict that holds all the geometry that is subtracted
:type sub_geometry: dict
:return: (apid, unaffected_geometry lsit, affected_geometry list)
:rtype: tuple
"""
unafected_geo = []
affected_geo = []
is_modified = False
for geo_el in target_geo:
new_geo_el = {}
if "solid" in geo_el:
for sub_solid_geo in sub_geometry["solid"]:
if geo_el["solid"].intersects(sub_solid_geo):
new_geo = geo_el["solid"].difference(sub_solid_geo)
if not new_geo.is_empty:
geo_el["solid"] = new_geo
is_modified = True
new_geo_el["solid"] = deepcopy(geo_el["solid"])
if "clear" in geo_el:
for sub_solid_geo in sub_geometry["clear"]:
if geo_el["clear"].intersects(sub_solid_geo):
new_geo = geo_el["clear"].difference(sub_solid_geo)
if not new_geo.is_empty:
geo_el["clear"] = new_geo
is_modified = True
new_geo_el["clear"] = deepcopy(geo_el["clear"])
if is_modified:
affected_geo.append(new_geo_el)
else:
unafected_geo.append(geo_el)
return apid, unafected_geo, affected_geo
def new_gerber_object(self, outname, output):
"""
:param outname: name for the new Gerber object
:type outname: str
:param output: a list made of tuples in format:
(aperture id in the target Gerber, unaffected_geometry list, affected_geometry list)
:type output: list
:return:
:rtype:
"""
def obj_init(grb_obj, app_obj):
grb_obj.apertures = deepcopy(self.new_apertures)
if '0' not in grb_obj.apertures:
grb_obj.apertures['0'] = {}
grb_obj.apertures['0']['type'] = 'REG'
grb_obj.apertures['0']['size'] = 0.0
grb_obj.apertures['0']['geometry'] = []
for apid, apid_val in list(grb_obj.apertures.items()):
for t in output:
new_apid = t[0]
if apid == new_apid:
surving_geo = t[1]
modified_geo = t[2]
if surving_geo:
apid_val['geometry'] = deepcopy(surving_geo)
else:
grb_obj.apertures.pop(apid, None)
if modified_geo:
grb_obj.apertures['0']['geometry'] += modified_geo
# delete the '0' aperture if it has no geometry
if not grb_obj.apertures['0']['geometry']:
grb_obj.apertures.pop('0', None)
poly_buff = []
follow_buff = []
for ap in grb_obj.apertures:
for elem in grb_obj.apertures[ap]['geometry']:
if 'solid' in elem:
solid_geo = elem['solid']
poly_buff.append(solid_geo)
if 'follow' in elem:
follow_buff.append(elem['follow'])
work_poly_buff = MultiPolygon(poly_buff)
try:
poly_buff = work_poly_buff.buffer(0.0000001)
except ValueError:
pass
try:
poly_buff = poly_buff.buffer(-0.0000001)
except ValueError:
pass
grb_obj.solid_geometry = deepcopy(poly_buff)
grb_obj.follow_geometry = deepcopy(follow_buff)
grb_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None,
local_use=grb_obj, use_thread=False)
with self.app.proc_container.new(_("Generating new object ...")):
ret = self.app.app_obj.new_object('gerber', outname, obj_init, autoselected=False)
if ret == 'fail':
self.app.inform.emit('[ERROR_NOTCL] %s' % _('Generating new object failed.'))
return
# GUI feedback
self.app.inform.emit('[success] %s: %s' % (_("Created"), outname))
# cleanup
self.new_apertures.clear()
self.new_solid_geometry[:] = []
self.results = []
def on_geo_intersection_click(self):
# reset previous values
self.new_tools.clear()
self.target_options.clear()
self.new_solid_geometry = []
self.sub_union = []
self.sub_type = "geo"
self.target_geo_obj_name = self.target_geo_combo.currentText()
if self.target_geo_obj_name == '':
self.app.inform.emit('[ERROR_NOTCL] %s' %
_("No Target object loaded."))
return
# Get target object.
try:
self.target_geo_obj = self.app.collection.get_by_name(self.target_geo_obj_name)
except Exception as e:
log.debug("ToolSub.on_geo_intersection_click() --> %s" % str(e))
self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
(_("Could not retrieve object"), self.target_geo_obj_name))
return "Could not retrieve object: %s" % self.target_grb_obj_name
self.sub_geo_obj_name = self.sub_geo_combo.currentText()
if self.sub_geo_obj_name == '':
self.app.inform.emit('[ERROR_NOTCL] %s' %
_("No Subtractor object loaded."))
return
# Get substractor object.
try:
self.sub_geo_obj = self.app.collection.get_by_name(self.sub_geo_obj_name)
except Exception as e:
log.debug("ToolSub.on_geo_intersection_click() --> %s" % str(e))
self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
(_("Could not retrieve object"), self.sub_geo_obj_name))
return "Could not retrieve object: %s" % self.sub_geo_obj_name
if self.sub_geo_obj.multigeo:
self.app.inform.emit('[ERROR_NOTCL] %s' %
_("Currently, the Subtractor geometry cannot be of type Multigeo."))
return
# create the target_options obj
# self.target_options = {}
# for k, v in self.target_geo_obj.options.items():
# if k != 'name':
# self.target_options[k] = v
# crate the new_tools dict structure
for tool in self.target_geo_obj.tools:
self.new_tools[tool] = {}
for key in self.target_geo_obj.tools[tool]:
if key == 'solid_geometry':
self.new_tools[tool][key] = []
else:
self.new_tools[tool][key] = deepcopy(self.target_geo_obj.tools[tool][key])
# add the promises
if self.target_geo_obj.multigeo:
for tool in self.target_geo_obj.tools:
self.promises.append(tool)
else:
self.promises.append("single")
self.sub_union = cascaded_union(self.sub_geo_obj.solid_geometry)
# start the QTimer to check for promises with 0.5 second period check
self.periodic_check(500, reset=True)
if self.target_geo_obj.multigeo:
for tool in self.target_geo_obj.tools:
geo = self.target_geo_obj.tools[tool]['solid_geometry']
self.app.worker_task.emit({'fcn': self.toolgeo_intersection,
'params': [tool, geo]})
else:
geo = self.target_geo_obj.solid_geometry
self.app.worker_task.emit({'fcn': self.toolgeo_intersection,
'params': ["single", geo]})
def toolgeo_intersection(self, tool, geo):
new_geometry = []
log.debug("Working on promise: %s" % str(tool))
if tool == "single":
text = _("Parsing solid_geometry ...")
else:
text = '%s: %s...' % (_("Parsing solid_geometry for tool"), str(tool))
with self.app.proc_container.new(text):
# resulting paths are closed resulting into Polygons
if self.close_paths_cb.isChecked():
new_geo = (cascaded_union(geo)).difference(self.sub_union)
if new_geo:
if not new_geo.is_empty:
new_geometry.append(new_geo)
# resulting paths are unclosed resulting in a multitude of rings
else:
try:
for geo_elem in geo:
if isinstance(geo_elem, Polygon):
for ring in self.poly2rings(geo_elem):
new_geo = ring.difference(self.sub_union)
if new_geo and not new_geo.is_empty:
new_geometry.append(new_geo)
elif isinstance(geo_elem, MultiPolygon):
for poly in geo_elem:
for ring in self.poly2rings(poly):
new_geo = ring.difference(self.sub_union)
if new_geo and not new_geo.is_empty:
new_geometry.append(new_geo)
elif isinstance(geo_elem, LineString):
new_geo = geo_elem.difference(self.sub_union)
if new_geo:
if not new_geo.is_empty:
new_geometry.append(new_geo)
elif isinstance(geo_elem, MultiLineString):
for line_elem in geo_elem:
new_geo = line_elem.difference(self.sub_union)
if new_geo and not new_geo.is_empty:
new_geometry.append(new_geo)
except TypeError:
if isinstance(geo, Polygon):
for ring in self.poly2rings(geo):
new_geo = ring.difference(self.sub_union)
if new_geo:
if not new_geo.is_empty:
new_geometry.append(new_geo)
elif isinstance(geo, LineString):
new_geo = geo.difference(self.sub_union)
if new_geo and not new_geo.is_empty:
new_geometry.append(new_geo)
elif isinstance(geo, MultiLineString):
for line_elem in geo:
new_geo = line_elem.difference(self.sub_union)
if new_geo and not new_geo.is_empty:
new_geometry.append(new_geo)
if new_geometry:
if tool == "single":
while not self.new_solid_geometry:
self.new_solid_geometry = deepcopy(new_geometry)
time.sleep(0.5)
else:
while not self.new_tools[tool]['solid_geometry']:
self.new_tools[tool]['solid_geometry'] = deepcopy(new_geometry)
time.sleep(0.5)
while True:
# removal from list is done in a multithreaded way therefore not always the removal can be done
# so we keep trying until it's done
if tool not in self.promises:
break
self.promises.remove(tool)
time.sleep(0.5)
log.debug("Promise fulfilled: %s" % str(tool))
def new_geo_object(self, outname):
geo_name = outname
def obj_init(geo_obj, app_obj):
# geo_obj.options = self.target_options
# create the target_options obj
for k, v in self.target_geo_obj.options.items():
geo_obj.options[k] = v
geo_obj.options['name'] = geo_name
if self.target_geo_obj.multigeo:
geo_obj.tools = deepcopy(self.new_tools)
# this turn on the FlatCAMCNCJob plot for multiple tools
geo_obj.multigeo = True
geo_obj.multitool = True
else:
geo_obj.solid_geometry = deepcopy(self.new_solid_geometry)
try:
geo_obj.tools = deepcopy(self.new_tools)
for tool in geo_obj.tools:
geo_obj.tools[tool]['solid_geometry'] = deepcopy(self.new_solid_geometry)
except Exception as e:
log.debug("ToolSub.new_geo_object() --> %s" % str(e))
geo_obj.multigeo = False
with self.app.proc_container.new(_("Generating new object ...")):
ret = self.app.app_obj.new_object('geometry', outname, obj_init, autoselected=False)
if ret == 'fail':
self.app.inform.emit('[ERROR_NOTCL] %s' %
_('Generating new object failed.'))
return
# Register recent file
self.app.file_opened.emit('geometry', outname)
# GUI feedback
self.app.inform.emit('[success] %s: %s' %
(_("Created"), outname))
# cleanup
self.new_tools.clear()
self.new_solid_geometry[:] = []
self.sub_union = []
def periodic_check(self, check_period, reset=False):
"""
This function starts an QTimer and it will periodically check if intersections are done
:param check_period: time at which to check periodically
:param reset: will reset the timer
:return:
"""
log.debug("ToolSub --> Periodic Check started.")
try:
self.check_thread.stop()
except (TypeError, AttributeError):
pass
if reset:
self.check_thread.setInterval(check_period)
try:
self.check_thread.timeout.disconnect(self.periodic_check_handler)
except (TypeError, AttributeError):
pass
self.check_thread.timeout.connect(self.periodic_check_handler)
self.check_thread.start(QtCore.QThread.HighPriority)
def periodic_check_handler(self):
"""
If the intersections workers finished then start creating the solid_geometry
:return:
"""
# log.debug("checking parsing --> %s" % str(self.parsing_promises))
try:
if not self.promises:
self.check_thread.stop()
self.job_finished.emit(True)
# reset the type of substraction for next time
self.sub_type = None
log.debug("ToolSub --> Periodic check finished.")
except Exception as e:
self.job_finished.emit(False)
log.debug("ToolSub().periodic_check_handler() --> %s" % str(e))
traceback.print_exc()
def on_job_finished(self, succcess):
"""
:param succcess: boolean, this parameter signal if all the apertures were processed
:return: None
"""
if succcess is True:
if self.sub_type == "gerber":
outname = self.target_gerber_combo.currentText() + '_sub'
# intersection jobs finished, start the creation of solid_geometry
self.app.worker_task.emit({'fcn': self.new_gerber_object,
'params': [outname]})
else:
outname = self.target_geo_combo.currentText() + '_sub'
# intersection jobs finished, start the creation of solid_geometry
self.app.worker_task.emit({'fcn': self.new_geo_object, 'params': [outname]})
else:
self.app.inform.emit('[ERROR_NOTCL] %s' % _('Generating new object failed.'))
def reset_fields(self):
self.target_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.sub_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.target_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
self.sub_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
@staticmethod
def poly2rings(poly):
return [poly.exterior] + [interior for interior in poly.interiors]
# end of file

946
appTools/ToolTransform.py Normal file
View File

@@ -0,0 +1,946 @@
# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 3/10/2019 #
# MIT Licence #
# ##########################################################
from PyQt5 import QtWidgets, QtGui, QtCore
from appTool import AppTool
from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, FCButton, OptionalInputSection, FCEntry, FCComboBox, \
NumericalEvalTupleEntry
import numpy as np
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
class ToolTransform(AppTool):
toolName = _("Object Transform")
rotateName = _("Rotate")
skewName = _("Skew/Shear")
scaleName = _("Scale")
flipName = _("Mirror (Flip)")
offsetName = _("Offset")
bufferName = _("Buffer")
def __init__(self, app):
AppTool.__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)
self.layout.addWidget(QtWidgets.QLabel(''))
# ## Layout
grid0 = QtWidgets.QGridLayout()
self.layout.addLayout(grid0)
grid0.setColumnStretch(0, 0)
grid0.setColumnStretch(1, 1)
grid0.setColumnStretch(2, 0)
grid0.addWidget(QtWidgets.QLabel(''))
# Reference
ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
ref_label.setToolTip(
_("The reference point for Rotate, Skew, Scale, Mirror.\n"
"Can be:\n"
"- Origin -> it is the 0, 0 point\n"
"- Selection -> the center of the bounding box of the selected objects\n"
"- Point -> a custom point defined by X,Y coordinates\n"
"- Object -> the center of the bounding box of a specific object")
)
self.ref_combo = FCComboBox()
self.ref_items = [_("Origin"), _("Selection"), _("Point"), _("Object")]
self.ref_combo.addItems(self.ref_items)
grid0.addWidget(ref_label, 0, 0)
grid0.addWidget(self.ref_combo, 0, 1, 1, 2)
self.point_label = QtWidgets.QLabel('%s:' % _("Value"))
self.point_label.setToolTip(
_("A point of reference in format X,Y.")
)
self.point_entry = NumericalEvalTupleEntry()
grid0.addWidget(self.point_label, 1, 0)
grid0.addWidget(self.point_entry, 1, 1, 1, 2)
self.point_button = FCButton(_("Add"))
self.point_button.setToolTip(
_("Add point coordinates from clipboard.")
)
grid0.addWidget(self.point_button, 2, 0, 1, 3)
# Type of object to be used as reference
self.type_object_label = QtWidgets.QLabel('%s:' % _("Type"))
self.type_object_label.setToolTip(
_("The type of object used as reference.")
)
self.type_obj_combo = FCComboBox()
self.type_obj_combo.addItem(_("Gerber"))
self.type_obj_combo.addItem(_("Excellon"))
self.type_obj_combo.addItem(_("Geometry"))
self.type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
self.type_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
self.type_obj_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
grid0.addWidget(self.type_object_label, 3, 0)
grid0.addWidget(self.type_obj_combo, 3, 1, 1, 2)
# Object to be used as reference
self.object_combo = FCComboBox()
self.object_combo.setModel(self.app.collection)
self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.object_combo.is_last = True
self.object_combo.setToolTip(
_("The object used as reference.\n"
"The used point is the center of it's bounding box.")
)
grid0.addWidget(self.object_combo, 4, 0, 1, 3)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid0.addWidget(separator_line, 5, 0, 1, 3)
# ## Rotate Title
rotate_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.rotateName)
grid0.addWidget(rotate_title_label, 6, 0, 1, 3)
self.rotate_label = QtWidgets.QLabel('%s:' % _("Angle"))
self.rotate_label.setToolTip(
_("Angle for Rotation action, in degrees.\n"
"Float number between -360 and 359.\n"
"Positive numbers for CW motion.\n"
"Negative numbers for CCW motion.")
)
self.rotate_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.rotate_entry.set_precision(self.decimals)
self.rotate_entry.setSingleStep(45)
self.rotate_entry.setWrapping(True)
self.rotate_entry.set_range(-360, 360)
# self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.rotate_button = FCButton(_("Rotate"))
self.rotate_button.setToolTip(
_("Rotate the selected object(s).\n"
"The point of reference is the middle of\n"
"the bounding box for all selected objects.")
)
self.rotate_button.setMinimumWidth(90)
grid0.addWidget(self.rotate_label, 7, 0)
grid0.addWidget(self.rotate_entry, 7, 1)
grid0.addWidget(self.rotate_button, 7, 2)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid0.addWidget(separator_line, 8, 0, 1, 3)
# ## Skew Title
skew_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.skewName)
grid0.addWidget(skew_title_label, 9, 0, 1, 2)
self.skew_link_cb = FCCheckBox()
self.skew_link_cb.setText(_("Link"))
self.skew_link_cb.setToolTip(
_("Link the Y entry to X entry and copy it's content.")
)
grid0.addWidget(self.skew_link_cb, 9, 2)
self.skewx_label = QtWidgets.QLabel('%s:' % _("X angle"))
self.skewx_label.setToolTip(
_("Angle for Skew action, in degrees.\n"
"Float number between -360 and 360.")
)
self.skewx_entry = FCDoubleSpinner(callback=self.confirmation_message)
# self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.skewx_entry.set_precision(self.decimals)
self.skewx_entry.set_range(-360, 360)
self.skewx_button = FCButton(_("Skew X"))
self.skewx_button.setToolTip(
_("Skew/shear the selected object(s).\n"
"The point of reference is the middle of\n"
"the bounding box for all selected objects."))
self.skewx_button.setMinimumWidth(90)
grid0.addWidget(self.skewx_label, 10, 0)
grid0.addWidget(self.skewx_entry, 10, 1)
grid0.addWidget(self.skewx_button, 10, 2)
self.skewy_label = QtWidgets.QLabel('%s:' % _("Y angle"))
self.skewy_label.setToolTip(
_("Angle for Skew action, in degrees.\n"
"Float number between -360 and 360.")
)
self.skewy_entry = FCDoubleSpinner(callback=self.confirmation_message)
# self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.skewy_entry.set_precision(self.decimals)
self.skewy_entry.set_range(-360, 360)
self.skewy_button = FCButton(_("Skew Y"))
self.skewy_button.setToolTip(
_("Skew/shear the selected object(s).\n"
"The point of reference is the middle of\n"
"the bounding box for all selected objects."))
self.skewy_button.setMinimumWidth(90)
grid0.addWidget(self.skewy_label, 12, 0)
grid0.addWidget(self.skewy_entry, 12, 1)
grid0.addWidget(self.skewy_button, 12, 2)
self.ois_sk = OptionalInputSection(self.skew_link_cb, [self.skewy_label, self.skewy_entry, self.skewy_button],
logic=False)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid0.addWidget(separator_line, 14, 0, 1, 3)
# ## Scale Title
scale_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.scaleName)
grid0.addWidget(scale_title_label, 15, 0, 1, 2)
self.scale_link_cb = FCCheckBox()
self.scale_link_cb.setText(_("Link"))
self.scale_link_cb.setToolTip(
_("Link the Y entry to X entry and copy it's content.")
)
grid0.addWidget(self.scale_link_cb, 15, 2)
self.scalex_label = QtWidgets.QLabel('%s:' % _("X factor"))
self.scalex_label.setToolTip(
_("Factor for scaling on X axis.")
)
self.scalex_entry = FCDoubleSpinner(callback=self.confirmation_message)
# self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.scalex_entry.set_precision(self.decimals)
self.scalex_entry.setMinimum(-1e6)
self.scalex_button = FCButton(_("Scale X"))
self.scalex_button.setToolTip(
_("Scale the selected object(s).\n"
"The point of reference depends on \n"
"the Scale reference checkbox state."))
self.scalex_button.setMinimumWidth(90)
grid0.addWidget(self.scalex_label, 17, 0)
grid0.addWidget(self.scalex_entry, 17, 1)
grid0.addWidget(self.scalex_button, 17, 2)
self.scaley_label = QtWidgets.QLabel('%s:' % _("Y factor"))
self.scaley_label.setToolTip(
_("Factor for scaling on Y axis.")
)
self.scaley_entry = FCDoubleSpinner(callback=self.confirmation_message)
# self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.scaley_entry.set_precision(self.decimals)
self.scaley_entry.setMinimum(-1e6)
self.scaley_button = FCButton(_("Scale Y"))
self.scaley_button.setToolTip(
_("Scale the selected object(s).\n"
"The point of reference depends on \n"
"the Scale reference checkbox state."))
self.scaley_button.setMinimumWidth(90)
grid0.addWidget(self.scaley_label, 19, 0)
grid0.addWidget(self.scaley_entry, 19, 1)
grid0.addWidget(self.scaley_button, 19, 2)
self.ois_s = OptionalInputSection(self.scale_link_cb,
[
self.scaley_label,
self.scaley_entry,
self.scaley_button
], logic=False)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid0.addWidget(separator_line, 21, 0, 1, 3)
# ## Flip Title
flip_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.flipName)
grid0.addWidget(flip_title_label, 23, 0, 1, 3)
self.flipx_button = FCButton(_("Flip on X"))
self.flipx_button.setToolTip(
_("Flip the selected object(s) over the X axis.")
)
self.flipy_button = FCButton(_("Flip on Y"))
self.flipy_button.setToolTip(
_("Flip the selected object(s) over the X axis.")
)
hlay0 = QtWidgets.QHBoxLayout()
grid0.addLayout(hlay0, 25, 0, 1, 3)
hlay0.addWidget(self.flipx_button)
hlay0.addWidget(self.flipy_button)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid0.addWidget(separator_line, 27, 0, 1, 3)
# ## Offset Title
offset_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.offsetName)
grid0.addWidget(offset_title_label, 29, 0, 1, 3)
self.offx_label = QtWidgets.QLabel('%s:' % _("X val"))
self.offx_label.setToolTip(
_("Distance to offset on X axis. In current units.")
)
self.offx_entry = FCDoubleSpinner(callback=self.confirmation_message)
# self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.offx_entry.set_precision(self.decimals)
self.offx_entry.setMinimum(-1e6)
self.offx_button = FCButton(_("Offset X"))
self.offx_button.setToolTip(
_("Offset the selected object(s).\n"
"The point of reference is the middle of\n"
"the bounding box for all selected objects.\n"))
self.offx_button.setMinimumWidth(90)
grid0.addWidget(self.offx_label, 31, 0)
grid0.addWidget(self.offx_entry, 31, 1)
grid0.addWidget(self.offx_button, 31, 2)
self.offy_label = QtWidgets.QLabel('%s:' % _("Y val"))
self.offy_label.setToolTip(
_("Distance to offset on Y axis. In current units.")
)
self.offy_entry = FCDoubleSpinner(callback=self.confirmation_message)
# self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.offy_entry.set_precision(self.decimals)
self.offy_entry.setMinimum(-1e6)
self.offy_button = FCButton(_("Offset Y"))
self.offy_button.setToolTip(
_("Offset the selected object(s).\n"
"The point of reference is the middle of\n"
"the bounding box for all selected objects.\n"))
self.offy_button.setMinimumWidth(90)
grid0.addWidget(self.offy_label, 32, 0)
grid0.addWidget(self.offy_entry, 32, 1)
grid0.addWidget(self.offy_button, 32, 2)
separator_line = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid0.addWidget(separator_line, 34, 0, 1, 3)
# ## Buffer Title
buffer_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.bufferName)
grid0.addWidget(buffer_title_label, 35, 0, 1, 2)
self.buffer_rounded_cb = FCCheckBox('%s' % _("Rounded"))
self.buffer_rounded_cb.setToolTip(
_("If checked then the buffer will surround the buffered shape,\n"
"every corner will be rounded.\n"
"If not checked then the buffer will follow the exact geometry\n"
"of the buffered shape.")
)
grid0.addWidget(self.buffer_rounded_cb, 35, 2)
self.buffer_label = QtWidgets.QLabel('%s:' % _("Distance"))
self.buffer_label.setToolTip(
_("A positive value will create the effect of dilation,\n"
"while a negative value will create the effect of erosion.\n"
"Each geometry element of the object will be increased\n"
"or decreased with the 'distance'.")
)
self.buffer_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.buffer_entry.set_precision(self.decimals)
self.buffer_entry.setSingleStep(0.1)
self.buffer_entry.setWrapping(True)
self.buffer_entry.set_range(-9999.9999, 9999.9999)
self.buffer_button = FCButton(_("Buffer D"))
self.buffer_button.setToolTip(
_("Create the buffer effect on each geometry,\n"
"element from the selected object, using the distance.")
)
self.buffer_button.setMinimumWidth(90)
grid0.addWidget(self.buffer_label, 37, 0)
grid0.addWidget(self.buffer_entry, 37, 1)
grid0.addWidget(self.buffer_button, 37, 2)
self.buffer_factor_label = QtWidgets.QLabel('%s:' % _("Value"))
self.buffer_factor_label.setToolTip(
_("A positive value will create the effect of dilation,\n"
"while a negative value will create the effect of erosion.\n"
"Each geometry element of the object will be increased\n"
"or decreased to fit the 'Value'. Value is a percentage\n"
"of the initial dimension.")
)
self.buffer_factor_entry = FCDoubleSpinner(callback=self.confirmation_message, suffix='%')
self.buffer_factor_entry.set_range(-100.0000, 1000.0000)
self.buffer_factor_entry.set_precision(self.decimals)
self.buffer_factor_entry.setWrapping(True)
self.buffer_factor_entry.setSingleStep(1)
self.buffer_factor_button = FCButton(_("Buffer F"))
self.buffer_factor_button.setToolTip(
_("Create the buffer effect on each geometry,\n"
"element from the selected object, using the factor.")
)
self.buffer_factor_button.setMinimumWidth(90)
grid0.addWidget(self.buffer_factor_label, 38, 0)
grid0.addWidget(self.buffer_factor_entry, 38, 1)
grid0.addWidget(self.buffer_factor_button, 38, 2)
grid0.addWidget(QtWidgets.QLabel(''), 42, 0, 1, 3)
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)
# ## Signals
self.ref_combo.currentIndexChanged.connect(self.on_reference_changed)
self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
self.point_button.clicked.connect(self.on_add_coords)
self.rotate_button.clicked.connect(self.on_rotate)
self.skewx_button.clicked.connect(self.on_skewx)
self.skewy_button.clicked.connect(self.on_skewy)
self.scalex_button.clicked.connect(self.on_scalex)
self.scaley_button.clicked.connect(self.on_scaley)
self.offx_button.clicked.connect(self.on_offx)
self.offy_button.clicked.connect(self.on_offy)
self.flipx_button.clicked.connect(self.on_flipx)
self.flipy_button.clicked.connect(self.on_flipy)
self.buffer_button.clicked.connect(self.on_buffer_by_distance)
self.buffer_factor_button.clicked.connect(self.on_buffer_by_factor)
self.reset_button.clicked.connect(self.set_tool_ui)
def run(self, toggle=True):
self.app.defaults.report_usage("ToolTransform()")
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])
AppTool.run(self)
self.set_tool_ui()
self.app.ui.notebook.setTabText(2, _("Transform Tool"))
def install(self, icon=None, separator=None, **kwargs):
AppTool.install(self, icon, separator, shortcut='Alt+T', **kwargs)
def set_tool_ui(self):
# ## Initialize form
self.ref_combo.set_value(self.app.defaults["tools_transform_reference"])
self.type_obj_combo.set_value(self.app.defaults["tools_transform_ref_object"])
self.point_entry.set_value(self.app.defaults["tools_transform_ref_point"])
self.rotate_entry.set_value(self.app.defaults["tools_transform_rotate"])
self.skewx_entry.set_value(self.app.defaults["tools_transform_skew_x"])
self.skewy_entry.set_value(self.app.defaults["tools_transform_skew_y"])
self.skew_link_cb.set_value(self.app.defaults["tools_transform_skew_link"])
self.scalex_entry.set_value(self.app.defaults["tools_transform_scale_x"])
self.scaley_entry.set_value(self.app.defaults["tools_transform_scale_y"])
self.scale_link_cb.set_value(self.app.defaults["tools_transform_scale_link"])
self.offx_entry.set_value(self.app.defaults["tools_transform_offset_x"])
self.offy_entry.set_value(self.app.defaults["tools_transform_offset_y"])
self.buffer_entry.set_value(self.app.defaults["tools_transform_buffer_dis"])
self.buffer_factor_entry.set_value(self.app.defaults["tools_transform_buffer_factor"])
self.buffer_rounded_cb.set_value(self.app.defaults["tools_transform_buffer_corner"])
# initial state is hidden
self.point_label.hide()
self.point_entry.hide()
self.point_button.hide()
self.type_object_label.hide()
self.type_obj_combo.hide()
self.object_combo.hide()
def on_type_obj_index_changed(self, index):
self.object_combo.setRootModelIndex(self.app.collection.index(index, 0, QtCore.QModelIndex()))
self.object_combo.setCurrentIndex(0)
self.object_combo.obj_type = {
_("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
}[self.type_obj_combo.get_value()]
def on_reference_changed(self, index):
if index == 0 or index == 1: # "Origin" or "Selection" reference
self.point_label.hide()
self.point_entry.hide()
self.point_button.hide()
self.type_object_label.hide()
self.type_obj_combo.hide()
self.object_combo.hide()
elif index == 2: # "Point" reference
self.point_label.show()
self.point_entry.show()
self.point_button.show()
self.type_object_label.hide()
self.type_obj_combo.hide()
self.object_combo.hide()
else: # "Object" reference
self.point_label.hide()
self.point_entry.hide()
self.point_button.hide()
self.type_object_label.show()
self.type_obj_combo.show()
self.object_combo.show()
def on_calculate_reference(self):
ref_val = self.ref_combo.currentIndex()
if ref_val == 0: # "Origin" reference
return 0, 0
elif ref_val == 1: # "Selection" reference
sel_list = self.app.collection.get_selected()
if sel_list:
xmin, ymin, xmax, ymax = self.alt_bounds(obj_list=sel_list)
px = (xmax + xmin) * 0.5
py = (ymax + ymin) * 0.5
return px, py
else:
self.app.inform.emit('[ERROR_NOTCL] %s' % _("No object selected."))
return "fail"
elif ref_val == 2: # "Point" reference
point_val = self.point_entry.get_value()
try:
px, py = eval('{}'.format(point_val))
return px, py
except Exception:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Incorrect format for Point value. Needs format X,Y"))
return "fail"
else: # "Object" reference
obj_name = self.object_combo.get_value()
ref_obj = self.app.collection.get_by_name(obj_name)
xmin, ymin, xmax, ymax = ref_obj.bounds()
px = (xmax + xmin) * 0.5
py = (ymax + ymin) * 0.5
return px, py
def on_add_coords(self):
val = self.app.clipboard.text()
self.point_entry.set_value(val)
def on_rotate(self):
value = float(self.rotate_entry.get_value())
if value == 0:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Rotate transformation can not be done for a value of 0."))
return
point = self.on_calculate_reference()
if point == 'fail':
return
self.app.worker_task.emit({'fcn': self.on_rotate_action, 'params': [value, point]})
def on_flipx(self):
axis = 'Y'
point = self.on_calculate_reference()
if point == 'fail':
return
self.app.worker_task.emit({'fcn': self.on_flip, 'params': [axis, point]})
def on_flipy(self):
axis = 'X'
point = self.on_calculate_reference()
if point == 'fail':
return
self.app.worker_task.emit({'fcn': self.on_flip, 'params': [axis, point]})
def on_skewx(self):
xvalue = float(self.skewx_entry.get_value())
if xvalue == 0:
return
if self.skew_link_cb.get_value():
yvalue = xvalue
else:
yvalue = 0
axis = 'X'
point = self.on_calculate_reference()
if point == 'fail':
return
self.app.worker_task.emit({'fcn': self.on_skew, 'params': [axis, xvalue, yvalue, point]})
def on_skewy(self):
xvalue = 0
yvalue = float(self.skewy_entry.get_value())
if yvalue == 0:
return
axis = 'Y'
point = self.on_calculate_reference()
if point == 'fail':
return
self.app.worker_task.emit({'fcn': self.on_skew, 'params': [axis, xvalue, yvalue, point]})
def on_scalex(self):
xvalue = float(self.scalex_entry.get_value())
if xvalue == 0 or xvalue == 1:
self.app.inform.emit('[WARNING_NOTCL] %s' %
_("Scale transformation can not be done for a factor of 0 or 1."))
return
if self.scale_link_cb.get_value():
yvalue = xvalue
else:
yvalue = 1
axis = 'X'
point = self.on_calculate_reference()
if point == 'fail':
return
self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue, point]})
def on_scaley(self):
xvalue = 1
yvalue = float(self.scaley_entry.get_value())
if yvalue == 0 or yvalue == 1:
self.app.inform.emit('[WARNING_NOTCL] %s' %
_("Scale transformation can not be done for a factor of 0 or 1."))
return
axis = 'Y'
point = self.on_calculate_reference()
if point == 'fail':
return
self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue, point]})
def on_offx(self):
value = float(self.offx_entry.get_value())
if value == 0:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Offset transformation can not be done for a value of 0."))
return
axis = 'X'
self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]})
def on_offy(self):
value = float(self.offy_entry.get_value())
if value == 0:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Offset transformation can not be done for a value of 0."))
return
axis = 'Y'
self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]})
def on_buffer_by_distance(self):
value = self.buffer_entry.get_value()
join = 1 if self.buffer_rounded_cb.get_value() else 2
self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join]})
def on_buffer_by_factor(self):
value = 1 + self.buffer_factor_entry.get_value() / 100.0
join = 1 if self.buffer_rounded_cb.get_value() else 2
# tell the buffer method to use the factor
factor = True
self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join, factor]})
def on_rotate_action(self, num, point):
obj_list = self.app.collection.get_selected()
if not obj_list:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to rotate!"))
return
else:
with self.app.proc_container.new(_("Appying Rotate")):
try:
px, py = point
for sel_obj in obj_list:
if sel_obj.kind == 'cncjob':
self.app.inform.emit(_("CNCJob objects can't be rotated."))
else:
sel_obj.rotate(-num, point=(px, py))
self.app.app_obj.object_changed.emit(sel_obj)
# add information to the object that it was changed and how much
sel_obj.options['rotate'] = num
sel_obj.plot()
self.app.inform.emit('[success] %s...' % _('Rotate done'))
except Exception as e:
self.app.inform.emit('[ERROR_NOTCL] %s %s, %s.' %
(_("Due of"), str(e), _("action was not executed.")))
return
def on_flip(self, axis, point):
obj_list = self.app.collection.get_selected()
if not obj_list:
self.app.inform.emit('[WARNING_NOTCL] %s!' % _("No object selected. Please Select an object to flip"))
return
else:
with self.app.proc_container.new(_("Applying Flip")):
try:
px, py = point
# execute mirroring
for sel_obj in obj_list:
if sel_obj.kind == 'cncjob':
self.app.inform.emit(_("CNCJob objects can't be mirrored/flipped."))
else:
if axis == 'X':
sel_obj.mirror('X', (px, py))
# add information to the object that it was changed and how much
# the axis is reversed because of the reference
if 'mirror_y' in sel_obj.options:
sel_obj.options['mirror_y'] = not sel_obj.options['mirror_y']
else:
sel_obj.options['mirror_y'] = True
self.app.inform.emit('[success] %s...' % _('Flip on the Y axis done'))
elif axis == 'Y':
sel_obj.mirror('Y', (px, py))
# add information to the object that it was changed and how much
# the axis is reversed because of the reference
if 'mirror_x' in sel_obj.options:
sel_obj.options['mirror_x'] = not sel_obj.options['mirror_x']
else:
sel_obj.options['mirror_x'] = True
self.app.inform.emit('[success] %s...' % _('Flip on the X axis done'))
self.app.app_obj.object_changed.emit(sel_obj)
sel_obj.plot()
except Exception as e:
self.app.inform.emit('[ERROR_NOTCL] %s %s, %s.' %
(_("Due of"), str(e), _("action was not executed.")))
return
def on_skew(self, axis, xvalue, yvalue, point):
obj_list = self.app.collection.get_selected()
if xvalue in [90, 180] or yvalue in [90, 180] or xvalue == yvalue == 0:
self.app.inform.emit('[WARNING_NOTCL] %s' %
_("Skew transformation can not be done for 0, 90 and 180 degrees."))
return
if not obj_list:
self.app.inform.emit('[WARNING_NOTCL] %s' %
_("No object selected. Please Select an object to shear/skew!"))
return
else:
with self.app.proc_container.new(_("Applying Skew")):
try:
px, py = point
for sel_obj in obj_list:
if sel_obj.kind == 'cncjob':
self.app.inform.emit(_("CNCJob objects can't be skewed."))
else:
sel_obj.skew(xvalue, yvalue, point=(px, py))
# add information to the object that it was changed and how much
sel_obj.options['skew_x'] = xvalue
sel_obj.options['skew_y'] = yvalue
self.app.app_obj.object_changed.emit(sel_obj)
sel_obj.plot()
self.app.inform.emit('[success] %s %s %s...' % (_('Skew on the'), str(axis), _("axis done")))
except Exception as e:
self.app.inform.emit('[ERROR_NOTCL] %s %s, %s.' %
(_("Due of"), str(e), _("action was not executed.")))
return
def on_scale(self, axis, xfactor, yfactor, point=None):
obj_list = self.app.collection.get_selected()
if not obj_list:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to scale!"))
return
else:
with self.app.proc_container.new(_("Applying Scale")):
try:
px, py = point
for sel_obj in obj_list:
if sel_obj.kind == 'cncjob':
self.app.inform.emit(_("CNCJob objects can't be scaled."))
else:
sel_obj.scale(xfactor, yfactor, point=(px, py))
# add information to the object that it was changed and how much
sel_obj.options['scale_x'] = xfactor
sel_obj.options['scale_y'] = yfactor
self.app.app_obj.object_changed.emit(sel_obj)
sel_obj.plot()
self.app.inform.emit('[success] %s %s %s...' % (_('Scale on the'), str(axis), _('axis done')))
except Exception as e:
self.app.inform.emit('[ERROR_NOTCL] %s %s, %s.' %
(_("Due of"), str(e), _("action was not executed.")))
return
def on_offset(self, axis, num):
obj_list = self.app.collection.get_selected()
if not obj_list:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to offset!"))
return
else:
with self.app.proc_container.new(_("Applying Offset")):
try:
for sel_obj in obj_list:
if sel_obj.kind == 'cncjob':
self.app.inform.emit(_("CNCJob objects can't be offset."))
else:
if axis == 'X':
sel_obj.offset((num, 0))
# add information to the object that it was changed and how much
sel_obj.options['offset_x'] = num
elif axis == 'Y':
sel_obj.offset((0, num))
# add information to the object that it was changed and how much
sel_obj.options['offset_y'] = num
self.app.app_obj.object_changed.emit(sel_obj)
sel_obj.plot()
self.app.inform.emit('[success] %s %s %s...' % (_('Offset on the'), str(axis), _('axis done')))
except Exception as e:
self.app.inform.emit('[ERROR_NOTCL] %s: %s.' %
(_("Action was not executed, due of"), str(e)))
return
def on_buffer_action(self, value, join, factor=None):
obj_list = self.app.collection.get_selected()
if not obj_list:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to buffer!"))
return
else:
with self.app.proc_container.new(_("Applying Buffer")):
try:
for sel_obj in obj_list:
if sel_obj.kind == 'cncjob':
self.app.inform.emit(_("CNCJob objects can't be buffered."))
elif sel_obj.kind.lower() == 'gerber':
sel_obj.buffer(value, join, factor)
sel_obj.source_file = self.app.export_gerber(obj_name=sel_obj.options['name'],
filename=None, local_use=sel_obj,
use_thread=False)
elif sel_obj.kind.lower() == 'excellon':
sel_obj.buffer(value, join, factor)
sel_obj.source_file = self.app.export_excellon(obj_name=sel_obj.options['name'],
filename=None, local_use=sel_obj,
use_thread=False)
elif sel_obj.kind.lower() == 'geometry':
sel_obj.buffer(value, join, factor)
self.app.app_obj.object_changed.emit(sel_obj)
sel_obj.plot()
self.app.inform.emit('[success] %s...' % _('Buffer done'))
except Exception as e:
self.app.log.debug("ToolTransform.on_buffer_action() --> %s" % str(e))
self.app.inform.emit('[ERROR_NOTCL] %s: %s.' %
(_("Action was not executed, due of"), str(e)))
return
@staticmethod
def alt_bounds(obj_list):
"""
Returns coordinates of rectangular bounds
of an object with geometry: (xmin, ymin, xmax, ymax).
"""
def bounds_rec(lst):
minx = np.Inf
miny = np.Inf
maxx = -np.Inf
maxy = -np.Inf
try:
for obj in lst:
if obj.kind != 'cncjob':
minx_, miny_, maxx_, maxy_ = bounds_rec(obj)
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 an object, return it's bounds
return lst.bounds()
return bounds_rec(obj_list)
# end of file

45
appTools/__init__.py Normal file
View File

@@ -0,0 +1,45 @@
from appTools.ToolCalculators import ToolCalculator
from appTools.ToolCalibration import ToolCalibration
from appTools.ToolDblSided import DblSidedTool
from appTools.ToolExtractDrills import ToolExtractDrills
from appTools.ToolAlignObjects import AlignObjects
from appTools.ToolFilm import Film
from appTools.ToolImage import ToolImage
from appTools.ToolDistance import Distance
from appTools.ToolDistanceMin import DistanceMin
from appTools.ToolMove import ToolMove
from appTools.ToolCutOut import CutOut
from appTools.ToolNCC import NonCopperClear
from appTools.ToolPaint import ToolPaint
from appTools.ToolIsolation import ToolIsolation
from appTools.ToolOptimal import ToolOptimal
from appTools.ToolPanelize import Panelize
from appTools.ToolPcbWizard import PcbWizard
from appTools.ToolPDF import ToolPDF
from appTools.ToolProperties import Properties
from appTools.ToolQRCode import QRCode
from appTools.ToolRulesCheck import RulesCheck
from appTools.ToolCopperThieving import ToolCopperThieving
from appTools.ToolFiducials import ToolFiducials
from appTools.ToolShell import FCShell
from appTools.ToolSolderPaste import SolderPaste
from appTools.ToolSub import ToolSub
from appTools.ToolTransform import ToolTransform
from appTools.ToolPunchGerber import ToolPunchGerber
from appTools.ToolInvertGerber import ToolInvertGerber
from appTools.ToolCorners import ToolCorners
from appTools.ToolEtchCompensation import ToolEtchCompensation