diff --git a/FlatCAMApp.py b/FlatCAMApp.py
index 36ee019d..dce2111f 100644
--- a/FlatCAMApp.py
+++ b/FlatCAMApp.py
@@ -141,7 +141,7 @@ class App(QtCore.QObject):
# ################## Version and VERSION DATE ##############################
# ##########################################################################
version = 8.992
- version_date = "2020/01/02"
+ version_date = "2020/01/20"
beta = True
engine = '3D'
@@ -240,6 +240,9 @@ class App(QtCore.QObject):
# signal emitted when jumping
jump_signal = pyqtSignal(tuple)
+ # signal emitted when jumping
+ locate_signal = pyqtSignal(tuple, str)
+
# close app signal
close_app_signal = pyqtSignal()
@@ -429,6 +432,7 @@ class App(QtCore.QObject):
"global_stats": dict(),
"global_tabs_detachable": True,
"global_jump_ref": 'abs',
+ "global_locate_pt": 'bl',
"global_tpdf_tmargin": 15.0,
"global_tpdf_bmargin": 10.0,
"global_tpdf_lmargin": 20.0,
@@ -524,8 +528,8 @@ class App(QtCore.QObject):
"global_cursor_type": "small",
"global_cursor_size": 20,
"global_cursor_width": 2,
- "global_cursor_color": '#000000',
- "global_cursor_color_enabled": False,
+ "global_cursor_color": '#FF0000',
+ "global_cursor_color_enabled": True,
# Gerber General
"gerber_plot": True,
@@ -954,6 +958,24 @@ class App(QtCore.QObject):
"tools_cal_toolchange_xy": '',
"tools_cal_sec_point": 'tl',
+ # Drills Extraction Tool
+ "tools_edrills_hole_type": 'fixed',
+ "tools_edrills_hole_fixed_dia": 0.5,
+ "tools_edrills_hole_prop_factor": 80.0,
+ "tools_edrills_circular_ring": 0.2,
+ "tools_edrills_oblong_ring": 0.2,
+ "tools_edrills_square_ring": 0.2,
+ "tools_edrills_rectangular_ring": 0.2,
+ "tools_edrills_others_ring": 0.2,
+ "tools_edrills_circular": True,
+ "tools_edrills_oblong": False,
+ "tools_edrills_square": False,
+ "tools_edrills_rectangular": False,
+ "tools_edrills_others": False,
+
+ # Align Objects Tool
+ "tools_align_objects_align_type": 'sp',
+
# Utilities
# file associations
"fa_excellon": 'drd, drl, exc, ncd, tap, xln',
@@ -1578,6 +1600,21 @@ class App(QtCore.QObject):
"tools_cal_toolchange_xy": self.ui.tools2_defaults_form.tools2_cal_group.toolchange_xy_entry,
"tools_cal_sec_point": self.ui.tools2_defaults_form.tools2_cal_group.second_point_radio,
+ # Extract Drills Tool
+ "tools_edrills_hole_type": self.ui.tools2_defaults_form.tools2_edrills_group.hole_size_radio,
+ "tools_edrills_hole_fixed_dia": self.ui.tools2_defaults_form.tools2_edrills_group.dia_entry,
+ "tools_edrills_hole_prop_factor": self.ui.tools2_defaults_form.tools2_edrills_group.factor_entry,
+ "tools_edrills_circular_ring": self.ui.tools2_defaults_form.tools2_edrills_group.circular_ring_entry,
+ "tools_edrills_oblong_ring": self.ui.tools2_defaults_form.tools2_edrills_group.oblong_ring_entry,
+ "tools_edrills_square_ring": self.ui.tools2_defaults_form.tools2_edrills_group.square_ring_entry,
+ "tools_edrills_rectangular_ring": self.ui.tools2_defaults_form.tools2_edrills_group.rectangular_ring_entry,
+ "tools_edrills_others_ring": self.ui.tools2_defaults_form.tools2_edrills_group.other_ring_entry,
+ "tools_edrills_circular": self.ui.tools2_defaults_form.tools2_edrills_group.circular_cb,
+ "tools_edrills_oblong": self.ui.tools2_defaults_form.tools2_edrills_group.oblong_cb,
+ "tools_edrills_square": self.ui.tools2_defaults_form.tools2_edrills_group.square_cb,
+ "tools_edrills_rectangular": self.ui.tools2_defaults_form.tools2_edrills_group.rectangular_cb,
+ "tools_edrills_others": self.ui.tools2_defaults_form.tools2_edrills_group.other_cb,
+
# Utilities
# File associations
"fa_excellon": self.ui.util_defaults_form.fa_excellon_group.exc_list_text,
@@ -1923,6 +1960,7 @@ class App(QtCore.QObject):
self.ui.menueditorigin.triggered.connect(self.on_set_origin)
self.ui.menueditjump.triggered.connect(self.on_jump_to)
+ self.ui.menueditlocate.triggered.connect(lambda: self.on_locate(obj=self.collection.get_active()))
self.ui.menuedittoggleunits.triggered.connect(self.on_toggle_units_click)
self.ui.menueditselectall.triggered.connect(self.on_selectall)
@@ -2464,12 +2502,14 @@ class App(QtCore.QObject):
self.qrcode_tool = None
self.copper_thieving_tool = None
self.fiducial_tool = None
+ self.edrills_tool = None
+ self.align_objects_tool = None
# always install tools only after the shell is initialized because the self.inform.emit() depends on shell
try:
self.install_tools()
- except AttributeError:
- pass
+ except AttributeError as e:
+ log.debug("App.__init__() install tools() --> %s" % str(e))
# ##################################################################################
# ########################### SETUP RECENT ITEMS ###################################
@@ -3017,13 +3057,6 @@ class App(QtCore.QObject):
:return: None
"""
- self.dblsidedtool = DblSidedTool(self)
- self.dblsidedtool.install(icon=QtGui.QIcon(self.resource_location + '/doubleside16.png'), separator=True)
-
- self.cal_exc_tool = ToolCalibration(self)
- self.cal_exc_tool.install(icon=QtGui.QIcon(self.resource_location + '/calibrate_16.png'), pos=self.ui.menutool,
- before=self.dblsidedtool.menuAction,
- separator=False)
self.distance_tool = Distance(self)
self.distance_tool.install(icon=QtGui.QIcon(self.resource_location + '/distance16.png'), pos=self.ui.menuedit,
before=self.ui.menueditorigin,
@@ -3035,6 +3068,20 @@ class App(QtCore.QObject):
before=self.ui.menueditorigin,
separator=True)
+ self.dblsidedtool = DblSidedTool(self)
+ self.dblsidedtool.install(icon=QtGui.QIcon(self.resource_location + '/doubleside16.png'), separator=False)
+
+ self.cal_exc_tool = ToolCalibration(self)
+ self.cal_exc_tool.install(icon=QtGui.QIcon(self.resource_location + '/calibrate_16.png'), pos=self.ui.menutool,
+ before=self.dblsidedtool.menuAction,
+ separator=False)
+
+ self.align_objects_tool = AlignObjects(self)
+ self.align_objects_tool.install(icon=QtGui.QIcon(self.resource_location + '/align16.png'), separator=False)
+
+ self.edrills_tool = ToolExtractDrills(self)
+ self.edrills_tool.install(icon=QtGui.QIcon(self.resource_location + '/drill16.png'), separator=True)
+
self.panelize_tool = Panelize(self)
self.panelize_tool.install(icon=QtGui.QIcon(self.resource_location + '/panelize16.png'))
@@ -3199,6 +3246,7 @@ class App(QtCore.QObject):
self.ui.distance_min_btn.triggered.connect(lambda: self.distance_min_tool.run(toggle=True))
self.ui.origin_btn.triggered.connect(self.on_set_origin)
self.ui.jmp_btn.triggered.connect(self.on_jump_to)
+ self.ui.locate_btn.triggered.connect(lambda: self.on_locate(obj=self.collection.get_active()))
self.ui.shell_btn.triggered.connect(self.on_toggle_shell)
self.ui.new_script_btn.triggered.connect(self.on_filenewscript)
@@ -3208,6 +3256,9 @@ class App(QtCore.QObject):
# Tools Toolbar Signals
self.ui.dblsided_btn.triggered.connect(lambda: self.dblsidedtool.run(toggle=True))
self.ui.cal_btn.triggered.connect(lambda: self.cal_exc_tool.run(toggle=True))
+ self.ui.align_btn.triggered.connect(lambda: self.align_objects_tool.run(toggle=True))
+ self.ui.extract_btn.triggered.connect(lambda: self.edrills_tool.run(toggle=True))
+
self.ui.cutout_btn.triggered.connect(lambda: self.cutout_tool.run(toggle=True))
self.ui.ncc_btn.triggered.connect(lambda: self.ncclear_tool.run(toggle=True))
self.ui.paint_btn.triggered.connect(lambda: self.paint_tool.run(toggle=True))
@@ -4237,9 +4288,20 @@ class App(QtCore.QObject):
obj.options['xmax'] = xmax
obj.options['ymax'] = ymax
except Exception as e:
- log.warning("The object has no bounds properties. %s" % str(e))
+ log.warning("App.new_object() -> The object has no bounds properties. %s" % str(e))
return "fail"
+ try:
+ if kind == 'excellon':
+ obj.fill_color = self.app.defaults["excellon_plot_fill"]
+ obj.outline_color = self.app.defaults["excellon_plot_line"]
+
+ if kind == 'gerber':
+ obj.fill_color = self.app.defaults["gerber_plot_fill"]
+ obj.outline_color = self.app.defaults["gerber_plot_line"]
+ except Exception as e:
+ log.warning("App.new_object() -> setting colors error. %s" % str(e))
+
# update the KeyWords list with the name of the file
self.myKeywords.append(obj.options['name'])
@@ -7140,15 +7202,13 @@ class App(QtCore.QObject):
obj.options['ymin'] = b
obj.options['xmax'] = c
obj.options['ymax'] = d
- self.inform.emit('[success] %s...' %
- _('Origin set'))
+ self.inform.emit('[success] %s...' % _('Origin set'))
if noplot_sig is False:
self.replot_signal.emit([])
if location is not None:
if len(location) != 2:
- self.inform.emit('[ERROR_NOTCL] %s...' %
- _("Origin coordinates specified but incomplete."))
+ self.inform.emit('[ERROR_NOTCL] %s...' % _("Origin coordinates specified but incomplete."))
return 'fail'
x, y = location
@@ -7235,7 +7295,151 @@ class App(QtCore.QObject):
self.jump_signal.emit(location)
- units = self.defaults['units'].upper()
+ if fit_center:
+ self.plotcanvas.fit_center(loc=location)
+
+ cursor = QtGui.QCursor()
+
+ if self.is_legacy is False:
+ # I don't know where those differences come from but they are constant for the current
+ # execution of the application and they are multiples of a value around 0.0263mm.
+ # In a random way sometimes they are more sometimes they are less
+ # if units == 'MM':
+ # cal_factor = 0.0263
+ # else:
+ # cal_factor = 0.0263 / 25.4
+
+ cal_location = (location[0], location[1])
+
+ canvas_origin = self.plotcanvas.native.mapToGlobal(QtCore.QPoint(0, 0))
+ jump_loc = self.plotcanvas.translate_coords_2((cal_location[0], cal_location[1]))
+
+ j_pos = (
+ int(canvas_origin.x() + round(jump_loc[0])),
+ int(canvas_origin.y() + round(jump_loc[1]))
+ )
+ cursor.setPos(j_pos[0], j_pos[1])
+ else:
+ # find the canvas origin which is in the top left corner
+ canvas_origin = self.plotcanvas.native.mapToGlobal(QtCore.QPoint(0, 0))
+ # determine the coordinates for the lowest left point of the canvas
+ x0, y0 = canvas_origin.x(), canvas_origin.y() + self.ui.right_layout.geometry().height()
+
+ # transform the given location from data coordinates to display coordinates. THe display coordinates are
+ # in pixels where the origin 0,0 is in the lowest left point of the display window (in our case is the
+ # canvas) and the point (width, height) is in the top-right location
+ loc = self.plotcanvas.axes.transData.transform_point(location)
+ j_pos = (
+ int(x0 + loc[0]),
+ int(y0 - loc[1])
+ )
+ cursor.setPos(j_pos[0], j_pos[1])
+ self.plotcanvas.mouse = [location[0], location[1]]
+ if self.defaults["global_cursor_color_enabled"] is True:
+ self.plotcanvas.draw_cursor(x_pos=location[0], y_pos=location[1], color=self.cursor_color_3D)
+ else:
+ self.plotcanvas.draw_cursor(x_pos=location[0], y_pos=location[1])
+
+ if self.grid_status():
+ # Update cursor
+ self.app_cursor.set_data(np.asarray([(location[0], location[1])]),
+ symbol='++', edge_color=self.cursor_color_3D,
+ edge_width=self.defaults["global_cursor_width"],
+ size=self.defaults["global_cursor_size"])
+
+ # Set the position label
+ self.ui.position_label.setText(" X: %.4f "
+ "Y: %.4f" % (location[0], location[1]))
+ # Set the relative position label
+ dx = location[0] - float(self.rel_point1[0])
+ dy = location[1] - float(self.rel_point1[1])
+ self.ui.rel_position_label.setText("Dx: %.4f Dy: "
+ "%.4f " % (dx, dy))
+
+ self.inform.emit('[success] %s' % _("Done."))
+ return location
+
+ def on_locate(self, obj, fit_center=True):
+ """
+ Jump to one of the corners (or center) of an object by setting the mouse cursor location
+ :return:
+
+ """
+ self.report_usage("on_locate()")
+
+ if obj is None:
+ self.inform.emit('[WARNING_NOTCL] %s' % _("There is no object selected..."))
+ return 'fail'
+
+ class DialogBoxChoice(QtWidgets.QDialog):
+ def __init__(self, title=None, icon=None, choice='bl'):
+ """
+
+ :param title: string with the window title
+ """
+ super(DialogBoxChoice, self).__init__()
+
+ self.ok = False
+
+ self.setWindowIcon(icon)
+ self.setWindowTitle(str(title))
+
+ self.form = QtWidgets.QFormLayout(self)
+
+ self.ref_radio = RadioSet([
+ {"label": _("Bottom-Left"), "value": "bl"},
+ {"label": _("Top-Left"), "value": "tl"},
+ {"label": _("Bottom-Right"), "value": "br"},
+ {"label": _("Top-Right"), "value": "tr"},
+ {"label": _("Center"), "value": "c"}
+ ], orientation='vertical', stretch=False)
+ self.ref_radio.set_value(choice)
+ self.form.addRow(self.ref_radio)
+
+ self.button_box = QtWidgets.QDialogButtonBox(
+ QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel,
+ Qt.Horizontal, parent=self)
+ self.form.addRow(self.button_box)
+
+ self.button_box.accepted.connect(self.accept)
+ self.button_box.rejected.connect(self.reject)
+
+ if self.exec_() == QtWidgets.QDialog.Accepted:
+ self.ok = True
+ self.location_point = self.ref_radio.get_value()
+ else:
+ self.ok = False
+ self.location_point = None
+
+ dia_box = DialogBoxChoice(title=_("Locate ..."),
+ icon=QtGui.QIcon(self.resource_location + '/locate16.png'),
+ choice=self.defaults['global_locate_pt'])
+
+ if dia_box.ok is True:
+ try:
+ location_point = dia_box.location_point
+ self.defaults['global_locate_pt'] = dia_box.location_point
+ except Exception:
+ return
+ else:
+ return
+
+ loc_b = obj.bounds()
+ if location_point == 'bl':
+ location = (loc_b[0], loc_b[1])
+ elif location_point == 'tl':
+ location = (loc_b[0], loc_b[3])
+ elif location_point == 'br':
+ location = (loc_b[2], loc_b[1])
+ elif location_point == 'tr':
+ location = (loc_b[2], loc_b[3])
+ else:
+ # center
+ cx = loc_b[0] + ((loc_b[2] - loc_b[0]) / 2)
+ cy = loc_b[1] + ((loc_b[3] - loc_b[1]) / 2)
+ location = (cx, cy)
+
+ self.locate_signal.emit(location, location_point)
if fit_center:
self.plotcanvas.fit_center(loc=location)
@@ -8465,7 +8669,7 @@ class App(QtCore.QObject):
self.draw_moving_selection_shape(self.pos, pos, color=self.defaults['global_alt_sel_line'],
face_color=self.defaults['global_alt_sel_fill'])
self.selection_type = False
- elif dx > 0:
+ elif dx >= 0:
self.draw_moving_selection_shape(self.pos, pos)
self.selection_type = True
else:
@@ -8862,6 +9066,7 @@ class App(QtCore.QObject):
pt4 = (float(sel_obj.options['xmin']), float(sel_obj.options['ymax']))
sel_rect = Polygon([pt1, pt2, pt3, pt4])
+
if self.defaults['units'].upper() == 'MM':
sel_rect = sel_rect.buffer(-0.1)
sel_rect = sel_rect.buffer(0.2)
@@ -10378,7 +10583,8 @@ class App(QtCore.QObject):
self.report_usage("export_svg()")
if filename is None:
- filename = self.defaults["global_last_save_folder"]
+ filename = self.defaults["global_last_save_folder"] if self.defaults["global_last_save_folder"] \
+ is not None else self.defaults["global_last_folder"]
self.log.debug("export_svg()")
@@ -10446,7 +10652,8 @@ class App(QtCore.QObject):
self.report_usage("save source file()")
if filename is None:
- filename = self.defaults["global_last_save_folder"]
+ filename = self.defaults["global_last_save_folder"] if self.defaults["global_last_save_folder"] \
+ is not None else self.defaults["global_last_folder"]
self.log.debug("save source file()")
@@ -10489,7 +10696,10 @@ class App(QtCore.QObject):
self.report_usage("export_excellon()")
if filename is None:
- filename = self.defaults["global_last_save_folder"] + '/' + 'exported_excellon'
+ if self.defaults["global_last_save_folder"]:
+ filename = self.defaults["global_last_save_folder"] + '/' + 'exported_excellon'
+ else:
+ filename = self.defaults["global_last_folder"] + '/' + 'exported_excellon'
self.log.debug("export_excellon()")
@@ -10645,7 +10855,8 @@ class App(QtCore.QObject):
self.report_usage("export_gerber()")
if filename is None:
- filename = self.defaults["global_last_save_folder"]
+ filename = self.defaults["global_last_save_folder"] if self.defaults["global_last_save_folder"] \
+ is not None else self.defaults["global_last_folder"]
self.log.debug("export_gerber()")
@@ -10781,7 +10992,8 @@ class App(QtCore.QObject):
self.report_usage("export_dxf()")
if filename is None:
- filename = self.defaults["global_last_save_folder"]
+ filename = self.defaults["global_last_save_folder"] if self.defaults["global_last_save_folder"] \
+ is not None else self.defaults["global_last_folder"]
self.log.debug("export_dxf()")
@@ -11983,8 +12195,13 @@ class App(QtCore.QObject):
plot_container = container
else:
plot_container = self.ui.right_layout
- print("step_1")
- if self.is_legacy is False:
+
+ modifier = QtWidgets.QApplication.queryKeyboardModifiers()
+ if self.is_legacy is True or modifier == QtCore.Qt.ControlModifier:
+ self.is_legacy = True
+ self.defaults["global_graphic_engine"] = "2D"
+ self.plotcanvas = PlotCanvasLegacy(plot_container, self)
+ else:
try:
self.plotcanvas = PlotCanvas(plot_container, self)
except Exception as er:
@@ -11997,13 +12214,9 @@ class App(QtCore.QObject):
msg += msg_txt
self.inform.emit(msg)
return 'fail'
- else:
- self.plotcanvas = PlotCanvasLegacy(plot_container, self)
- print("step_2")
# So it can receive key presses
self.plotcanvas.native.setFocus()
- print("step_3")
self.mm = self.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move_over_plot)
self.mp = self.plotcanvas.graph_event_connect('mouse_press', self.on_mouse_click_over_plot)
@@ -12012,28 +12225,22 @@ class App(QtCore.QObject):
# Keys over plot enabled
self.kp = self.plotcanvas.graph_event_connect('key_press', self.ui.keyPressEvent)
- print("step_4")
if self.defaults['global_cursor_type'] == 'small':
self.app_cursor = self.plotcanvas.new_cursor()
else:
self.app_cursor = self.plotcanvas.new_cursor(big=True)
- print("step_5")
-
if self.ui.grid_snap_btn.isChecked():
self.app_cursor.enabled = True
else:
self.app_cursor.enabled = False
- print("step_6")
-
if self.is_legacy is False:
self.hover_shapes = ShapeCollection(parent=self.plotcanvas.view.scene, layers=1)
else:
# will use the default Matplotlib axes
self.hover_shapes = ShapeCollectionLegacy(obj=self, app=self, name='hover')
- print("step_7")
def on_zoom_fit(self, event):
"""
@@ -12262,19 +12469,12 @@ class App(QtCore.QObject):
new_line_color = color_variant(new_color[:7], 0.7)
for sel_obj in sel_obj_list:
- if self.is_legacy is False:
- sel_obj.fill_color = new_color
- sel_obj.outline_color = new_line_color
+ sel_obj.fill_color = new_color
+ sel_obj.outline_color = new_line_color
- sel_obj.shapes.redraw(
- update_colors=(new_color, new_line_color)
- )
- else:
- sel_obj.fill_color = new_color
- sel_obj.outline_color = new_line_color
- sel_obj.shapes.redraw(
- update_colors=(new_color, new_line_color)
- )
+ sel_obj.shapes.redraw(
+ update_colors=(new_color, new_line_color)
+ )
def on_grid_snap_triggered(self, state):
if state:
diff --git a/FlatCAMObj.py b/FlatCAMObj.py
index 712d766a..6b98f6f2 100644
--- a/FlatCAMObj.py
+++ b/FlatCAMObj.py
@@ -1307,7 +1307,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
else:
iso_name = outname
- # TODO: This is ugly. Create way to pass data into init function.
def iso_init(geo_obj, app_obj):
# Propagate options
geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
@@ -1318,8 +1317,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
iso_offset = dia * ((2 * i + 1) / 2.0) - (i * overlap * dia)
# if milling type is climb then the move is counter-clockwise around features
- mill_t = 1 if milling_type == 'cl' else 0
- geom = self.generate_envelope(iso_offset, mill_t, geometry=work_geo, env_iso_type=iso_t,
+ mill_dir = 1 if milling_type == 'cl' else 0
+ geom = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
follow=follow, nr_passes=i)
if geom == 'fail':
@@ -1438,7 +1437,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
else:
iso_name = outname
- # TODO: This is ugly. Create way to pass data into init function.
def iso_init(geo_obj, app_obj):
# Propagate options
geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
@@ -1448,9 +1446,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
geo_obj.tool_type = 'C1'
# if milling type is climb then the move is counter-clockwise around features
- mill_t = 1 if milling_type == 'cl' else 0
- mill_t = 1 if milling_type == 'cl' else 0
- geom = self.generate_envelope(offset, mill_t, geometry=work_geo, env_iso_type=iso_t,
+ mill_dir = 1 if milling_type == 'cl' else 0
+ geom = self.generate_envelope(offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
follow=follow,
nr_passes=i)
@@ -2641,7 +2638,10 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
horizontal_header.setDefaultSectionSize(70)
horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
horizontal_header.resizeSection(0, 20)
- horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
+ if self.app.defaults["global_app_level"] == 'b':
+ horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+ else:
+ horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Stretch)
diff --git a/README.md b/README.md
index db7ea631..2c446e8a 100644
--- a/README.md
+++ b/README.md
@@ -9,19 +9,59 @@ CAD program, and create G-Code for Isolation routing.
=================================================
-8.01.2019
+15.01.2020
+
+- added key shortcuts and toolbar icons for the new tools: Align Object Tool (ALT+A) and Extract Drills (ALT+I)
+- added new functionality (key shortcut SHIFT+J) to locate the corners of the bounding box (and center) in a selected object
+
+14.01.2020
+
+- in Extract Drill Tool added a new method of drills extraction. The methods are: fixed diameter, fixed annular ring and proportional
+- in Align Objects Tool finished the Single Point method of alignment
+- working on the Dual Point option in Align Objects Tool - angle has to be recalculated
+- finished Dual Point option in Align Objects Tool
+
+13.01.2020
+
+- fixed a small GUI issue in Excellon UI when Basic mode is active
+- started the add of a new Tool: Align Objects Tool which will align (sync) objects of Gerber or Excellon type
+- fixed an issue in Gerber parser introduced recently due of changes made to make Gerber files produced by Sprint Layout
+- working on the Align Objects Tool
+
+12.01.2020
+
+- improved the circle approximation resolution
+- fixed an issue in Gerber parser with detecting old kind of units
+- if CTRL key is pressed during app startup the app will start in the Legacy(2D) graphic engine compatibility mode
+
+11.01.2020
+
+- fixed an issue in the Distance Tool
+- expanded the Extract Drills Tool to use a particular annular ring for each type of aperture flash (pad)
+- Extract Drills Tool: fixed issue with oblong pads and with pads made from aperture macros
+- Extract Drills Tool: added controls in Edit -> Preferences
+
+10.02.2020
+
+- working on a new tool: Extract Drills Tool who will create a Excellon object out of the apertures of a Gerber object
+- finished the GUI in the Extract Drills Tool
+- fixed issue in Film Tool where some parameters names in calls of method export_positive() were not matching the actual parameters name
+- finished the Extract Drills Tool
+- fixed a small issue in the DoubleSided Tool
+
+8.01.2020
- working in NCC Tool
- selected rows in the Tools Tables will stay colored in blue after loosing focus instead of the default gray
- in NCC Tool the Tool name in the Parameters section will be the Tool ID in the Tool Table
- added an exception catch in case the plotcanvas init failed for the OpenGL graphic engine and warn user about what happened
-7.01.2019
+7.01.2020
- solved issue #368 - when using the Enable/Disable prj context menu entries the plotted status is not updated in the object properties
- updates in NCC Tool
-6.01.2019
+6.01.2020
- working on new NCC Tool
diff --git a/camlib.py b/camlib.py
index 1e58a423..063478b1 100644
--- a/camlib.py
+++ b/camlib.py
@@ -458,8 +458,8 @@ class Geometry(object):
"""
defaults = {
- "units": 'in',
- "geo_steps_per_circle": 64
+ "units": 'mm',
+ # "geo_steps_per_circle": 128
}
def __init__(self, geo_steps_per_circle=None):
@@ -528,13 +528,13 @@ class Geometry(object):
self.solid_geometry = []
if type(self.solid_geometry) is list:
- self.solid_geometry.append(Point(origin).buffer(
- radius, int(int(self.geo_steps_per_circle) / 4)))
+ self.solid_geometry.append(Point(origin).buffer(radius, int(self.geo_steps_per_circle)))
return
try:
- self.solid_geometry = self.solid_geometry.union(Point(origin).buffer(
- radius, int(int(self.geo_steps_per_circle) / 4)))
+ self.solid_geometry = self.solid_geometry.union(
+ Point(origin).buffer(radius, int(self.geo_steps_per_circle))
+ )
except Exception as e:
log.error("Failed to run union on polygons. %s" % str(e))
return
@@ -944,7 +944,7 @@ class Geometry(object):
geo_iso.append(pol)
else:
corner_type = 1 if corner is None else corner
- geo_iso.append(pol.buffer(offset, int(int(self.geo_steps_per_circle) / 4), join_style=corner_type))
+ geo_iso.append(pol.buffer(offset, int(self.geo_steps_per_circle), join_style=corner_type))
pol_nr += 1
disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
@@ -959,8 +959,7 @@ class Geometry(object):
geo_iso.append(working_geo)
else:
corner_type = 1 if corner is None else corner
- geo_iso.append(working_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4),
- join_style=corner_type))
+ geo_iso.append(working_geo.buffer(offset, int(self.geo_steps_per_circle), join_style=corner_type))
self.app.proc_container.update_view_text(' %s' % _("Buffering"))
geo_iso = unary_union(geo_iso)
@@ -1225,7 +1224,7 @@ class Geometry(object):
# Can only result in a Polygon or MultiPolygon
# NOTE: The resulting polygon can be "empty".
- current = polygon.buffer((-tooldia / 1.999999), int(int(steps_per_circle) / 4))
+ current = polygon.buffer((-tooldia / 1.999999), int(steps_per_circle))
if current.area == 0:
# Otherwise, trying to to insert current.exterior == None
# into the FlatCAMStorage will fail.
@@ -1254,7 +1253,7 @@ class Geometry(object):
QtWidgets.QApplication.processEvents()
# Can only result in a Polygon or MultiPolygon
- current = current.buffer(-tooldia * (1 - overlap), int(int(steps_per_circle) / 4))
+ current = current.buffer(-tooldia * (1 - overlap), int(steps_per_circle))
if current.area > 0:
# current can be a MultiPolygon
@@ -1372,11 +1371,12 @@ class Geometry(object):
# Clean inside edges (contours) of the original polygon
if contour:
- outer_edges = [x.exterior for x in autolist(
- polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle / 4)))]
+ outer_edges = [
+ x.exterior for x in autolist(polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle)))
+ ]
inner_edges = []
# Over resulting polygons
- for x in autolist(polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle / 4))):
+ for x in autolist(polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle))):
for y in x.interiors: # Over interiors of each polygon
inner_edges.append(y)
# geoms += outer_edges + inner_edges
@@ -1626,7 +1626,7 @@ class Geometry(object):
# Straight line from current_pt to pt.
# Is the toolpath inside the geometry?
walk_path = LineString([current_pt, pt])
- walk_cut = walk_path.buffer(tooldia / 2, int(steps_per_circle / 4))
+ walk_cut = walk_path.buffer(tooldia / 2, int(steps_per_circle))
if walk_cut.within(boundary) and walk_path.length < max_walk:
# log.debug("Walk to path #%d is inside. Joining." % path_count)
@@ -4213,7 +4213,7 @@ class CNCjob(Geometry):
radius = np.sqrt(gobj['I']**2 + gobj['J']**2)
start = np.arctan2(-gobj['J'], -gobj['I'])
stop = np.arctan2(-center[1] + y, -center[0] + x)
- path += arc(center, radius, start, stop, arcdir[current['G']], int(self.steps_per_circle / 4))
+ path += arc(center, radius, start, stop, arcdir[current['G']], int(self.steps_per_circle))
current['X'] = x
current['Y'] = y
@@ -4362,8 +4362,7 @@ class CNCjob(Geometry):
visible=visible, layer=1)
else:
# For Incremental coordinates type G91
- self.app.inform.emit('[ERROR_NOTCL] %s' %
- _('G91 coordinates not implemented ...'))
+ self.app.inform.emit('[ERROR_NOTCL] %s' % _('G91 coordinates not implemented ...'))
for geo in gcode_parsed:
if geo['kind'][0] == 'T':
current_position = geo['geom'].coords[0]
diff --git a/flatcamGUI/FlatCAMGUI.py b/flatcamGUI/FlatCAMGUI.py
index 111a9895..86974e3f 100644
--- a/flatcamGUI/FlatCAMGUI.py
+++ b/flatcamGUI/FlatCAMGUI.py
@@ -373,6 +373,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
QtGui.QIcon(self.app.resource_location + '/origin16.png'), _('Se&t Origin\tO'))
self.menueditjump = self.menuedit.addAction(
QtGui.QIcon(self.app.resource_location + '/jump_to16.png'), _('Jump to Location\tJ'))
+ self.menueditlocate = self.menuedit.addAction(
+ QtGui.QIcon(self.app.resource_location + '/locate16.png'), _('Locate in Object\tSHIFT+J'))
# Separator
self.menuedit.addSeparator()
@@ -825,6 +827,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
QtGui.QIcon(self.app.resource_location + '/origin32.png'), _('Set Origin'))
self.jmp_btn = self.toolbargeo.addAction(
QtGui.QIcon(self.app.resource_location + '/jump_to16.png'), _('Jump to Location'))
+ self.locate_btn = self.toolbargeo.addAction(
+ QtGui.QIcon(self.app.resource_location + '/locate32.png'), _('Locate in Object'))
# ########################################################################
# ########################## View Toolbar# ###############################
@@ -859,6 +863,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
# ########################################################################
self.dblsided_btn = self.toolbartools.addAction(
QtGui.QIcon(self.app.resource_location + '/doubleside32.png'), _("2Sided Tool"))
+ self.align_btn = self.toolbartools.addAction(
+ QtGui.QIcon(self.app.resource_location + '/align32.png'), _("Align Objects Tool"))
+ self.extract_btn = self.toolbartools.addAction(
+ QtGui.QIcon(self.app.resource_location + '/extract_drill32.png'), _("Extract Drills Tool"))
+
self.cutout_btn = self.toolbartools.addAction(
QtGui.QIcon(self.app.resource_location + '/cut16_bis.png'), _("Cutout Tool"))
self.ncc_btn = self.toolbartools.addAction(
@@ -1474,6 +1483,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
SHIFT+G |
%s |
+
+ SHIFT+J |
+ %s |
+
SHIFT+M |
%s |
@@ -1506,6 +1519,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
|
|
+
+ ALT+A |
+ %s |
+
ALT+C |
%s |
@@ -1518,6 +1535,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
ALT+E |
%s |
+
+ ALT+I |
+ %s |
+
ALT+J |
%s |
@@ -1637,11 +1658,13 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
# SHIFT section
_("Copy Obj_Name"),
- _("Toggle Code Editor"), _("Toggle the axis"), _("Distance Minimum Tool"), _("Open Preferences Window"),
+ _("Toggle Code Editor"), _("Toggle the axis"), _("Locate in Object"), _("Distance Minimum Tool"),
+ _("Open Preferences Window"),
_("Rotate by 90 degree CCW"), _("Run a Script"), _("Toggle the workspace"), _("Skew on X axis"),
_("Skew on Y axis"),
# ALT section
- _("Calculators Tool"), _("2-Sided PCB Tool"), _("Transformations Tool"), _("Fiducials Tool"),
+ _("Align Objects Tool"), _("Calculators Tool"), _("2-Sided PCB Tool"), _("Transformations Tool"),
+ _("Extract Drills Tool"), _("Fiducials Tool"),
_("Solder Paste Dispensing Tool"),
_("Film PCB Tool"), _("Non-Copper Clearing Tool"), _("Optimal Tool"),
_("Paint Area Tool"), _("QRCode Tool"), _("Rules Check Tool"),
@@ -2457,8 +2480,12 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
QtGui.QIcon(self.app.resource_location + '/origin32.png'), _('Set Origin'))
self.jmp_btn = self.toolbargeo.addAction(
QtGui.QIcon(self.app.resource_location + '/jump_to16.png'), _('Jump to Location'))
+ self.locate_btn = self.toolbargeo.addAction(
+ QtGui.QIcon(self.app.resource_location + '/locate32.png'), _('Locate in Object'))
- # ## View Toolbar # ##
+ # ########################################################################
+ # ########################## View Toolbar# ###############################
+ # ########################################################################
self.replot_btn = self.toolbarview.addAction(
QtGui.QIcon(self.app.resource_location + '/replot32.png'), _("&Replot"))
self.clear_plot_btn = self.toolbarview.addAction(
@@ -2470,9 +2497,9 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
self.zoom_fit_btn = self.toolbarview.addAction(
QtGui.QIcon(self.app.resource_location + '/zoom_fit32.png'), _("Zoom Fit"))
- # self.toolbarview.setVisible(False)
-
- # ## Shell Toolbar # ##
+ # ########################################################################
+ # ########################## Shell Toolbar# ##############################
+ # ########################################################################
self.shell_btn = self.toolbarshell.addAction(
QtGui.QIcon(self.app.resource_location + '/shell32.png'), _("&Command Line"))
self.new_script_btn = self.toolbarshell.addAction(
@@ -2485,6 +2512,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
# ## Tools Toolbar # ##
self.dblsided_btn = self.toolbartools.addAction(
QtGui.QIcon(self.app.resource_location + '/doubleside32.png'), _("2Sided Tool"))
+ self.align_btn = self.toolbartools.addAction(
+ QtGui.QIcon(self.app.resource_location + '/align32.png'), _("Align Objects Tool"))
+ self.extract_btn = self.toolbartools.addAction(
+ QtGui.QIcon(self.app.resource_location + '/extract_drill32.png'), _("Extract Drills Tool"))
+
self.cutout_btn = self.toolbartools.addAction(
QtGui.QIcon(self.app.resource_location + '/cut16_bis.png'), _("&Cutout Tool"))
self.ncc_btn = self.toolbartools.addAction(
@@ -2498,10 +2530,13 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
self.film_btn = self.toolbartools.addAction(
QtGui.QIcon(self.app.resource_location + '/film16.png'), _("Film Tool"))
self.solder_btn = self.toolbartools.addAction(
- QtGui.QIcon(self.app.resource_location + '/solderpastebis32.png'),
- _("SolderPaste Tool"))
+ QtGui.QIcon(self.app.resource_location + '/solderpastebis32.png'), _("SolderPaste Tool"))
self.sub_btn = self.toolbartools.addAction(
QtGui.QIcon(self.app.resource_location + '/sub32.png'), _("Subtract Tool"))
+ self.rules_btn = self.toolbartools.addAction(
+ QtGui.QIcon(self.app.resource_location + '/rules32.png'), _("Rules Tool"))
+ self.optimal_btn = self.toolbartools.addAction(
+ QtGui.QIcon(self.app.resource_location + '/open_excellon32.png'), _("Optimal Tool"))
self.toolbartools.addSeparator()
@@ -2834,6 +2869,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
if key == QtCore.Qt.Key_G:
self.app.on_toggle_axis()
+ # Locate in Object
+ if key == QtCore.Qt.Key_J:
+ self.app.on_locate(obj=self.app.collection.get_active())
+
# Run Distance Minimum Tool
if key == QtCore.Qt.Key_M:
self.app.distance_min_tool.run()
@@ -2882,6 +2921,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
if key == Qt.Key_3:
self.app.disable_other_plots()
+ # Align in Object Tool
+ if key == QtCore.Qt.Key_A:
+ self.app.align_objects_tool.run(toggle=True)
+
# Calculator Tool
if key == QtCore.Qt.Key_C:
self.app.calculator_tool.run(toggle=True)
@@ -2906,6 +2949,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
self.app.on_toggle_grid_lines()
return
+ # Align in Object Tool
+ if key == QtCore.Qt.Key_I:
+ self.app.edrills_tool.run(toggle=True)
+
# Fiducials Tool
if key == QtCore.Qt.Key_J:
self.app.fiducial_tool.run(toggle=True)
diff --git a/flatcamGUI/GUIElements.py b/flatcamGUI/GUIElements.py
index f52aced9..05314ffe 100644
--- a/flatcamGUI/GUIElements.py
+++ b/flatcamGUI/GUIElements.py
@@ -2009,6 +2009,10 @@ class FCTable(QtWidgets.QTableWidget):
palette = QtGui.QPalette()
palette.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Highlight,
palette.color(QtGui.QPalette.Active, QtGui.QPalette.Highlight))
+
+ # make inactive rows text some color as active; may be useful in the future
+ # palette.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.HighlightedText,
+ # palette.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))
self.setPalette(palette)
if drag_drop:
diff --git a/flatcamGUI/PlotCanvas.py b/flatcamGUI/PlotCanvas.py
index a2db822a..b2fd945a 100644
--- a/flatcamGUI/PlotCanvas.py
+++ b/flatcamGUI/PlotCanvas.py
@@ -32,11 +32,11 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
:param container: The parent container in which to draw plots.
:rtype: PlotCanvas
"""
- print("step_1_1")
- super(PlotCanvas, self).__init__()
+ # super(PlotCanvas, self).__init__()
+ # QtCore.QObject.__init__(self)
# VisPyCanvas.__init__(self)
- print("step_1_2")
+ super().__init__()
# VisPyCanvas does not allow new attributes. Override.
self.unfreeze()
@@ -46,8 +46,6 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
# Parent container
self.container = container
- print("step_1_3")
-
settings = QtCore.QSettings("Open Source", "FlatCAM")
if settings.contains("theme"):
theme = settings.value('theme', type=str)
@@ -117,8 +115,6 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
}
)
- print("step_1_4")
-
#
self.create_native()
self.native.setParent(self.fcapp.ui)
@@ -126,8 +122,6 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
#
self.container.addWidget(self.native)
- print("step_1_5")
-
# ## AXIS # ##
self.v_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 0.8), vertical=True,
parent=self.view.scene)
@@ -135,15 +129,11 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
self.h_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 0.8), vertical=False,
parent=self.view.scene)
- print("step_1_6")
-
# draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
# all CNC have a limited workspace
if self.fcapp.defaults['global_workspace'] is True:
self.draw_workspace(workspace_size=self.fcapp.defaults["global_workspaceT"])
- print("step_1_7")
-
self.line_parent = None
if self.fcapp.defaults["global_cursor_color_enabled"]:
c_color = Color(self.fcapp.defaults["global_cursor_color"]).rgba
@@ -156,8 +146,6 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
self.cursor_h_line = InfiniteLine(pos=None, color=c_color, vertical=False,
parent=self.line_parent)
- print("step_1_8")
-
self.shape_collections = []
self.shape_collection = self.new_shape_collection()
@@ -171,10 +159,7 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
self.big_cursor = None
# Keep VisPy canvas happy by letting it be "frozen" again.
self.freeze()
- print("step_1_9")
-
self.fit_view()
- print("step_1_10")
self.graph_event_connect('mouse_wheel', self.on_mouse_scroll)
diff --git a/flatcamGUI/PreferencesUI.py b/flatcamGUI/PreferencesUI.py
index 365fd17e..41ae3f32 100644
--- a/flatcamGUI/PreferencesUI.py
+++ b/flatcamGUI/PreferencesUI.py
@@ -242,19 +242,23 @@ class Tools2PreferencesUI(QtWidgets.QWidget):
self.tools2_cal_group = Tools2CalPrefGroupUI(decimals=self.decimals)
self.tools2_cal_group.setMinimumWidth(220)
+ self.tools2_edrills_group = Tools2EDrillsPrefGroupUI(decimals=self.decimals)
+ self.tools2_edrills_group.setMinimumWidth(220)
+
self.vlay = QtWidgets.QVBoxLayout()
self.vlay.addWidget(self.tools2_checkrules_group)
self.vlay.addWidget(self.tools2_optimal_group)
self.vlay1 = QtWidgets.QVBoxLayout()
self.vlay1.addWidget(self.tools2_qrcode_group)
+ self.vlay1.addWidget(self.tools2_fiducials_group)
self.vlay2 = QtWidgets.QVBoxLayout()
self.vlay2.addWidget(self.tools2_cfill_group)
self.vlay3 = QtWidgets.QVBoxLayout()
- self.vlay3.addWidget(self.tools2_fiducials_group)
self.vlay3.addWidget(self.tools2_cal_group)
+ self.vlay3.addWidget(self.tools2_edrills_group)
self.layout.addLayout(self.vlay)
self.layout.addLayout(self.vlay1)
@@ -333,7 +337,8 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
# Theme selection
self.theme_label = QtWidgets.QLabel('%s:' % _('Theme'))
self.theme_label.setToolTip(
- _("Select a theme for FlatCAM.")
+ _("Select a theme for FlatCAM.\n"
+ "It will theme the plot area.")
)
self.theme_radio = RadioSet([
@@ -356,6 +361,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
self.theme_button = FCButton(_("Apply Theme"))
self.theme_button.setToolTip(
_("Select a theme for FlatCAM.\n"
+ "It will theme the plot area.\n"
"The application will restart after change.")
)
grid0.addWidget(self.theme_button, 2, 0, 1, 3)
@@ -1587,14 +1593,6 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
"After change, it will be applied at next App start.")
)
self.worker_number_sb = FCSpinner()
- self.worker_number_sb.setToolTip(
- _("The number of Qthreads made available to the App.\n"
- "A bigger number may finish the jobs more quickly but\n"
- "depending on your computer speed, may make the App\n"
- "unresponsive. Can have a value between 2 and 16.\n"
- "Default value is 2.\n"
- "After change, it will be applied at next App start.")
- )
self.worker_number_sb.set_range(2, 16)
grid0.addWidget(self.worker_number_label, 25, 0)
@@ -1604,21 +1602,13 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
tol_label = QtWidgets.QLabel('%s:' % _("Geo Tolerance"))
tol_label.setToolTip(_(
"This value can counter the effect of the Circle Steps\n"
- "parameter. Default value is 0.01.\n"
+ "parameter. Default value is 0.005.\n"
"A lower value will increase the detail both in image\n"
"and in Gcode for the circles, with a higher cost in\n"
"performance. Higher value will provide more\n"
"performance at the expense of level of detail."
))
self.tol_entry = FCDoubleSpinner()
- self.tol_entry.setToolTip(_(
- "This value can counter the effect of the Circle Steps\n"
- "parameter. Default value is 0.01.\n"
- "A lower value will increase the detail both in image\n"
- "and in Gcode for the circles, with a higher cost in\n"
- "performance. Higher value will provide more\n"
- "performance at the expense of level of detail."
- ))
self.tol_entry.setSingleStep(0.001)
self.tol_entry.set_precision(6)
@@ -7632,6 +7622,218 @@ class Tools2CalPrefGroupUI(OptionsGroupUI):
self.layout.addStretch()
+class Tools2EDrillsPrefGroupUI(OptionsGroupUI):
+ def __init__(self, decimals=4, parent=None):
+
+ super(Tools2EDrillsPrefGroupUI, self).__init__(self)
+
+ self.setTitle(str(_("Extract Drills Options")))
+ self.decimals = decimals
+
+ # ## 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('%s:' % _('Parameters'))
+ self.param_label.setToolTip(
+ _("Parameters used for this tool.")
+ )
+ grid_lay.addWidget(self.param_label, 0, 0, 1, 2)
+
+ self.padt_label = QtWidgets.QLabel("%s:" % _("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(
+ _("Create drills from 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(
+ _("Create drills from 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(
+ _("Create drills from 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(
+ _("Create drills from 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(
+ _("Create drills from other types of pad shape.")
+ )
+
+ 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)
+
+ # ## Axis
+ self.hole_size_radio = RadioSet(
+ [
+ {'label': _("Fixed Diameter"), 'value': 'fixed'},
+ {'label': _("Fixed Annular Ring"), 'value': 'ring'},
+ {'label': _("Proportional"), 'value': 'prop'}
+ ],
+ orientation='vertical',
+ stretch=False)
+ self.hole_size_label = QtWidgets.QLabel('%s:' % _("Method"))
+ self.hole_size_label.setToolTip(
+ _("The selected method of extracting the drills. 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"))
+
+ grid_lay.addWidget(self.hole_size_label, 9, 0)
+ grid_lay.addWidget(self.hole_size_radio, 9, 1)
+
+ # grid_lay1.addWidget(QtWidgets.QLabel(''))
+
+ 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)
+
+ # Annular Ring
+ self.fixed_label = QtWidgets.QLabel('%s' % _("Fixed Diameter"))
+ grid_lay.addWidget(self.fixed_label, 11, 0, 1, 2)
+
+ # Diameter value
+ self.dia_entry = FCDoubleSpinner()
+ self.dia_entry.set_precision(self.decimals)
+ self.dia_entry.set_range(0.0000, 9999.9999)
+
+ self.dia_label = QtWidgets.QLabel('%s:' % _("value"))
+ self.dia_label.setToolTip(
+ _("Fixed hole diameter.")
+ )
+
+ grid_lay.addWidget(self.dia_label, 12, 0)
+ grid_lay.addWidget(self.dia_entry, 12, 1)
+
+ # Annular Ring value
+ self.ring_label = QtWidgets.QLabel('%s' % _("Fixed Annular Ring"))
+ self.ring_label.setToolTip(
+ _("The size of annular ring.\n"
+ "The copper sliver between the drill hole exterior\n"
+ "and the margin of the copper pad.")
+ )
+ grid_lay.addWidget(self.ring_label, 13, 0, 1, 2)
+
+ # Circular Annular Ring Value
+ self.circular_ring_label = QtWidgets.QLabel('%s:' % _("Circular"))
+ self.circular_ring_label.setToolTip(
+ _("The size of annular ring for circular pads.")
+ )
+
+ self.circular_ring_entry = FCDoubleSpinner()
+ self.circular_ring_entry.set_precision(self.decimals)
+ self.circular_ring_entry.set_range(0.0000, 9999.9999)
+
+ grid_lay.addWidget(self.circular_ring_label, 14, 0)
+ grid_lay.addWidget(self.circular_ring_entry, 14, 1)
+
+ # Oblong Annular Ring Value
+ self.oblong_ring_label = QtWidgets.QLabel('%s:' % _("Oblong"))
+ self.oblong_ring_label.setToolTip(
+ _("The size of annular ring for oblong pads.")
+ )
+
+ self.oblong_ring_entry = FCDoubleSpinner()
+ self.oblong_ring_entry.set_precision(self.decimals)
+ self.oblong_ring_entry.set_range(0.0000, 9999.9999)
+
+ grid_lay.addWidget(self.oblong_ring_label, 15, 0)
+ grid_lay.addWidget(self.oblong_ring_entry, 15, 1)
+
+ # Square Annular Ring Value
+ self.square_ring_label = QtWidgets.QLabel('%s:' % _("Square"))
+ self.square_ring_label.setToolTip(
+ _("The size of annular ring for square pads.")
+ )
+
+ self.square_ring_entry = FCDoubleSpinner()
+ self.square_ring_entry.set_precision(self.decimals)
+ self.square_ring_entry.set_range(0.0000, 9999.9999)
+
+ grid_lay.addWidget(self.square_ring_label, 16, 0)
+ grid_lay.addWidget(self.square_ring_entry, 16, 1)
+
+ # Rectangular Annular Ring Value
+ self.rectangular_ring_label = QtWidgets.QLabel('%s:' % _("Rectangular"))
+ self.rectangular_ring_label.setToolTip(
+ _("The size of annular ring for rectangular pads.")
+ )
+
+ self.rectangular_ring_entry = FCDoubleSpinner()
+ self.rectangular_ring_entry.set_precision(self.decimals)
+ self.rectangular_ring_entry.set_range(0.0000, 9999.9999)
+
+ grid_lay.addWidget(self.rectangular_ring_label, 17, 0)
+ grid_lay.addWidget(self.rectangular_ring_entry, 17, 1)
+
+ # Others Annular Ring Value
+ self.other_ring_label = QtWidgets.QLabel('%s:' % _("Others"))
+ self.other_ring_label.setToolTip(
+ _("The size of annular ring for other pads.")
+ )
+
+ self.other_ring_entry = FCDoubleSpinner()
+ self.other_ring_entry.set_precision(self.decimals)
+ self.other_ring_entry.set_range(0.0000, 9999.9999)
+
+ grid_lay.addWidget(self.other_ring_label, 18, 0)
+ grid_lay.addWidget(self.other_ring_entry, 18, 1)
+
+ self.prop_label = QtWidgets.QLabel('%s' % _("Proportional Diameter"))
+ grid_lay.addWidget(self.prop_label, 19, 0, 1, 2)
+
+ # Factor value
+ self.factor_entry = FCDoubleSpinner(suffix='%')
+ self.factor_entry.set_precision(self.decimals)
+ self.factor_entry.set_range(0.0000, 100.0000)
+ self.factor_entry.setSingleStep(0.1)
+
+ self.factor_label = QtWidgets.QLabel('%s:' % _("Factor"))
+ self.factor_label.setToolTip(
+ _("Proportional Diameter.\n"
+ "The drill diameter will be a fraction of the pad size.")
+ )
+
+ grid_lay.addWidget(self.factor_label, 20, 0)
+ grid_lay.addWidget(self.factor_entry, 20, 1)
+
+ self.layout.addStretch()
+
+
class FAExcPrefGroupUI(OptionsGroupUI):
def __init__(self, decimals=4, parent=None):
# OptionsGroupUI.__init__(self, "Excellon File associations Preferences", parent=None)
diff --git a/flatcamGUI/VisPyCanvas.py b/flatcamGUI/VisPyCanvas.py
index cc9aab7d..7d7efe13 100644
--- a/flatcamGUI/VisPyCanvas.py
+++ b/flatcamGUI/VisPyCanvas.py
@@ -24,7 +24,8 @@ black = Color("#000000")
class VisPyCanvas(scene.SceneCanvas):
def __init__(self, config=None):
- scene.SceneCanvas.__init__(self, keys=None, config=config)
+ # scene.SceneCanvas.__init__(self, keys=None, config=config)
+ super().__init__(config=config, keys=None)
self.unfreeze()
diff --git a/flatcamParsers/ParseGerber.py b/flatcamParsers/ParseGerber.py
index 6f79c31a..16ce8f37 100644
--- a/flatcamParsers/ParseGerber.py
+++ b/flatcamParsers/ParseGerber.py
@@ -595,6 +595,7 @@ class Gerber(Geometry):
match = self.units_re.search(gline)
if match:
obs_gerber_units = {'0': 'IN', '1': 'MM'}[match.group(1)]
+ self.units = obs_gerber_units
log.warning("Gerber obsolete units found = %s" % obs_gerber_units)
# Changed for issue #80
# self.convert_units({'0': 'IN', '1': 'MM'}[match.group(1)])
@@ -834,7 +835,8 @@ class Gerber(Geometry):
# --- Buffered ---
geo_dict = dict()
if current_aperture in self.apertures:
- buff_value = float(self.apertures[current_aperture]['size']) / 2.0
+ # the following line breaks loading of Circuit Studio Gerber files
+ # buff_value = float(self.apertures[current_aperture]['size']) / 2.0
# region_geo = Polygon(path).buffer(buff_value, int(self.steps_per_circle))
region_geo = Polygon(path) # Sprint Layout Gerbers with ground fill are crashed with above
else:
diff --git a/flatcamTools/ToolAlignObjects.py b/flatcamTools/ToolAlignObjects.py
new file mode 100644
index 00000000..d1e77442
--- /dev/null
+++ b/flatcamTools/ToolAlignObjects.py
@@ -0,0 +1,499 @@
+# ##########################################################
+# 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)
+
+ # Form Layout
+ grid0 = QtWidgets.QGridLayout()
+ grid0.setColumnStretch(0, 0)
+ grid0.setColumnStretch(1, 1)
+ self.layout.addLayout(grid0)
+
+ self.aligned_label = QtWidgets.QLabel('%s' % _("Selection of the WORKING object"))
+ grid0.addWidget(self.aligned_label, 0, 0, 1, 2)
+
+ # Type of object to be aligned
+ self.type_obj_combo = FCComboBox()
+ self.type_obj_combo.addItem("Gerber")
+ self.type_obj_combo.addItem("Excellon")
+
+ 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_label = QtWidgets.QLabel('%s:' % _("Object Type"))
+ self.type_obj_combo_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.")
+ )
+ grid0.addWidget(self.type_obj_combo_label, 2, 0)
+ grid0.addWidget(self.type_obj_combo, 2, 1)
+
+ # 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.setCurrentIndex(1)
+
+ self.object_label = QtWidgets.QLabel('%s:' % _("Object"))
+ self.object_label.setToolTip(
+ _("Object to be aligned.")
+ )
+
+ grid0.addWidget(self.object_label, 3, 0)
+ grid0.addWidget(self.object_combo, 3, 1)
+
+ separator_line = QtWidgets.QFrame()
+ separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+ separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+ grid0.addWidget(separator_line, 4, 0, 1, 2)
+
+ self.aligned_label = QtWidgets.QLabel('%s' % _("Selection of the TARGET object"))
+ self.aligned_label.setToolTip(
+ _("Object to which the other objects will be aligned to (moved to).")
+ )
+ grid0.addWidget(self.aligned_label, 6, 0, 1, 2)
+
+ # Type of object to be aligned to = aligner
+ self.type_aligner_obj_combo = FCComboBox()
+ self.type_aligner_obj_combo.addItem("Gerber")
+ self.type_aligner_obj_combo.addItem("Excellon")
+
+ self.type_aligner_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
+ self.type_aligner_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
+
+ self.type_aligner_obj_combo_label = QtWidgets.QLabel('%s:' % _("Object Type"))
+ self.type_aligner_obj_combo_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.type_aligner_obj_combo_label, 7, 0)
+ grid0.addWidget(self.type_aligner_obj_combo, 7, 1)
+
+ # 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.setCurrentIndex(1)
+
+ self.aligner_object_label = QtWidgets.QLabel('%s:' % _("Object"))
+ self.aligner_object_label.setToolTip(
+ _("Object to be aligned to. Aligner.")
+ )
+
+ grid0.addWidget(self.aligner_object_label, 8, 0)
+ grid0.addWidget(self.aligner_object_combo, 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)
+
+ # Alignment Type
+ self.a_type_lbl = QtWidgets.QLabel('%s:' % _("Alignment Type"))
+ self.a_type_lbl.setToolTip(
+ _("The type of alignment can be:\n"
+ "- Single Point -> it require a single point of sync, the action will be a translation\n"
+ "- Dual Point -> it require two points of sync, the action will be translation followed by rotation")
+ )
+ self.a_type_radio = RadioSet(
+ [
+ {'label': _('Single Point'), 'value': 'sp'},
+ {'label': _('Dual Point'), 'value': 'dp'}
+ ],
+ orientation='horizontal',
+ stretch=False
+ )
+
+ grid0.addWidget(self.a_type_lbl, 10, 0, 1, 2)
+ grid0.addWidget(self.a_type_radio, 11, 0, 1, 2)
+
+ separator_line = QtWidgets.QFrame()
+ separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+ separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+ grid0.addWidget(separator_line, 12, 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_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
+ self.type_aligner_obj_combo.currentIndexChanged.connect(self.on_type_aligner_index_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 = list()
+
+ 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.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 = list()
+ 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"])
+
+ if self.local_connected is True:
+ self.disconnect_cal_events()
+
+ 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)
+
+ def on_type_aligner_index_changed(self):
+ obj_type = self.type_aligner_obj_combo.currentIndex()
+ self.aligner_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+ self.aligner_object_combo.setCurrentIndex(0)
+
+ 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 = list()
+ 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()))
diff --git a/flatcamTools/ToolDblSided.py b/flatcamTools/ToolDblSided.py
index 3308c159..0b00fc6e 100644
--- a/flatcamTools/ToolDblSided.py
+++ b/flatcamTools/ToolDblSided.py
@@ -533,16 +533,17 @@ class DblSidedTool(FlatCAMTool):
"Add them and retry."))
return
- drills = []
+ drills = list()
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"})
- if 'solid_geometry' not in tools:
- tools["1"]['solid_geometry'] = []
+ if 'solid_geometry' not in tools["1"]:
+ tools["1"]['solid_geometry'] = list()
else:
+ tools["1"]['solid_geometry'].append(point)
tools["1"]['solid_geometry'].append(point_mirror)
def obj_init(obj_inst, app_inst):
diff --git a/flatcamTools/ToolDistance.py b/flatcamTools/ToolDistance.py
index c1f9ed8b..1a5bc568 100644
--- a/flatcamTools/ToolDistance.py
+++ b/flatcamTools/ToolDistance.py
@@ -361,11 +361,12 @@ class Distance(FlatCAMTool):
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
+ 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:
+ pass
self.total_distance_entry.set_value('%.*f' % (self.decimals, abs(d)))
self.app.ui.rel_position_label.setText(
@@ -424,11 +425,13 @@ class Distance(FlatCAMTool):
if len(self.points) == 1:
self.utility_geometry(pos=pos)
# and display the temporary angle
- try:
- angle = math.degrees(math.atan(dy / dx))
- self.angle_entry.set_value('%.*f' % (self.decimals, angle))
- except Exception as e:
- pass
+ 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))
diff --git a/flatcamTools/ToolExtractDrills.py b/flatcamTools/ToolExtractDrills.py
new file mode 100644
index 00000000..d75a61b5
--- /dev/null
+++ b/flatcamTools/ToolExtractDrills.py
@@ -0,0 +1,697 @@
+# ##########################################################
+# 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
+
+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.empty_lb = QtWidgets.QLabel("")
+ self.layout.addWidget(self.empty_lb)
+
+ # ## Grid Layout
+ grid_lay = QtWidgets.QGridLayout()
+ self.layout.addLayout(grid_lay)
+ grid_lay.setColumnStretch(0, 1)
+ grid_lay.setColumnStretch(1, 0)
+
+ # ## Gerber Object
+ self.gerber_object_combo = QtWidgets.QComboBox()
+ self.gerber_object_combo.setModel(self.app.collection)
+ self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+ self.gerber_object_combo.setCurrentIndex(1)
+
+ self.grb_label = QtWidgets.QLabel("%s:" % _("GERBER"))
+ self.grb_label.setToolTip('%s.' % _("Gerber 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("%s" % _("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(
+ _("Create drills from 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(
+ _("Create drills from 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(
+ _("Create drills from 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(
+ _("Create drills from 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(
+ _("Create drills from other types of pad shape.")
+ )
+
+ 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('%s' % _("Method"))
+ grid1.addWidget(self.method_label, 2, 0, 1, 2)
+
+ # ## Axis
+ self.hole_size_radio = RadioSet(
+ [
+ {'label': _("Fixed Diameter"), 'value': 'fixed'},
+ {'label': _("Fixed Annular Ring"), 'value': 'ring'},
+ {'label': _("Proportional"), 'value': 'prop'}
+ ],
+ orientation='vertical',
+ stretch=False)
+
+ self.hole_size_label = QtWidgets.QLabel('%s:' % _("Hole Size"))
+ self.hole_size_label.setToolTip(
+ _("The selected method of extracting the drills. 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.hole_size_label, 3, 0)
+ grid1.addWidget(self.hole_size_radio, 3, 1)
+
+ # 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('%s' % _("Fixed Diameter"))
+ grid1.addWidget(self.fixed_label, 6, 0, 1, 2)
+
+ # Diameter value
+ self.dia_entry = FCDoubleSpinner()
+ self.dia_entry.set_precision(self.decimals)
+ self.dia_entry.set_range(0.0000, 9999.9999)
+
+ self.dia_label = QtWidgets.QLabel('%s:' % _("Value"))
+ self.dia_label.setToolTip(
+ _("Fixed hole diameter.")
+ )
+
+ 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('%s' % _("Fixed Annular Ring"))
+ self.ring_label.setToolTip(
+ _("The size of annular ring.\n"
+ "The copper sliver between the drill hole exterior\n"
+ "and the margin of the copper pad.")
+ )
+ 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()
+ 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()
+ 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()
+ 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()
+ 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()
+ 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('%s' % _("Proportional Diameter"))
+ grid3.addWidget(self.prop_label, 2, 0, 1, 2)
+
+ # Diameter value
+ self.factor_entry = FCDoubleSpinner(suffix='%')
+ self.factor_entry.set_precision(self.decimals)
+ self.factor_entry.set_range(0.0000, 100.0000)
+ self.factor_entry.setSingleStep(0.1)
+
+ self.factor_label = QtWidgets.QLabel('%s:' % _("Value"))
+ self.factor_label.setToolTip(
+ _("Proportional Diameter.\n"
+ "The drill diameter will be a fraction of the pad size.")
+ )
+
+ 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.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 = list()
+ tools = dict()
+
+ 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'] = list()
+ 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'] = list()
+ 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'] = list()
+ 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)
diff --git a/flatcamTools/ToolFilm.py b/flatcamTools/ToolFilm.py
index ead110b1..a3542e29 100644
--- a/flatcamTools/ToolFilm.py
+++ b/flatcamTools/ToolFilm.py
@@ -752,7 +752,7 @@ class Film(FlatCAMTool):
skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
skew_reference=skew_reference,
mirror=mirror,
- pagesize=pagesize, orientation=orientation, color=color, opacity=1.0,
+ pagesize_val=pagesize, orientation_val=orientation, color_val=color, opacity_val=1.0,
ftype=ftype
)
@@ -1080,23 +1080,28 @@ class Film(FlatCAMTool):
skew_factor_x=None, skew_factor_y=None, skew_reference='center',
mirror=None, orientation_val='p', pagesize_val='A4', color_val='black', opacity_val=1.0,
use_thread=True, ftype='svg'):
+
"""
Exports a Geometry Object to an SVG file in positive black.
- :param obj_name: the name of the FlatCAM object to be saved as SVG
- :param box_name: the name of the FlatCAM object to be used as delimitation of the content to be saved
- :param filename: Path to the SVG file to save to.
+ :param obj_name: the name of the FlatCAM object to be saved
+ :param box_name: the name of the FlatCAM object to be used as delimitation of the content to be saved
+ :param filename: Path to the file to save to.
:param scale_stroke_factor: factor by which to change/scale the thickness of the features
- :param scale_factor_x: factor to scale the svg geometry on the X axis
- :param scale_factor_y: factor to scale the svg geometry on the Y axis
- :param skew_factor_x: factor to skew the svg geometry on the X axis
- :param skew_factor_y: factor to skew the svg geometry on the Y axis
- :param skew_reference: reference to use for skew. Can be 'bottomleft', 'bottomright', 'topleft', 'topright' and
- those are the 4 points of the bounding box of the geometry to be skewed.
- :param mirror: can be 'x' or 'y' or 'both'. Axis on which to mirror the svg geometry
+ :param scale_factor_x: factor to scale the geometry on the X axis
+ :param scale_factor_y: factor to scale the geometry on the Y axis
+ :param skew_factor_x: factor to skew the geometry on the X axis
+ :param skew_factor_y: factor to skew the geometry on the Y axis
+ :param skew_reference: reference to use for skew. Can be 'bottomleft', 'bottomright', 'topleft',
+ 'topright' and those are the 4 points of the bounding box of the geometry to be skewed.
+ :param mirror: can be 'x' or 'y' or 'both'. Axis on which to mirror the svg geometry
+ :param orientation_val:
+ :param pagesize_val:
+ :param color_val:
+ :param opacity_val:
+ :param use_thread: if to be run in a separate thread; boolean
+ :param ftype: the type of file for saving the film: 'svg', 'png' or 'pdf'
- :param use_thread: if to be run in a separate thread; boolean
- :param ftype: the type of file for saving the film: 'svg', 'png' or 'pdf'
:return:
"""
self.app.report_usage("export_positive()")
diff --git a/flatcamTools/ToolNonCopperClear.py b/flatcamTools/ToolNonCopperClear.py
index c97f0ac3..b8e365c5 100644
--- a/flatcamTools/ToolNonCopperClear.py
+++ b/flatcamTools/ToolNonCopperClear.py
@@ -651,7 +651,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
}
# #############################################################################
- # ############################ SGINALS ########################################
+ # ############################ SIGNALS ########################################
# #############################################################################
self.addtool_btn.clicked.connect(self.on_tool_add)
self.addtool_entry.returnPressed.connect(self.on_tool_add)
diff --git a/flatcamTools/__init__.py b/flatcamTools/__init__.py
index d986974e..a593e6c4 100644
--- a/flatcamTools/__init__.py
+++ b/flatcamTools/__init__.py
@@ -1,11 +1,12 @@
import sys
-
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
@@ -17,10 +18,10 @@ from flatcamTools.ToolDistanceMin import DistanceMin
from flatcamTools.ToolMove import ToolMove
from flatcamTools.ToolNonCopperClear import NonCopperClear
+from flatcamTools.ToolPaint import ToolPaint
from flatcamTools.ToolOptimal import ToolOptimal
-from flatcamTools.ToolPaint import ToolPaint
from flatcamTools.ToolPanelize import Panelize
from flatcamTools.ToolPcbWizard import PcbWizard
from flatcamTools.ToolPDF import ToolPDF
diff --git a/share/align16.png b/share/align16.png
new file mode 100644
index 00000000..d21f1cec
Binary files /dev/null and b/share/align16.png differ
diff --git a/share/align32.png b/share/align32.png
new file mode 100644
index 00000000..b81511a2
Binary files /dev/null and b/share/align32.png differ
diff --git a/share/extract_drill16.png b/share/extract_drill16.png
new file mode 100644
index 00000000..4b3a29ab
Binary files /dev/null and b/share/extract_drill16.png differ
diff --git a/share/extract_drill32.png b/share/extract_drill32.png
new file mode 100644
index 00000000..41f740f2
Binary files /dev/null and b/share/extract_drill32.png differ
diff --git a/share/locate16.png b/share/locate16.png
new file mode 100644
index 00000000..1cdc85ed
Binary files /dev/null and b/share/locate16.png differ
diff --git a/share/locate32.png b/share/locate32.png
new file mode 100644
index 00000000..66d630b0
Binary files /dev/null and b/share/locate32.png differ