- renamed classes to have shorter names and grouped
This commit is contained in:
@@ -1,495 +0,0 @@
|
||||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Date: 1/13/2020 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
from PyQt5 import QtWidgets, QtGui, QtCore
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
|
||||
from flatcamGUI.GUIElements import FCComboBox, RadioSet
|
||||
|
||||
import math
|
||||
|
||||
from shapely.geometry import Point
|
||||
from shapely.affinity import translate
|
||||
|
||||
import gettext
|
||||
import FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
import logging
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
log = logging.getLogger('base')
|
||||
|
||||
|
||||
class AlignObjects(FlatCAMTool):
|
||||
|
||||
toolName = _("Align Objects")
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__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])
|
||||
|
||||
FlatCAMTool.run(self)
|
||||
self.set_tool_ui()
|
||||
|
||||
self.app.ui.notebook.setTabText(2, _("Align Tool"))
|
||||
|
||||
def install(self, icon=None, separator=None, **kwargs):
|
||||
FlatCAMTool.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.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)
|
||||
|
||||
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.app.inform.emit('%s: %s' % (_("First Point"), _("Click on the START point.")))
|
||||
self.target_obj = self.aligned_obj
|
||||
self.set_color()
|
||||
|
||||
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()))
|
||||
@@ -1,370 +0,0 @@
|
||||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Date: 3/10/2019 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from flatcamGUI.GUIElements import FCSpinner, FCDoubleSpinner, FCEntry
|
||||
import math
|
||||
|
||||
import gettext
|
||||
import FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
|
||||
class ToolCalculator(FlatCAMTool):
|
||||
|
||||
toolName = _("Calculators")
|
||||
v_shapeName = _("V-Shape Tool Calculator")
|
||||
unitsName = _("Units Calculator")
|
||||
eplateName = _("ElectroPlating Calculator")
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__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])
|
||||
|
||||
FlatCAMTool.run(self)
|
||||
|
||||
self.set_tool_ui()
|
||||
|
||||
self.app.ui.notebook.setTabText(2, _("Calc. Tool"))
|
||||
|
||||
def install(self, icon=None, separator=None, **kwargs):
|
||||
FlatCAMTool.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
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,423 +0,0 @@
|
||||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Date: 5/17/2020 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from flatcamGUI.GUIElements import FCDoubleSpinner, FCCheckBox, FCComboBox, FCButton
|
||||
|
||||
from shapely.geometry import MultiPolygon, LineString
|
||||
|
||||
from copy import deepcopy
|
||||
import logging
|
||||
|
||||
import gettext
|
||||
import FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
log = logging.getLogger('base')
|
||||
|
||||
|
||||
class ToolCorners(FlatCAMTool):
|
||||
|
||||
toolName = _("Corner Markers Tool")
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__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 Object"))
|
||||
self.object_label.setToolTip(
|
||||
_("The Gerber object that 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)
|
||||
|
||||
# ## 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)
|
||||
|
||||
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])
|
||||
|
||||
FlatCAMTool.run(self)
|
||||
|
||||
self.set_tool_ui()
|
||||
|
||||
self.app.ui.notebook.setTabText(2, _("Corners Tool"))
|
||||
|
||||
def install(self, icon=None, separator=None, **kwargs):
|
||||
FlatCAMTool.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"]))
|
||||
|
||||
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=2)
|
||||
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."))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,901 +0,0 @@
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from flatcamGUI.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 FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
log = logging.getLogger('base')
|
||||
|
||||
|
||||
class DblSidedTool(FlatCAMTool):
|
||||
|
||||
toolName = _("2-Sided PCB")
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__init__(self, app)
|
||||
self.decimals = self.app.decimals
|
||||
|
||||
# ## Title
|
||||
title_label = QtWidgets.QLabel("%s" % self.toolName)
|
||||
title_label.setStyleSheet("""
|
||||
QLabel
|
||||
{
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
self.layout.addWidget(title_label)
|
||||
|
||||
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):
|
||||
FlatCAMTool.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])
|
||||
|
||||
FlatCAMTool.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.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.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.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.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('')
|
||||
@@ -1,636 +0,0 @@
|
||||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Date: 3/10/2019 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from flatcamGUI.VisPyVisuals import *
|
||||
from flatcamGUI.GUIElements import FCEntry, FCButton, FCCheckBox
|
||||
|
||||
from shapely.geometry import Point, MultiLineString, Polygon
|
||||
|
||||
import FlatCAMTranslation as fcTranslate
|
||||
from camlib import FlatCAMRTreeStorage
|
||||
from flatcamEditors.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(FlatCAMTool):
|
||||
|
||||
toolName = _("Distance Tool")
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__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 flatcamGUI.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):
|
||||
FlatCAMTool.install(self, icon, separator, shortcut='Ctrl+M', **kwargs)
|
||||
|
||||
def set_tool_ui(self):
|
||||
# Remove anything else in the GUI
|
||||
self.app.ui.tool_scroll_area.takeWidget()
|
||||
|
||||
# Put ourselves in the GUI
|
||||
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 <b>Dy</b>: "
|
||||
# "%.*f " %
|
||||
# (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)))
|
||||
|
||||
if dx != 0.0:
|
||||
try:
|
||||
angle = math.degrees(math.atan(dy / dx))
|
||||
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>: {} <b>Dy</b>: {} ".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(
|
||||
" <b>X</b>: {} <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>: {} <b>Dy</b>: {} ".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
|
||||
if dx != 0.0:
|
||||
try:
|
||||
angle = math.degrees(math.atan(dy / dx))
|
||||
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
|
||||
@@ -1,306 +0,0 @@
|
||||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Date: 09/29/2019 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from flatcamGUI.VisPyVisuals import *
|
||||
from flatcamGUI.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 FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
log = logging.getLogger('base')
|
||||
|
||||
|
||||
class DistanceMin(FlatCAMTool):
|
||||
|
||||
toolName = _("Minimum Distance Tool")
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__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):
|
||||
FlatCAMTool.install(self, icon, separator, shortcut='Shift+M', **kwargs)
|
||||
|
||||
def set_tool_ui(self):
|
||||
# Remove anything else in the GUI
|
||||
self.app.ui.tool_scroll_area.takeWidget()
|
||||
|
||||
# Put oneself in the GUI
|
||||
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
|
||||
@@ -1,694 +0,0 @@
|
||||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Date: 1/10/2020 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, FCComboBox
|
||||
|
||||
from shapely.geometry import Point
|
||||
|
||||
import logging
|
||||
import gettext
|
||||
import FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
log = logging.getLogger('base')
|
||||
|
||||
|
||||
class ToolExtractDrills(FlatCAMTool):
|
||||
|
||||
toolName = _("Extract Drills")
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__init__(self, app)
|
||||
self.decimals = self.app.decimals
|
||||
|
||||
# ## Title
|
||||
title_label = QtWidgets.QLabel("%s" % self.toolName)
|
||||
title_label.setStyleSheet("""
|
||||
QLabel
|
||||
{
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
self.layout.addWidget(title_label)
|
||||
|
||||
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):
|
||||
FlatCAMTool.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])
|
||||
|
||||
FlatCAMTool.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.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)
|
||||
@@ -1,923 +0,0 @@
|
||||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Date: 11/21/2019 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from flatcamGUI.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 FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
log = logging.getLogger('base')
|
||||
|
||||
|
||||
class ToolFiducials(FlatCAMTool):
|
||||
|
||||
toolName = _("Fiducials Tool")
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__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.layout.addWidget(self.points_table)
|
||||
self.layout.addWidget(QtWidgets.QLabel(''))
|
||||
|
||||
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)
|
||||
|
||||
# ## 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>" % _("Copper 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])
|
||||
|
||||
FlatCAMTool.run(self)
|
||||
|
||||
self.set_tool_ui()
|
||||
|
||||
self.app.ui.notebook.setTabText(2, _("Fiducials Tool"))
|
||||
|
||||
def install(self, icon=None, separator=None, **kwargs):
|
||||
FlatCAMTool.install(self, icon, separator, shortcut='Alt+J', **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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,297 +0,0 @@
|
||||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Date: 3/10/2019 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
from PyQt5 import QtGui, QtWidgets
|
||||
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from flatcamGUI.GUIElements import RadioSet, FCComboBox, FCSpinner
|
||||
|
||||
import gettext
|
||||
import FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
|
||||
class ToolImage(FlatCAMTool):
|
||||
|
||||
toolName = _("Image as Object")
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__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])
|
||||
|
||||
FlatCAMTool.run(self)
|
||||
self.set_tool_ui()
|
||||
|
||||
self.app.ui.notebook.setTabText(2, _("Image Tool"))
|
||||
|
||||
def install(self, icon=None, separator=None, **kwargs):
|
||||
FlatCAMTool.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.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))
|
||||
@@ -1,304 +0,0 @@
|
||||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Date: 2/14/2020 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from flatcamGUI.GUIElements import FCButton, FCDoubleSpinner, RadioSet, FCComboBox
|
||||
|
||||
from shapely.geometry import box
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
import logging
|
||||
import gettext
|
||||
import FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
log = logging.getLogger('base')
|
||||
|
||||
|
||||
class ToolInvertGerber(FlatCAMTool):
|
||||
|
||||
toolName = _("Invert Gerber Tool")
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.decimals = self.app.decimals
|
||||
|
||||
FlatCAMTool.__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)
|
||||
|
||||
grid0.addWidget(QtWidgets.QLabel(""), 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):
|
||||
FlatCAMTool.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])
|
||||
|
||||
FlatCAMTool.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)
|
||||
|
||||
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)
|
||||
|
||||
for td in new_apertures:
|
||||
print(td, new_apertures[td])
|
||||
|
||||
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.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
|
||||
@@ -1,341 +0,0 @@
|
||||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Date: 3/10/2019 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from flatcamGUI.VisPyVisuals import *
|
||||
|
||||
from copy import copy
|
||||
import logging
|
||||
import gettext
|
||||
import FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
log = logging.getLogger('base')
|
||||
|
||||
|
||||
class ToolMove(FlatCAMTool):
|
||||
|
||||
toolName = _("Move")
|
||||
replot_signal = QtCore.pyqtSignal(list)
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__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 flatcamGUI.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):
|
||||
FlatCAMTool.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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,590 +0,0 @@
|
||||
# ##########################################################
|
||||
# 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 FlatCAMTool import FlatCAMTool
|
||||
from flatcamGUI.GUIElements import OptionalHideInputSection, FCTextArea, FCEntry, FCSpinner, FCCheckBox, FCComboBox
|
||||
from FlatCAMCommon import GracefulException as grace
|
||||
|
||||
from shapely.geometry import MultiPolygon
|
||||
from shapely.ops import nearest_points
|
||||
|
||||
import numpy as np
|
||||
|
||||
import logging
|
||||
import gettext
|
||||
import FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
log = logging.getLogger('base')
|
||||
|
||||
|
||||
class ToolOptimal(FlatCAMTool):
|
||||
|
||||
toolName = _("Optimal Tool")
|
||||
|
||||
update_text = QtCore.pyqtSignal(list)
|
||||
update_sec_distances = QtCore.pyqtSignal(dict)
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__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, self.gerber_object_combo)
|
||||
|
||||
# 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):
|
||||
FlatCAMTool.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])
|
||||
|
||||
FlatCAMTool.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)
|
||||
@@ -1,361 +0,0 @@
|
||||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Date: 4/23/2019 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from FlatCAMCommon import GracefulException as grace
|
||||
from flatcamParsers.ParsePDF import PdfParser
|
||||
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 FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
log = logging.getLogger('base')
|
||||
|
||||
|
||||
class ToolPDF(FlatCAMTool):
|
||||
"""
|
||||
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):
|
||||
FlatCAMTool.__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):
|
||||
FlatCAMTool.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.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.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 GUI
|
||||
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()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,826 +0,0 @@
|
||||
# ##########################################################
|
||||
# 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 FlatCAMTool import FlatCAMTool
|
||||
|
||||
from flatcamGUI.GUIElements import FCSpinner, FCDoubleSpinner, RadioSet, FCCheckBox, OptionalInputSection, FCComboBox
|
||||
from FlatCAMCommon import GracefulException as 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 FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
import logging
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
log = logging.getLogger('base')
|
||||
|
||||
|
||||
class Panelize(FlatCAMTool):
|
||||
|
||||
toolName = _("Panelize PCB")
|
||||
|
||||
def __init__(self, app):
|
||||
self.decimals = app.decimals
|
||||
|
||||
FlatCAMTool.__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])
|
||||
|
||||
FlatCAMTool.run(self)
|
||||
self.set_tool_ui()
|
||||
|
||||
self.app.ui.notebook.setTabText(2, _("Panel. Tool"))
|
||||
|
||||
def install(self, icon=None, separator=None, **kwargs):
|
||||
FlatCAMTool.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.new_object("excellon", self.outname, job_init_excellon, plot=True, autoselected=True)
|
||||
else:
|
||||
self.app.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()))
|
||||
@@ -1,469 +0,0 @@
|
||||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Date: 4/15/2019 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from flatcamGUI.GUIElements import RadioSet, FCSpinner, FCButton, FCTable
|
||||
|
||||
import re
|
||||
import os
|
||||
from datetime import datetime
|
||||
from io import StringIO
|
||||
|
||||
import gettext
|
||||
import FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
|
||||
class PcbWizard(FlatCAMTool):
|
||||
|
||||
file_loaded = QtCore.pyqtSignal(str, str)
|
||||
|
||||
toolName = _("PcbWizard Import Tool")
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__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])
|
||||
|
||||
FlatCAMTool.run(self)
|
||||
self.set_tool_ui()
|
||||
|
||||
self.app.ui.notebook.setTabText(2, _("PCBWizard Tool"))
|
||||
|
||||
def install(self, icon=None, separator=None, **kwargs):
|
||||
FlatCAMTool.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.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.'))
|
||||
@@ -1,593 +0,0 @@
|
||||
# ##########################################################
|
||||
# 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 FlatCAMTool import FlatCAMTool
|
||||
from flatcamGUI.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 FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
log = logging.getLogger('base')
|
||||
|
||||
|
||||
class Properties(FlatCAMTool):
|
||||
toolName = _("Properties")
|
||||
|
||||
calculations_finished = QtCore.pyqtSignal(float, float, float, float, float, object)
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__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])
|
||||
|
||||
FlatCAMTool.run(self)
|
||||
self.set_tool_ui()
|
||||
|
||||
self.properties()
|
||||
|
||||
def install(self, icon=None, separator=None, **kwargs):
|
||||
FlatCAMTool.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
|
||||
@@ -1,994 +0,0 @@
|
||||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Date: 1/24/2020 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, FCComboBox
|
||||
|
||||
from copy import deepcopy
|
||||
import logging
|
||||
from shapely.geometry import MultiPolygon, Point
|
||||
|
||||
import gettext
|
||||
import FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
log = logging.getLogger('base')
|
||||
|
||||
|
||||
class ToolPunchGerber(FlatCAMTool):
|
||||
|
||||
toolName = _("Punch Gerber")
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__init__(self, app)
|
||||
|
||||
self.decimals = self.app.decimals
|
||||
|
||||
# Title
|
||||
title_label = QtWidgets.QLabel("%s" % self.toolName)
|
||||
title_label.setStyleSheet("""
|
||||
QLabel
|
||||
{
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
self.layout.addWidget(title_label)
|
||||
|
||||
# Punch Drill holes
|
||||
self.layout.addWidget(QtWidgets.QLabel(""))
|
||||
|
||||
# ## Grid Layout
|
||||
grid_lay = QtWidgets.QGridLayout()
|
||||
self.layout.addLayout(grid_lay)
|
||||
grid_lay.setColumnStretch(0, 1)
|
||||
grid_lay.setColumnStretch(1, 0)
|
||||
|
||||
# ## Gerber Object
|
||||
self.gerber_object_combo = 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 into which to punch holes"))
|
||||
|
||||
grid_lay.addWidget(self.grb_label, 0, 0, 1, 2)
|
||||
grid_lay.addWidget(self.gerber_object_combo, 1, 0, 1, 2)
|
||||
|
||||
separator_line = QtWidgets.QFrame()
|
||||
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
grid_lay.addWidget(separator_line, 2, 0, 1, 2)
|
||||
|
||||
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, 3, 0, 1, 2)
|
||||
|
||||
# Select all
|
||||
self.select_all_cb = FCCheckBox('%s' % _("ALL"))
|
||||
grid_lay.addWidget(self.select_all_cb)
|
||||
|
||||
# Circular Aperture Selection
|
||||
self.circular_cb = FCCheckBox('%s' % _("Circular"))
|
||||
self.circular_cb.setToolTip(
|
||||
_("Process Circular Pads.")
|
||||
)
|
||||
|
||||
grid_lay.addWidget(self.circular_cb, 5, 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, 6, 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, 7, 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, 8, 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, 9, 0, 1, 2)
|
||||
|
||||
separator_line = QtWidgets.QFrame()
|
||||
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
grid_lay.addWidget(separator_line, 10, 0, 1, 2)
|
||||
|
||||
# Grid Layout
|
||||
grid0 = QtWidgets.QGridLayout()
|
||||
self.layout.addLayout(grid0)
|
||||
grid0.setColumnStretch(0, 0)
|
||||
grid0.setColumnStretch(1, 1)
|
||||
|
||||
self.method_label = QtWidgets.QLabel('<b>%s:</b>' % _("Method"))
|
||||
self.method_label.setToolTip(
|
||||
_("The punch hole source can be:\n"
|
||||
"- Excellon Object-> the Excellon object drills center will serve as reference.\n"
|
||||
"- Fixed Diameter -> will try to use the pads center as reference adding fixed diameter holes.\n"
|
||||
"- Fixed Annular Ring -> will try to keep a set annular ring.\n"
|
||||
"- Proportional -> will make a Gerber punch hole having the diameter a percentage of the pad diameter.")
|
||||
)
|
||||
self.method_punch = RadioSet(
|
||||
[
|
||||
{'label': _('Excellon'), 'value': 'exc'},
|
||||
{'label': _("Fixed Diameter"), 'value': 'fixed'},
|
||||
{'label': _("Fixed Annular Ring"), 'value': 'ring'},
|
||||
{'label': _("Proportional"), 'value': 'prop'}
|
||||
],
|
||||
orientation='vertical',
|
||||
stretch=False)
|
||||
grid0.addWidget(self.method_label, 0, 0, 1, 2)
|
||||
grid0.addWidget(self.method_punch, 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.exc_label = QtWidgets.QLabel('<b>%s</b>' % _("Excellon"))
|
||||
self.exc_label.setToolTip(
|
||||
_("Remove the geometry of Excellon from the Gerber to create the holes in pads.")
|
||||
)
|
||||
|
||||
self.exc_combo = FCComboBox()
|
||||
self.exc_combo.setModel(self.app.collection)
|
||||
self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
|
||||
self.exc_combo.is_last = True
|
||||
self.exc_combo.obj_type = "Excellon"
|
||||
|
||||
grid0.addWidget(self.exc_label, 3, 0, 1, 2)
|
||||
grid0.addWidget(self.exc_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)
|
||||
|
||||
# Fixed Dia
|
||||
self.fixed_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Diameter"))
|
||||
grid0.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.")
|
||||
)
|
||||
|
||||
grid0.addWidget(self.dia_label, 8, 0)
|
||||
grid0.addWidget(self.dia_entry, 8, 1)
|
||||
|
||||
separator_line = QtWidgets.QFrame()
|
||||
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
grid0.addWidget(separator_line, 9, 0, 1, 2)
|
||||
|
||||
self.ring_frame = QtWidgets.QFrame()
|
||||
self.ring_frame.setContentsMargins(0, 0, 0, 0)
|
||||
grid0.addWidget(self.ring_frame, 10, 0, 1, 2)
|
||||
|
||||
self.ring_box = QtWidgets.QVBoxLayout()
|
||||
self.ring_box.setContentsMargins(0, 0, 0, 0)
|
||||
self.ring_frame.setLayout(self.ring_box)
|
||||
|
||||
# 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.")
|
||||
)
|
||||
self.ring_box.addWidget(self.ring_label)
|
||||
|
||||
# ## Grid Layout
|
||||
self.grid1 = QtWidgets.QGridLayout()
|
||||
self.grid1.setColumnStretch(0, 0)
|
||||
self.grid1.setColumnStretch(1, 1)
|
||||
self.ring_box.addLayout(self.grid1)
|
||||
|
||||
# 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)
|
||||
|
||||
self.grid1.addWidget(self.circular_ring_label, 3, 0)
|
||||
self.grid1.addWidget(self.circular_ring_entry, 3, 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)
|
||||
|
||||
self.grid1.addWidget(self.oblong_ring_label, 4, 0)
|
||||
self.grid1.addWidget(self.oblong_ring_entry, 4, 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)
|
||||
|
||||
self.grid1.addWidget(self.square_ring_label, 5, 0)
|
||||
self.grid1.addWidget(self.square_ring_entry, 5, 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)
|
||||
|
||||
self.grid1.addWidget(self.rectangular_ring_label, 6, 0)
|
||||
self.grid1.addWidget(self.rectangular_ring_entry, 6, 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)
|
||||
|
||||
self.grid1.addWidget(self.other_ring_label, 7, 0)
|
||||
self.grid1.addWidget(self.other_ring_entry, 7, 1)
|
||||
|
||||
separator_line = QtWidgets.QFrame()
|
||||
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
grid0.addWidget(separator_line, 11, 0, 1, 2)
|
||||
|
||||
# Proportional value
|
||||
self.prop_label = QtWidgets.QLabel('<b>%s</b>' % _("Proportional Diameter"))
|
||||
grid0.addWidget(self.prop_label, 12, 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.")
|
||||
)
|
||||
|
||||
grid0.addWidget(self.factor_label, 13, 0)
|
||||
grid0.addWidget(self.factor_entry, 13, 1)
|
||||
|
||||
separator_line3 = QtWidgets.QFrame()
|
||||
separator_line3.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
separator_line3.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
grid0.addWidget(separator_line3, 14, 0, 1, 2)
|
||||
|
||||
# Buttons
|
||||
self.punch_object_button = QtWidgets.QPushButton(_("Punch Gerber"))
|
||||
self.punch_object_button.setToolTip(
|
||||
_("Create a Gerber object from the selected object, within\n"
|
||||
"the specified box.")
|
||||
)
|
||||
self.punch_object_button.setStyleSheet("""
|
||||
QPushButton
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
self.layout.addWidget(self.punch_object_button)
|
||||
|
||||
self.layout.addStretch()
|
||||
|
||||
# ## Reset Tool
|
||||
self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
|
||||
self.reset_button.setToolTip(
|
||||
_("Will reset the tool parameters.")
|
||||
)
|
||||
self.reset_button.setStyleSheet("""
|
||||
QPushButton
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
self.layout.addWidget(self.reset_button)
|
||||
|
||||
self.units = self.app.defaults['units']
|
||||
|
||||
# self.cb_items = [
|
||||
# self.grid1.itemAt(w).widget() for w in range(self.grid1.count())
|
||||
# if isinstance(self.grid1.itemAt(w).widget(), FCCheckBox)
|
||||
# ]
|
||||
|
||||
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)
|
||||
|
||||
# ## Signals
|
||||
self.method_punch.activated_custom.connect(self.on_method)
|
||||
self.reset_button.clicked.connect(self.set_tool_ui)
|
||||
self.punch_object_button.clicked.connect(self.on_generate_object)
|
||||
|
||||
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 run(self, toggle=True):
|
||||
self.app.defaults.report_usage("ToolPunchGerber()")
|
||||
|
||||
if toggle:
|
||||
# if the splitter is hidden, display it, else hide it but only if the current widget is the same
|
||||
if self.app.ui.splitter.sizes()[0] == 0:
|
||||
self.app.ui.splitter.setSizes([1, 1])
|
||||
else:
|
||||
try:
|
||||
if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
|
||||
# if tab is populated with the tool but it does not have the focus, focus on it
|
||||
if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
|
||||
# focus on Tool Tab
|
||||
self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
|
||||
else:
|
||||
self.app.ui.splitter.setSizes([0, 1])
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if self.app.ui.splitter.sizes()[0] == 0:
|
||||
self.app.ui.splitter.setSizes([1, 1])
|
||||
|
||||
FlatCAMTool.run(self)
|
||||
|
||||
self.set_tool_ui()
|
||||
|
||||
self.app.ui.notebook.setTabText(2, _("Punch Tool"))
|
||||
|
||||
def install(self, icon=None, separator=None, **kwargs):
|
||||
FlatCAMTool.install(self, icon, separator, shortcut='Alt+H', **kwargs)
|
||||
|
||||
def set_tool_ui(self):
|
||||
self.reset_fields()
|
||||
|
||||
self.ui_connect()
|
||||
self.method_punch.set_value(self.app.defaults["tools_punch_hole_type"])
|
||||
self.select_all_cb.set_value(False)
|
||||
|
||||
self.dia_entry.set_value(float(self.app.defaults["tools_punch_hole_fixed_dia"]))
|
||||
|
||||
self.circular_ring_entry.set_value(float(self.app.defaults["tools_punch_circular_ring"]))
|
||||
self.oblong_ring_entry.set_value(float(self.app.defaults["tools_punch_oblong_ring"]))
|
||||
self.square_ring_entry.set_value(float(self.app.defaults["tools_punch_square_ring"]))
|
||||
self.rectangular_ring_entry.set_value(float(self.app.defaults["tools_punch_rectangular_ring"]))
|
||||
self.other_ring_entry.set_value(float(self.app.defaults["tools_punch_others_ring"]))
|
||||
|
||||
self.circular_cb.set_value(self.app.defaults["tools_punch_circular"])
|
||||
self.oblong_cb.set_value(self.app.defaults["tools_punch_oblong"])
|
||||
self.square_cb.set_value(self.app.defaults["tools_punch_square"])
|
||||
self.rectangular_cb.set_value(self.app.defaults["tools_punch_rectangular"])
|
||||
self.other_cb.set_value(self.app.defaults["tools_punch_others"])
|
||||
|
||||
self.factor_entry.set_value(float(self.app.defaults["tools_punch_hole_prop_factor"]))
|
||||
|
||||
def on_select_all(self, state):
|
||||
self.ui_disconnect()
|
||||
if state:
|
||||
self.circular_cb.setChecked(True)
|
||||
self.oblong_cb.setChecked(True)
|
||||
self.square_cb.setChecked(True)
|
||||
self.rectangular_cb.setChecked(True)
|
||||
self.other_cb.setChecked(True)
|
||||
else:
|
||||
self.circular_cb.setChecked(False)
|
||||
self.oblong_cb.setChecked(False)
|
||||
self.square_cb.setChecked(False)
|
||||
self.rectangular_cb.setChecked(False)
|
||||
self.other_cb.setChecked(False)
|
||||
self.ui_connect()
|
||||
|
||||
def on_method(self, val):
|
||||
self.exc_label.setEnabled(False)
|
||||
self.exc_combo.setEnabled(False)
|
||||
self.fixed_label.setEnabled(False)
|
||||
self.dia_label.setEnabled(False)
|
||||
self.dia_entry.setEnabled(False)
|
||||
self.ring_frame.setEnabled(False)
|
||||
self.prop_label.setEnabled(False)
|
||||
self.factor_label.setEnabled(False)
|
||||
self.factor_entry.setEnabled(False)
|
||||
|
||||
if val == 'exc':
|
||||
self.exc_label.setEnabled(True)
|
||||
self.exc_combo.setEnabled(True)
|
||||
elif val == 'fixed':
|
||||
self.fixed_label.setEnabled(True)
|
||||
self.dia_label.setEnabled(True)
|
||||
self.dia_entry.setEnabled(True)
|
||||
elif val == 'ring':
|
||||
self.ring_frame.setEnabled(True)
|
||||
elif val == 'prop':
|
||||
self.prop_label.setEnabled(True)
|
||||
self.factor_label.setEnabled(True)
|
||||
self.factor_entry.setEnabled(True)
|
||||
|
||||
def ui_connect(self):
|
||||
self.select_all_cb.stateChanged.connect(self.on_select_all)
|
||||
|
||||
def ui_disconnect(self):
|
||||
try:
|
||||
self.select_all_cb.stateChanged.disconnect()
|
||||
except (AttributeError, TypeError):
|
||||
pass
|
||||
|
||||
def on_generate_object(self):
|
||||
|
||||
# get the Gerber file who is the source of the punched Gerber
|
||||
selection_index = self.gerber_object_combo.currentIndex()
|
||||
model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
|
||||
|
||||
try:
|
||||
grb_obj = model_index.internalPointer().obj
|
||||
except Exception:
|
||||
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
|
||||
return
|
||||
|
||||
name = grb_obj.options['name'].rpartition('.')[0]
|
||||
outname = name + "_punched"
|
||||
|
||||
punch_method = self.method_punch.get_value()
|
||||
|
||||
new_options = {}
|
||||
for opt in grb_obj.options:
|
||||
new_options[opt] = deepcopy(grb_obj.options[opt])
|
||||
|
||||
if punch_method == 'exc':
|
||||
|
||||
# get the Excellon file whose geometry will create the punch holes
|
||||
selection_index = self.exc_combo.currentIndex()
|
||||
model_index = self.app.collection.index(selection_index, 0, self.exc_combo.rootModelIndex())
|
||||
|
||||
try:
|
||||
exc_obj = model_index.internalPointer().obj
|
||||
except Exception:
|
||||
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Excellon object loaded ..."))
|
||||
return
|
||||
|
||||
# this is the punching geometry
|
||||
exc_solid_geometry = MultiPolygon(exc_obj.solid_geometry)
|
||||
if isinstance(grb_obj.solid_geometry, list):
|
||||
grb_solid_geometry = MultiPolygon(grb_obj.solid_geometry)
|
||||
else:
|
||||
grb_solid_geometry = grb_obj.solid_geometry
|
||||
|
||||
# create the punched Gerber solid_geometry
|
||||
punched_solid_geometry = grb_solid_geometry.difference(exc_solid_geometry)
|
||||
|
||||
# update the gerber apertures to include the clear geometry so it can be exported successfully
|
||||
new_apertures = deepcopy(grb_obj.apertures)
|
||||
new_apertures_items = new_apertures.items()
|
||||
|
||||
# find maximum aperture id
|
||||
new_apid = max([int(x) for x, __ in new_apertures_items])
|
||||
|
||||
# store here the clear geometry, the key is the drill size
|
||||
holes_apertures = {}
|
||||
|
||||
for apid, val in new_apertures_items:
|
||||
for elem in val['geometry']:
|
||||
# make it work only for Gerber Flashes who are Points in 'follow'
|
||||
if 'solid' in elem and isinstance(elem['follow'], Point):
|
||||
for drill in exc_obj.drills:
|
||||
clear_apid_size = exc_obj.tools[drill['tool']]['C']
|
||||
|
||||
# since there may be drills that do not drill into a pad we test only for drills in a pad
|
||||
if drill['point'].within(elem['solid']):
|
||||
geo_elem = {}
|
||||
geo_elem['clear'] = drill['point']
|
||||
|
||||
if clear_apid_size not in holes_apertures:
|
||||
holes_apertures[clear_apid_size] = {}
|
||||
holes_apertures[clear_apid_size]['type'] = 'C'
|
||||
holes_apertures[clear_apid_size]['size'] = clear_apid_size
|
||||
holes_apertures[clear_apid_size]['geometry'] = []
|
||||
|
||||
holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem))
|
||||
|
||||
# add the clear geometry to new apertures; it's easier than to test if there are apertures with the same
|
||||
# size and add there the clear geometry
|
||||
for hole_size, ap_val in holes_apertures.items():
|
||||
new_apid += 1
|
||||
new_apertures[str(new_apid)] = deepcopy(ap_val)
|
||||
|
||||
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(punched_solid_geometry)
|
||||
new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None,
|
||||
local_use=new_obj, use_thread=False)
|
||||
|
||||
self.app.new_object('gerber', outname, init_func)
|
||||
elif punch_method == 'fixed':
|
||||
punch_size = float(self.dia_entry.get_value())
|
||||
|
||||
if punch_size == 0.0:
|
||||
self.app.inform.emit('[WARNING_NOTCL] %s' % _("The value of the fixed diameter is 0.0. Aborting."))
|
||||
return 'fail'
|
||||
|
||||
punching_geo = []
|
||||
for apid in grb_obj.apertures:
|
||||
if grb_obj.apertures[apid]['type'] == 'C' and self.circular_cb.get_value():
|
||||
if punch_size >= float(grb_obj.apertures[apid]['size']):
|
||||
self.app.inform.emit('[ERROR_NOTCL] %s' %
|
||||
_("Could not generate punched hole Gerber because the punch hole size"
|
||||
" is bigger than some of the apertures in the Gerber object."))
|
||||
return 'fail'
|
||||
else:
|
||||
for elem in grb_obj.apertures[apid]['geometry']:
|
||||
if 'follow' in elem:
|
||||
if isinstance(elem['follow'], Point):
|
||||
punching_geo.append(elem['follow'].buffer(punch_size / 2))
|
||||
elif grb_obj.apertures[apid]['type'] == 'R':
|
||||
if punch_size >= float(grb_obj.apertures[apid]['width']) or \
|
||||
punch_size >= float(grb_obj.apertures[apid]['height']):
|
||||
self.app.inform.emit('[ERROR_NOTCL] %s' %
|
||||
_("Could not generate punched hole Gerber because the punch hole size"
|
||||
" is bigger than some of the apertures in the Gerber object."))
|
||||
return 'fail'
|
||||
elif round(float(grb_obj.apertures[apid]['width']), self.decimals) == \
|
||||
round(float(grb_obj.apertures[apid]['height']), self.decimals) and \
|
||||
self.square_cb.get_value():
|
||||
for elem in grb_obj.apertures[apid]['geometry']:
|
||||
if 'follow' in elem:
|
||||
if isinstance(elem['follow'], Point):
|
||||
punching_geo.append(elem['follow'].buffer(punch_size / 2))
|
||||
elif round(float(grb_obj.apertures[apid]['width']), self.decimals) != \
|
||||
round(float(grb_obj.apertures[apid]['height']), self.decimals) and \
|
||||
self.rectangular_cb.get_value():
|
||||
for elem in grb_obj.apertures[apid]['geometry']:
|
||||
if 'follow' in elem:
|
||||
if isinstance(elem['follow'], Point):
|
||||
punching_geo.append(elem['follow'].buffer(punch_size / 2))
|
||||
elif grb_obj.apertures[apid]['type'] == 'O' and self.oblong_cb.get_value():
|
||||
for elem in grb_obj.apertures[apid]['geometry']:
|
||||
if 'follow' in elem:
|
||||
if isinstance(elem['follow'], Point):
|
||||
punching_geo.append(elem['follow'].buffer(punch_size / 2))
|
||||
elif grb_obj.apertures[apid]['type'] not in ['C', 'R', 'O'] and self.other_cb.get_value():
|
||||
for elem in grb_obj.apertures[apid]['geometry']:
|
||||
if 'follow' in elem:
|
||||
if isinstance(elem['follow'], Point):
|
||||
punching_geo.append(elem['follow'].buffer(punch_size / 2))
|
||||
|
||||
punching_geo = MultiPolygon(punching_geo)
|
||||
if isinstance(grb_obj.solid_geometry, list):
|
||||
temp_solid_geometry = MultiPolygon(grb_obj.solid_geometry)
|
||||
else:
|
||||
temp_solid_geometry = grb_obj.solid_geometry
|
||||
punched_solid_geometry = temp_solid_geometry.difference(punching_geo)
|
||||
|
||||
if punched_solid_geometry == temp_solid_geometry:
|
||||
self.app.inform.emit('[WARNING_NOTCL] %s' %
|
||||
_("Could not generate punched hole Gerber because the newly created object "
|
||||
"geometry is the same as the one in the source object geometry..."))
|
||||
return 'fail'
|
||||
|
||||
# update the gerber apertures to include the clear geometry so it can be exported successfully
|
||||
new_apertures = deepcopy(grb_obj.apertures)
|
||||
new_apertures_items = new_apertures.items()
|
||||
|
||||
# find maximum aperture id
|
||||
new_apid = max([int(x) for x, __ in new_apertures_items])
|
||||
|
||||
# store here the clear geometry, the key is the drill size
|
||||
holes_apertures = {}
|
||||
|
||||
for apid, val in new_apertures_items:
|
||||
for elem in val['geometry']:
|
||||
# make it work only for Gerber Flashes who are Points in 'follow'
|
||||
if 'solid' in elem and isinstance(elem['follow'], Point):
|
||||
for geo in punching_geo:
|
||||
clear_apid_size = punch_size
|
||||
|
||||
# since there may be drills that do not drill into a pad we test only for drills in a pad
|
||||
if geo.within(elem['solid']):
|
||||
geo_elem = {}
|
||||
geo_elem['clear'] = geo.centroid
|
||||
|
||||
if clear_apid_size not in holes_apertures:
|
||||
holes_apertures[clear_apid_size] = {}
|
||||
holes_apertures[clear_apid_size]['type'] = 'C'
|
||||
holes_apertures[clear_apid_size]['size'] = clear_apid_size
|
||||
holes_apertures[clear_apid_size]['geometry'] = []
|
||||
|
||||
holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem))
|
||||
|
||||
# add the clear geometry to new apertures; it's easier than to test if there are apertures with the same
|
||||
# size and add there the clear geometry
|
||||
for hole_size, ap_val in holes_apertures.items():
|
||||
new_apid += 1
|
||||
new_apertures[str(new_apid)] = deepcopy(ap_val)
|
||||
|
||||
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(punched_solid_geometry)
|
||||
new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None,
|
||||
local_use=new_obj, use_thread=False)
|
||||
|
||||
self.app.new_object('gerber', outname, init_func)
|
||||
elif punch_method == 'ring':
|
||||
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()
|
||||
|
||||
dia = None
|
||||
|
||||
if isinstance(grb_obj.solid_geometry, list):
|
||||
temp_solid_geometry = MultiPolygon(grb_obj.solid_geometry)
|
||||
else:
|
||||
temp_solid_geometry = grb_obj.solid_geometry
|
||||
|
||||
punched_solid_geometry = temp_solid_geometry
|
||||
|
||||
new_apertures = deepcopy(grb_obj.apertures)
|
||||
new_apertures_items = new_apertures.items()
|
||||
|
||||
# find maximum aperture id
|
||||
new_apid = max([int(x) for x, __ in new_apertures_items])
|
||||
|
||||
# store here the clear geometry, the key is the new aperture size
|
||||
holes_apertures = {}
|
||||
|
||||
for apid, apid_value in grb_obj.apertures.items():
|
||||
ap_type = apid_value['type']
|
||||
punching_geo = []
|
||||
|
||||
if ap_type == 'C' and self.circular_cb.get_value():
|
||||
dia = float(apid_value['size']) - (2 * circ_r_val)
|
||||
for elem in apid_value['geometry']:
|
||||
if 'follow' in elem and isinstance(elem['follow'], Point):
|
||||
punching_geo.append(elem['follow'].buffer(dia / 2))
|
||||
|
||||
elif ap_type == 'O' and self.oblong_cb.get_value():
|
||||
width = float(apid_value['width'])
|
||||
height = float(apid_value['height'])
|
||||
|
||||
if width > height:
|
||||
dia = float(apid_value['height']) - (2 * oblong_r_val)
|
||||
else:
|
||||
dia = float(apid_value['width']) - (2 * oblong_r_val)
|
||||
|
||||
for elem in grb_obj.apertures[apid]['geometry']:
|
||||
if 'follow' in elem:
|
||||
if isinstance(elem['follow'], Point):
|
||||
punching_geo.append(elem['follow'].buffer(dia / 2))
|
||||
|
||||
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():
|
||||
dia = float(apid_value['height']) - (2 * square_r_val)
|
||||
|
||||
for elem in grb_obj.apertures[apid]['geometry']:
|
||||
if 'follow' in elem:
|
||||
if isinstance(elem['follow'], Point):
|
||||
punching_geo.append(elem['follow'].buffer(dia / 2))
|
||||
elif 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)
|
||||
|
||||
for elem in grb_obj.apertures[apid]['geometry']:
|
||||
if 'follow' in elem:
|
||||
if isinstance(elem['follow'], Point):
|
||||
punching_geo.append(elem['follow'].buffer(dia / 2))
|
||||
|
||||
elif 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)
|
||||
|
||||
for elem in grb_obj.apertures[apid]['geometry']:
|
||||
if 'follow' in elem:
|
||||
if isinstance(elem['follow'], Point):
|
||||
punching_geo.append(elem['follow'].buffer(dia / 2))
|
||||
|
||||
# if dia is None then none of the above applied so we skip the following
|
||||
if dia is None:
|
||||
continue
|
||||
|
||||
punching_geo = MultiPolygon(punching_geo)
|
||||
|
||||
if punching_geo is None or punching_geo.is_empty:
|
||||
continue
|
||||
|
||||
punched_solid_geometry = punched_solid_geometry.difference(punching_geo)
|
||||
|
||||
# update the gerber apertures to include the clear geometry so it can be exported successfully
|
||||
for elem in apid_value['geometry']:
|
||||
# make it work only for Gerber Flashes who are Points in 'follow'
|
||||
if 'solid' in elem and isinstance(elem['follow'], Point):
|
||||
clear_apid_size = dia
|
||||
for geo in punching_geo:
|
||||
|
||||
# since there may be drills that do not drill into a pad we test only for geos in a pad
|
||||
if geo.within(elem['solid']):
|
||||
geo_elem = {}
|
||||
geo_elem['clear'] = geo.centroid
|
||||
|
||||
if clear_apid_size not in holes_apertures:
|
||||
holes_apertures[clear_apid_size] = {}
|
||||
holes_apertures[clear_apid_size]['type'] = 'C'
|
||||
holes_apertures[clear_apid_size]['size'] = clear_apid_size
|
||||
holes_apertures[clear_apid_size]['geometry'] = []
|
||||
|
||||
holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem))
|
||||
|
||||
# add the clear geometry to new apertures; it's easier than to test if there are apertures with the same
|
||||
# size and add there the clear geometry
|
||||
for hole_size, ap_val in holes_apertures.items():
|
||||
new_apid += 1
|
||||
new_apertures[str(new_apid)] = deepcopy(ap_val)
|
||||
|
||||
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(punched_solid_geometry)
|
||||
new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None,
|
||||
local_use=new_obj, use_thread=False)
|
||||
|
||||
self.app.new_object('gerber', outname, init_func)
|
||||
|
||||
elif punch_method == 'prop':
|
||||
prop_factor = self.factor_entry.get_value() / 100.0
|
||||
|
||||
dia = None
|
||||
|
||||
if isinstance(grb_obj.solid_geometry, list):
|
||||
temp_solid_geometry = MultiPolygon(grb_obj.solid_geometry)
|
||||
else:
|
||||
temp_solid_geometry = grb_obj.solid_geometry
|
||||
|
||||
punched_solid_geometry = temp_solid_geometry
|
||||
|
||||
new_apertures = deepcopy(grb_obj.apertures)
|
||||
new_apertures_items = new_apertures.items()
|
||||
|
||||
# find maximum aperture id
|
||||
new_apid = max([int(x) for x, __ in new_apertures_items])
|
||||
|
||||
# store here the clear geometry, the key is the new aperture size
|
||||
holes_apertures = {}
|
||||
|
||||
for apid, apid_value in grb_obj.apertures.items():
|
||||
ap_type = apid_value['type']
|
||||
punching_geo = []
|
||||
|
||||
if ap_type == 'C' and self.circular_cb.get_value():
|
||||
dia = float(apid_value['size']) * prop_factor
|
||||
for elem in apid_value['geometry']:
|
||||
if 'follow' in elem and isinstance(elem['follow'], Point):
|
||||
punching_geo.append(elem['follow'].buffer(dia / 2))
|
||||
|
||||
elif ap_type == 'O' and self.oblong_cb.get_value():
|
||||
width = float(apid_value['width'])
|
||||
height = float(apid_value['height'])
|
||||
|
||||
if width > height:
|
||||
dia = float(apid_value['height']) * prop_factor
|
||||
else:
|
||||
dia = float(apid_value['width']) * prop_factor
|
||||
|
||||
for elem in grb_obj.apertures[apid]['geometry']:
|
||||
if 'follow' in elem:
|
||||
if isinstance(elem['follow'], Point):
|
||||
punching_geo.append(elem['follow'].buffer(dia / 2))
|
||||
|
||||
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():
|
||||
dia = float(apid_value['height']) * prop_factor
|
||||
|
||||
for elem in grb_obj.apertures[apid]['geometry']:
|
||||
if 'follow' in elem:
|
||||
if isinstance(elem['follow'], Point):
|
||||
punching_geo.append(elem['follow'].buffer(dia / 2))
|
||||
elif self.rectangular_cb.get_value():
|
||||
if width > height:
|
||||
dia = float(apid_value['height']) * prop_factor
|
||||
else:
|
||||
dia = float(apid_value['width']) * prop_factor
|
||||
|
||||
for elem in grb_obj.apertures[apid]['geometry']:
|
||||
if 'follow' in elem:
|
||||
if isinstance(elem['follow'], Point):
|
||||
punching_geo.append(elem['follow'].buffer(dia / 2))
|
||||
|
||||
elif 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
|
||||
|
||||
for elem in grb_obj.apertures[apid]['geometry']:
|
||||
if 'follow' in elem:
|
||||
if isinstance(elem['follow'], Point):
|
||||
punching_geo.append(elem['follow'].buffer(dia / 2))
|
||||
|
||||
# if dia is None then none of the above applied so we skip the following
|
||||
if dia is None:
|
||||
continue
|
||||
|
||||
punching_geo = MultiPolygon(punching_geo)
|
||||
|
||||
if punching_geo is None or punching_geo.is_empty:
|
||||
continue
|
||||
|
||||
punched_solid_geometry = punched_solid_geometry.difference(punching_geo)
|
||||
|
||||
# update the gerber apertures to include the clear geometry so it can be exported successfully
|
||||
for elem in apid_value['geometry']:
|
||||
# make it work only for Gerber Flashes who are Points in 'follow'
|
||||
if 'solid' in elem and isinstance(elem['follow'], Point):
|
||||
clear_apid_size = dia
|
||||
for geo in punching_geo:
|
||||
|
||||
# since there may be drills that do not drill into a pad we test only for geos in a pad
|
||||
if geo.within(elem['solid']):
|
||||
geo_elem = {}
|
||||
geo_elem['clear'] = geo.centroid
|
||||
|
||||
if clear_apid_size not in holes_apertures:
|
||||
holes_apertures[clear_apid_size] = {}
|
||||
holes_apertures[clear_apid_size]['type'] = 'C'
|
||||
holes_apertures[clear_apid_size]['size'] = clear_apid_size
|
||||
holes_apertures[clear_apid_size]['geometry'] = []
|
||||
|
||||
holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem))
|
||||
|
||||
# add the clear geometry to new apertures; it's easier than to test if there are apertures with the same
|
||||
# size and add there the clear geometry
|
||||
for hole_size, ap_val in holes_apertures.items():
|
||||
new_apid += 1
|
||||
new_apertures[str(new_apid)] = deepcopy(ap_val)
|
||||
|
||||
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(punched_solid_geometry)
|
||||
new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None,
|
||||
local_use=new_obj, use_thread=False)
|
||||
|
||||
self.app.new_object('gerber', outname, init_func)
|
||||
|
||||
def reset_fields(self):
|
||||
self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
|
||||
self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
|
||||
self.ui_disconnect()
|
||||
@@ -1,888 +0,0 @@
|
||||
# ##########################################################
|
||||
# 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 FlatCAMTool import FlatCAMTool
|
||||
from flatcamGUI.GUIElements import RadioSet, FCTextArea, FCSpinner, FCEntry, FCCheckBox, FCComboBox, FCFileSaveDialog
|
||||
from flatcamParsers.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 FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
log = logging.getLogger('base')
|
||||
|
||||
|
||||
class QRCode(FlatCAMTool):
|
||||
|
||||
toolName = _("QRCode Tool")
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__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>" % _("Object"))
|
||||
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, 0, 1, 1, 2)
|
||||
i_grid_lay.addWidget(QtWidgets.QLabel(''), 1, 0)
|
||||
|
||||
# ## 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>' % _('QRCode 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)
|
||||
|
||||
# Text box
|
||||
self.text_label = QtWidgets.QLabel('%s:' % _("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...")
|
||||
)
|
||||
grid_lay.addWidget(self.text_label, 5, 0)
|
||||
grid_lay.addWidget(self.text_data, 6, 0, 1, 2)
|
||||
|
||||
# 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])
|
||||
|
||||
FlatCAMTool.run(self)
|
||||
|
||||
self.set_tool_ui()
|
||||
|
||||
self.app.ui.notebook.setTabText(2, _("QRCode Tool"))
|
||||
|
||||
def install(self, icon=None, separator=None, **kwargs):
|
||||
FlatCAMTool.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',
|
||||
filter=_filter)
|
||||
except TypeError:
|
||||
filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export PNG"), 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',
|
||||
filter=_filter)
|
||||
except TypeError:
|
||||
filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export SVG"), 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)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,519 +0,0 @@
|
||||
# ##########################################################
|
||||
# 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
|
||||
from PyQt5.QtWidgets import QVBoxLayout, QWidget
|
||||
from flatcamGUI.GUIElements import _BrowserTextEdit, _ExpandableTextEdit
|
||||
import html
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import tkinter as tk
|
||||
import tclCommands
|
||||
|
||||
import gettext
|
||||
import FlatCAMTranslation 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._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)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self._browser)
|
||||
layout.addWidget(self._edit)
|
||||
|
||||
self._history = [''] # current empty line
|
||||
self._historyIndex = 0
|
||||
|
||||
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', ' ')
|
||||
|
||||
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.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("Exec command Exception: %s" % (result + '\n'))
|
||||
if no_echo is False:
|
||||
self.append_error('ERROR: ' + 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")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,758 +0,0 @@
|
||||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Date: 4/24/2019 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from flatcamGUI.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 FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
log = logging.getLogger('base')
|
||||
|
||||
|
||||
class ToolSub(FlatCAMTool):
|
||||
|
||||
job_finished = QtCore.pyqtSignal(bool)
|
||||
|
||||
toolName = _("Subtract Tool")
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.decimals = self.app.decimals
|
||||
|
||||
FlatCAMTool.__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 Objects"))
|
||||
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 Objects"))
|
||||
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 = []
|
||||
|
||||
try:
|
||||
self.intersect_btn.clicked.disconnect(self.on_grb_intersection_click)
|
||||
except (TypeError, AttributeError):
|
||||
pass
|
||||
self.intersect_btn.clicked.connect(self.on_grb_intersection_click)
|
||||
|
||||
try:
|
||||
self.intersect_geo_btn.clicked.disconnect()
|
||||
except (TypeError, AttributeError):
|
||||
pass
|
||||
self.intersect_geo_btn.clicked.connect(self.on_geo_intersection_click)
|
||||
self.job_finished.connect(self.on_job_finished)
|
||||
self.reset_button.clicked.connect(self.set_tool_ui)
|
||||
|
||||
def install(self, icon=None, separator=None, **kwargs):
|
||||
FlatCAMTool.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])
|
||||
|
||||
FlatCAMTool.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] = {}
|
||||
self.new_apertures[apid]['type'] = 'C'
|
||||
self.new_apertures[apid]['size'] = self.target_grb_obj.apertures[apid]['size']
|
||||
self.new_apertures[apid]['geometry'] = []
|
||||
|
||||
geo_solid_union_list = []
|
||||
geo_follow_union_list = []
|
||||
geo_clear_union_list = []
|
||||
|
||||
for apid1 in self.sub_grb_obj.apertures:
|
||||
if 'geometry' in self.sub_grb_obj.apertures[apid1]:
|
||||
for elem in self.sub_grb_obj.apertures[apid1]['geometry']:
|
||||
if 'solid' in elem:
|
||||
geo_solid_union_list.append(elem['solid'])
|
||||
if 'follow' in elem:
|
||||
geo_follow_union_list.append(elem['follow'])
|
||||
if 'clear' in elem:
|
||||
geo_clear_union_list.append(elem['clear'])
|
||||
|
||||
self.app.inform.emit('%s' % _("Processing geometry from Subtractor Gerber object."))
|
||||
self.sub_solid_union = cascaded_union(geo_solid_union_list)
|
||||
self.sub_follow_union = cascaded_union(geo_follow_union_list)
|
||||
self.sub_clear_union = cascaded_union(geo_clear_union_list)
|
||||
|
||||
# add the promises
|
||||
for apid in self.target_grb_obj.apertures:
|
||||
self.promises.append(apid)
|
||||
|
||||
# start the QTimer to check for promises with 0.5 second period check
|
||||
self.periodic_check(500, reset=True)
|
||||
|
||||
for apid in self.target_grb_obj.apertures:
|
||||
geo = self.target_grb_obj.apertures[apid]['geometry']
|
||||
self.app.worker_task.emit({'fcn': self.aperture_intersection, 'params': [apid, geo]})
|
||||
|
||||
def aperture_intersection(self, apid, geo):
|
||||
new_geometry = []
|
||||
|
||||
log.debug("Working on promise: %s" % str(apid))
|
||||
|
||||
with self.app.proc_container.new('%s: %s...' % (_("Parsing geometry for aperture"), str(apid))):
|
||||
|
||||
for geo_el in geo:
|
||||
new_el = {}
|
||||
|
||||
if 'solid' in geo_el:
|
||||
work_geo = geo_el['solid']
|
||||
if self.sub_solid_union:
|
||||
if work_geo.intersects(self.sub_solid_union):
|
||||
new_geo = work_geo.difference(self.sub_solid_union)
|
||||
new_geo = new_geo.buffer(0)
|
||||
if new_geo:
|
||||
if not new_geo.is_empty:
|
||||
new_el['solid'] = new_geo
|
||||
else:
|
||||
new_el['solid'] = work_geo
|
||||
else:
|
||||
new_el['solid'] = work_geo
|
||||
else:
|
||||
new_el['solid'] = work_geo
|
||||
else:
|
||||
new_el['solid'] = work_geo
|
||||
|
||||
if 'follow' in geo_el:
|
||||
work_geo = geo_el['follow']
|
||||
if self.sub_follow_union:
|
||||
if work_geo.intersects(self.sub_follow_union):
|
||||
new_geo = work_geo.difference(self.sub_follow_union)
|
||||
new_geo = new_geo.buffer(0)
|
||||
if new_geo:
|
||||
if not new_geo.is_empty:
|
||||
new_el['follow'] = new_geo
|
||||
else:
|
||||
new_el['follow'] = work_geo
|
||||
else:
|
||||
new_el['follow'] = work_geo
|
||||
else:
|
||||
new_el['follow'] = work_geo
|
||||
else:
|
||||
new_el['follow'] = work_geo
|
||||
|
||||
if 'clear' in geo_el:
|
||||
work_geo = geo_el['clear']
|
||||
if self.sub_clear_union:
|
||||
if work_geo.intersects(self.sub_clear_union):
|
||||
new_geo = work_geo.difference(self.sub_clear_union)
|
||||
new_geo = new_geo.buffer(0)
|
||||
if new_geo:
|
||||
if not new_geo.is_empty:
|
||||
new_el['clear'] = new_geo
|
||||
else:
|
||||
new_el['clear'] = work_geo
|
||||
else:
|
||||
new_el['clear'] = work_geo
|
||||
else:
|
||||
new_el['clear'] = work_geo
|
||||
else:
|
||||
new_el['clear'] = work_geo
|
||||
|
||||
new_geometry.append(deepcopy(new_el))
|
||||
|
||||
self.app.inform.emit('%s: %s...' % (_("Finished parsing geometry for aperture"), str(apid)))
|
||||
|
||||
if new_geometry:
|
||||
while not self.new_apertures[apid]['geometry']:
|
||||
self.new_apertures[apid]['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 apid not in self.promises:
|
||||
break
|
||||
|
||||
self.promises.remove(apid)
|
||||
time.sleep(0.5)
|
||||
|
||||
log.debug("Promise fulfilled: %s" % str(apid))
|
||||
|
||||
def new_gerber_object(self, outname):
|
||||
|
||||
def obj_init(grb_obj, app_obj):
|
||||
|
||||
grb_obj.apertures = deepcopy(self.new_apertures)
|
||||
|
||||
poly_buff = []
|
||||
follow_buff = []
|
||||
for ap in self.new_apertures:
|
||||
for elem in self.new_apertures[ap]['geometry']:
|
||||
poly_buff.append(elem['solid'])
|
||||
follow_buff.append(elem['follow'])
|
||||
|
||||
work_poly_buff = cascaded_union(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)
|
||||
|
||||
with self.app.proc_container.new(_("Generating new object ...")):
|
||||
ret = self.app.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[:] = []
|
||||
try:
|
||||
self.sub_union[:] = []
|
||||
except TypeError:
|
||||
self.sub_union = []
|
||||
|
||||
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.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
|
||||
@@ -1,956 +0,0 @@
|
||||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Date: 3/10/2019 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
from FlatCAMTool import FlatCAMTool
|
||||
from flatcamGUI.GUIElements import FCDoubleSpinner, FCCheckBox, FCButton, OptionalInputSection, FCEntry
|
||||
|
||||
import gettext
|
||||
import FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
|
||||
class ToolTransform(FlatCAMTool):
|
||||
|
||||
toolName = _("Object Transform")
|
||||
rotateName = _("Rotate")
|
||||
skewName = _("Skew/Shear")
|
||||
scaleName = _("Scale")
|
||||
flipName = _("Mirror (Flip)")
|
||||
offsetName = _("Offset")
|
||||
bufferName = _("Buffer")
|
||||
|
||||
def __init__(self, app):
|
||||
FlatCAMTool.__init__(self, app)
|
||||
self.decimals = self.app.decimals
|
||||
|
||||
# ## Title
|
||||
title_label = QtWidgets.QLabel("%s" % self.toolName)
|
||||
title_label.setStyleSheet("""
|
||||
QLabel
|
||||
{
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
self.layout.addWidget(title_label)
|
||||
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(''))
|
||||
|
||||
# ## Rotate Title
|
||||
rotate_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.rotateName)
|
||||
grid0.addWidget(rotate_title_label, 0, 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()
|
||||
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, 1, 0)
|
||||
grid0.addWidget(self.rotate_entry, 1, 1)
|
||||
grid0.addWidget(self.rotate_button, 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, 3)
|
||||
|
||||
# ## Skew Title
|
||||
skew_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.skewName)
|
||||
grid0.addWidget(skew_title_label, 3, 0, 1, 3)
|
||||
|
||||
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()
|
||||
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, 4, 0)
|
||||
grid0.addWidget(self.skewx_entry, 4, 1)
|
||||
grid0.addWidget(self.skewx_button, 4, 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()
|
||||
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, 5, 0)
|
||||
grid0.addWidget(self.skewy_entry, 5, 1)
|
||||
grid0.addWidget(self.skewy_button, 5, 2)
|
||||
|
||||
separator_line = QtWidgets.QFrame()
|
||||
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
grid0.addWidget(separator_line, 6, 0, 1, 3)
|
||||
|
||||
# ## Scale Title
|
||||
scale_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.scaleName)
|
||||
grid0.addWidget(scale_title_label, 7, 0, 1, 3)
|
||||
|
||||
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()
|
||||
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, 8, 0)
|
||||
grid0.addWidget(self.scalex_entry, 8, 1)
|
||||
grid0.addWidget(self.scalex_button, 8, 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()
|
||||
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, 9, 0)
|
||||
grid0.addWidget(self.scaley_entry, 9, 1)
|
||||
grid0.addWidget(self.scaley_button, 9, 2)
|
||||
|
||||
self.scale_link_cb = FCCheckBox()
|
||||
self.scale_link_cb.setText(_("Link"))
|
||||
self.scale_link_cb.setToolTip(
|
||||
_("Scale the selected object(s)\n"
|
||||
"using the Scale_X factor for both axis.")
|
||||
)
|
||||
|
||||
self.scale_zero_ref_cb = FCCheckBox()
|
||||
self.scale_zero_ref_cb.setText('%s' % _("Scale Reference"))
|
||||
self.scale_zero_ref_cb.setToolTip(
|
||||
_("Scale the selected object(s)\n"
|
||||
"using the origin reference when checked,\n"
|
||||
"and the center of the biggest bounding box\n"
|
||||
"of the selected objects when unchecked."))
|
||||
|
||||
self.ois_scale = OptionalInputSection(self.scale_link_cb, [self.scaley_entry, self.scaley_button], logic=False)
|
||||
|
||||
grid0.addWidget(self.scale_link_cb, 10, 0)
|
||||
grid0.addWidget(self.scale_zero_ref_cb, 10, 1, 1, 2)
|
||||
|
||||
separator_line = QtWidgets.QFrame()
|
||||
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
grid0.addWidget(separator_line, 11, 0, 1, 3)
|
||||
|
||||
# ## Offset Title
|
||||
offset_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.offsetName)
|
||||
grid0.addWidget(offset_title_label, 12, 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()
|
||||
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, 13, 0)
|
||||
grid0.addWidget(self.offx_entry, 13, 1)
|
||||
grid0.addWidget(self.offx_button, 13, 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()
|
||||
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, 14, 0)
|
||||
grid0.addWidget(self.offy_entry, 14, 1)
|
||||
grid0.addWidget(self.offy_button, 14, 2)
|
||||
|
||||
separator_line = QtWidgets.QFrame()
|
||||
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
grid0.addWidget(separator_line, 15, 0, 1, 3)
|
||||
|
||||
# ## Flip Title
|
||||
flip_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.flipName)
|
||||
grid0.addWidget(flip_title_label, 16, 0, 1, 3)
|
||||
|
||||
self.flipx_button = FCButton()
|
||||
self.flipx_button.setToolTip(
|
||||
_("Flip the selected object(s) over the X axis.")
|
||||
)
|
||||
|
||||
self.flipy_button = FCButton()
|
||||
self.flipy_button.setToolTip(
|
||||
_("Flip the selected object(s) over the X axis.")
|
||||
)
|
||||
|
||||
hlay0 = QtWidgets.QHBoxLayout()
|
||||
grid0.addLayout(hlay0, 17, 0, 1, 3)
|
||||
|
||||
hlay0.addWidget(self.flipx_button)
|
||||
hlay0.addWidget(self.flipy_button)
|
||||
|
||||
self.flip_ref_cb = FCCheckBox()
|
||||
self.flip_ref_cb.setText('%s' % _("Mirror Reference"))
|
||||
self.flip_ref_cb.setToolTip(
|
||||
_("Flip the selected object(s)\n"
|
||||
"around the point in Point Entry Field.\n"
|
||||
"\n"
|
||||
"The point coordinates can be captured by\n"
|
||||
"left click on canvas together with pressing\n"
|
||||
"SHIFT key. \n"
|
||||
"Then click Add button to insert coordinates.\n"
|
||||
"Or enter the coords in format (x, y) in the\n"
|
||||
"Point Entry field and click Flip on X(Y)"))
|
||||
|
||||
grid0.addWidget(self.flip_ref_cb, 18, 0, 1, 3)
|
||||
|
||||
self.flip_ref_label = QtWidgets.QLabel('%s:' % _("Ref. Point"))
|
||||
self.flip_ref_label.setToolTip(
|
||||
_("Coordinates in format (x, y) used as reference for mirroring.\n"
|
||||
"The 'x' in (x, y) will be used when using Flip on X and\n"
|
||||
"the 'y' in (x, y) will be used when using Flip on Y.")
|
||||
)
|
||||
self.flip_ref_entry = FCEntry()
|
||||
# self.flip_ref_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
# self.flip_ref_entry.setFixedWidth(70)
|
||||
|
||||
self.flip_ref_button = FCButton()
|
||||
self.flip_ref_button.setToolTip(
|
||||
_("The point coordinates can be captured by\n"
|
||||
"left click on canvas together with pressing\n"
|
||||
"SHIFT key. Then click Add button to insert."))
|
||||
|
||||
self.ois_flip = OptionalInputSection(self.flip_ref_cb, [self.flip_ref_entry, self.flip_ref_button], logic=True)
|
||||
|
||||
hlay1 = QtWidgets.QHBoxLayout()
|
||||
grid0.addLayout(hlay1, 19, 0, 1, 3)
|
||||
|
||||
hlay1.addWidget(self.flip_ref_label)
|
||||
hlay1.addWidget(self.flip_ref_entry)
|
||||
|
||||
grid0.addWidget(self.flip_ref_button, 20, 0, 1, 3)
|
||||
|
||||
separator_line = QtWidgets.QFrame()
|
||||
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
grid0.addWidget(separator_line, 21, 0, 1, 3)
|
||||
|
||||
# ## Buffer Title
|
||||
buffer_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.bufferName)
|
||||
grid0.addWidget(buffer_title_label, 22, 0, 1, 3)
|
||||
|
||||
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()
|
||||
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, 23, 0)
|
||||
grid0.addWidget(self.buffer_entry, 23, 1)
|
||||
grid0.addWidget(self.buffer_button, 23, 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()
|
||||
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, 24, 0)
|
||||
grid0.addWidget(self.buffer_factor_entry, 24, 1)
|
||||
grid0.addWidget(self.buffer_factor_button, 24, 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, 25, 0, 1, 3)
|
||||
|
||||
grid0.addWidget(QtWidgets.QLabel(''), 26, 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.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.flip_ref_button.clicked.connect(self.on_flip_add_coords)
|
||||
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)
|
||||
|
||||
# self.rotate_entry.returnPressed.connect(self.on_rotate)
|
||||
# self.skewx_entry.returnPressed.connect(self.on_skewx)
|
||||
# self.skewy_entry.returnPressed.connect(self.on_skewy)
|
||||
# self.scalex_entry.returnPressed.connect(self.on_scalex)
|
||||
# self.scaley_entry.returnPressed.connect(self.on_scaley)
|
||||
# self.offx_entry.returnPressed.connect(self.on_offx)
|
||||
# self.offy_entry.returnPressed.connect(self.on_offy)
|
||||
# self.buffer_entry.returnPressed.connect(self.on_buffer_by_distance)
|
||||
|
||||
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])
|
||||
|
||||
FlatCAMTool.run(self)
|
||||
self.set_tool_ui()
|
||||
|
||||
self.app.ui.notebook.setTabText(2, _("Transform Tool"))
|
||||
|
||||
def install(self, icon=None, separator=None, **kwargs):
|
||||
FlatCAMTool.install(self, icon, separator, shortcut='Alt+T', **kwargs)
|
||||
|
||||
def set_tool_ui(self):
|
||||
self.rotate_button.set_value(_("Rotate"))
|
||||
self.skewx_button.set_value(_("Skew X"))
|
||||
self.skewy_button.set_value(_("Skew Y"))
|
||||
self.scalex_button.set_value(_("Scale X"))
|
||||
self.scaley_button.set_value(_("Scale Y"))
|
||||
self.scale_link_cb.set_value(True)
|
||||
self.scale_zero_ref_cb.set_value(True)
|
||||
self.offx_button.set_value(_("Offset X"))
|
||||
self.offy_button.set_value(_("Offset Y"))
|
||||
self.flipx_button.set_value(_("Flip on X"))
|
||||
self.flipy_button.set_value(_("Flip on Y"))
|
||||
self.flip_ref_cb.set_value(True)
|
||||
self.flip_ref_button.set_value(_("Add"))
|
||||
self.buffer_button.set_value(_("Buffer D"))
|
||||
self.buffer_factor_button.set_value(_("Buffer F"))
|
||||
|
||||
# ## Initialize form
|
||||
if self.app.defaults["tools_transform_rotate"]:
|
||||
self.rotate_entry.set_value(self.app.defaults["tools_transform_rotate"])
|
||||
else:
|
||||
self.rotate_entry.set_value(0.0)
|
||||
|
||||
if self.app.defaults["tools_transform_skew_x"]:
|
||||
self.skewx_entry.set_value(self.app.defaults["tools_transform_skew_x"])
|
||||
else:
|
||||
self.skewx_entry.set_value(0.0)
|
||||
|
||||
if self.app.defaults["tools_transform_skew_y"]:
|
||||
self.skewy_entry.set_value(self.app.defaults["tools_transform_skew_y"])
|
||||
else:
|
||||
self.skewy_entry.set_value(0.0)
|
||||
|
||||
if self.app.defaults["tools_transform_scale_x"]:
|
||||
self.scalex_entry.set_value(self.app.defaults["tools_transform_scale_x"])
|
||||
else:
|
||||
self.scalex_entry.set_value(1.0)
|
||||
|
||||
if self.app.defaults["tools_transform_scale_y"]:
|
||||
self.scaley_entry.set_value(self.app.defaults["tools_transform_scale_y"])
|
||||
else:
|
||||
self.scaley_entry.set_value(1.0)
|
||||
|
||||
if self.app.defaults["tools_transform_scale_link"]:
|
||||
self.scale_link_cb.set_value(self.app.defaults["tools_transform_scale_link"])
|
||||
else:
|
||||
self.scale_link_cb.set_value(True)
|
||||
|
||||
if self.app.defaults["tools_transform_scale_reference"]:
|
||||
self.scale_zero_ref_cb.set_value(self.app.defaults["tools_transform_scale_reference"])
|
||||
else:
|
||||
self.scale_zero_ref_cb.set_value(True)
|
||||
|
||||
if self.app.defaults["tools_transform_offset_x"]:
|
||||
self.offx_entry.set_value(self.app.defaults["tools_transform_offset_x"])
|
||||
else:
|
||||
self.offx_entry.set_value(0.0)
|
||||
|
||||
if self.app.defaults["tools_transform_offset_y"]:
|
||||
self.offy_entry.set_value(self.app.defaults["tools_transform_offset_y"])
|
||||
else:
|
||||
self.offy_entry.set_value(0.0)
|
||||
|
||||
if self.app.defaults["tools_transform_mirror_reference"]:
|
||||
self.flip_ref_cb.set_value(self.app.defaults["tools_transform_mirror_reference"])
|
||||
else:
|
||||
self.flip_ref_cb.set_value(False)
|
||||
|
||||
if self.app.defaults["tools_transform_mirror_point"]:
|
||||
self.flip_ref_entry.set_value(self.app.defaults["tools_transform_mirror_point"])
|
||||
else:
|
||||
self.flip_ref_entry.set_value("0, 0")
|
||||
|
||||
if self.app.defaults["tools_transform_buffer_dis"]:
|
||||
self.buffer_entry.set_value(self.app.defaults["tools_transform_buffer_dis"])
|
||||
else:
|
||||
self.buffer_entry.set_value(0.0)
|
||||
|
||||
if self.app.defaults["tools_transform_buffer_factor"]:
|
||||
self.buffer_factor_entry.set_value(self.app.defaults["tools_transform_buffer_factor"])
|
||||
else:
|
||||
self.buffer_factor_entry.set_value(100.0)
|
||||
|
||||
if self.app.defaults["tools_transform_buffer_corner"]:
|
||||
self.buffer_rounded_cb.set_value(self.app.defaults["tools_transform_buffer_corner"])
|
||||
else:
|
||||
self.buffer_rounded_cb.set_value(True)
|
||||
|
||||
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."))
|
||||
self.app.worker_task.emit({'fcn': self.on_rotate_action, 'params': [value]})
|
||||
return
|
||||
|
||||
def on_flipx(self):
|
||||
axis = 'Y'
|
||||
|
||||
self.app.worker_task.emit({'fcn': self.on_flip, 'params': [axis]})
|
||||
return
|
||||
|
||||
def on_flipy(self):
|
||||
axis = 'X'
|
||||
|
||||
self.app.worker_task.emit({'fcn': self.on_flip, 'params': [axis]})
|
||||
return
|
||||
|
||||
def on_flip_add_coords(self):
|
||||
val = self.app.clipboard.text()
|
||||
self.flip_ref_entry.set_value(val)
|
||||
|
||||
def on_skewx(self):
|
||||
value = float(self.skewx_entry.get_value())
|
||||
axis = 'X'
|
||||
|
||||
self.app.worker_task.emit({'fcn': self.on_skew, 'params': [axis, value]})
|
||||
return
|
||||
|
||||
def on_skewy(self):
|
||||
value = float(self.skewy_entry.get_value())
|
||||
axis = 'Y'
|
||||
|
||||
self.app.worker_task.emit({'fcn': self.on_skew, 'params': [axis, value]})
|
||||
return
|
||||
|
||||
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 = (0, 0)
|
||||
if self.scale_zero_ref_cb.get_value():
|
||||
self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue, point]})
|
||||
else:
|
||||
self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue]})
|
||||
|
||||
return
|
||||
|
||||
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 = (0, 0)
|
||||
if self.scale_zero_ref_cb.get_value():
|
||||
self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue, point]})
|
||||
else:
|
||||
self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue]})
|
||||
|
||||
return
|
||||
|
||||
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]})
|
||||
return
|
||||
|
||||
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]})
|
||||
return
|
||||
|
||||
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]})
|
||||
return
|
||||
|
||||
def on_buffer_by_factor(self):
|
||||
value = 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]})
|
||||
return
|
||||
|
||||
def on_rotate_action(self, num):
|
||||
obj_list = self.app.collection.get_selected()
|
||||
xminlist = []
|
||||
yminlist = []
|
||||
xmaxlist = []
|
||||
ymaxlist = []
|
||||
|
||||
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:
|
||||
# first get a bounding box to fit all
|
||||
for obj in obj_list:
|
||||
if obj.kind == 'cncjob':
|
||||
pass
|
||||
else:
|
||||
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)
|
||||
|
||||
px = 0.5 * (xminimal + xmaximal)
|
||||
py = 0.5 * (yminimal + ymaximal)
|
||||
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.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):
|
||||
obj_list = self.app.collection.get_selected()
|
||||
xminlist = []
|
||||
yminlist = []
|
||||
xmaxlist = []
|
||||
ymaxlist = []
|
||||
|
||||
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:
|
||||
# get mirroring coords from the point entry
|
||||
if self.flip_ref_cb.isChecked():
|
||||
px, py = eval('{}'.format(self.flip_ref_entry.text()))
|
||||
# get mirroing coords from the center of an all-enclosing bounding box
|
||||
else:
|
||||
# first get a bounding box to fit all
|
||||
for obj in obj_list:
|
||||
if obj.kind == 'cncjob':
|
||||
pass
|
||||
else:
|
||||
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)
|
||||
|
||||
px = 0.5 * (xminimal + xmaximal)
|
||||
py = 0.5 * (yminimal + ymaximal)
|
||||
|
||||
# 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.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, num):
|
||||
obj_list = self.app.collection.get_selected()
|
||||
xminlist = []
|
||||
yminlist = []
|
||||
|
||||
if num == 0 or num == 90 or num == 180:
|
||||
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:
|
||||
# first get a bounding box to fit all
|
||||
for obj in obj_list:
|
||||
if obj.kind == 'cncjob':
|
||||
pass
|
||||
else:
|
||||
xmin, ymin, xmax, ymax = obj.bounds()
|
||||
xminlist.append(xmin)
|
||||
yminlist.append(ymin)
|
||||
|
||||
# get the minimum x,y and maximum x,y for all objects selected
|
||||
xminimal = min(xminlist)
|
||||
yminimal = min(yminlist)
|
||||
|
||||
for sel_obj in obj_list:
|
||||
if sel_obj.kind == 'cncjob':
|
||||
self.app.inform.emit(_("CNCJob objects can't be skewed."))
|
||||
else:
|
||||
if axis == 'X':
|
||||
sel_obj.skew(num, 0, point=(xminimal, yminimal))
|
||||
# add information to the object that it was changed and how much
|
||||
sel_obj.options['skew_x'] = num
|
||||
elif axis == 'Y':
|
||||
sel_obj.skew(0, num, point=(xminimal, yminimal))
|
||||
# add information to the object that it was changed and how much
|
||||
sel_obj.options['skew_y'] = num
|
||||
self.app.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()
|
||||
xminlist = []
|
||||
yminlist = []
|
||||
xmaxlist = []
|
||||
ymaxlist = []
|
||||
|
||||
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:
|
||||
# first get a bounding box to fit all
|
||||
for obj in obj_list:
|
||||
if obj.kind == 'cncjob':
|
||||
pass
|
||||
else:
|
||||
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)
|
||||
|
||||
if point is None:
|
||||
px = 0.5 * (xminimal + xmaximal)
|
||||
py = 0.5 * (yminimal + ymaximal)
|
||||
else:
|
||||
px = 0
|
||||
py = 0
|
||||
|
||||
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.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.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, %s.' %
|
||||
(_("Due of"), str(e), _("action was not executed.")))
|
||||
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.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, %s.' %
|
||||
(_("Due of"), str(e), _("action was not executed.")))
|
||||
return
|
||||
|
||||
# end of file
|
||||
@@ -1,43 +0,0 @@
|
||||
|
||||
from flatcamTools.ToolCalculators import ToolCalculator
|
||||
from flatcamTools.ToolCalibration import ToolCalibration
|
||||
from flatcamTools.ToolCutOut import CutOut
|
||||
|
||||
from flatcamTools.ToolDblSided import DblSidedTool
|
||||
from flatcamTools.ToolExtractDrills import ToolExtractDrills
|
||||
from flatcamTools.ToolAlignObjects import AlignObjects
|
||||
|
||||
from flatcamTools.ToolFilm import Film
|
||||
|
||||
from flatcamTools.ToolImage import ToolImage
|
||||
|
||||
from flatcamTools.ToolDistance import Distance
|
||||
from flatcamTools.ToolDistanceMin import DistanceMin
|
||||
|
||||
from flatcamTools.ToolMove import ToolMove
|
||||
|
||||
from flatcamTools.ToolNCC import NonCopperClear
|
||||
from flatcamTools.ToolPaint import ToolPaint
|
||||
|
||||
from flatcamTools.ToolOptimal import ToolOptimal
|
||||
|
||||
from flatcamTools.ToolPanelize import Panelize
|
||||
from flatcamTools.ToolPcbWizard import PcbWizard
|
||||
from flatcamTools.ToolPDF import ToolPDF
|
||||
from flatcamTools.ToolProperties import Properties
|
||||
|
||||
from flatcamTools.ToolQRCode import QRCode
|
||||
from flatcamTools.ToolRulesCheck import RulesCheck
|
||||
|
||||
from flatcamTools.ToolCopperThieving import ToolCopperThieving
|
||||
from flatcamTools.ToolFiducials import ToolFiducials
|
||||
|
||||
from flatcamTools.ToolShell import FCShell
|
||||
from flatcamTools.ToolSolderPaste import SolderPaste
|
||||
from flatcamTools.ToolSub import ToolSub
|
||||
|
||||
from flatcamTools.ToolTransform import ToolTransform
|
||||
from flatcamTools.ToolPunchGerber import ToolPunchGerber
|
||||
|
||||
from flatcamTools.ToolInvertGerber import ToolInvertGerber
|
||||
from flatcamTools.ToolCorners import ToolCorners
|
||||
Reference in New Issue
Block a user