# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# File Author: Marius Adrian Stanciu (c) #
# Date: 1/13/2020 #
# MIT Licence #
# ##########################################################
from appTool import *
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class AlignObjects(AppTool):
pluginName = _("Align Objects")
def __init__(self, app):
AppTool.__init__(self, app)
self.app = app
self.decimals = app.decimals
self.canvas = self.app.plotcanvas
# #############################################################################
# ######################### Tool GUI ##########################################
# #############################################################################
self.ui = AlignUI(layout=self.layout, app=self.app)
self.pluginName = self.ui.pluginName
self.connect_signals_at_init()
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 connect_signals_at_init(self):
# Signals
self.ui.align_object_button.clicked.connect(self.on_align)
self.ui.type_obj_radio.activated_custom.connect(self.on_type_obj_changed)
self.ui.type_aligner_obj_radio.activated_custom.connect(self.on_type_aligner_changed)
self.ui.reset_button.clicked.connect(self.set_tool_ui)
def run(self, toggle=True):
self.app.defaults.report_usage("ToolAlignObjects()")
if toggle:
# if the splitter is hidden, display it
if self.app.ui.splitter.sizes()[0] == 0:
self.app.ui.splitter.setSizes([1, 1])
# if the Tool Tab is hidden display it, else hide it but only if the objectName is the same
found_idx = None
for idx in range(self.app.ui.notebook.count()):
if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab":
found_idx = idx
break
# show the Tab
if not found_idx:
try:
self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin"))
except RuntimeError:
self.app.ui.plugin_tab = QtWidgets.QWidget()
self.app.ui.plugin_tab.setObjectName("plugin_tab")
self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab)
self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2)
self.app.ui.plugin_scroll_area = VerticalScrollArea()
self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area)
self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin"))
# focus on Tool Tab
self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab)
try:
if self.app.ui.plugin_scroll_area.widget().objectName() == self.pluginName and found_idx:
# if the Tool Tab is not focused, focus on it
if not self.app.ui.notebook.currentWidget() is self.app.ui.plugin_tab:
# focus on Tool Tab
self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab)
else:
# else remove the Tool Tab
self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
self.app.ui.notebook.removeTab(2)
# if there are no objects loaded in the app then hide the Notebook widget
if not self.app.collection.get_list():
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.clear_ui(self.layout)
self.ui = AlignUI(layout=self.layout, app=self.app)
self.pluginName = self.ui.pluginName
self.connect_signals_at_init()
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.ui.a_type_radio.set_value(self.app.options["tools_align_objects_align_type"])
self.ui.type_obj_radio.set_value('grb')
self.ui.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.ui.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
self.ui.object_combo.setCurrentIndex(0)
self.ui.object_combo.obj_type = {'grb': "Gerber", 'exc': "Excellon"}[val]
def on_type_aligner_changed(self, val):
obj_type = {'grb': 0, 'exc': 1}[val]
self.ui.aligner_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
self.ui.aligner_object_combo.setCurrentIndex(0)
self.ui.aligner_object_combo.obj_type = {'grb': "Gerber", 'exc': "Excellon"}[val]
def on_align(self):
self.app.delete_selection_shape()
obj_sel_index = self.ui.object_combo.currentIndex()
obj_model_index = self.app.collection.index(obj_sel_index, 0, self.ui.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.ui.aligner_object_combo.currentIndex()
aligner_obj_model_index = self.app.collection.index(
aligner_obj_sel_index, 0, self.ui.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.ui.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.use_3d_engine:
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.use_3d_engine:
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.tools.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.obj_options['xmin'] = a
self.aligned_obj.obj_options['ymin'] = b
self.aligned_obj.obj_options['xmax'] = c
self.aligned_obj.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
try:
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)
except Exception:
pass
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.use_3d_engine:
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.ui.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.ui.aligner_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
class AlignUI:
pluginName = _("Align Objects")
def __init__(self, layout, app):
self.app = app
self.decimals = self.app.decimals
self.layout = layout
# ## Title
title_label = FCLabel("%s" % self.pluginName)
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.layout.addWidget(title_label)
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)
self.title_box = QtWidgets.QHBoxLayout()
self.tools_box.addLayout(self.title_box)
# #############################################################################################################
# Moving Object Frame
# #############################################################################################################
self.aligned_label = FCLabel('%s' % _("MOVING object"))
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.")
)
self.tools_box.addWidget(self.aligned_label)
m_frame = FCFrame()
self.tools_box.addWidget(m_frame)
# Grid Layout
grid0 = GLay(v_spacing=5, h_spacing=3)
m_frame.setLayout(grid0)
# Type of object to be aligned
self.type_obj_radio = RadioSet([
{"label": _("Gerber"), "value": "grb"},
{"label": _("Excellon"), "value": "exc"},
])
grid0.addWidget(self.type_obj_radio, 0, 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, 2, 0, 1, 2)
# #############################################################################################################
# Destination Object Frame
# #############################################################################################################
self.aligned_label = FCLabel('%s' % _("DESTINATION 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.")
)
self.tools_box.addWidget(self.aligned_label)
d_frame = FCFrame()
self.tools_box.addWidget(d_frame)
# Grid Layout
grid1 = GLay(v_spacing=5, h_spacing=3)
d_frame.setLayout(grid1)
# Type of object to be aligned to = aligner
self.type_aligner_obj_radio = RadioSet([
{"label": _("Gerber"), "value": "grb"},
{"label": _("Excellon"), "value": "exc"},
])
grid1.addWidget(self.type_aligner_obj_radio, 0, 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.")
)
grid1.addWidget(self.aligner_object_combo, 2, 0, 1, 2)
# #############################################################################################################
# Parameters Frame
# #############################################################################################################
self.param_label = FCLabel('%s' % _('Parameters'))
self.tools_box.addWidget(self.param_label)
par_frame = FCFrame()
self.tools_box.addWidget(par_frame)
# Grid Layout
grid2 = GLay(v_spacing=5, h_spacing=3)
par_frame.setLayout(grid2)
# Alignment Type
self.a_type_lbl = FCLabel('%s:' % _("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'}
])
grid2.addWidget(self.a_type_lbl, 0, 0, 1, 2)
grid2.addWidget(self.a_type_radio, 2, 0, 1, 2)
# GLay.set_common_column_size([grid0, grid1, grid2], 0, FCLabel)
# Buttons
self.align_object_button = FCButton(_("Align Object"))
self.align_object_button.setIcon(QtGui.QIcon(self.app.resource_location + '/align16.png'))
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.tools_box.addWidget(self.align_object_button)
self.layout.addStretch(1)
# ## Reset Tool
self.reset_button = FCButton(_("Reset Tool"))
self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
self.reset_button.setToolTip(
_("Will reset the tool parameters.")
)
self.reset_button.setStyleSheet("""
QPushButton
{
font-weight: bold;
}
""")
self.layout.addWidget(self.reset_button)
# #################################### FINSIHED GUI ###########################
# #############################################################################
def confirmation_message(self, accepted, minval, maxval):
if accepted is False:
self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
self.decimals,
minval,
self.decimals,
maxval), False)
else:
self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
def confirmation_message_int(self, accepted, minval, maxval):
if accepted is False:
self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
(_("Edited value is out of range"), minval, maxval), False)
else:
self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)