diff --git a/FlatCAM.py b/FlatCAM.py index 489af513..b1701a99 100644 --- a/FlatCAM.py +++ b/FlatCAM.py @@ -1,7 +1,8 @@ -import sys +import sys, os from PyQt5 import sip from PyQt5 import QtGui, QtCore, QtWidgets +from PyQt5.QtCore import QSettings, Qt from FlatCAMApp import App from multiprocessing import freeze_support import VisPyPatches @@ -31,7 +32,31 @@ if __name__ == '__main__': debug_trace() VisPyPatches.apply_patches() + # apply High DPI support + settings = QSettings("Open Source", "FlatCAM") + if settings.contains("hdpi"): + hdpi_support = settings.value('hdpi', type=int) + else: + hdpi_support = 0 + + if hdpi_support == 2: + os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" + else: + os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "0" + app = QtWidgets.QApplication(sys.argv) + + # apply style + settings = QSettings("Open Source", "FlatCAM") + if settings.contains("style"): + style = settings.value('style', type=str) + app.setStyle(style) + + if hdpi_support == 2: + app.setAttribute(Qt.AA_EnableHighDpiScaling, True) + else: + app.setAttribute(Qt.AA_EnableHighDpiScaling, False) + fc = App() sys.exit(app.exec_()) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 3b91a598..5b462aca 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -14,6 +14,7 @@ import os import random import logging import simplejson as json +import lzma import re import os @@ -92,8 +93,8 @@ class App(QtCore.QObject): log.addHandler(handler) # Version - version = 8.909 - version_date = "2019/02/16" + version = 8.910 + version_date = "2019/02/23" beta = True # current date now @@ -311,6 +312,9 @@ class App(QtCore.QObject): "global_send_stats": self.general_defaults_form.general_app_group.send_stats_cb, "global_project_at_startup": self.general_defaults_form.general_app_group.project_startup_cb, "global_project_autohide": self.general_defaults_form.general_app_group.project_autohide_cb, + "global_app_level": self.general_defaults_form.general_app_group.app_level_radio, + "global_compression_level": self.general_defaults_form.general_app_group.compress_combo, + "global_save_compressed": self.general_defaults_form.general_app_group.save_type_cb, "global_gridx": self.general_defaults_form.general_gui_group.gridx_entry, "global_gridy": self.general_defaults_form.general_gui_group.gridy_entry, @@ -457,6 +461,7 @@ class App(QtCore.QObject): "tools_panelize_constrain": self.tools_defaults_form.tools_panelize_group.pconstrain_cb, "tools_panelize_constrainx": self.tools_defaults_form.tools_panelize_group.px_width_entry, "tools_panelize_constrainy": self.tools_defaults_form.tools_panelize_group.py_height_entry, + "tools_panelize_panel_type": self.tools_defaults_form.tools_panelize_group.panel_type_radio, "tools_calc_vshape_tip_dia": self.tools_defaults_form.tools_calculators_group.tip_dia_entry, "tools_calc_vshape_tip_angle": self.tools_defaults_form.tools_calculators_group.tip_angle_entry, @@ -464,16 +469,56 @@ class App(QtCore.QObject): "tools_calc_electro_length": self.tools_defaults_form.tools_calculators_group.pcblength_entry, "tools_calc_electro_width": self.tools_defaults_form.tools_calculators_group.pcbwidth_entry, "tools_calc_electro_cdensity": self.tools_defaults_form.tools_calculators_group.cdensity_entry, - "tools_calc_electro_growth": self.tools_defaults_form.tools_calculators_group.growth_entry + "tools_calc_electro_growth": self.tools_defaults_form.tools_calculators_group.growth_entry, + + "tools_transform_rotate": self.tools_defaults_form.tools_transform_group.rotate_entry, + "tools_transform_skew_x": self.tools_defaults_form.tools_transform_group.skewx_entry, + "tools_transform_skew_y": self.tools_defaults_form.tools_transform_group.skewy_entry, + "tools_transform_scale_x": self.tools_defaults_form.tools_transform_group.scalex_entry, + "tools_transform_scale_y": self.tools_defaults_form.tools_transform_group.scaley_entry, + "tools_transform_scale_link": self.tools_defaults_form.tools_transform_group.link_cb, + "tools_transform_scale_reference": self.tools_defaults_form.tools_transform_group.reference_cb, + "tools_transform_offset_x": self.tools_defaults_form.tools_transform_group.offx_entry, + "tools_transform_offset_y": self.tools_defaults_form.tools_transform_group.offy_entry, + "tools_transform_mirror_reference": self.tools_defaults_form.tools_transform_group.mirror_reference_cb, + "tools_transform_mirror_point": self.tools_defaults_form.tools_transform_group.flip_ref_entry, + + "tools_solderpaste_tools": self.tools_defaults_form.tools_solderpaste_group.nozzle_tool_dia_entry, + "tools_solderpaste_new": self.tools_defaults_form.tools_solderpaste_group.addtool_entry, + "tools_solderpaste_z_start": self.tools_defaults_form.tools_solderpaste_group.z_start_entry, + "tools_solderpaste_z_dispense": self.tools_defaults_form.tools_solderpaste_group.z_dispense_entry, + "tools_solderpaste_z_stop": self.tools_defaults_form.tools_solderpaste_group.z_stop_entry, + "tools_solderpaste_z_travel": self.tools_defaults_form.tools_solderpaste_group.z_travel_entry, + "tools_solderpaste_z_toolchange": self.tools_defaults_form.tools_solderpaste_group.z_toolchange_entry, + "tools_solderpaste_xy_toolchange": self.tools_defaults_form.tools_solderpaste_group.xy_toolchange_entry, + "tools_solderpaste_frxy": self.tools_defaults_form.tools_solderpaste_group.frxy_entry, + "tools_solderpaste_frz": self.tools_defaults_form.tools_solderpaste_group.frz_entry, + "tools_solderpaste_frz_dispense": self.tools_defaults_form.tools_solderpaste_group.frz_dispense_entry, + "tools_solderpaste_speedfwd": self.tools_defaults_form.tools_solderpaste_group.speedfwd_entry, + "tools_solderpaste_dwellfwd": self.tools_defaults_form.tools_solderpaste_group.dwellfwd_entry, + "tools_solderpaste_speedrev": self.tools_defaults_form.tools_solderpaste_group.speedrev_entry, + "tools_solderpaste_dwellrev": self.tools_defaults_form.tools_solderpaste_group.dwellrev_entry, + "tools_solderpaste_pp": self.tools_defaults_form.tools_solderpaste_group.pp_combo + } - # loads postprocessors + + ############################# + #### LOAD POSTPROCESSORS #### + ############################# + self.postprocessors = load_postprocessors(self) for name in list(self.postprocessors.keys()): + # 'Paste' postprocessors are to be used only in the Solder Paste Dispensing Tool + if name.partition('_')[0] == 'Paste': + self.tools_defaults_form.tools_solderpaste_group.pp_combo.addItem(name) + continue + self.geometry_defaults_form.geometry_opt_group.pp_geometry_name_cb.addItem(name) # HPGL postprocessor is only for Geometry objects therefore it should not be in the Excellon Preferences if name == 'hpgl': continue + self.excellon_defaults_form.excellon_opt_group.pp_excellon_name_cb.addItem(name) self.defaults = LoudDict() @@ -486,6 +531,7 @@ class App(QtCore.QObject): "global_send_stats": True, "global_project_at_startup": False, "global_project_autohide": True, + "global_app_level": 'b', "global_gridx": 1.0, "global_gridy": 1.0, @@ -531,6 +577,9 @@ class App(QtCore.QObject): "global_shell_shape": [500, 300], # Shape of the shell in pixels. "global_shell_at_startup": False, # Show the shell at startup. "global_recent_limit": 10, # Max. items in recent list. + "global_compression_level": 3, + "global_save_compressed": True, + "fit_key": 'V', "zoom_out_key": '-', "zoom_in_key": '=', @@ -666,6 +715,7 @@ class App(QtCore.QObject): "tools_panelize_constrain": False, "tools_panelize_constrainx": 0.0, "tools_panelize_constrainy": 0.0, + "tools_panelize_panel_type": 'gerber', "tools_calc_vshape_tip_dia": 0.007874, "tools_calc_vshape_tip_angle": 30, @@ -673,7 +723,36 @@ class App(QtCore.QObject): "tools_calc_electro_length": 10.0, "tools_calc_electro_width": 10.0, "tools_calc_electro_cdensity":13.0, - "tools_calc_electro_growth": 10.0 + "tools_calc_electro_growth": 10.0, + + "tools_transform_rotate": 90, + "tools_transform_skew_x": 0.0, + "tools_transform_skew_y": 0.0, + "tools_transform_scale_x": 1.0, + "tools_transform_scale_y": 1.0, + "tools_transform_scale_link": True, + "tools_transform_scale_reference": True, + "tools_transform_offset_x": 0.0, + "tools_transform_offset_y": 0.0, + "tools_transform_mirror_reference": False, + "tools_transform_mirror_point": (0, 0), + + "tools_solderpaste_tools": "1.0, 0.3", + "tools_solderpaste_new": 0.3, + "tools_solderpaste_z_start": 0.005, + "tools_solderpaste_z_dispense": 0.01, + "tools_solderpaste_z_stop": 0.005, + "tools_solderpaste_z_travel": 0.1, + "tools_solderpaste_z_toolchange": 1.0, + "tools_solderpaste_xy_toolchange": "0.0, 0.0", + "tools_solderpaste_frxy": 3.0, + "tools_solderpaste_frz": 3.0, + "tools_solderpaste_frz_dispense": 1.0, + "tools_solderpaste_speedfwd": 20, + "tools_solderpaste_dwellfwd": 1, + "tools_solderpaste_speedrev": 10, + "tools_solderpaste_dwellrev": 1, + "tools_solderpaste_pp": 'Paste_1' }) ############################### @@ -1069,7 +1148,6 @@ class App(QtCore.QObject): self.ui.menufilenewexc.triggered.connect(self.new_excellon_object) self.ui.menufileopengerber.triggered.connect(self.on_fileopengerber) - self.ui.menufileopengerber_follow.triggered.connect(self.on_fileopengerber_follow) self.ui.menufileopenexcellon.triggered.connect(self.on_fileopenexcellon) self.ui.menufileopengcode.triggered.connect(self.on_fileopengcode) self.ui.menufileopenproject.triggered.connect(self.on_file_openproject) @@ -1129,7 +1207,7 @@ class App(QtCore.QObject): self.ui.menuoptions_transform_flipx.triggered.connect(self.on_flipx) self.ui.menuoptions_transform_flipy.triggered.connect(self.on_flipy) - + self.ui.menuoptions_view_source.triggered.connect(self.on_view_source) self.ui.menuviewdisableall.triggered.connect(self.disable_all_plots) self.ui.menuviewdisableother.triggered.connect(self.disable_other_plots) @@ -1156,6 +1234,8 @@ class App(QtCore.QObject): self.ui.menuprojectenable.triggered.connect(lambda: self.enable_plots(self.collection.get_selected())) self.ui.menuprojectdisable.triggered.connect(lambda: self.disable_plots(self.collection.get_selected())) self.ui.menuprojectgeneratecnc.triggered.connect(lambda: self.generate_cnc_job(self.collection.get_selected())) + self.ui.menuprojectviewsource.triggered.connect(self.on_view_source) + self.ui.menuprojectcopy.triggered.connect(self.on_copy_object) self.ui.menuprojectedit.triggered.connect(self.object2editor) @@ -1262,7 +1342,7 @@ class App(QtCore.QObject): self.general_defaults_form.general_gui_group.wk_cb.currentIndexChanged.connect(self.on_workspace_modified) self.general_defaults_form.general_gui_group.workspace_cb.stateChanged.connect(self.on_workspace) - self.general_defaults_form.general_gui_group.layout_combo.activated.connect(self.on_layout) + self.general_defaults_form.general_gui_set_group.layout_combo.activated.connect(self.on_layout) # Modify G-CODE Plot Area TAB self.ui.code_editor.textChanged.connect(self.handleTextChanged) @@ -1468,7 +1548,7 @@ class App(QtCore.QObject): if not factory_defaults: self.save_factory_defaults(silent=False) # ONLY AT FIRST STARTUP INIT THE GUI LAYOUT TO 'COMPACT' - self.on_layout(layout='compact') + self.on_layout(index=None, lay='compact') factory_file.close() # and then make the factory_defaults.FlatConfig file read_only os it can't be modified after creation. @@ -1544,13 +1624,16 @@ class App(QtCore.QObject): self.panelize_tool.install(icon=QtGui.QIcon('share/panel16.png')) self.film_tool = Film(self) - self.film_tool.install(icon=QtGui.QIcon('share/film16.png'), separator=True) + self.film_tool.install(icon=QtGui.QIcon('share/film16.png')) + + self.paste_tool = SolderPaste(self) + self.paste_tool.install(icon=QtGui.QIcon('share/solderpastebis32.png'), separator=True) self.move_tool = ToolMove(self) self.move_tool.install(icon=QtGui.QIcon('share/move16.png'), pos=self.ui.menuedit, before=self.ui.menueditorigin) - self.cutout_tool = ToolCutOut(self) + self.cutout_tool = CutOut(self) self.cutout_tool.install(icon=QtGui.QIcon('share/cut16.png'), pos=self.ui.menutool, before=self.measurement_tool.menuAction) @@ -1634,6 +1717,10 @@ class App(QtCore.QObject): # store the Geometry Editor Toolbar visibility before entering in the Editor self.geo_editor.toolbar_old_state = True if self.ui.geo_edit_toolbar.isVisible() else False self.geo_editor.edit_fcgeometry(edited_object) + + # we set the notebook to hidden + self.ui.splitter.setSizes([0, 1]) + # set call source to the Editor we go into self.call_source = 'geo_editor' @@ -1701,6 +1788,10 @@ class App(QtCore.QObject): self.inform.emit("[WARNING_NOTCL]Select a Geometry or Excellon Object to update.") return + # if notebook is hidden we show it + if self.ui.splitter.sizes()[0] == 0: + self.ui.splitter.setSizes([1, 1]) + # restore the call_source to app self.call_source = 'app' @@ -3363,13 +3454,12 @@ class App(QtCore.QObject): self.general_defaults_form.general_gui_group.workspace_cb.setChecked(True) self.on_workspace() - def on_layout(self, layout=None): + def on_layout(self, index, lay=None): self.report_usage("on_layout()") - - if layout is None: - current_layout= self.general_defaults_form.general_gui_group.layout_combo.get_value().lower() + if lay: + current_layout = lay else: - current_layout = layout + current_layout = self.general_defaults_form.general_gui_set_group.layout_combo.get_value().lower() settings = QSettings("Open Source", "FlatCAM") settings.setValue('layout', current_layout) @@ -3588,34 +3678,46 @@ class App(QtCore.QObject): # work only if the notebook tab on focus is the Selected_Tab and only if the object is Geometry if notebook_widget_name == 'selected_tab': if str(type(self.collection.get_active())) == "": - tool_add_popup = FCInputDialog(title="New Tool ...", - text='Enter a Tool Diameter:', - min=0.0000, max=99.9999, decimals=4) - tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png')) + # Tool add works for Geometry only if Advanced is True in Preferences + if self.defaults["global_advanced"] is True: + tool_add_popup = FCInputDialog(title="New Tool ...", + text='Enter a Tool Diameter:', + min=0.0000, max=99.9999, decimals=4) + tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png')) - val, ok = tool_add_popup.get_value() - if ok: - if float(val) == 0: + val, ok = tool_add_popup.get_value() + if ok: + if float(val) == 0: + self.inform.emit( + "[WARNING_NOTCL] Please enter a tool diameter with non-zero value, in Float format.") + return + self.collection.get_active().on_tool_add(dia=float(val)) + else: self.inform.emit( - "[WARNING_NOTCL] Please enter a tool diameter with non-zero value, in Float format.") - return - self.collection.get_active().on_tool_add(dia=float(val)) + "[WARNING_NOTCL] Adding Tool cancelled ...") else: - self.inform.emit( - "[WARNING_NOTCL] Adding Tool cancelled ...") + msgbox = QtWidgets.QMessageBox() + msgbox.setText("Adding Tool works only when Advanced is checked.\n" + "Go to Preferences -> General - Show Advanced Options.") + msgbox.setWindowTitle("Tool adding ...") + msgbox.setWindowIcon(QtGui.QIcon('share/warning.png')) + msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok) + msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok) + msgbox.exec_() # work only if the notebook tab on focus is the Tools_Tab if notebook_widget_name == 'tool_tab': tool_widget = self.ui.tool_scroll_area.widget().objectName() + tool_add_popup = FCInputDialog(title="New Tool ...", + text='Enter a Tool Diameter:', + min=0.0000, max=99.9999, decimals=4) + tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png')) + + val, ok = tool_add_popup.get_value() + # and only if the tool is NCC Tool if tool_widget == self.ncclear_tool.toolName: - tool_add_popup = FCInputDialog(title="New Tool ...", - text='Enter a Tool Diameter:', - min=0.0000, max=99.9999, decimals=4) - tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png')) - - val, ok = tool_add_popup.get_value() if ok: if float(val) == 0: self.inform.emit( @@ -3627,12 +3729,6 @@ class App(QtCore.QObject): "[WARNING_NOTCL] Adding Tool cancelled ...") # and only if the tool is Paint Area Tool elif tool_widget == self.paint_tool.toolName: - tool_add_popup = FCInputDialog(title="New Tool ...", - text='Enter a Tool Diameter:', - min=0.0000, max=99.9999, decimals=4) - tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png')) - - val, ok = tool_add_popup.get_value() if ok: if float(val) == 0: self.inform.emit( @@ -3642,6 +3738,18 @@ class App(QtCore.QObject): else: self.inform.emit( "[WARNING_NOTCL] Adding Tool cancelled ...") + # and only if the tool is Solder Paste Dispensing Tool + elif tool_widget == self.paste_tool.toolName: + if ok: + if float(val) == 0: + self.inform.emit( + "[WARNING_NOTCL] Please enter a tool diameter with non-zero value, in Float format.") + return + self.paste_tool.on_tool_add(dia=float(val)) + else: + self.inform.emit( + "[WARNING_NOTCL] Adding Tool cancelled ...") + # It's meant to delete tools in tool tables via a 'Delete' shortcut key but only if certain conditions are met # See description bellow. @@ -3665,6 +3773,9 @@ class App(QtCore.QObject): elif tool_widget == self.paint_tool.toolName: self.paint_tool.on_tool_delete() + # and only if the tool is Solder Paste Dispensing Tool + elif tool_widget == self.paste_tool.toolName: + self.paste_tool.on_tool_delete() else: self.on_delete() @@ -3934,7 +4045,7 @@ class App(QtCore.QObject): obj.mirror('X', [px, py]) obj.plot() self.object_changed.emit(obj) - + self.inform.emit("[success] Flip on Y axis done.") except Exception as e: self.inform.emit("[ERROR_NOTCL] Due of %s, Flip action was not executed." % str(e)) return @@ -3974,7 +4085,7 @@ class App(QtCore.QObject): obj.mirror('Y', [px, py]) obj.plot() self.object_changed.emit(obj) - + self.inform.emit("[success] Flip on X axis done.") except Exception as e: self.inform.emit("[ERROR_NOTCL] Due of %s, Flip action was not executed." % str(e)) return @@ -3993,7 +4104,8 @@ class App(QtCore.QObject): else: if silent is False: rotatebox = FCInputDialog(title="Transform", text="Enter the Angle value:", - min=-360, max=360, decimals=3) + min=-360, max=360, decimals=4, + init_val=float(self.defaults['tools_transform_rotate'])) num, ok = rotatebox.get_value() else: num = preset @@ -4018,9 +4130,10 @@ class App(QtCore.QObject): py = 0.5 * (yminimal + ymaximal) for sel_obj in obj_list: - sel_obj.rotate(-num, point=(px, py)) + sel_obj.rotate(-float(num), point=(px, py)) sel_obj.plot() self.object_changed.emit(sel_obj) + self.inform.emit("[success] Rotation done.") except Exception as e: self.inform.emit("[ERROR_NOTCL] Due of %s, rotation movement was not executed." % str(e)) return @@ -4036,7 +4149,8 @@ class App(QtCore.QObject): self.inform.emit("[WARNING_NOTCL] No object selected to Skew/Shear on X axis.") else: skewxbox = FCInputDialog(title="Transform", text="Enter the Angle value:", - min=-360, max=360, decimals=3) + min=-360, max=360, decimals=4, + init_val=float(self.defaults['tools_transform_skew_x'])) num, ok = skewxbox.get_value() if ok: # first get a bounding box to fit all @@ -4053,6 +4167,7 @@ class App(QtCore.QObject): obj.skew(num, 0, point=(xminimal, yminimal)) obj.plot() self.object_changed.emit(obj) + self.inform.emit("[success] Skew on X axis done.") def on_skewy(self): self.report_usage("on_skewy()") @@ -4065,7 +4180,8 @@ class App(QtCore.QObject): self.inform.emit("[WARNING_NOTCL] No object selected to Skew/Shear on Y axis.") else: skewybox = FCInputDialog(title="Transform", text="Enter the Angle value:", - min=-360, max=360, decimals=3) + min=-360, max=360, decimals=4, + init_val=float(self.defaults['tools_transform_skew_y'])) num, ok = skewybox.get_value() if ok: # first get a bounding box to fit all @@ -4082,6 +4198,7 @@ class App(QtCore.QObject): obj.skew(0, num, point=(xminimal, yminimal)) obj.plot() self.object_changed.emit(obj) + self.inform.emit("[success] Skew on Y axis done.") def delete_first_selected(self): # Keep this for later @@ -4728,10 +4845,46 @@ class App(QtCore.QObject): self.on_file_exportexcellon() elif type(obj) == FlatCAMCNCjob: obj.on_exportgcode_button_click() + elif type(obj) == FlatCAMGerber: + self.on_file_exportgerber() + def on_view_source(self): + + try: + obj = self.collection.get_active() + except: + self.inform.emit("[WARNING_NOTCL] Select an Gerber or Excellon file to view it's source.") + + # add the tab if it was closed + self.ui.plot_tab_area.addTab(self.ui.cncjob_tab, "Code Editor") + # first clear previous text in text editor (if any) + self.ui.code_editor.clear() + + # Switch plot_area to CNCJob tab + self.ui.plot_tab_area.setCurrentWidget(self.ui.cncjob_tab) + + # then append the text from GCode to the text editor + file = StringIO(obj.source_file) + try: + for line in file: + proc_line = str(line).strip('\n') + self.ui.code_editor.append(proc_line) + except Exception as e: + log.debug('App.on_view_source() -->%s' % str(e)) + self.inform.emit('[ERROR]App.on_view_source() -->%s' % str(e)) + return + + self.ui.code_editor.moveCursor(QtGui.QTextCursor.Start) + + self.handleTextChanged() + self.ui.show() + + # if type(obj) == FlatCAMGerber: + # self.on_file_exportdxf() + # elif type(obj) == FlatCAMExcellon: + # self.on_file_exportexcellon() def obj_move(self): self.report_usage("obj_move()") - self.move_tool.run() def on_fileopengerber(self): @@ -4770,42 +4923,6 @@ class App(QtCore.QObject): self.worker_task.emit({'fcn': self.open_gerber, 'params': [filename]}) - def on_fileopengerber_follow(self): - """ - File menu callback for opening a Gerber. - - :return: None - """ - - self.report_usage("on_fileopengerber_follow") - App.log.debug("on_fileopengerber_follow()") - _filter_ = "Gerber Files (*.gbr *.ger *.gtl *.gbl *.gts *.gbs *.gtp *.gbp *.gto *.gbo *.gm1 *.gml *.gm3 *.gko " \ - "*.cmp *.sol *.stc *.sts *.plc *.pls *.crc *.crs *.tsm *.bsm *.ly2 *.ly15 *.dim *.mil *.grb" \ - "*.top *.bot *.smt *.smb *.sst *.ssb *.spt *.spb *.pho *.gdo *.art *.gbd);;" \ - "Protel Files (*.gtl *.gbl *.gts *.gbs *.gto *.gbo *.gtp *.gbp *.gml *.gm1 *.gm3 *.gko);;" \ - "Eagle Files (*.cmp *.sol *.stc *.sts *.plc *.pls *.crc *.crs *.tsm *.bsm *.ly2 *.ly15 *.dim *.mil);;" \ - "OrCAD Files (*.top *.bot *.smt *.smb *.sst *.ssb *.spt *.spb);;" \ - "Allegro Files (*.art);;" \ - "Mentor Files (*.pho *.gdo);;" \ - "All Files (*.*)" - try: - filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open Gerber with Follow", - directory=self.get_last_folder(), filter=_filter_) - except TypeError: - filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open Gerber with Follow", filter=_filter_) - - # The Qt methods above will return a QString which can cause problems later. - # So far json.dump() will fail to serialize it. - # TODO: Improve the serialization methods and remove this fix. - filename = str(filename) - follow = True - - if filename == "": - self.inform.emit("[WARNING_NOTCL]Open Gerber-Follow cancelled.") - else: - self.worker_task.emit({'fcn': self.open_gerber, - 'params': [filename, follow]}) - def on_fileopenexcellon(self): """ File menu callback for opening an Excellon file. @@ -4975,6 +5092,45 @@ class App(QtCore.QObject): write_png(filename, data) self.file_saved.emit("png", filename) + def on_file_exportgerber(self): + """ + Callback for menu item File->Export SVG. + + :return: None + """ + self.report_usage("on_file_exportgerber") + App.log.debug("on_file_exportgerber()") + + obj = self.collection.get_active() + if obj is None: + self.inform.emit("[WARNING_NOTCL] No object selected. Please Select an Gerber object to export.") + return + + # Check for more compatible types and add as required + if not isinstance(obj, FlatCAMGerber): + self.inform.emit("[ERROR_NOTCL] Failed. Only Gerber objects can be saved as Gerber files...") + return + + name = self.collection.get_active().options["name"] + + filter = "Gerber File (*.GBR);;Gerber File (*.GRB);;All Files (*.*)" + try: + filename, _ = QtWidgets.QFileDialog.getSaveFileName( + caption="Export Gerber", + directory=self.get_last_save_folder() + '/' + name, + filter=filter) + except TypeError: + filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export Gerber", filter=filter) + + filename = str(filename) + + if filename == "": + self.inform.emit("[WARNING_NOTCL]Export Gerber cancelled.") + return + else: + self.export_gerber(name, filename) + self.file_saved.emit("Gerber", filename) + def on_file_exportexcellon(self): """ Callback for menu item File->Export SVG. @@ -5213,15 +5369,15 @@ class App(QtCore.QObject): except IOError: exists = False - msg = "Project file exists. Overwrite?" - if exists: - msgbox = QtWidgets.QMessageBox() - msgbox.setInformativeText(msg) - msgbox.setStandardButtons(QtWidgets.QMessageBox.Cancel |QtWidgets.QMessageBox.Ok) - msgbox.setDefaultButton(QtWidgets.QMessageBox.Cancel) - result = msgbox.exec_() - if result ==QtWidgets.QMessageBox.Cancel: - return + # msg = "Project file exists. Overwrite?" + # if exists: + # msgbox = QtWidgets.QMessageBox() + # msgbox.setInformativeText(msg) + # msgbox.setStandardButtons(QtWidgets.QMessageBox.Cancel |QtWidgets.QMessageBox.Ok) + # msgbox.setDefaultButton(QtWidgets.QMessageBox.Cancel) + # result = msgbox.exec_() + # if result ==QtWidgets.QMessageBox.Cancel: + # return if thread is True: self.worker_task.emit({'fcn': self.save_project, @@ -5527,9 +5683,38 @@ class App(QtCore.QObject): else: make_black_film() + def export_gerber(self, obj_name, filename, use_thread=True): + """ + Exports a Gerber Object to an Gerber file. + + :param filename: Path to the Gerber file to save to. + :return: + """ + self.report_usage("export_gerber()") + + if filename is None: + filename = self.defaults["global_last_save_folder"] + + self.log.debug("export_gerber()") + + obj = self.collection.get_by_name(obj_name) + + file_string = StringIO(obj.source_file) + time_string = "{:%A, %d %B %Y at %H:%M}".format(datetime.now()) + + with open(filename, 'w') as file: + file.writelines('G04*\n') + file.writelines('G04 GERBER (RE)GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s*\n' % + (str(self.version), str(self.version_date))) + file.writelines('G04 Filename: %s*\n' % str(obj_name)) + file.writelines('G04 Created on : %s*\n' % time_string) + + for line in file_string: + file.writelines(line) + def export_excellon(self, obj_name, filename, use_thread=True): """ - Exports a Geometry Object to an Excellon file. + Exports a Excellon Object to an Excellon file. :param filename: Path to the Excellon file to save to. :return: @@ -5834,7 +6019,7 @@ class App(QtCore.QObject): self.inform.emit("[success] Opened: " + filename) self.progress.emit(100) - def open_gerber(self, filename, follow=False, outname=None): + def open_gerber(self, filename, outname=None): """ Opens a Gerber file, parses it and creates a new object for it in the program. Thread-safe. @@ -5858,7 +6043,7 @@ class App(QtCore.QObject): # Opening the file happens here self.progress.emit(30) try: - gerber_obj.parse_file(filename, follow=follow) + gerber_obj.parse_file(filename) except IOError: app_obj.inform.emit("[ERROR_NOTCL] Failed to open file: " + filename) app_obj.progress.emit(0) @@ -5886,10 +6071,7 @@ class App(QtCore.QObject): # Further parsing self.progress.emit(70) # TODO: Note the mixture of self and app_obj used here - if follow is False: - App.log.debug("open_gerber()") - else: - App.log.debug("open_gerber() with 'follow' attribute") + App.log.debug("open_gerber()") with self.proc_container.new("Opening Gerber") as proc: @@ -6063,7 +6245,7 @@ class App(QtCore.QObject): """ App.log.debug("Opening project: " + filename) - # Open and parse + # Open and parse an uncompressed Project file try: f = open(filename, 'r') except IOError: @@ -6077,7 +6259,16 @@ class App(QtCore.QObject): App.log.error("Failed to parse project file: %s" % filename) self.inform.emit("[ERROR_NOTCL] Failed to parse project file: %s" % filename) f.close() - return + + # Open and parse a compressed Project file + try: + with lzma.open(filename) as f: + file_content = f.read().decode('utf-8') + d = json.loads(file_content, object_hook=dict2obj) + except IOError: + App.log.error("Failed to open project file: %s" % filename) + self.inform.emit("[ERROR_NOTCL] Failed to open project file: %s" % filename) + return self.file_opened.emit("project", filename) @@ -6101,7 +6292,6 @@ class App(QtCore.QObject): obj_inst.from_dict(obj) App.log.debug(obj['kind'] + ": " + obj['options']['name']) self.new_object(obj['kind'], obj['options']['name'], obj_init, active=False, fit=False, plot=True) - self.plot_all() self.inform.emit("[success] Project loaded from: " + filename) @@ -6160,6 +6350,12 @@ class App(QtCore.QObject): self.defaults["global_def_win_w"], self.defaults["global_def_win_h"]) self.ui.splitter.setSizes([self.defaults["def_notebook_width"], 0]) + + settings = QSettings("Open Source", "FlatCAM") + if settings.contains("maximized_gui"): + maximized_ui = settings.value('maximized_gui', type=bool) + if maximized_ui is True: + self.ui.showMaximized() except KeyError: pass @@ -6174,7 +6370,7 @@ class App(QtCore.QObject): for obj in self.collection.get_list(): def worker_task(obj): with self.proc_container.new("Plotting"): - obj.plot() + obj.plot(kind=self.defaults["cncjob_plot_kind"]) if zoom: self.object_plotted.emit(obj) @@ -6793,37 +6989,45 @@ The normal flow when working in FlatCAM is the following:

"options": self.options, "version": self.version} - # Open file - try: - f = open(filename, 'w') - except IOError: - App.log.error("[ERROR] Failed to open file for saving: %s", filename) - return - - # Write - json.dump(d, f, default=to_dict, indent=2, sort_keys=True) - f.close() - - # verification of the saved project - # Open and parse - try: - saved_f = open(filename, 'r') - except IOError: - self.inform.emit("[ERROR_NOTCL] Failed to verify project file: %s. Retry to save it." % filename) - return - - try: - saved_d = json.load(saved_f, object_hook=dict2obj) - except: - self.inform.emit("[ERROR_NOTCL] Failed to parse saved project file: %s. Retry to save it." % filename) - f.close() - return - saved_f.close() - - if 'version' in saved_d: + if self.defaults["global_save_compressed"] is True: + with lzma.open(filename, "w", preset=int(self.defaults['global_compression_level'])) as f: + g = json.dumps(d, default=to_dict, indent=2, sort_keys=True).encode('utf-8') + # # Write + f.write(g) self.inform.emit("[success] Project saved to: %s" % filename) else: - self.inform.emit("[ERROR_NOTCL] Failed to save project file: %s. Retry to save it." % filename) + # Open file + try: + f = open(filename, 'w') + except IOError: + App.log.error("[ERROR] Failed to open file for saving: %s", filename) + return + + # Write + json.dump(d, f, default=to_dict, indent=2, sort_keys=True) + f.close() + + # verification of the saved project + # Open and parse + try: + saved_f = open(filename, 'r') + except IOError: + self.inform.emit("[ERROR_NOTCL] Failed to verify project file: %s. Retry to save it." % filename) + return + + try: + saved_d = json.load(saved_f, object_hook=dict2obj) + except: + self.inform.emit( + "[ERROR_NOTCL] Failed to parse saved project file: %s. Retry to save it." % filename) + f.close() + return + saved_f.close() + + if 'version' in saved_d: + self.inform.emit("[success] Project saved to: %s" % filename) + else: + self.inform.emit("[ERROR_NOTCL] Failed to save project file: %s. Retry to save it." % filename) def on_options_app2project(self): """ diff --git a/FlatCAMEditor.py b/FlatCAMEditor.py index c0bc8261..214f62dc 100644 --- a/FlatCAMEditor.py +++ b/FlatCAMEditor.py @@ -27,7 +27,7 @@ from numpy.linalg import solve from rtree import index as rtindex from GUIElements import OptionalInputSection, FCCheckBox, FCEntry, FCEntry2, FCComboBox, FCTextAreaRich, \ - VerticalScrollArea, FCTable, FCDoubleSpinner + VerticalScrollArea, FCTable, FCDoubleSpinner, FCButton, EvalEntry2, FCInputDialog from ParseFont import * from vispy.scene.visuals import Markers from copy import copy @@ -47,7 +47,14 @@ class BufferSelectionTool(FlatCAMTool): self.draw_app = draw_app # Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName)) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) # this way I can hide/show the frame @@ -63,7 +70,7 @@ class BufferSelectionTool(FlatCAMTool): self.buffer_tools_box.addLayout(form_layout) # Buffer distance - self.buffer_distance_entry = LengthEntry() + self.buffer_distance_entry = FCEntry() form_layout.addRow("Buffer distance:", self.buffer_distance_entry) self.buffer_corner_lbl = QtWidgets.QLabel("Buffer corner:") self.buffer_corner_lbl.setToolTip( @@ -103,22 +110,62 @@ class BufferSelectionTool(FlatCAMTool): # Init GUI self.buffer_distance_entry.set_value(0.01) + def run(self): + self.app.report_usage("Geo Editor ToolBuffer()") + FlatCAMTool.run(self) + + # if the splitter us hidden, display it + if self.app.ui.splitter.sizes()[0] == 0: + self.app.ui.splitter.setSizes([1, 1]) + + self.app.ui.notebook.setTabText(2, "Buffer Tool") + def on_buffer(self): - buffer_distance = self.buffer_distance_entry.get_value() + try: + buffer_distance = float(self.buffer_distance_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + buffer_distance = float(self.buffer_distance_entry.get_value().replace(',', '.')) + self.buffer_distance_entry.set_value(buffer_distance) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Buffer distance value is missing or wrong format. " + "Add it and retry.") + return # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment # I populated the combobox such that the index coincide with the join styles value (which is really an INT) join_style = self.buffer_corner_cb.currentIndex() + 1 self.draw_app.buffer(buffer_distance, join_style) def on_buffer_int(self): - buffer_distance = self.buffer_distance_entry.get_value() + try: + buffer_distance = float(self.buffer_distance_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + buffer_distance = float(self.buffer_distance_entry.get_value().replace(',', '.')) + self.buffer_distance_entry.set_value(buffer_distance) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Buffer distance value is missing or wrong format. " + "Add it and retry.") + return # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment # I populated the combobox such that the index coincide with the join styles value (which is really an INT) join_style = self.buffer_corner_cb.currentIndex() + 1 self.draw_app.buffer_int(buffer_distance, join_style) def on_buffer_ext(self): - buffer_distance = self.buffer_distance_entry.get_value() + try: + buffer_distance = float(self.buffer_distance_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + buffer_distance = float(self.buffer_distance_entry.get_value().replace(',', '.')) + self.buffer_distance_entry.set_value(buffer_distance) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Buffer distance value is missing or wrong format. " + "Add it and retry.") + return # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment # I populated the combobox such that the index coincide with the join styles value (which is really an INT) join_style = self.buffer_corner_cb.currentIndex() + 1 @@ -128,6 +175,7 @@ class BufferSelectionTool(FlatCAMTool): self.buffer_tool_frame.hide() self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) + class TextInputTool(FlatCAMTool): """ Simple input for buffer distance. @@ -153,7 +201,14 @@ class TextInputTool(FlatCAMTool): self.text_tool_frame.setLayout(self.text_tools_box) # Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName)) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.text_tools_box.addWidget(title_label) # Form Layout @@ -333,7 +388,7 @@ class PaintOptionsTool(FlatCAMTool): Inputs to specify how to paint the selected polygons. """ - toolName = "Paint Options" + toolName = "Paint Tool" def __init__(self, app, fcdraw): FlatCAMTool.__init__(self, app) @@ -342,7 +397,14 @@ class PaintOptionsTool(FlatCAMTool): self.fcdraw = fcdraw ## Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName)) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) grid = QtWidgets.QGridLayout() @@ -356,7 +418,7 @@ class PaintOptionsTool(FlatCAMTool): ) grid.addWidget(ptdlabel, 0, 0) - self.painttooldia_entry = LengthEntry() + self.painttooldia_entry = FCEntry() grid.addWidget(self.painttooldia_entry, 0, 1) # Overlap @@ -373,7 +435,8 @@ class PaintOptionsTool(FlatCAMTool): "due of too many paths." ) grid.addWidget(ovlabel, 1, 0) - self.paintoverlap_entry = LengthEntry() + self.paintoverlap_entry = FCEntry() + self.paintoverlap_entry.setValidator(QtGui.QDoubleValidator(0.0000, 1.0000, 4)) grid.addWidget(self.paintoverlap_entry, 1, 1) # Margin @@ -384,7 +447,7 @@ class PaintOptionsTool(FlatCAMTool): "be painted." ) grid.addWidget(marginlabel, 2, 0) - self.paintmargin_entry = LengthEntry() + self.paintmargin_entry = FCEntry() grid.addWidget(self.paintmargin_entry, 2, 1) # Method @@ -434,18 +497,89 @@ class PaintOptionsTool(FlatCAMTool): ## Signals self.paint_button.clicked.connect(self.on_paint) - ## Init GUI - self.painttooldia_entry.set_value(0) - self.paintoverlap_entry.set_value(0) - self.paintmargin_entry.set_value(0) - self.paintmethod_combo.set_value("seed") + self.set_tool_ui() + def run(self): + self.app.report_usage("Geo Editor ToolPaint()") + FlatCAMTool.run(self) + + # if the splitter us hidden, display it + if self.app.ui.splitter.sizes()[0] == 0: + self.app.ui.splitter.setSizes([1, 1]) + + self.app.ui.notebook.setTabText(2, "Paint Tool") + + def set_tool_ui(self): + ## Init GUI + if self.app.defaults["tools_painttooldia"]: + self.painttooldia_entry.set_value(self.app.defaults["tools_painttooldia"]) + else: + self.painttooldia_entry.set_value(0.0) + + if self.app.defaults["tools_paintoverlap"]: + self.paintoverlap_entry.set_value(self.app.defaults["tools_paintoverlap"]) + else: + self.paintoverlap_entry.set_value(0.0) + + if self.app.defaults["tools_paintmargin"]: + self.paintmargin_entry.set_value(self.app.defaults["tools_paintmargin"]) + else: + self.paintmargin_entry.set_value(0.0) + + if self.app.defaults["tools_paintmethod"]: + self.paintmethod_combo.set_value(self.app.defaults["tools_paintmethod"]) + else: + self.paintmethod_combo.set_value("seed") + + if self.app.defaults["tools_pathconnect"]: + self.pathconnect_cb.set_value(self.app.defaults["tools_pathconnect"]) + else: + self.pathconnect_cb.set_value(False) + + if self.app.defaults["tools_paintcontour"]: + self.paintcontour_cb.set_value(self.app.defaults["tools_paintcontour"]) + else: + self.paintcontour_cb.set_value(False) def on_paint(self): + if not self.fcdraw.selected: + self.app.inform.emit("[WARNING_NOTCL] Paint cancelled. No shape selected.") + return - tooldia = self.painttooldia_entry.get_value() - overlap = self.paintoverlap_entry.get_value() - margin = self.paintmargin_entry.get_value() + try: + tooldia = float(self.painttooldia_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + tooldia = float(self.painttooldia_entry.get_value().replace(',', '.')) + self.painttooldia_entry.set_value(tooldia) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Tool diameter value is missing or wrong format. " + "Add it and retry.") + return + try: + overlap = float(self.paintoverlap_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + overlap = float(self.paintoverlap_entry.get_value().replace(',', '.')) + self.paintoverlap_entry.set_value(overlap) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Overlap value is missing or wrong format. " + "Add it and retry.") + return + + try: + margin = float(self.paintmargin_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + margin = float(self.paintmargin_entry.get_value().replace(',', '.')) + self.paintmargin_entry.set_value(margin) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Margin distance value is missing or wrong format. " + "Add it and retry.") + return method = self.paintmethod_combo.get_value() contour = self.paintcontour_cb.get_value() connect = self.pathconnect_cb.get_value() @@ -455,6 +589,971 @@ class PaintOptionsTool(FlatCAMTool): self.app.ui.notebook.setTabText(2, "Tools") self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) + self.app.ui.splitter.setSizes([0, 1]) + + +class TransformEditorTool(FlatCAMTool): + """ + Inputs to specify how to paint the selected polygons. + """ + + toolName = "Transform Tool" + rotateName = "Rotate" + skewName = "Skew/Shear" + scaleName = "Scale" + flipName = "Mirror (Flip)" + offsetName = "Offset" + + def __init__(self, app, draw_app): + FlatCAMTool.__init__(self, app) + + self.app = app + self.draw_app = draw_app + + self.transform_lay = QtWidgets.QVBoxLayout() + self.layout.addLayout(self.transform_lay) + ## Title + title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName)) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) + self.transform_lay.addWidget(title_label) + + self.empty_label = QtWidgets.QLabel("") + self.empty_label.setFixedWidth(50) + + self.empty_label1 = QtWidgets.QLabel("") + self.empty_label1.setFixedWidth(70) + self.empty_label2 = QtWidgets.QLabel("") + self.empty_label2.setFixedWidth(70) + self.empty_label3 = QtWidgets.QLabel("") + self.empty_label3.setFixedWidth(70) + self.empty_label4 = QtWidgets.QLabel("") + self.empty_label4.setFixedWidth(70) + self.transform_lay.addWidget(self.empty_label) + + ## Rotate Title + rotate_title_label = QtWidgets.QLabel("%s" % self.rotateName) + self.transform_lay.addWidget(rotate_title_label) + + ## Layout + form_layout = QtWidgets.QFormLayout() + self.transform_lay.addLayout(form_layout) + form_child = QtWidgets.QHBoxLayout() + + self.rotate_label = QtWidgets.QLabel("Angle:") + self.rotate_label.setToolTip( + "Angle for Rotation action, in degrees.\n" + "Float number between -360 and 359.\n" + "Positive numbers for CW motion.\n" + "Negative numbers for CCW motion." + ) + self.rotate_label.setFixedWidth(50) + + self.rotate_entry = FCEntry() + # self.rotate_entry.setFixedWidth(60) + self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + + self.rotate_button = FCButton() + self.rotate_button.set_value("Rotate") + self.rotate_button.setToolTip( + "Rotate the selected shape(s).\n" + "The point of reference is the middle of\n" + "the bounding box for all selected shapes." + ) + self.rotate_button.setFixedWidth(60) + + form_child.addWidget(self.rotate_entry) + form_child.addWidget(self.rotate_button) + + form_layout.addRow(self.rotate_label, form_child) + + self.transform_lay.addWidget(self.empty_label1) + + ## Skew Title + skew_title_label = QtWidgets.QLabel("%s" % self.skewName) + self.transform_lay.addWidget(skew_title_label) + + ## Form Layout + form1_layout = QtWidgets.QFormLayout() + self.transform_lay.addLayout(form1_layout) + form1_child_1 = QtWidgets.QHBoxLayout() + form1_child_2 = QtWidgets.QHBoxLayout() + + self.skewx_label = QtWidgets.QLabel("Angle X:") + self.skewx_label.setToolTip( + "Angle for Skew action, in degrees.\n" + "Float number between -360 and 359." + ) + self.skewx_label.setFixedWidth(50) + self.skewx_entry = FCEntry() + self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + # self.skewx_entry.setFixedWidth(60) + + self.skewx_button = FCButton() + self.skewx_button.set_value("Skew X") + self.skewx_button.setToolTip( + "Skew/shear the selected shape(s).\n" + "The point of reference is the middle of\n" + "the bounding box for all selected shapes.") + self.skewx_button.setFixedWidth(60) + + self.skewy_label = QtWidgets.QLabel("Angle Y:") + self.skewy_label.setToolTip( + "Angle for Skew action, in degrees.\n" + "Float number between -360 and 359." + ) + self.skewy_label.setFixedWidth(50) + self.skewy_entry = FCEntry() + self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + # self.skewy_entry.setFixedWidth(60) + + self.skewy_button = FCButton() + self.skewy_button.set_value("Skew Y") + self.skewy_button.setToolTip( + "Skew/shear the selected shape(s).\n" + "The point of reference is the middle of\n" + "the bounding box for all selected shapes.") + self.skewy_button.setFixedWidth(60) + + form1_child_1.addWidget(self.skewx_entry) + form1_child_1.addWidget(self.skewx_button) + + form1_child_2.addWidget(self.skewy_entry) + form1_child_2.addWidget(self.skewy_button) + + form1_layout.addRow(self.skewx_label, form1_child_1) + form1_layout.addRow(self.skewy_label, form1_child_2) + + self.transform_lay.addWidget(self.empty_label2) + + ## Scale Title + scale_title_label = QtWidgets.QLabel("%s" % self.scaleName) + self.transform_lay.addWidget(scale_title_label) + + ## Form Layout + form2_layout = QtWidgets.QFormLayout() + self.transform_lay.addLayout(form2_layout) + form2_child_1 = QtWidgets.QHBoxLayout() + form2_child_2 = QtWidgets.QHBoxLayout() + + self.scalex_label = QtWidgets.QLabel("Factor X:") + self.scalex_label.setToolTip( + "Factor for Scale action over X axis." + ) + self.scalex_label.setFixedWidth(50) + self.scalex_entry = FCEntry() + self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + # self.scalex_entry.setFixedWidth(60) + + self.scalex_button = FCButton() + self.scalex_button.set_value("Scale X") + self.scalex_button.setToolTip( + "Scale the selected shape(s).\n" + "The point of reference depends on \n" + "the Scale reference checkbox state.") + self.scalex_button.setFixedWidth(60) + + self.scaley_label = QtWidgets.QLabel("Factor Y:") + self.scaley_label.setToolTip( + "Factor for Scale action over Y axis." + ) + self.scaley_label.setFixedWidth(50) + self.scaley_entry = FCEntry() + self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + # self.scaley_entry.setFixedWidth(60) + + self.scaley_button = FCButton() + self.scaley_button.set_value("Scale Y") + self.scaley_button.setToolTip( + "Scale the selected shape(s).\n" + "The point of reference depends on \n" + "the Scale reference checkbox state.") + self.scaley_button.setFixedWidth(60) + + self.scale_link_cb = FCCheckBox() + self.scale_link_cb.set_value(True) + self.scale_link_cb.setText("Link") + self.scale_link_cb.setToolTip( + "Scale the selected shape(s)\n" + "using the Scale Factor X for both axis.") + self.scale_link_cb.setFixedWidth(50) + + self.scale_zero_ref_cb = FCCheckBox() + self.scale_zero_ref_cb.set_value(True) + self.scale_zero_ref_cb.setText("Scale Reference") + self.scale_zero_ref_cb.setToolTip( + "Scale the selected shape(s)\n" + "using the origin reference when checked,\n" + "and the center of the biggest bounding box\n" + "of the selected shapes when unchecked.") + + form2_child_1.addWidget(self.scalex_entry) + form2_child_1.addWidget(self.scalex_button) + + form2_child_2.addWidget(self.scaley_entry) + form2_child_2.addWidget(self.scaley_button) + + form2_layout.addRow(self.scalex_label, form2_child_1) + form2_layout.addRow(self.scaley_label, form2_child_2) + form2_layout.addRow(self.scale_link_cb, self.scale_zero_ref_cb) + self.ois_scale = OptionalInputSection(self.scale_link_cb, [self.scaley_entry, self.scaley_button], logic=False) + + self.transform_lay.addWidget(self.empty_label3) + + ## Offset Title + offset_title_label = QtWidgets.QLabel("%s" % self.offsetName) + self.transform_lay.addWidget(offset_title_label) + + ## Form Layout + form3_layout = QtWidgets.QFormLayout() + self.transform_lay.addLayout(form3_layout) + form3_child_1 = QtWidgets.QHBoxLayout() + form3_child_2 = QtWidgets.QHBoxLayout() + + self.offx_label = QtWidgets.QLabel("Value X:") + self.offx_label.setToolTip( + "Value for Offset action on X axis." + ) + self.offx_label.setFixedWidth(50) + self.offx_entry = FCEntry() + self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + # self.offx_entry.setFixedWidth(60) + + self.offx_button = FCButton() + self.offx_button.set_value("Offset X") + self.offx_button.setToolTip( + "Offset the selected shape(s).\n" + "The point of reference is the middle of\n" + "the bounding box for all selected shapes.\n") + self.offx_button.setFixedWidth(60) + + self.offy_label = QtWidgets.QLabel("Value Y:") + self.offy_label.setToolTip( + "Value for Offset action on Y axis." + ) + self.offy_label.setFixedWidth(50) + self.offy_entry = FCEntry() + self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + # self.offy_entry.setFixedWidth(60) + + self.offy_button = FCButton() + self.offy_button.set_value("Offset Y") + self.offy_button.setToolTip( + "Offset the selected shape(s).\n" + "The point of reference is the middle of\n" + "the bounding box for all selected shapes.\n") + self.offy_button.setFixedWidth(60) + + form3_child_1.addWidget(self.offx_entry) + form3_child_1.addWidget(self.offx_button) + + form3_child_2.addWidget(self.offy_entry) + form3_child_2.addWidget(self.offy_button) + + form3_layout.addRow(self.offx_label, form3_child_1) + form3_layout.addRow(self.offy_label, form3_child_2) + + self.transform_lay.addWidget(self.empty_label4) + + ## Flip Title + flip_title_label = QtWidgets.QLabel("%s" % self.flipName) + self.transform_lay.addWidget(flip_title_label) + + ## Form Layout + form4_layout = QtWidgets.QFormLayout() + form4_child_hlay = QtWidgets.QHBoxLayout() + self.transform_lay.addLayout(form4_child_hlay) + self.transform_lay.addLayout(form4_layout) + form4_child_1 = QtWidgets.QHBoxLayout() + + self.flipx_button = FCButton() + self.flipx_button.set_value("Flip on X") + self.flipx_button.setToolTip( + "Flip the selected shape(s) over the X axis.\n" + "Does not create a new shape.\n " + ) + self.flipx_button.setFixedWidth(60) + + self.flipy_button = FCButton() + self.flipy_button.set_value("Flip on Y") + self.flipy_button.setToolTip( + "Flip the selected shape(s) over the X axis.\n" + "Does not create a new shape.\n " + ) + self.flipy_button.setFixedWidth(60) + + self.flip_ref_cb = FCCheckBox() + self.flip_ref_cb.set_value(True) + self.flip_ref_cb.setText("Ref Pt") + self.flip_ref_cb.setToolTip( + "Flip the selected shape(s)\n" + "around the point in Point Entry Field.\n" + "\n" + "The point coordinates can be captured by\n" + "left click on canvas together with pressing\n" + "SHIFT key. \n" + "Then click Add button to insert coordinates.\n" + "Or enter the coords in format (x, y) in the\n" + "Point Entry field and click Flip on X(Y)") + self.flip_ref_cb.setFixedWidth(50) + + self.flip_ref_label = QtWidgets.QLabel("Point:") + self.flip_ref_label.setToolTip( + "Coordinates in format (x, y) used as reference for mirroring.\n" + "The 'x' in (x, y) will be used when using Flip on X and\n" + "the 'y' in (x, y) will be used when using Flip on Y and" + ) + self.flip_ref_label.setFixedWidth(50) + self.flip_ref_entry = EvalEntry2("(0, 0)") + self.flip_ref_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + # self.flip_ref_entry.setFixedWidth(60) + + self.flip_ref_button = FCButton() + self.flip_ref_button.set_value("Add") + self.flip_ref_button.setToolTip( + "The point coordinates can be captured by\n" + "left click on canvas together with pressing\n" + "SHIFT key. Then click Add button to insert.") + self.flip_ref_button.setFixedWidth(60) + + form4_child_hlay.addStretch() + form4_child_hlay.addWidget(self.flipx_button) + form4_child_hlay.addWidget(self.flipy_button) + + form4_child_1.addWidget(self.flip_ref_entry) + form4_child_1.addWidget(self.flip_ref_button) + + form4_layout.addRow(self.flip_ref_cb) + form4_layout.addRow(self.flip_ref_label, form4_child_1) + self.ois_flip = OptionalInputSection(self.flip_ref_cb, + [self.flip_ref_entry, self.flip_ref_button], logic=True) + + self.transform_lay.addStretch() + + ## Signals + self.rotate_button.clicked.connect(self.on_rotate) + self.skewx_button.clicked.connect(self.on_skewx) + self.skewy_button.clicked.connect(self.on_skewy) + self.scalex_button.clicked.connect(self.on_scalex) + self.scaley_button.clicked.connect(self.on_scaley) + self.offx_button.clicked.connect(self.on_offx) + self.offy_button.clicked.connect(self.on_offy) + self.flipx_button.clicked.connect(self.on_flipx) + self.flipy_button.clicked.connect(self.on_flipy) + self.flip_ref_button.clicked.connect(self.on_flip_add_coords) + + self.rotate_entry.returnPressed.connect(self.on_rotate) + self.skewx_entry.returnPressed.connect(self.on_skewx) + self.skewy_entry.returnPressed.connect(self.on_skewy) + self.scalex_entry.returnPressed.connect(self.on_scalex) + self.scaley_entry.returnPressed.connect(self.on_scaley) + self.offx_entry.returnPressed.connect(self.on_offx) + self.offy_entry.returnPressed.connect(self.on_offy) + + self.set_tool_ui() + + def run(self): + self.app.report_usage("Geo Editor Transform Tool()") + FlatCAMTool.run(self) + self.set_tool_ui() + + # if the splitter us hidden, display it + if self.app.ui.splitter.sizes()[0] == 0: + self.app.ui.splitter.setSizes([1, 1]) + + self.app.ui.notebook.setTabText(2, "Transform Tool") + + def install(self, icon=None, separator=None, **kwargs): + FlatCAMTool.install(self, icon, separator, shortcut='ALT+T', **kwargs) + + def set_tool_ui(self): + ## Initialize form + if self.app.defaults["tools_transform_rotate"]: + self.rotate_entry.set_value(self.app.defaults["tools_transform_rotate"]) + else: + self.rotate_entry.set_value(0.0) + + if self.app.defaults["tools_transform_skew_x"]: + self.skewx_entry.set_value(self.app.defaults["tools_transform_skew_x"]) + else: + self.skewx_entry.set_value(0.0) + + if self.app.defaults["tools_transform_skew_y"]: + self.skewy_entry.set_value(self.app.defaults["tools_transform_skew_y"]) + else: + self.skewy_entry.set_value(0.0) + + if self.app.defaults["tools_transform_scale_x"]: + self.scalex_entry.set_value(self.app.defaults["tools_transform_scale_x"]) + else: + self.scalex_entry.set_value(1.0) + + if self.app.defaults["tools_transform_scale_y"]: + self.scaley_entry.set_value(self.app.defaults["tools_transform_scale_y"]) + else: + self.scaley_entry.set_value(1.0) + + if self.app.defaults["tools_transform_scale_link"]: + self.scale_link_cb.set_value(self.app.defaults["tools_transform_scale_link"]) + else: + self.scale_link_cb.set_value(True) + + if self.app.defaults["tools_transform_scale_reference"]: + self.scale_zero_ref_cb.set_value(self.app.defaults["tools_transform_scale_reference"]) + else: + self.scale_zero_ref_cb.set_value(True) + + if self.app.defaults["tools_transform_offset_x"]: + self.offx_entry.set_value(self.app.defaults["tools_transform_offset_x"]) + else: + self.offx_entry.set_value(0.0) + + if self.app.defaults["tools_transform_offset_y"]: + self.offy_entry.set_value(self.app.defaults["tools_transform_offset_y"]) + else: + self.offy_entry.set_value(0.0) + + if self.app.defaults["tools_transform_mirror_reference"]: + self.flip_ref_cb.set_value(self.app.defaults["tools_transform_mirror_reference"]) + else: + self.flip_ref_cb.set_value(False) + + if self.app.defaults["tools_transform_mirror_point"]: + self.flip_ref_entry.set_value(self.app.defaults["tools_transform_mirror_point"]) + else: + self.flip_ref_entry.set_value((0, 0)) + + def template(self): + if not self.fcdraw.selected: + self.app.inform.emit("[WARNING_NOTCL] Transformation cancelled. No shape selected.") + return + + + self.draw_app.select_tool("select") + self.app.ui.notebook.setTabText(2, "Tools") + self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) + + self.app.ui.splitter.setSizes([0, 1]) + + def on_rotate(self, sig=None, val=None): + if val: + value = val + else: + try: + value = float(self.rotate_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + value = float(self.rotate_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Rotate, " + "use a number.") + return + self.app.worker_task.emit({'fcn': self.on_rotate_action, + 'params': [value]}) + # self.on_rotate_action(value) + return + + def on_flipx(self): + # self.on_flip("Y") + axis = 'Y' + self.app.worker_task.emit({'fcn': self.on_flip, + 'params': [axis]}) + return + + def on_flipy(self): + # self.on_flip("X") + axis = 'X' + self.app.worker_task.emit({'fcn': self.on_flip, + 'params': [axis]}) + return + + def on_flip_add_coords(self): + val = self.app.clipboard.text() + self.flip_ref_entry.set_value(val) + + def on_skewx(self, sig=None, val=None): + if val: + value = val + else: + try: + value = float(self.skewx_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + value = float(self.skewx_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Skew X, " + "use a number.") + return + + # self.on_skew("X", value) + axis = 'X' + self.app.worker_task.emit({'fcn': self.on_skew, + 'params': [axis, value]}) + return + + def on_skewy(self, sig=None, val=None): + if val: + value = val + else: + try: + value = float(self.skewy_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + value = float(self.skewy_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Skew Y, " + "use a number.") + return + + # self.on_skew("Y", value) + axis = 'Y' + self.app.worker_task.emit({'fcn': self.on_skew, + 'params': [axis, value]}) + return + + def on_scalex(self, sig=None, val=None): + if val: + xvalue = val + else: + try: + xvalue = float(self.scalex_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + xvalue = float(self.scalex_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Scale X, " + "use a number.") + return + + # scaling to zero has no sense so we remove it, because scaling with 1 does nothing + if xvalue == 0: + xvalue = 1 + if self.scale_link_cb.get_value(): + yvalue = xvalue + else: + yvalue = 1 + + axis = 'X' + point = (0, 0) + if self.scale_zero_ref_cb.get_value(): + self.app.worker_task.emit({'fcn': self.on_scale, + 'params': [axis, xvalue, yvalue, point]}) + # self.on_scale("X", xvalue, yvalue, point=(0,0)) + else: + # self.on_scale("X", xvalue, yvalue) + self.app.worker_task.emit({'fcn': self.on_scale, + 'params': [axis, xvalue, yvalue]}) + + return + + def on_scaley(self, sig=None, val=None): + xvalue = 1 + if val: + yvalue = val + else: + try: + yvalue = float(self.scaley_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + yvalue = float(self.scaley_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Scale Y, " + "use a number.") + return + + # scaling to zero has no sense so we remove it, because scaling with 1 does nothing + if yvalue == 0: + yvalue = 1 + + axis = 'Y' + point = (0, 0) + if self.scale_zero_ref_cb.get_value(): + self.app.worker_task.emit({'fcn': self.on_scale, + 'params': [axis, xvalue, yvalue, point]}) + # self.on_scale("Y", xvalue, yvalue, point=(0,0)) + else: + # self.on_scale("Y", xvalue, yvalue) + self.app.worker_task.emit({'fcn': self.on_scale, + 'params': [axis, xvalue, yvalue]}) + + return + + def on_offx(self, sig=None, val=None): + if val: + value = val + else: + try: + value = float(self.offx_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + value = float(self.offx_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Offset X, " + "use a number.") + return + + # self.on_offset("X", value) + axis = 'X' + self.app.worker_task.emit({'fcn': self.on_offset, + 'params': [axis, value]}) + return + + def on_offy(self, sig=None, val=None): + if val: + value = val + else: + try: + value = float(self.offy_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + value = float(self.offy_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Offset Y, " + "use a number.") + return + + # self.on_offset("Y", value) + axis = 'Y' + self.app.worker_task.emit({'fcn': self.on_offset, + 'params': [axis, value]}) + return + + def on_rotate_action(self, num): + shape_list = self.draw_app.selected + xminlist = [] + yminlist = [] + xmaxlist = [] + ymaxlist = [] + + if not shape_list: + self.app.inform.emit("[WARNING_NOTCL] No shape selected. Please Select a shape to rotate!") + return + else: + with self.app.proc_container.new("Appying Rotate"): + try: + # first get a bounding box to fit all + for sha in shape_list: + xmin, ymin, xmax, ymax = sha.bounds() + xminlist.append(xmin) + yminlist.append(ymin) + xmaxlist.append(xmax) + ymaxlist.append(ymax) + + # get the minimum x,y and maximum x,y for all objects selected + xminimal = min(xminlist) + yminimal = min(yminlist) + xmaximal = max(xmaxlist) + ymaximal = max(ymaxlist) + + self.app.progress.emit(20) + + for sel_sha in shape_list: + px = 0.5 * (xminimal + xmaximal) + py = 0.5 * (yminimal + ymaximal) + + sel_sha.rotate(-num, point=(px, py)) + self.draw_app.replot() + # self.draw_app.add_shape(DrawToolShape(sel_sha.geo)) + + # self.draw_app.transform_complete.emit() + + self.app.inform.emit("[success] Done. Rotate completed.") + + self.app.progress.emit(100) + + except Exception as e: + self.app.inform.emit("[ERROR_NOTCL] Due of %s, rotation movement was not executed." % str(e)) + return + + def on_flip(self, axis): + shape_list = self.draw_app.selected + xminlist = [] + yminlist = [] + xmaxlist = [] + ymaxlist = [] + + if not shape_list: + self.app.inform.emit("[WARNING_NOTCL] No shape selected. Please Select a shape to flip!") + return + else: + with self.app.proc_container.new("Applying Flip"): + try: + # get mirroring coords from the point entry + if self.flip_ref_cb.isChecked(): + px, py = eval('{}'.format(self.flip_ref_entry.text())) + # get mirroing coords from the center of an all-enclosing bounding box + else: + # first get a bounding box to fit all + for sha in shape_list: + xmin, ymin, xmax, ymax = sha.bounds() + xminlist.append(xmin) + yminlist.append(ymin) + xmaxlist.append(xmax) + ymaxlist.append(ymax) + + # get the minimum x,y and maximum x,y for all objects selected + xminimal = min(xminlist) + yminimal = min(yminlist) + xmaximal = max(xmaxlist) + ymaximal = max(ymaxlist) + + px = 0.5 * (xminimal + xmaximal) + py = 0.5 * (yminimal + ymaximal) + + self.app.progress.emit(20) + + # execute mirroring + for sha in shape_list: + if axis is 'X': + sha.mirror('X', (px, py)) + self.app.inform.emit('[success] Flip on the Y axis done ...') + elif axis is 'Y': + sha.mirror('Y', (px, py)) + self.app.inform.emit('[success] Flip on the X axis done ...') + self.draw_app.replot() + + # self.draw_app.add_shape(DrawToolShape(sha.geo)) + # + # self.draw_app.transform_complete.emit() + + self.app.progress.emit(100) + + except Exception as e: + self.app.inform.emit("[ERROR_NOTCL] Due of %s, Flip action was not executed." % str(e)) + return + + def on_skew(self, axis, num): + shape_list = self.draw_app.selected + xminlist = [] + yminlist = [] + + if not shape_list: + self.app.inform.emit("[WARNING_NOTCL] No shape selected. Please Select a shape to shear/skew!") + return + else: + with self.app.proc_container.new("Applying Skew"): + try: + # first get a bounding box to fit all + for sha in shape_list: + xmin, ymin, xmax, ymax = sha.bounds() + xminlist.append(xmin) + yminlist.append(ymin) + + # get the minimum x,y and maximum x,y for all objects selected + xminimal = min(xminlist) + yminimal = min(yminlist) + + self.app.progress.emit(20) + + for sha in shape_list: + if axis is 'X': + sha.skew(num, 0, point=(xminimal, yminimal)) + elif axis is 'Y': + sha.skew(0, num, point=(xminimal, yminimal)) + self.draw_app.replot() + + # self.draw_app.add_shape(DrawToolShape(sha.geo)) + # + # self.draw_app.transform_complete.emit() + + self.app.inform.emit('[success] Skew on the %s axis done ...' % str(axis)) + self.app.progress.emit(100) + + except Exception as e: + self.app.inform.emit("[ERROR_NOTCL] Due of %s, Skew action was not executed." % str(e)) + return + + def on_scale(self, axis, xfactor, yfactor, point=None): + shape_list = self.draw_app.selected + xminlist = [] + yminlist = [] + xmaxlist = [] + ymaxlist = [] + + if not shape_list: + self.app.inform.emit("[WARNING_NOTCL] No shape selected. Please Select a shape to scale!") + return + else: + with self.app.proc_container.new("Applying Scale"): + try: + # first get a bounding box to fit all + for sha in shape_list: + xmin, ymin, xmax, ymax = sha.bounds() + xminlist.append(xmin) + yminlist.append(ymin) + xmaxlist.append(xmax) + ymaxlist.append(ymax) + + # get the minimum x,y and maximum x,y for all objects selected + xminimal = min(xminlist) + yminimal = min(yminlist) + xmaximal = max(xmaxlist) + ymaximal = max(ymaxlist) + + self.app.progress.emit(20) + + if point is None: + px = 0.5 * (xminimal + xmaximal) + py = 0.5 * (yminimal + ymaximal) + else: + px = 0 + py = 0 + + for sha in shape_list: + sha.scale(xfactor, yfactor, point=(px, py)) + self.draw_app.replot() + + # self.draw_app.add_shape(DrawToolShape(sha.geo)) + # + # self.draw_app.transform_complete.emit() + + self.app.inform.emit('[success] Scale on the %s axis done ...' % str(axis)) + self.app.progress.emit(100) + except Exception as e: + self.app.inform.emit("[ERROR_NOTCL] Due of %s, Scale action was not executed." % str(e)) + return + + def on_offset(self, axis, num): + shape_list = self.draw_app.selected + xminlist = [] + yminlist = [] + + if not shape_list: + self.app.inform.emit("[WARNING_NOTCL] No shape selected. Please Select a shape to offset!") + return + else: + with self.app.proc_container.new("Applying Offset"): + try: + # first get a bounding box to fit all + for sha in shape_list: + xmin, ymin, xmax, ymax = sha.bounds() + xminlist.append(xmin) + yminlist.append(ymin) + + # get the minimum x,y and maximum x,y for all objects selected + xminimal = min(xminlist) + yminimal = min(yminlist) + self.app.progress.emit(20) + + for sha in shape_list: + if axis is 'X': + sha.offset((num, 0)) + elif axis is 'Y': + sha.offset((0, num)) + self.draw_app.replot() + + # self.draw_app.add_shape(DrawToolShape(sha.geo)) + # + # self.draw_app.transform_complete.emit() + + self.app.inform.emit('[success] Offset on the %s axis done ...' % str(axis)) + self.app.progress.emit(100) + + except Exception as e: + self.app.inform.emit("[ERROR_NOTCL] Due of %s, Offset action was not executed." % str(e)) + return + + def on_rotate_key(self): + val_box = FCInputDialog(title="Rotate ...", + text='Enter an Angle Value (degrees):', + min=-359.9999, max=360.0000, decimals=4, + init_val=float(self.app.defaults['tools_transform_rotate'])) + val_box.setWindowIcon(QtGui.QIcon('share/rotate.png')) + + val, ok = val_box.get_value() + if ok: + self.on_rotate(val=val) + self.app.inform.emit( + "[success] Geometry shape rotate done...") + return + else: + self.app.inform.emit( + "[WARNING_NOTCL] Geometry shape rotate cancelled...") + + def on_offx_key(self): + units = self.app.general_options_form.general_app_group.units_radio.get_value().lower() + + val_box = FCInputDialog(title="Offset on X axis ...", + text=('Enter a distance Value (%s):' % str(units)), + min=-9999.9999, max=10000.0000, decimals=4, + init_val=float(self.app.defaults['tools_transform_offset_x'])) + val_box.setWindowIcon(QtGui.QIcon('share/offsetx32.png')) + + val, ok = val_box.get_value() + if ok: + self.on_offx(val=val) + self.app.inform.emit( + "[success] Geometry shape offset on X axis done...") + return + else: + self.app.inform.emit( + "[WARNING_NOTCL] Geometry shape offset X cancelled...") + + def on_offy_key(self): + units = self.app.general_options_form.general_app_group.units_radio.get_value().lower() + + val_box = FCInputDialog(title="Offset on Y axis ...", + text=('Enter a distance Value (%s):' % str(units)), + min=-9999.9999, max=10000.0000, decimals=4, + init_val=float(self.app.defaults['tools_transform_offset_y'])) + val_box.setWindowIcon(QtGui.QIcon('share/offsety32.png')) + + val, ok = val_box.get_value() + if ok: + self.on_offx(val=val) + self.app.inform.emit( + "[success] Geometry shape offset on Y axis done...") + return + else: + self.app.inform.emit( + "[WARNING_NOTCL] Geometry shape offset Y cancelled...") + + def on_skewx_key(self): + val_box = FCInputDialog(title="Skew on X axis ...", + text='Enter an Angle Value (degrees):', + min=-359.9999, max=360.0000, decimals=4, + init_val=float(self.app.defaults['tools_transform_skew_x'])) + val_box.setWindowIcon(QtGui.QIcon('share/skewX.png')) + + val, ok = val_box.get_value() + if ok: + self.on_skewx(val=val) + self.app.inform.emit( + "[success] Geometry shape skew on X axis done...") + return + else: + self.app.inform.emit( + "[WARNING_NOTCL] Geometry shape skew X cancelled...") + + def on_skewy_key(self): + val_box = FCInputDialog(title="Skew on Y axis ...", + text='Enter an Angle Value (degrees):', + min=-359.9999, max=360.0000, decimals=4, + init_val=float(self.app.defaults['tools_transform_skew_y'])) + val_box.setWindowIcon(QtGui.QIcon('share/skewY.png')) + + val, ok = val_box.get_value() + if ok: + self.on_skewx(val=val) + self.app.inform.emit( + "[success] Geometry shape skew on Y axis done...") + return + else: + self.app.inform.emit( + "[WARNING_NOTCL] Geometry shape skew Y cancelled...") + class DrawToolShape(object): """ @@ -515,6 +1614,205 @@ class DrawToolShape(object): def get_all_points(self): return DrawToolShape.get_pts(self) + def bounds(self): + """ + Returns coordinates of rectangular bounds + of geometry: (xmin, ymin, xmax, ymax). + """ + # fixed issue of getting bounds only for one level lists of objects + # now it can get bounds for nested lists of objects + def bounds_rec(shape): + if type(shape) is list: + minx = Inf + miny = Inf + maxx = -Inf + maxy = -Inf + + for k in shape: + minx_, miny_, maxx_, maxy_ = bounds_rec(k) + minx = min(minx, minx_) + miny = min(miny, miny_) + maxx = max(maxx, maxx_) + maxy = max(maxy, maxy_) + return minx, miny, maxx, maxy + else: + # it's a Shapely object, return it's bounds + return shape.bounds + + bounds_coords = bounds_rec(self.geo) + return bounds_coords + + def mirror(self, axis, point): + """ + Mirrors the shape around a specified axis passing through + the given point. + + :param axis: "X" or "Y" indicates around which axis to mirror. + :type axis: str + :param point: [x, y] point belonging to the mirror axis. + :type point: list + :return: None + """ + + px, py = point + xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis] + + def mirror_geom(shape): + if type(shape) is list: + new_obj = [] + for g in shape: + new_obj.append(mirror_geom(g)) + return new_obj + else: + return affinity.scale(shape, xscale, yscale, origin=(px,py)) + + try: + self.geo = mirror_geom(self.geo) + except AttributeError: + log.debug("DrawToolShape.mirror() --> Failed to mirror. No shape selected") + + def rotate(self, angle, point): + """ + Rotate a shape by an angle (in degrees) around the provided coordinates. + + Parameters + ---------- + The angle of rotation are specified in degrees (default). Positive angles are + counter-clockwise and negative are clockwise rotations. + + The point of origin can be a keyword 'center' for the bounding box + center (default), 'centroid' for the geometry's centroid, a Point object + or a coordinate tuple (x0, y0). + + See shapely manual for more information: + http://toblerity.org/shapely/manual.html#affine-transformations + """ + + px, py = point + + def rotate_geom(shape): + if type(shape) is list: + new_obj = [] + for g in shape: + new_obj.append(rotate_geom(g)) + return new_obj + else: + return affinity.rotate(shape, angle, origin=(px, py)) + + try: + self.geo = rotate_geom(self.geo) + except AttributeError: + log.debug("DrawToolShape.rotate() --> Failed to rotate. No shape selected") + + def skew(self, angle_x, angle_y, point): + """ + Shear/Skew a shape by angles along x and y dimensions. + + Parameters + ---------- + angle_x, angle_y : float, float + The shear angle(s) for the x and y axes respectively. These can be + specified in either degrees (default) or radians by setting + use_radians=True. + point: tuple of coordinates (x,y) + + See shapely manual for more information: + http://toblerity.org/shapely/manual.html#affine-transformations + """ + px, py = point + + def skew_geom(shape): + if type(shape) is list: + new_obj = [] + for g in shape: + new_obj.append(skew_geom(g)) + return new_obj + else: + return affinity.skew(shape, angle_x, angle_y, origin=(px, py)) + + try: + self.geo = skew_geom(self.geo) + except AttributeError: + log.debug("DrawToolShape.skew() --> Failed to skew. No shape selected") + + def offset(self, vect): + """ + Offsets all shapes by a given vector/ + + :param vect: (x, y) vector by which to offset the shape geometry + :type vect: tuple + :return: None + :rtype: None + """ + + try: + dx, dy = vect + except TypeError: + log.debug("DrawToolShape.offset() --> An (x,y) pair of values are needed. " + "Probable you entered only one value in the Offset field.") + return + + def translate_recursion(geom): + if type(geom) == list: + geoms=list() + for local_geom in geom: + geoms.append(translate_recursion(local_geom)) + return geoms + else: + return affinity.translate(geom, xoff=dx, yoff=dy) + + try: + self.geo = translate_recursion(self.geo) + except AttributeError: + log.debug("DrawToolShape.offset() --> Failed to offset. No shape selected") + + def scale(self, xfactor, yfactor=None, point=None): + """ + Scales all shape geometry by a given factor. + + :param xfactor: Factor by which to scale the shape's geometry/ + :type xfactor: float + :param yfactor: Factor by which to scale the shape's geometry/ + :type yfactor: float + :return: None + :rtype: None + """ + + try: + xfactor = float(xfactor) + except: + log.debug("DrawToolShape.offset() --> Scale factor has to be a number: integer or float.") + return + + if yfactor is None: + yfactor = xfactor + else: + try: + yfactor = float(yfactor) + except: + log.debug("DrawToolShape.offset() --> Scale factor has to be a number: integer or float.") + return + + if point is None: + px = 0 + py = 0 + else: + px, py = point + + def scale_recursion(geom): + if type(geom) == list: + geoms=list() + for local_geom in geom: + geoms.append(scale_recursion(local_geom)) + return geoms + else: + return affinity.scale(geom, xfactor, yfactor, origin=(px, py)) + + try: + self.geo = scale_recursion(self.geo) + except AttributeError: + log.debug("DrawToolShape.scale() --> Failed to scale. No shape selected") + class DrawToolUtilityShape(DrawToolShape): """ @@ -583,7 +1881,7 @@ class FCCircle(FCShapeTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_circle' + self.name = 'circle' self.start_msg = "Click on CENTER ..." self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"] @@ -622,7 +1920,7 @@ class FCCircle(FCShapeTool): class FCArc(FCShapeTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_arc' + self.name = 'arc' self.start_msg = "Click on CENTER ..." @@ -812,7 +2110,7 @@ class FCRectangle(FCShapeTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_rectangle' + self.name = 'rectangle' self.start_msg = "Click on 1st corner ..." @@ -852,7 +2150,7 @@ class FCPolygon(FCShapeTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_polygon' + self.name = 'polygon' self.start_msg = "Click on 1st point ..." @@ -899,7 +2197,7 @@ class FCPath(FCPolygon): def make(self): self.geometry = DrawToolShape(LineString(self.points)) - self.name = 'fc_path' + self.name = 'path' self.draw_app.in_action = False self.complete = True @@ -922,7 +2220,7 @@ class FCPath(FCPolygon): class FCSelect(DrawTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_select' + self.name = 'select' self.storage = self.draw_app.storage # self.shape_buffer = self.draw_app.shape_buffer @@ -1001,7 +2299,7 @@ class FCSelect(DrawTool): class FCDrillSelect(DrawTool): def __init__(self, exc_editor_app): DrawTool.__init__(self, exc_editor_app) - self.name = 'fc_drill_select' + self.name = 'drill_select' self.exc_editor_app = exc_editor_app self.storage = self.exc_editor_app.storage_dict @@ -1159,9 +2457,12 @@ class FCDrillSelect(DrawTool): class FCMove(FCShapeTool): def __init__(self, draw_app): FCShapeTool.__init__(self, draw_app) - self.name = 'fc_move' + self.name = 'move' # self.shape_buffer = self.draw_app.shape_buffer + if not self.draw_app.selected: + self.draw_app.app.inform.emit("[WARNING_NOTCL] Move cancelled. No shape selected.") + return self.origin = None self.destination = None self.start_msg = "Click on reference point." @@ -1217,8 +2518,14 @@ class FCMove(FCShapeTool): dx = data[0] - self.origin[0] dy = data[1] - self.origin[1] - for geom in self.draw_app.get_selected(): - geo_list.append(affinity.translate(geom.geo, xoff=dx, yoff=dy)) + + try: + for geom in self.draw_app.get_selected(): + geo_list.append(affinity.translate(geom.geo, xoff=dx, yoff=dy)) + except AttributeError: + self.draw_app.select_tool('select') + self.draw_app.selected = [] + return return DrawToolUtilityShape(geo_list) # return DrawToolUtilityShape([affinity.translate(geom.geo, xoff=dx, yoff=dy) @@ -1228,7 +2535,7 @@ class FCMove(FCShapeTool): class FCCopy(FCMove): def __init__(self, draw_app): FCMove.__init__(self, draw_app) - self.name = 'fc_copy' + self.name = 'copy' def make(self): # Create new geometry @@ -1243,7 +2550,7 @@ class FCCopy(FCMove): class FCText(FCShapeTool): def __init__(self, draw_app): FCShapeTool.__init__(self, draw_app) - self.name = 'fc_text' + self.name = 'text' # self.shape_buffer = self.draw_app.shape_buffer self.draw_app = draw_app @@ -1295,7 +2602,7 @@ class FCText(FCShapeTool): class FCBuffer(FCShapeTool): def __init__(self, draw_app): FCShapeTool.__init__(self, draw_app) - self.name = 'fc_buffer' + self.name = 'buffer' # self.shape_buffer = self.draw_app.shape_buffer self.draw_app = draw_app @@ -1306,35 +2613,85 @@ class FCBuffer(FCShapeTool): self.buff_tool = BufferSelectionTool(self.app, self.draw_app) self.buff_tool.run() self.app.ui.notebook.setTabText(2, "Buffer Tool") + if self.draw_app.app.ui.splitter.sizes()[0] == 0: + self.draw_app.app.ui.splitter.setSizes([1, 1]) self.activate() def on_buffer(self): - buffer_distance = self.buff_tool.buffer_distance_entry.get_value() + if not self.draw_app.selected: + self.app.inform.emit("[WARNING_NOTCL] Buffer cancelled. No shape selected.") + return + + try: + buffer_distance = float(self.buff_tool.buffer_distance_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + buffer_distance = float(self.buff_tool.buffer_distance_entry.get_value().replace(',', '.')) + self.buff_tool.buffer_distance_entry.set_value(buffer_distance) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Buffer distance value is missing or wrong format. " + "Add it and retry.") + return # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment # I populated the combobox such that the index coincide with the join styles value (whcih is really an INT) join_style = self.buff_tool.buffer_corner_cb.currentIndex() + 1 self.draw_app.buffer(buffer_distance, join_style) self.app.ui.notebook.setTabText(2, "Tools") + self.draw_app.app.ui.splitter.setSizes([0, 1]) + self.disactivate() self.draw_app.app.inform.emit("[success]Done. Buffer Tool completed.") def on_buffer_int(self): - buffer_distance = self.buff_tool.buffer_distance_entry.get_value() + if not self.draw_app.selected: + self.app.inform.emit("[WARNING_NOTCL] Buffer cancelled. No shape selected.") + return + + try: + buffer_distance = float(self.buff_tool.buffer_distance_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + buffer_distance = float(self.buff_tool.buffer_distance_entry.get_value().replace(',', '.')) + self.buff_tool.buffer_distance_entry.set_value(buffer_distance) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Buffer distance value is missing or wrong format. " + "Add it and retry.") + return # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment # I populated the combobox such that the index coincide with the join styles value (whcih is really an INT) join_style = self.buff_tool.buffer_corner_cb.currentIndex() + 1 self.draw_app.buffer_int(buffer_distance, join_style) self.app.ui.notebook.setTabText(2, "Tools") + self.draw_app.app.ui.splitter.setSizes([0, 1]) + self.disactivate() self.draw_app.app.inform.emit("[success]Done. Buffer Int Tool completed.") def on_buffer_ext(self): - buffer_distance = self.buff_tool.buffer_distance_entry.get_value() + if not self.draw_app.selected: + self.app.inform.emit("[WARNING_NOTCL] Buffer cancelled. No shape selected.") + return + + try: + buffer_distance = float(self.buff_tool.buffer_distance_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + buffer_distance = float(self.buff_tool.buffer_distance_entry.get_value().replace(',', '.')) + self.buff_tool.buffer_distance_entry.set_value(buffer_distance) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Buffer distance value is missing or wrong format. " + "Add it and retry.") + return # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment # I populated the combobox such that the index coincide with the join styles value (whcih is really an INT) join_style = self.buff_tool.buffer_corner_cb.currentIndex() + 1 self.draw_app.buffer_ext(buffer_distance, join_style) self.app.ui.notebook.setTabText(2, "Tools") + self.draw_app.app.ui.splitter.setSizes([0, 1]) + self.disactivate() self.draw_app.app.inform.emit("[success]Done. Buffer Ext Tool completed.") @@ -1363,7 +2720,7 @@ class FCBuffer(FCShapeTool): class FCPaint(FCShapeTool): def __init__(self, draw_app): FCShapeTool.__init__(self, draw_app) - self.name = 'fc_paint' + self.name = 'paint' # self.shape_buffer = self.draw_app.shape_buffer self.draw_app = draw_app @@ -1371,58 +2728,21 @@ class FCPaint(FCShapeTool): self.start_msg = "Create Paint geometry ..." self.origin = (0, 0) - self.paint_tool = PaintOptionsTool(self.app, self.draw_app) - self.paint_tool.run() - self.app.ui.notebook.setTabText(2, "Paint Tool") + self.draw_app.paint_tool.run() -class FCRotate(FCShapeTool): +class FCTransform(FCShapeTool): def __init__(self, draw_app): FCShapeTool.__init__(self, draw_app) - self.name = 'fc_rotate' + self.name = 'transformation' - geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y)) + # self.shape_buffer = self.draw_app.shape_buffer + self.draw_app = draw_app + self.app = draw_app.app - if isinstance(geo, DrawToolShape) and geo.geo is not None: - self.draw_app.draw_utility_geometry(geo=geo) - - self.draw_app.app.inform.emit("Click anywhere to finish the Rotation") - - def set_origin(self, origin): - self.origin = origin - - def make(self): - # Create new geometry - # dx = self.origin[0] - # dy = self.origin[1] - self.geometry = [DrawToolShape(affinity.rotate(geom.geo, angle = -90, origin='center')) - for geom in self.draw_app.get_selected()] - # Delete old - self.draw_app.delete_selected() - self.complete = True - self.draw_app.app.inform.emit("[success]Done. Geometry rotate completed.") - - # MS: automatically select the Select Tool after finishing the action but is not working yet :( - #self.draw_app.select_tool("select") - - def on_key(self, key): - if key == 'Enter' or key == QtCore.Qt.Key_Enter: - self.make() - return "Done" - - def click(self, point): - self.make() - return "Done." - - def utility_geometry(self, data=None): - """ - Temporary geometry on screen while using this tool. - - :param data: - :return: - """ - return DrawToolUtilityShape([affinity.rotate(geom.geo, angle = -90, origin='center') - for geom in self.draw_app.get_selected()]) + self.start_msg = "Shape transformations ..." + self.origin = (0, 0) + self.draw_app.transform_tool.run() class FCDrillAdd(FCShapeTool): @@ -1432,7 +2752,7 @@ class FCDrillAdd(FCShapeTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_drill_add' + self.name = 'drill_add' self.selected_dia = None try: @@ -1504,7 +2824,7 @@ class FCDrillArray(FCShapeTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_drill_array' + self.name = 'drill_array' self.draw_app.array_frame.show() @@ -1705,7 +3025,7 @@ class FCDrillArray(FCShapeTool): class FCDrillResize(FCShapeTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_drill_resize' + self.name = 'drill_resize' self.draw_app.app.inform.emit("Click on the Drill(s) to resize ...") self.resize_dia = None @@ -1808,7 +3128,7 @@ class FCDrillResize(FCShapeTool): class FCDrillMove(FCShapeTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_drill_move' + self.name = 'drill_move' # self.shape_buffer = self.draw_app.shape_buffer self.origin = None @@ -1901,7 +3221,7 @@ class FCDrillMove(FCShapeTool): class FCDrillCopy(FCDrillMove): def __init__(self, draw_app): FCDrillMove.__init__(self, draw_app) - self.name = 'fc_drill_copy' + self.name = 'drill_copy' def make(self): # Create new geometry @@ -1935,6 +3255,8 @@ class FCDrillCopy(FCDrillMove): ######################## class FlatCAMGeoEditor(QtCore.QObject): + transform_complete = QtCore.pyqtSignal() + draw_shape_idx = -1 def __init__(self, app, disabled=False): @@ -1954,6 +3276,8 @@ class FlatCAMGeoEditor(QtCore.QObject): self.app.ui.geo_add_text_menuitem.triggered.connect(lambda: self.select_tool('text')) self.app.ui.geo_paint_menuitem.triggered.connect(self.on_paint_tool) self.app.ui.geo_buffer_menuitem.triggered.connect(self.on_buffer_tool) + self.app.ui.geo_transform_menuitem.triggered.connect(self.on_transform_tool) + self.app.ui.geo_delete_menuitem.triggered.connect(self.on_delete_btn) self.app.ui.geo_union_menuitem.triggered.connect(self.union) self.app.ui.geo_intersection_menuitem.triggered.connect(self.intersection) @@ -1970,6 +3294,8 @@ class FlatCAMGeoEditor(QtCore.QObject): self.app.ui.geo_move_menuitem.triggered.connect(self.on_move) self.app.ui.geo_cornersnap_menuitem.triggered.connect(self.on_corner_snap) + self.transform_complete.connect(self.on_transform_complete) + ## Toolbar events and properties self.tools = { "select": {"button": self.app.ui.geo_select_btn, @@ -1992,8 +3318,8 @@ class FlatCAMGeoEditor(QtCore.QObject): "constructor": FCPaint}, "move": {"button": self.app.ui.geo_move_btn, "constructor": FCMove}, - "rotate": {"button": self.app.ui.geo_rotate_btn, - "constructor": FCRotate}, + "transform": {"button": self.app.ui.geo_transform_btn, + "constructor": FCTransform}, "copy": {"button": self.app.ui.geo_copy_btn, "constructor": FCCopy} } @@ -2038,6 +3364,9 @@ class FlatCAMGeoEditor(QtCore.QObject): # signal that there is an action active like polygon or path self.in_action = False + # this will flag if the Editor "tools" are launched from key shortcuts (True) or from menu toolbar (False) + self.launched_from_shortcuts = False + def make_callback(thetool): def f(): self.on_tool_select(thetool) @@ -2103,10 +3432,17 @@ class FlatCAMGeoEditor(QtCore.QObject): # if using Paint store here the tool diameter used self.paint_tooldia = None + self.paint_tool = PaintOptionsTool(self.app, self) + self.transform_tool = TransformEditorTool(self.app, self) + def pool_recreated(self, pool): self.shapes.pool = pool self.tool_shape.pool = pool + def on_transform_complete(self): + self.delete_selected() + self.replot() + def activate(self): self.connect_canvas_event_handlers() self.shapes.enabled = True @@ -2349,6 +3685,10 @@ class FlatCAMGeoEditor(QtCore.QObject): paint_tool = PaintOptionsTool(self.app, self) paint_tool.run() + def on_transform_tool(self): + transform_tool = TransformEditorTool(self.app, self) + transform_tool.run() + def on_tool_select(self, tool): """ Behavior of the toolbar. Tool initialization. @@ -2410,6 +3750,13 @@ class FlatCAMGeoEditor(QtCore.QObject): self.pos = (x, y) + modifiers = QtWidgets.QApplication.keyboardModifiers() + # If the SHIFT key is pressed when LMB is clicked then the coordinates are copied to clipboard + if modifiers == QtCore.Qt.ShiftModifier: + self.app.clipboard.setText( + self.app.defaults["global_point_clipboard_format"] % (self.pos[0], self.pos[1])) + return + # Selection with left mouse button if self.active_tool is not None and event.button is 1: # Dispatch event to active_tool @@ -2420,9 +3767,21 @@ class FlatCAMGeoEditor(QtCore.QObject): if isinstance(self.active_tool, FCShapeTool) and self.active_tool.complete: self.on_shape_complete() - # MS: always return to the Select Tool - self.select_tool("select") - return + # MS: always return to the Select Tool if modifier key is not pressed + # else return to the current tool + key_modifier = QtWidgets.QApplication.keyboardModifiers() + if self.app.defaults["global_mselect_key"] == 'Control': + modifier_to_use = Qt.ControlModifier + else: + modifier_to_use = Qt.ShiftModifier + + # if modifier key is pressed then we add to the selected list the current shape but if + # it's already in the selected list, we removed it. Therefore first click selects, second deselects. + if key_modifier == modifier_to_use: + self.select_tool(self.active_tool.name) + else: + self.select_tool("select") + return if isinstance(self.active_tool, FCSelect): # self.app.log.debug("Replotting after click.") @@ -2540,8 +3899,20 @@ class FlatCAMGeoEditor(QtCore.QObject): if self.active_tool.complete: self.on_shape_complete() self.app.inform.emit("[success]Done.") - # automatically make the selection tool active after completing current action - self.select_tool('select') + + # MS: always return to the Select Tool if modifier key is not pressed + # else return to the current tool + key_modifier = QtWidgets.QApplication.keyboardModifiers() + if self.app.defaults["global_mselect_key"] == 'Control': + modifier_to_use = Qt.ControlModifier + else: + modifier_to_use = Qt.ShiftModifier + + if key_modifier == modifier_to_use: + self.select_tool(self.active_tool.name) + else: + self.select_tool("select") + except Exception as e: log.warning("Error: %s" % str(e)) return @@ -2557,7 +3928,7 @@ class FlatCAMGeoEditor(QtCore.QObject): # Dispatch event to active_tool # msg = self.active_tool.click(self.snap(event.xdata, event.ydata)) msg = self.active_tool.click_release((self.pos[0], self.pos[1])) - self.app.inform.emit(msg) + # self.app.inform.emit(msg) self.replot() except Exception as e: log.warning("Error: %s" % str(e)) @@ -2585,7 +3956,8 @@ class FlatCAMGeoEditor(QtCore.QObject): # add the object to the selected shapes self.selected.append(obj) else: - self.selected.append(obj) + if obj not in self.selected: + self.selected.append(obj) self.replot() def draw_utility_geometry(self, geo): @@ -2634,7 +4006,6 @@ class FlatCAMGeoEditor(QtCore.QObject): tempref = [s for s in self.selected] for shape in tempref: self.delete_shape(shape) - self.selected = [] def delete_shape(self, shape): @@ -2644,7 +4015,6 @@ class FlatCAMGeoEditor(QtCore.QObject): return self.storage.remove(shape) - if shape in self.selected: self.selected.remove(shape) # TODO: Check performance @@ -2653,9 +4023,23 @@ class FlatCAMGeoEditor(QtCore.QObject): self.on_tool_select('move') def on_move_click(self): + if not self.selected: + self.app.inform.emit("[WARNING_NOTCL] Move cancelled. No shape selected.") + return self.on_move() self.active_tool.set_origin(self.snap(self.x, self.y)) + def on_copy_click(self): + if not self.selected: + self.app.inform.emit("[WARNING_NOTCL] Copy cancelled. No shape selected.") + return + + self.app.ui.geo_copy_btn.setChecked(True) + self.app.geo_editor.on_tool_select('copy') + self.app.geo_editor.active_tool.set_origin(self.app.geo_editor.snap( + self.app.geo_editor.x, self.app.geo_editor.y)) + self.app.inform.emit("Click on target point.") + def on_corner_snap(self): self.app.ui.corner_snap_btn.trigger() @@ -3147,9 +4531,9 @@ class FlatCAMGeoEditor(QtCore.QObject): results = [] - if tooldia >= overlap: + if overlap >= 1: self.app.inform.emit( - "[ERROR_NOTCL] Could not do Paint. Overlap value has to be less than Tool Dia value.") + "[ERROR_NOTCL] Could not do Paint. Overlap value has to be less than 1.00 (100%).") return def recurse(geometry, reset=True): @@ -3512,15 +4896,15 @@ class FlatCAMExcEditor(QtCore.QObject): self.tools_exc = { "select": {"button": self.app.ui.select_drill_btn, "constructor": FCDrillSelect}, - "add": {"button": self.app.ui.add_drill_btn, + "drill_add": {"button": self.app.ui.add_drill_btn, "constructor": FCDrillAdd}, - "add_array": {"button": self.app.ui.add_drill_array_btn, + "drill_array": {"button": self.app.ui.add_drill_array_btn, "constructor": FCDrillArray}, - "resize": {"button": self.app.ui.resize_drill_btn, + "drill_resize": {"button": self.app.ui.resize_drill_btn, "constructor": FCDrillResize}, - "copy": {"button": self.app.ui.copy_drill_btn, + "drill_copy": {"button": self.app.ui.copy_drill_btn, "constructor": FCDrillCopy}, - "move": {"button": self.app.ui.move_drill_btn, + "drill_move": {"button": self.app.ui.move_drill_btn, "constructor": FCDrillMove}, } @@ -4505,9 +5889,20 @@ class FlatCAMExcEditor(QtCore.QObject): if self.current_storage is not None: self.on_exc_shape_complete(self.current_storage) self.build_ui() - # MS: always return to the Select Tool - self.select_tool("select") - return + # MS: always return to the Select Tool if modifier key is not pressed + # else return to the current tool + key_modifier = QtWidgets.QApplication.keyboardModifiers() + if self.draw_app.app.defaults["global_mselect_key"] == 'Control': + modifier_to_use = Qt.ControlModifier + else: + modifier_to_use = Qt.ShiftModifier + # if modifier key is pressed then we add to the selected list the current shape but if it's already + # in the selected list, we removed it. Therefore first click selects, second deselects. + if key_modifier == modifier_to_use: + self.select_tool(self.active_tool.name) + else: + self.select_tool("select") + return if isinstance(self.active_tool, FCDrillSelect): # self.app.log.debug("Replotting after click.") diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 770b16ff..b9626ab3 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -67,16 +67,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow): 'Open &Gerber ...\tCTRL+G', self) self.menufile_open.addAction(self.menufileopengerber) - # Open gerber with follow... - self.menufileopengerber_follow = QtWidgets.QAction(QtGui.QIcon('share/flatcam_icon24.png'), - 'Open &Gerber (w/ Follow) ...', self) - self.menufileopengerber_follow.setToolTip( - "Will open a Gerber file with the 'follow' attribute.\n" - "This will actually 'trace' the features of a Gerber file and\n" - "the resulting Gerber geometry will have no volume, it will be\n" - "made out of lines." - ) - self.menufile_open.addAction(self.menufileopengerber_follow) self.menufile_open.addSeparator() # Open Excellon ... @@ -292,6 +282,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # Separator self.menuoptions.addSeparator() + self.menuoptions_view_source = self.menuoptions.addAction(QtGui.QIcon('share/source32.png'), + "View source\tALT+S") + # Separator + self.menuoptions.addSeparator() + ### View ### self.menuview = self.menu.addMenu('&View') self.menuviewenable = self.menuview.addAction(QtGui.QIcon('share/replot16.png'), 'Enable all plots\tALT+1') @@ -330,8 +325,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.menuhelp_manual = self.menuhelp.addAction(QtGui.QIcon('share/globe16.png'), 'Help\tF1') self.menuhelp_home = self.menuhelp.addAction(QtGui.QIcon('share/home16.png'), 'FlatCAM.org') self.menuhelp.addSeparator() - self.menuhelp_videohelp = self.menuhelp.addAction(QtGui.QIcon('share/youtube32.png'), 'YouTube Channel\tF2') self.menuhelp_shortcut_list = self.menuhelp.addAction(QtGui.QIcon('share/shortcuts24.png'), 'Shortcuts List\tF3') + self.menuhelp_videohelp = self.menuhelp.addAction(QtGui.QIcon('share/youtube32.png'), 'YouTube Channel\tF4') self.menuhelp_about = self.menuhelp.addAction(QtGui.QIcon('share/about32.png'), 'About') @@ -373,10 +368,13 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.geo_editor_menu.addSeparator() self.geo_move_menuitem = self.geo_editor_menu.addAction(QtGui.QIcon('share/move32.png'), "Move\tM") self.geo_buffer_menuitem = self.geo_editor_menu.addAction( - QtGui.QIcon('share/buffer16.png'), "Buffer Selection\tB" + QtGui.QIcon('share/buffer16.png'), "Buffer Tool\tB" ) self.geo_paint_menuitem = self.geo_editor_menu.addAction( - QtGui.QIcon('share/paint16.png'), "Paint Selection\tI" + QtGui.QIcon('share/paint16.png'), "Paint Tool\tI" + ) + self.geo_transform_menuitem = self.geo_editor_menu.addAction( + QtGui.QIcon('share/transform.png'), "Transform Tool\tALT+R" ) self.geo_editor_menu.addSeparator() self.geo_cornersnap_menuitem = self.geo_editor_menu.addAction( @@ -419,7 +417,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.menuprojectdisable = self.menuproject.addAction(QtGui.QIcon('share/clear_plot32.png'), 'Disable Plot') self.menuproject.addSeparator() self.menuprojectgeneratecnc = self.menuproject.addAction(QtGui.QIcon('share/cnc32.png'), 'Generate CNC') - self.menuproject.addSeparator() + self.menuprojectviewsource = self.menuproject.addAction(QtGui.QIcon('share/source32.png'), 'View Source') self.menuprojectedit = self.menuproject.addAction(QtGui.QIcon('share/edit_ok32.png'), 'Edit') self.menuprojectcopy = self.menuproject.addAction(QtGui.QIcon('share/copy32.png'), 'Copy') @@ -522,7 +520,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.shell_btn = self.toolbartools.addAction(QtGui.QIcon('share/shell32.png'), "&Command Line") ### Drill Editor Toolbar ### - self.select_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select 'Esc'") + self.select_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select") self.add_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/plus16.png'), 'Add Drill Hole') self.add_drill_array_btn = self.exc_edit_toolbar.addAction( QtGui.QIcon('share/addarray16.png'), 'Add Drill Hole Array') @@ -536,7 +534,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.move_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), "Move Drill") ### Geometry Editor Toolbar ### - self.geo_select_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select 'Esc'") + self.geo_select_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select") self.geo_add_circle_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/circle32.png'), 'Add Circle') self.geo_add_arc_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/arc32.png'), 'Add Arc') self.geo_add_rectangle_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/rectangle32.png'), @@ -559,13 +557,13 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.geo_edit_toolbar.addSeparator() self.geo_cutpath_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/cutpath32.png'), 'Cut Path') - self.geo_copy_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/copy32.png'), "Copy Objects 'c'") - self.geo_rotate_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/rotate.png'), "Rotate Objects 'Space'") + self.geo_copy_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/copy32.png'), "Copy Shape(s)") + self.geo_delete_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/deleteshape32.png'), "Delete Shape '-'") - + self.geo_transform_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/transform.png'), "Transformations") self.geo_edit_toolbar.addSeparator() - self.geo_move_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), "Move Objects 'm'") + self.geo_move_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), "Move Objects ") ### Snap Toolbar ### # Snap GRID toolbar is always active to facilitate usage of measurements done on GRID @@ -983,6 +981,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow): ALT+D  2-Sided PCB Tool + + ALT+K +  Solder Paste Dispensing Tool + ALT+L  Film PCB Tool @@ -999,6 +1001,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow): ALT+R  Transformations Tool + + ALT+S +  View File Source + ALT+U  Cutout PCB Tool @@ -1028,7 +1034,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):  Open Online Manual - F2 + F4  Open Online Tutorials @@ -1131,16 +1137,56 @@ class FlatCAMGUI(QtWidgets.QMainWindow): X -  Polygon Cut Tool +  Flip shape on X axis + + + Y +  Flip shape on Y axis     + + SHIFT+X +  Skew shape on X axis + + + SHIFT+Y +  Skew shape on Y axis + + +   +   + + + ALT+R +  Editor Transformation Tool + + + ALT+X +  Offset shape on X axis + + + ALT+Y +  Offset shape on Y axis + + +   +   + + + CTRL+M +  Measurement Tool + CTRL+S  Save Object and Exit Editor + + CTRL+X +  Polygon Cut Tool +     @@ -1294,8 +1340,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.code_editor.setStyleSheet(stylesheet) self.buttonPreview = QtWidgets.QPushButton('Print Preview') - self.buttonPrint = QtWidgets.QPushButton('Print CNC Code') - self.buttonFind = QtWidgets.QPushButton('Find in CNC Code') + self.buttonPrint = QtWidgets.QPushButton('Print Code') + self.buttonFind = QtWidgets.QPushButton('Find in Code') self.buttonFind.setFixedWidth(100) self.buttonPreview.setFixedWidth(100) self.entryFind = FCEntry() @@ -1309,8 +1355,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow): "When checked it will replace all instances in the 'Find' box\n" "with the text in the 'Replace' box.." ) - self.buttonOpen = QtWidgets.QPushButton('Open CNC Code') - self.buttonSave = QtWidgets.QPushButton('Save CNC Code') + self.buttonOpen = QtWidgets.QPushButton('Open Code') + self.buttonSave = QtWidgets.QPushButton('Save Code') self.cncjob_tab_layout.addWidget(self.code_editor, 0, 0, 1, 5) @@ -1473,7 +1519,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.shell_btn = self.toolbartools.addAction(QtGui.QIcon('share/shell32.png'), "&Command Line") ### Drill Editor Toolbar ### - self.select_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select 'Esc'") + self.select_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select") self.add_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/plus16.png'), 'Add Drill Hole') self.add_drill_array_btn = self.exc_edit_toolbar.addAction( QtGui.QIcon('share/addarray16.png'), 'Add Drill Hole Array') @@ -1508,12 +1554,12 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.geo_edit_toolbar.addSeparator() self.geo_cutpath_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/cutpath32.png'), 'Cut Path') - self.geo_copy_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/copy32.png'), "Copy Objects 'c'") - self.geo_rotate_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/rotate.png'), "Rotate Objects 'Space'") - self.geo_delete_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/deleteshape32.png'), "Delete Shape '-'") + self.geo_copy_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/copy32.png'), "Copy Objects") + self.geo_delete_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/deleteshape32.png'), "Delete Shape") + self.geo_transform_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/transform.png'), "Transformations") self.geo_edit_toolbar.addSeparator() - self.geo_move_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), "Move Objects 'm'") + self.geo_move_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), "Move Objects") ### Snap Toolbar ### # Snap GRID toolbar is always active to facilitate usage of measurements done on GRID @@ -1647,7 +1693,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # Rotate Object by 90 degree CCW if key == QtCore.Qt.Key_R: - self.app.on_rotate(silent=True, preset=-90) + self.app.on_rotate(silent=True, preset=-self.app.defaults['tools_transform_rotate']) return # Run a Script @@ -1691,6 +1737,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.app.dblsidedtool.run() return + # Solder Paste Dispensing Tool + if key == QtCore.Qt.Key_K: + self.app.paste_tool.run() + return + # Film Tool if key == QtCore.Qt.Key_L: self.app.film_tool.run() @@ -1711,6 +1762,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.app.transform_tool.run() return + # View Source Object Content + if key == QtCore.Qt.Key_S: + self.app.on_view_source() + return + # Cutout Tool if key == QtCore.Qt.Key_U: self.app.cutout_tool.run() @@ -1730,14 +1786,14 @@ class FlatCAMGUI(QtWidgets.QMainWindow): if key == QtCore.Qt.Key_F1 or key == 'F1': webbrowser.open(self.app.manual_url) - # Open Video Help - if key == QtCore.Qt.Key_F2 or key == 'F2': - webbrowser.open(self.app.video_url) - # Show shortcut list if key == QtCore.Qt.Key_F3 or key == 'F3': self.app.on_shortcut_list() + # Open Video Help + if key == QtCore.Qt.Key_F4 or key == 'F4': + webbrowser.open(self.app.video_url) + # Switch to Project Tab if key == QtCore.Qt.Key_1: self.app.on_select_tab('project') @@ -1818,7 +1874,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # Rotate Object by 90 degree CW if key == QtCore.Qt.Key_R: - self.app.on_rotate(silent=True, preset=90) + self.app.on_rotate(silent=True, preset=self.app.defaults['tools_transform_rotate']) # Shell toggle if key == QtCore.Qt.Key_S: @@ -1865,10 +1921,51 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.app.measurement_tool.run() return + # Cut Action Tool + if key == QtCore.Qt.Key_X or key == 'X': + if self.app.geo_editor.get_selected() is not None: + self.app.geo_editor.cutpath() + else: + msg = 'Please first select a geometry item to be cutted\n' \ + 'then select the geometry item that will be cutted\n' \ + 'out of the first item. In the end press ~X~ key or\n' \ + 'the toolbar button.' + + messagebox = QtWidgets.QMessageBox() + messagebox.setText(msg) + messagebox.setWindowTitle("Warning") + messagebox.setWindowIcon(QtGui.QIcon('share/warning.png')) + messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok) + messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok) + messagebox.exec_() + return + elif modifiers == QtCore.Qt.ShiftModifier: - pass + # Skew on X axis + if key == QtCore.Qt.Key_X or key == 'X': + self.app.geo_editor.transform_tool.on_skewx_key() + return + + # Skew on Y axis + if key == QtCore.Qt.Key_Y or key == 'Y': + self.app.geo_editor.transform_tool.on_skewy_key() + return elif modifiers == QtCore.Qt.AltModifier: - pass + + # Transformation Tool + if key == QtCore.Qt.Key_R or key == 'R': + self.app.geo_editor.select_tool('transform') + return + + # Offset on X axis + if key == QtCore.Qt.Key_X or key == 'X': + self.app.geo_editor.transform_tool.on_offx_key() + return + + # Offset on Y axis + if key == QtCore.Qt.Key_Y or key == 'Y': + self.app.geo_editor.transform_tool.on_offy_key() + return elif modifiers == QtCore.Qt.NoModifier: # toggle display of Notebook area if key == QtCore.Qt.Key_QuoteLeft or key == '`': @@ -1878,7 +1975,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # complete automatically, like a polygon or path. if key == QtCore.Qt.Key_Enter or key == 'Enter': if isinstance(self.app.geo_editor.active_tool, FCShapeTool): - if self.app.geo_editor.active_tool.name == 'fc_rotate': + if self.app.geo_editor.active_tool.name == 'rotate': self.app.geo_editor.active_tool.make() if self.app.geo_editor.active_tool.complete: @@ -1906,10 +2003,15 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.app.inform.emit("[WARNING_NOTCL]Cancelled.") self.app.geo_editor.delete_utility_geometry() + + # deselect any shape that might be selected + self.app.geo_editor.selected = [] + self.app.geo_editor.replot() - # self.select_btn.setChecked(True) - # self.on_tool_select('select') self.app.geo_editor.select_tool('select') + + # hide the notebook + self.app.ui.splitter.setSizes([0, 1]) return # Delete selected object @@ -1919,10 +2021,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # Move if key == QtCore.Qt.Key_Space or key == 'Space': - self.app.ui.geo_rotate_btn.setChecked(True) - self.app.geo_editor.on_tool_select('rotate') - self.app.geo_editor.active_tool.set_origin( - self.app.geo_editor.snap(self.app.geo_editor.x, self.app.geo_editor.y)) + self.app.geo_editor.transform_tool.on_rotate_key() if key == QtCore.Qt.Key_Minus or key == '-': self.app.plotcanvas.zoom(1 / self.app.defaults['zoom_ratio'], @@ -1954,11 +2053,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # Copy if key == QtCore.Qt.Key_C or key == 'C': - self.app.ui.geo_copy_btn.setChecked(True) - self.app.geo_editor.on_tool_select('copy') - self.app.geo_editor.active_tool.set_origin(self.app.geo_editor.snap( - self.app.geo_editor.x, self.app.geo_editor.y)) - self.app.inform.emit("Click on target point.") + self.app.geo_editor.on_copy_click() # Substract Tool if key == QtCore.Qt.Key_E or key == 'E': @@ -2057,23 +2152,15 @@ class FlatCAMGUI(QtWidgets.QMainWindow): if key == QtCore.Qt.Key_V or key == 'V': self.app.on_zoom_fit(None) - # Cut Action Tool + # Flip on X axis if key == QtCore.Qt.Key_X or key == 'X': - if self.app.geo_editor.get_selected() is not None: - self.app.geo_editor.cutpath() - else: - msg = 'Please first select a geometry item to be cutted\n' \ - 'then select the geometry item that will be cutted\n' \ - 'out of the first item. In the end press ~X~ key or\n' \ - 'the toolbar button.' \ + self.app.geo_editor.transform_tool.on_flipx() + return - messagebox = QtWidgets.QMessageBox() - messagebox.setText(msg) - messagebox.setWindowTitle("Warning") - messagebox.setWindowIcon(QtGui.QIcon('share/warning.png')) - messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok) - messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok) - messagebox.exec_() + # Flip on Y axis + if key == QtCore.Qt.Key_Y or key == 'Y': + self.app.geo_editor.transform_tool.on_flipy() + return # Propagate to tool response = None @@ -2177,7 +2264,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.app.exc_editor.x = self.app.mouse[0] self.app.exc_editor.y = self.app.mouse[1] - self.app.exc_editor.select_tool('add_array') + self.app.exc_editor.select_tool('drill_array') return # Copy @@ -2186,7 +2273,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): if self.app.exc_editor.selected: self.app.inform.emit("Click on target point.") self.app.ui.copy_drill_btn.setChecked(True) - self.app.exc_editor.on_tool_select('copy') + self.app.exc_editor.on_tool_select('drill_copy') self.app.exc_editor.active_tool.set_origin( (self.app.exc_editor.snap_x, self.app.exc_editor.snap_y)) else: @@ -2202,7 +2289,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.app.exc_editor.x = self.app.mouse[0] self.app.exc_editor.y = self.app.mouse[1] - self.app.exc_editor.select_tool('add') + self.app.exc_editor.select_tool('drill_add') return # Grid Snap @@ -2232,7 +2319,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): if self.app.exc_editor.selected: self.app.inform.emit("Click on target point.") self.app.ui.move_drill_btn.setChecked(True) - self.app.exc_editor.on_tool_select('move') + self.app.exc_editor.on_tool_select('drill_move') self.app.exc_editor.active_tool.set_origin( (self.app.exc_editor.snap_x, self.app.exc_editor.snap_y)) else: @@ -2242,7 +2329,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # Resize Tool if key == QtCore.Qt.Key_R or key == 'R': self.app.exc_editor.launched_from_shortcuts = True - self.app.exc_editor.select_tool('resize') + self.app.exc_editor.select_tool('drill_resize') return # Add Tool @@ -2345,7 +2432,9 @@ class FlatCAMGUI(QtWidgets.QMainWindow): grect = self.geometry() # self.splitter.sizes()[0] is actually the size of the "notebook" - self.geom_update.emit(grect.x(), grect.y(), grect.width(), grect.height(), self.splitter.sizes()[0]) + if not self.isMaximized(): + self.geom_update.emit(grect.x(), grect.y(), grect.width(), grect.height(), self.splitter.sizes()[0]) + self.final_save.emit() if self.app.should_we_quit is True: @@ -2358,6 +2447,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # save toolbar state to file settings = QSettings("Open Source", "FlatCAM") settings.setValue('saved_gui_state', self.saveState()) + settings.setValue('maximized_gui', self.isMaximized()) # This will write the setting to the platform specific storage. del settings @@ -2380,8 +2470,13 @@ class GeneralPreferencesUI(QtWidgets.QWidget): self.general_gui_group = GeneralGUIPrefGroupUI() self.general_gui_group.setFixedWidth(250) + self.general_gui_set_group = GeneralGUISetGroupUI() + self.general_gui_set_group.setFixedWidth(250) + self.layout.addWidget(self.general_app_group) self.layout.addWidget(self.general_gui_group) + self.layout.addWidget(self.general_gui_set_group) + self.layout.addStretch() @@ -2467,22 +2562,33 @@ class ToolsPreferencesUI(QtWidgets.QWidget): self.tools_calculators_group = ToolsCalculatorsPrefGroupUI() self.tools_calculators_group.setMinimumWidth(220) + self.tools_transform_group = ToolsTransformPrefGroupUI() + self.tools_transform_group.setMinimumWidth(200) + + self.tools_solderpaste_group = ToolsSolderpastePrefGroupUI() + self.tools_solderpaste_group.setMinimumWidth(200) + self.vlay = QtWidgets.QVBoxLayout() self.vlay.addWidget(self.tools_ncc_group) self.vlay.addWidget(self.tools_paint_group) + self.vlay.addWidget(self.tools_film_group) self.vlay1 = QtWidgets.QVBoxLayout() self.vlay1.addWidget(self.tools_cutout_group) + self.vlay1.addWidget(self.tools_transform_group) self.vlay1.addWidget(self.tools_2sided_group) - self.vlay1.addWidget(self.tools_film_group) self.vlay2 = QtWidgets.QVBoxLayout() self.vlay2.addWidget(self.tools_panelize_group) self.vlay2.addWidget(self.tools_calculators_group) + self.vlay3 = QtWidgets.QVBoxLayout() + self.vlay3.addWidget(self.tools_solderpaste_group) + self.layout.addLayout(self.vlay) self.layout.addLayout(self.vlay1) self.layout.addLayout(self.vlay2) + self.layout.addLayout(self.vlay3) self.layout.addStretch() @@ -2745,22 +2851,10 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI): self.form_box_child_11.addWidget(self.sel_draw_color_button) self.form_box_child_11.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) - # Theme selection - self.layout_label = QtWidgets.QLabel('Layout:') - self.alt_sf_color_label.setToolTip( - "Select an layout for FlatCAM." - ) - self.layout_combo = FCComboBox() - self.layout_combo.addItem("Choose ...") - self.layout_combo.addItem("Standard") - self.layout_combo.addItem("Compact") - self.layout_combo.setCurrentIndex(0) - # Just to add empty rows self.spacelabel = QtWidgets.QLabel('') # Add (label - input field) pair to the QFormLayout - self.form_box.addRow(self.spacelabel, self.spacelabel) self.form_box.addRow(self.gridx_label, self.gridx_entry) @@ -2783,12 +2877,121 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI): self.form_box.addRow(self.sel_draw_color_label, self.form_box_child_11) self.form_box.addRow(self.spacelabel, self.spacelabel) - self.form_box.addRow(self.layout_label, self.layout_combo) + # Add the QFormLayout that holds the Application general defaults # to the main layout of this TAB self.layout.addLayout(self.form_box) +class GeneralGUISetGroupUI(OptionsGroupUI): + def __init__(self, parent=None): + super(GeneralGUISetGroupUI, self).__init__(self) + + self.setTitle(str("GUI Settings")) + + # Create a form layout for the Application general settings + self.form_box = QtWidgets.QFormLayout() + + + # Layout selection + self.layout_label = QtWidgets.QLabel('Layout:') + self.layout_label.setToolTip( + "Select an layout for FlatCAM.\n" + "It is applied immediately." + ) + self.layout_combo = FCComboBox() + self.layout_combo.addItem("Choose ...") + self.layout_combo.addItem("Standard") + self.layout_combo.addItem("Compact") + self.layout_combo.setCurrentIndex(0) + + # Style selection + self.style_label = QtWidgets.QLabel('Style:') + self.style_label.setToolTip( + "Select an style for FlatCAM.\n" + "It will be applied at the next app start." + ) + self.style_combo = FCComboBox() + self.style_combo.addItems(QtWidgets.QStyleFactory.keys()) + # find current style + index = self.style_combo.findText(QtWidgets.qApp.style().objectName(), QtCore.Qt.MatchFixedString) + self.style_combo.setCurrentIndex(index) + self.style_combo.activated[str].connect(self.handle_style) + + # Enable High DPI Support + self.hdpi_label = QtWidgets.QLabel('HDPI Support:') + self.hdpi_label.setToolTip( + "Enable High DPI support for FlatCAM.\n" + "It will be applied at the next app start." + ) + self.hdpi_cb = FCCheckBox() + settings = QSettings("Open Source", "FlatCAM") + if settings.contains("hdpi"): + self.hdpi_cb.set_value(settings.value('hdpi', type=int)) + else: + self.hdpi_cb.set_value(False) + self.hdpi_cb.stateChanged.connect(self.handle_hdpi) + + # Clear Settings + self.clear_label = QtWidgets.QLabel('Clear GUI Settings:') + self.clear_label.setToolTip( + "Clear the GUI settings for FlatCAM,\n" + "such as: layout, gui state, style, hdpi support etc." + ) + self.clear_btn = FCButton("Clear") + self.clear_btn.clicked.connect(self.handle_clear) + # Just to add empty rows + self.spacelabel = QtWidgets.QLabel('') + + # Add (label - input field) pair to the QFormLayout + self.form_box.addRow(self.spacelabel, self.spacelabel) + + self.form_box.addRow(self.layout_label, self.layout_combo) + self.form_box.addRow(self.style_label, self.style_combo) + self.form_box.addRow(self.hdpi_label, self.hdpi_cb) + self.form_box.addRow(self.clear_label, self.clear_btn) + + # Add the QFormLayout that holds the Application general defaults + # to the main layout of this TAB + self.layout.addLayout(self.form_box) + + def handle_style(self, style): + # set current style + settings = QSettings("Open Source", "FlatCAM") + settings.setValue('style', style) + + # This will write the setting to the platform specific storage. + del settings + + def handle_hdpi(self, state): + # set current HDPI + settings = QSettings("Open Source", "FlatCAM") + settings.setValue('hdpi', state) + + # This will write the setting to the platform specific storage. + del settings + + def handle_clear(self): + msgbox = QtWidgets.QMessageBox() + # msgbox.setText("Save changes ...") + msgbox.setText("Are you sure you want to delete the GUI Settings? " + "\n" + ) + msgbox.setWindowTitle("Clear GUI Settings") + msgbox.setWindowIcon(QtGui.QIcon('share/trash32.png')) + msgbox.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + msgbox.setDefaultButton(QtWidgets.QMessageBox.No) + + response = msgbox.exec_() + + if response == QtWidgets.QMessageBox.Yes: + settings = QSettings("Open Source", "FlatCAM") + for key in settings.allKeys(): + settings.remove(key) + # This will write the setting to the platform specific storage. + del settings + self.app.inform.emit("[success] GUI settings deleted ...") + class GeneralAppPrefGroupUI(OptionsGroupUI): def __init__(self, parent=None): super(GeneralAppPrefGroupUI, self).__init__(self) @@ -2806,6 +3009,16 @@ class GeneralAppPrefGroupUI(OptionsGroupUI): self.units_radio = RadioSet([{'label': 'IN', 'value': 'IN'}, {'label': 'MM', 'value': 'MM'}]) + # Application Level for FlatCAM + self.app_level_label = QtWidgets.QLabel('APP. LEVEL:') + self.app_level_label.setToolTip("Choose the default level of usage for FlatCAM.\n" + "BASIC level -> reduced functionality, best for beginner's.\n" + "ADVANCED level -> full functionality.\n\n" + "The choice here will influence the parameters in\n" + "the Selected Tab for all kinds of FlatCAM objects.") + self.app_level_radio = RadioSet([{'label': 'Basic', 'value': 'b'}, + {'label': 'Advanced', 'value': 'a'}]) + # Languages for FlatCAM self.languagelabel = QtWidgets.QLabel('Languages:') self.languagelabel.setToolTip("Set the language used throughout FlatCAM.") @@ -2896,6 +3109,7 @@ class GeneralAppPrefGroupUI(OptionsGroupUI): # Add (label - input field) pair to the QFormLayout self.form_box.addRow(self.unitslabel, self.units_radio) + self.form_box.addRow(self.app_level_label, self.app_level_radio) self.form_box.addRow(self.languagelabel, self.language_cb) self.form_box.addRow(self.languagespace, self.language_apply_btn) @@ -2915,6 +3129,43 @@ class GeneralAppPrefGroupUI(OptionsGroupUI): # to the main layout of this TAB self.layout.addLayout(self.form_box) + # hlay = QtWidgets.QHBoxLayout() + # self.layout.addLayout(hlay) + # hlay.addStretch() + + # Save compressed project CB + self.save_type_cb = FCCheckBox('Save Compressed Project') + self.save_type_cb.setToolTip( + "Whether to save a compressed or uncompressed project.\n" + "When checked it will save a compressed FlatCAM project." + ) + # self.advanced_cb.setLayoutDirection(QtCore.Qt.RightToLeft) + self.layout.addWidget(self.save_type_cb) + + hlay1 = QtWidgets.QHBoxLayout() + self.layout.addLayout(hlay1) + + # Project LZMA Comppression Level + self.compress_combo = FCComboBox() + self.compress_label = QtWidgets.QLabel('Compress Level:') + self.compress_label.setToolTip( + "The level of compression used when saving\n" + "a FlatCAM project. Higher value means better compression\n" + "but require more RAM and time." + ) + # self.advanced_cb.setLayoutDirection(QtCore.Qt.RightToLeft) + self.compress_combo.addItems([str(i) for i in range(10)]) + + hlay1.addWidget(self.compress_label) + hlay1.addWidget(self.compress_combo) + + self.proj_ois = OptionalInputSection(self.save_type_cb, [self.compress_label, self.compress_combo], True) + + self.form_box_2 = QtWidgets.QFormLayout() + self.layout.addLayout(self.form_box_2) + + self.layout.addStretch() + class GerberGenPrefGroupUI(OptionsGroupUI): def __init__(self, parent=None): @@ -3330,8 +3581,11 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI): self.optimization_time_label.setDisabled(True) self.optimization_time_entry.setDisabled(True) - ## Create CNC Job - self.cncjob_label = QtWidgets.QLabel('Create CNC Job') + ###################### + ## ADVANCED OPTIONS ## + ###################### + + self.cncjob_label = QtWidgets.QLabel('Advanced Options:') self.cncjob_label.setToolTip( "Parameters used to create a CNC Job object\n" "for this drill object that are not changed very often." @@ -3611,8 +3865,8 @@ class ExcellonExpPrefGroupUI(OptionsGroupUI): # Plot options self.export_options_label = QtWidgets.QLabel("Export Options:") self.export_options_label.setToolTip( - "The parameters set here are used in the file exported" - "when using the File -> Export -> Export Excellon menu entry." + "The parameters set here are used in the file exported\n" + "when using the File -> Export -> Export Excellon menu entry." ) self.layout.addWidget(self.export_options_label) @@ -3776,9 +4030,9 @@ class GeometryGenPrefGroupUI(OptionsGroupUI): # ------------------------------ - ## Create CNC Job + ## Advanced Options # ------------------------------ - self.cncjob_label = QtWidgets.QLabel('Create CNC Job:') + self.cncjob_label = QtWidgets.QLabel('Advanced Options:') self.cncjob_label.setToolTip( "Parameters to create a CNC Job object\n" "tracing the contours of a Geometry object." @@ -4628,6 +4882,19 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI): grid0.addWidget(self.rows_label, 3, 0) grid0.addWidget(self.prows, 3, 1) + ## Type of resulting Panel object + self.panel_type_radio = RadioSet([{'label': 'Gerber', 'value': 'gerber'}, + {'label': 'Geo', 'value': 'geometry'}]) + self.panel_type_label = QtWidgets.QLabel("Panel Type:") + self.panel_type_label.setToolTip( + "Choose the type of object for the panel object:\n" + "- Gerber\n" + "- Geometry" + ) + + grid0.addWidget(self.panel_type_label, 4, 0) + grid0.addWidget(self.panel_type_radio, 4, 1) + ## Constrains self.pconstrain_cb = FCCheckBox("Constrain within:") self.pconstrain_cb.setToolTip( @@ -4637,7 +4904,7 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI): "the final panel will have as many columns and rows as\n" "they fit completely within selected area." ) - grid0.addWidget(self.pconstrain_cb, 4, 0) + grid0.addWidget(self.pconstrain_cb, 5, 0) self.px_width_entry = FCEntry() self.x_width_lbl = QtWidgets.QLabel("Width (DX):") @@ -4645,8 +4912,8 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI): "The width (DX) within which the panel must fit.\n" "In current units." ) - grid0.addWidget(self.x_width_lbl, 5, 0) - grid0.addWidget(self.px_width_entry, 5, 1) + grid0.addWidget(self.x_width_lbl, 6, 0) + grid0.addWidget(self.px_width_entry, 6, 1) self.py_height_entry = FCEntry() self.y_height_lbl = QtWidgets.QLabel("Height (DY):") @@ -4654,8 +4921,8 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI): "The height (DY)within which the panel must fit.\n" "In current units." ) - grid0.addWidget(self.y_height_lbl, 6, 0) - grid0.addWidget(self.py_height_entry, 6, 1) + grid0.addWidget(self.y_height_lbl, 7, 0) + grid0.addWidget(self.py_height_entry, 7, 1) self.layout.addStretch() @@ -4757,6 +5024,306 @@ class ToolsCalculatorsPrefGroupUI(OptionsGroupUI): self.layout.addStretch() +class ToolsTransformPrefGroupUI(OptionsGroupUI): + def __init__(self, parent=None): + + super(ToolsTransformPrefGroupUI, self).__init__(self) + + self.setTitle(str("Transform Tool Options")) + + ## Transformations + self.transform_label = QtWidgets.QLabel("Parameters:") + self.transform_label.setToolTip( + "Various transformations that can be applied\n" + "on a FlatCAM object." + ) + self.layout.addWidget(self.transform_label) + + grid0 = QtWidgets.QGridLayout() + self.layout.addLayout(grid0) + + ## Rotate Angle + self.rotate_entry = FCEntry() + self.rotate_label = QtWidgets.QLabel("Rotate Angle:") + self.rotate_label.setToolTip( + "Angle for rotation. In degrees." + ) + grid0.addWidget(self.rotate_label, 0, 0) + grid0.addWidget(self.rotate_entry, 0, 1) + + ## Skew/Shear Angle on X axis + self.skewx_entry = FCEntry() + self.skewx_label = QtWidgets.QLabel("Skew_X angle:") + self.skewx_label.setToolTip( + "Angle for Skew/Shear on X axis. In degrees." + ) + grid0.addWidget(self.skewx_label, 1, 0) + grid0.addWidget(self.skewx_entry, 1, 1) + + ## Skew/Shear Angle on Y axis + self.skewy_entry = FCEntry() + self.skewy_label = QtWidgets.QLabel("Skew_Y angle:") + self.skewy_label.setToolTip( + "Angle for Skew/Shear on Y axis. In degrees." + ) + grid0.addWidget(self.skewy_label, 2, 0) + grid0.addWidget(self.skewy_entry, 2, 1) + + ## Scale factor on X axis + self.scalex_entry = FCEntry() + self.scalex_label = QtWidgets.QLabel("Scale_X factor:") + self.scalex_label.setToolTip( + "Factor for scaling on X axis." + ) + grid0.addWidget(self.scalex_label, 3, 0) + grid0.addWidget(self.scalex_entry, 3, 1) + + ## Scale factor on X axis + self.scaley_entry = FCEntry() + self.scaley_label = QtWidgets.QLabel("Scale_Y factor:") + self.scaley_label.setToolTip( + "Factor for scaling on Y axis." + ) + grid0.addWidget(self.scaley_label, 4, 0) + grid0.addWidget(self.scaley_entry, 4, 1) + + ## Link Scale factors + self.link_cb = FCCheckBox("Link") + self.link_cb.setToolTip( + "Scale the selected object(s)\n" + "using the Scale_X factor for both axis." + ) + grid0.addWidget(self.link_cb, 5, 0) + + ## Scale Reference + self.reference_cb = FCCheckBox("Scale Reference") + self.reference_cb.setToolTip( + "Scale the selected object(s)\n" + "using the origin reference when checked,\n" + "and the center of the biggest bounding box\n" + "of the selected objects when unchecked." + ) + grid0.addWidget(self.reference_cb, 5, 1) + + ## Offset distance on X axis + self.offx_entry = FCEntry() + self.offx_label = QtWidgets.QLabel("Offset_X val:") + self.offx_label.setToolTip( + "Distance to offset on X axis. In current units." + ) + grid0.addWidget(self.offx_label, 6, 0) + grid0.addWidget(self.offx_entry, 6, 1) + + ## Offset distance on Y axis + self.offy_entry = FCEntry() + self.offy_label = QtWidgets.QLabel("Offset_Y val:") + self.offy_label.setToolTip( + "Distance to offset on Y axis. In current units." + ) + grid0.addWidget(self.offy_label, 7, 0) + grid0.addWidget(self.offy_entry, 7, 1) + + ## Mirror (Flip) Reference Point + self.mirror_reference_cb = FCCheckBox("Mirror Reference") + self.mirror_reference_cb.setToolTip( + "Flip the selected object(s)\n" + "around the point in Point Entry Field.\n" + "\n" + "The point coordinates can be captured by\n" + "left click on canvas together with pressing\n" + "SHIFT key. \n" + "Then click Add button to insert coordinates.\n" + "Or enter the coords in format (x, y) in the\n" + "Point Entry field and click Flip on X(Y)") + grid0.addWidget(self.mirror_reference_cb, 8, 1) + + self.flip_ref_label = QtWidgets.QLabel(" Mirror Ref. Point:") + self.flip_ref_label.setToolTip( + "Coordinates in format (x, y) used as reference for mirroring.\n" + "The 'x' in (x, y) will be used when using Flip on X and\n" + "the 'y' in (x, y) will be used when using Flip on Y and" + ) + self.flip_ref_entry = EvalEntry2("(0, 0)") + + grid0.addWidget(self.flip_ref_label, 9, 0) + grid0.addWidget(self.flip_ref_entry, 9, 1) + + self.layout.addStretch() + + +class ToolsSolderpastePrefGroupUI(OptionsGroupUI): + def __init__(self, parent=None): + + super(ToolsSolderpastePrefGroupUI, self).__init__(self) + + self.setTitle(str("SolderPaste Tool Options")) + + ## Solder Paste Dispensing + self.solderpastelabel = QtWidgets.QLabel("Parameters:") + self.solderpastelabel.setToolTip( + "A tool to create GCode for dispensing\n" + "solder paste onto a PCB." + ) + self.layout.addWidget(self.solderpastelabel) + + grid0 = QtWidgets.QGridLayout() + self.layout.addLayout(grid0) + + # Nozzle Tool Diameters + nozzletdlabel = QtWidgets.QLabel('Tools dia:') + nozzletdlabel.setToolTip( + "Diameters of nozzle tools, separated by ','" + ) + self.nozzle_tool_dia_entry = FCEntry() + grid0.addWidget(nozzletdlabel, 0, 0) + grid0.addWidget(self.nozzle_tool_dia_entry, 0, 1) + + # New Nozzle Tool Dia + self.addtool_entry_lbl = QtWidgets.QLabel('New Nozzle Dia:') + self.addtool_entry_lbl.setToolTip( + "Diameter for the new Nozzle tool to add in the Tool Table" + ) + self.addtool_entry = FCEntry() + grid0.addWidget(self.addtool_entry_lbl, 1, 0) + grid0.addWidget(self.addtool_entry, 1, 1) + + # Z dispense start + self.z_start_entry = FCEntry() + self.z_start_label = QtWidgets.QLabel("Z Dispense Start:") + self.z_start_label.setToolTip( + "The height (Z) when solder paste dispensing starts." + ) + grid0.addWidget(self.z_start_label, 2, 0) + grid0.addWidget(self.z_start_entry, 2, 1) + + # Z dispense + self.z_dispense_entry = FCEntry() + self.z_dispense_label = QtWidgets.QLabel("Z Dispense:") + self.z_dispense_label.setToolTip( + "The height (Z) when doing solder paste dispensing." + ) + grid0.addWidget(self.z_dispense_label, 3, 0) + grid0.addWidget(self.z_dispense_entry, 3, 1) + + # Z dispense stop + self.z_stop_entry = FCEntry() + self.z_stop_label = QtWidgets.QLabel("Z Dispense Stop:") + self.z_stop_label.setToolTip( + "The height (Z) when solder paste dispensing stops." + ) + grid0.addWidget(self.z_stop_label, 4, 0) + grid0.addWidget(self.z_stop_entry, 4, 1) + + # Z travel + self.z_travel_entry = FCEntry() + self.z_travel_label = QtWidgets.QLabel("Z Travel:") + self.z_travel_label.setToolTip( + "The height (Z) for travel between pads\n" + "(without dispensing solder paste)." + ) + grid0.addWidget(self.z_travel_label, 5, 0) + grid0.addWidget(self.z_travel_entry, 5, 1) + + # Z toolchange location + self.z_toolchange_entry = FCEntry() + self.z_toolchange_label = QtWidgets.QLabel("Z Toolchange:") + self.z_toolchange_label.setToolTip( + "The height (Z) for tool (nozzle) change." + ) + grid0.addWidget(self.z_toolchange_label, 6, 0) + grid0.addWidget(self.z_toolchange_entry, 6, 1) + + # X,Y Toolchange location + self.xy_toolchange_entry = FCEntry() + self.xy_toolchange_label = QtWidgets.QLabel("XY Toolchange:") + self.xy_toolchange_label.setToolTip( + "The X,Y location for tool (nozzle) change.\n" + "The format is (x, y) where x and y are real numbers." + ) + grid0.addWidget(self.xy_toolchange_label, 7, 0) + grid0.addWidget(self.xy_toolchange_entry, 7, 1) + + # Feedrate X-Y + self.frxy_entry = FCEntry() + self.frxy_label = QtWidgets.QLabel("Feedrate X-Y:") + self.frxy_label.setToolTip( + "Feedrate (speed) while moving on the X-Y plane." + ) + grid0.addWidget(self.frxy_label, 8, 0) + grid0.addWidget(self.frxy_entry, 8, 1) + + # Feedrate Z + self.frz_entry = FCEntry() + self.frz_label = QtWidgets.QLabel("Feedrate Z:") + self.frz_label.setToolTip( + "Feedrate (speed) while moving vertically\n" + "(on Z plane)." + ) + grid0.addWidget(self.frz_label, 9, 0) + grid0.addWidget(self.frz_entry, 9, 1) + + # Feedrate Z Dispense + self.frz_dispense_entry = FCEntry() + self.frz_dispense_label = QtWidgets.QLabel("Feedrate Z Dispense:") + self.frz_dispense_label.setToolTip( + "Feedrate (speed) while moving up vertically\n" + " to Dispense position (on Z plane)." + ) + grid0.addWidget(self.frz_dispense_label, 10, 0) + grid0.addWidget(self.frz_dispense_entry, 10, 1) + + # Spindle Speed Forward + self.speedfwd_entry = FCEntry() + self.speedfwd_label = QtWidgets.QLabel("Spindle Speed FWD:") + self.speedfwd_label.setToolTip( + "The dispenser speed while pushing solder paste\n" + "through the dispenser nozzle." + ) + grid0.addWidget(self.speedfwd_label, 11, 0) + grid0.addWidget(self.speedfwd_entry, 11, 1) + + # Dwell Forward + self.dwellfwd_entry = FCEntry() + self.dwellfwd_label = QtWidgets.QLabel("Dwell FWD:") + self.dwellfwd_label.setToolTip( + "Pause after solder dispensing." + ) + grid0.addWidget(self.dwellfwd_label, 12, 0) + grid0.addWidget(self.dwellfwd_entry, 12, 1) + + # Spindle Speed Reverse + self.speedrev_entry = FCEntry() + self.speedrev_label = QtWidgets.QLabel("Spindle Speed REV:") + self.speedrev_label.setToolTip( + "The dispenser speed while retracting solder paste\n" + "through the dispenser nozzle." + ) + grid0.addWidget(self.speedrev_label, 13, 0) + grid0.addWidget(self.speedrev_entry, 13, 1) + + # Dwell Reverse + self.dwellrev_entry = FCEntry() + self.dwellrev_label = QtWidgets.QLabel("Dwell REV:") + self.dwellrev_label.setToolTip( + "Pause after solder paste dispenser retracted,\n" + "to allow pressure equilibrium." + ) + grid0.addWidget(self.dwellrev_label, 14, 0) + grid0.addWidget(self.dwellrev_entry, 14, 1) + + # Postprocessors + pp_label = QtWidgets.QLabel('PostProcessors:') + pp_label.setToolTip( + "Files that control the GCode generation." + ) + + self.pp_combo = FCComboBox() + grid0.addWidget(pp_label, 15, 0) + grid0.addWidget(self.pp_combo, 15, 1) + + self.layout.addStretch() + + class FlatCAMActivityView(QtWidgets.QWidget): def __init__(self, parent=None): diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 34ff30db..699f9eb3 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -106,7 +106,12 @@ class FlatCAMObj(QtCore.QObject): if attr == 'options': self.options.update(d[attr]) else: - setattr(self, attr, d[attr]) + try: + setattr(self, attr, d[attr]) + except KeyError: + log.debug("FlatCAMObj.from_dict() --> KeyError: %s. Means that we are loading an old project that don't" + "have all attributes in the latest FlatCAM." % str(attr)) + pass def on_options_change(self, key): # Update form on programmatically options change @@ -366,9 +371,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber): if grb_final.solid_geometry is None: grb_final.solid_geometry = [] + grb_final.follow_geometry = [] if type(grb_final.solid_geometry) is not list: grb_final.solid_geometry = [grb_final.solid_geometry] + grb_final.follow_geometry = [grb_final.follow_geometry] for grb in grb_list: for option in grb.options: @@ -384,8 +391,10 @@ class FlatCAMGerber(FlatCAMObj, Gerber): else: # If not list, just append for geos in grb.solid_geometry: grb_final.solid_geometry.append(geos) + grb_final.follow_geometry.append(geos) grb_final.solid_geometry = MultiPolygon(grb_final.solid_geometry) + grb_final.follow_geometry = MultiPolygon(grb_final.follow_geometry) def __init__(self, name): Gerber.__init__(self, steps_per_circle=int(self.app.defaults["gerber_circle_steps"])) @@ -413,15 +422,15 @@ class FlatCAMGerber(FlatCAMObj, Gerber): # type of isolation: 0 = exteriors, 1 = interiors, 2 = complete isolation (both interiors and exteriors) self.iso_type = 2 - # Attributes to be included in serialization - # Always append to it because it carries contents - # from predecessors. - self.ser_attrs += ['options', 'kind'] - self.multigeo = False + self.follow = False + self.apertures_row = 0 + # store the source file here + self.source_file = "" + # assert isinstance(self.ui, GerberObjectUI) # self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) # self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click) @@ -431,6 +440,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber): # self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click) # self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click) + # Attributes to be included in serialization + # Always append to it because it carries contents + # from predecessors. + self.ser_attrs += ['options', 'kind'] + def set_ui(self, ui): """ Maps options with GUI inputs. @@ -474,6 +488,20 @@ class FlatCAMGerber(FlatCAMObj, Gerber): self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click) self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click) self.ui.aperture_table_visibility_cb.stateChanged.connect(self.on_aperture_table_visibility_change) + self.ui.follow_cb.stateChanged.connect(self.on_follow_cb_click) + + # Show/Hide Advanced Options + if self.app.defaults["global_app_level"] == 'b': + self.ui.level.setText('Basic') + self.ui.apertures_table_label.hide() + self.ui.aperture_table_visibility_cb.hide() + self.ui.milling_type_label.hide() + self.ui.milling_type_radio.hide() + self.ui.generate_ext_iso_button.hide() + self.ui.generate_int_iso_button.hide() + + else: + self.ui.level.setText('Advanced') self.build_ui() @@ -654,7 +682,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber): def on_int_iso_button_click(self, *args): - if self.ui.follow_cb.get_value() == True: + if self.ui.follow_cb.get_value() is True: obj = self.app.collection.get_active() obj.follow() # in the end toggle the visibility of the origin object so we can see the generated Geometry @@ -666,9 +694,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber): def on_iso_button_click(self, *args): - if self.ui.follow_cb.get_value() == True: + if self.ui.follow_cb.get_value() is True: obj = self.app.collection.get_active() - obj.follow() + obj.follow_geo() # in the end toggle the visibility of the origin object so we can see the generated Geometry obj.ui.plot_cb.toggle() else: @@ -676,7 +704,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber): self.read_form() self.isolate() - def follow(self, outname=None): + def follow_geo(self, outname=None): """ Creates a geometry object "following" the gerber paths. @@ -694,7 +722,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber): def follow_init(follow_obj, app): # Propagate options follow_obj.options["cnctooldia"] = float(self.options["isotooldia"]) - follow_obj.solid_geometry = self.solid_geometry + follow_obj.solid_geometry = self.follow_geometry # TODO: Do something if this is None. Offer changing name? try: @@ -703,7 +731,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber): return "Operation failed: %s" % str(e) def isolate(self, iso_type=None, dia=None, passes=None, overlap=None, - outname=None, combine=None, milling_type=None): + outname=None, combine=None, milling_type=None, follow=None): """ Creates an isolation routing geometry object in the project. @@ -714,6 +742,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber): :param outname: Base name of the output object :return: None """ + + if dia is None: dia = float(self.options["isotooldia"]) if passes is None: @@ -734,7 +764,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber): base_name = self.options["name"] + "_iso" base_name = outname or base_name - def generate_envelope(offset, invert, envelope_iso_type=2): + def generate_envelope(offset, invert, envelope_iso_type=2, follow=None): # isolation_geometry produces an envelope that is going on the left of the geometry # (the copper features). To leave the least amount of burrs on the features # the tool needs to travel on the right side of the features (this is called conventional milling) @@ -742,7 +772,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber): # the other passes overlap preceding ones and cut the left over copper. It is better for them # to cut on the right side of the left over copper i.e on the left side of the features. try: - geom = self.isolation_geometry(offset, iso_type=envelope_iso_type) + geom = self.isolation_geometry(offset, iso_type=envelope_iso_type, follow=follow) except Exception as e: log.debug(str(e)) return 'fail' @@ -782,9 +812,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber): # if milling type is climb then the move is counter-clockwise around features if milling_type == 'cl': # geom = generate_envelope (offset, i == 0) - geom = generate_envelope(iso_offset, 1, envelope_iso_type=self.iso_type) + geom = generate_envelope(iso_offset, 1, envelope_iso_type=self.iso_type, follow=follow) else: - geom = generate_envelope(iso_offset, 0, envelope_iso_type=self.iso_type) + geom = generate_envelope(iso_offset, 0, envelope_iso_type=self.iso_type, follow=follow) geo_obj.solid_geometry.append(geom) # detect if solid_geometry is empty and this require list flattening which is "heavy" @@ -834,9 +864,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber): # if milling type is climb then the move is counter-clockwise around features if milling_type == 'cl': # geo_obj.solid_geometry = generate_envelope(offset, i == 0) - geo_obj.solid_geometry = generate_envelope(offset, 1, envelope_iso_type=self.iso_type) + geo_obj.solid_geometry = generate_envelope(offset, 1, envelope_iso_type=self.iso_type, + follow=follow) else: - geo_obj.solid_geometry = generate_envelope(offset, 0, envelope_iso_type=self.iso_type) + geo_obj.solid_geometry = generate_envelope(offset, 0, envelope_iso_type=self.iso_type, + follow=follow) # detect if solid_geometry is empty and this require list flattening which is "heavy" # or just looking in the lists (they are one level depth) and if any is not empty @@ -876,6 +908,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber): self.read_form_item('multicolored') self.plot() + def on_follow_cb_click(self): + if self.muted_ui: + return + self.plot() + def on_aperture_table_visibility_change(self): if self.ui.aperture_table_visibility_cb.isChecked(): self.ui.apertures_table.setVisible(True) @@ -921,7 +958,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber): else: face_color = self.app.defaults['global_plot_fill'] - geometry = self.solid_geometry + # if the Follow Geometry checkbox is checked then plot only the follow geometry + if self.ui.follow_cb.get_value(): + geometry = self.follow_geometry + else: + geometry = self.solid_geometry # Make sure geometry is iterable. try: @@ -941,6 +982,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber): self.add_shape(shape=g, color=color, face_color=random_color() if self.options['multicolored'] else face_color, visible=self.options['plot']) + elif type(g) == Point: + pass else: for el in g: self.add_shape(shape=el, color=color, @@ -951,6 +994,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber): if type(g) == Polygon or type(g) == LineString: self.add_shape(shape=g, color=random_color() if self.options['multicolored'] else 'black', visible=self.options['plot']) + elif type(g) == Point: + pass else: for el in g: self.add_shape(shape=el, color=random_color() if self.options['multicolored'] else 'black', @@ -1076,11 +1121,6 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): # dict to hold the tool number as key and tool offset as value self.tool_offset ={} - # Attributes to be included in serialization - # Always append to it because it carries contents - # from predecessors. - self.ser_attrs += ['options', 'kind'] - # variable to store the total amount of drills per job self.tot_drill_cnt = 0 self.tool_row = 0 @@ -1092,8 +1132,16 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): # variable to store the distance travelled self.travel_distance = 0.0 + # store the source file here + self.source_file = "" + self.multigeo = False + # Attributes to be included in serialization + # Always append to it because it carries contents + # from predecessors. + self.ser_attrs += ['options', 'kind'] + @staticmethod def merge(exc_list, exc_final): """ @@ -1522,6 +1570,24 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): dia = float('%.3f' % float(value['C'])) self.tool_offset[dia] = t_default_offset + # Show/Hide Advanced Options + if self.app.defaults["global_app_level"] == 'b': + self.ui.level.setText('Basic') + + self.ui.tools_table.setColumnHidden(4, True) + self.ui.estartz_label.hide() + self.ui.estartz_entry.hide() + self.ui.eendz_label.hide() + self.ui.eendz_entry.hide() + self.ui.feedrate_rapid_label.hide() + self.ui.feedrate_rapid_entry.hide() + self.ui.pdepth_label.hide() + self.ui.pdepth_entry.hide() + self.ui.feedrate_probe_label.hide() + self.ui.feedrate_probe_entry.hide() + else: + self.ui.level.setText('Advanced') + assert isinstance(self.ui, ExcellonObjectUI), \ "Expected a ExcellonObjectUI, got %s" % type(self.ui) self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) @@ -1989,8 +2055,14 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): tools = self.get_selected_tools_list() if len(tools) == 0: - self.app.inform.emit("[ERROR_NOTCL]Please select one or more tools from the list and try again.") - return + # if there is a single tool in the table (remember that the last 2 rows are for totals and do not count in + # tool number) it means that there are 3 rows (1 tool and 2 totals). + # in this case regardless of the selection status of that tool, use it. + if self.ui.tools_table.rowCount() == 3: + tools.append(self.ui.tools_table.item(0, 0).text()) + else: + self.app.inform.emit("[ERROR_NOTCL]Please select one or more tools from the list and try again.") + return xmin = self.options['xmin'] ymin = self.options['ymin'] @@ -2462,8 +2534,14 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): # flag to store if the V-Shape tool is selected in self.ui.geo_tools_table self.v_tool_type = None + # flag to store if the Geometry is type 'multi-geometry' meaning that each tool has it's own geometry + # the default value is False self.multigeo = False + # flag to store if the geometry is part of a special group of geometries that can't be processed by the default + # engine of FlatCAM. Most likely are generated by some of tools and are special cases of geometries. + self. special_group = None + # Attributes to be included in serialization # Always append to it because it carries contents # from predecessors. @@ -2732,6 +2810,30 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): self.ui.geo_tools_table.addContextMenu( "Delete", lambda: self.on_tool_delete(all=None), icon=QtGui.QIcon("share/delete32.png")) + # Show/Hide Advanced Options + if self.app.defaults["global_app_level"] == 'b': + self.ui.level.setText('Basic') + + self.ui.geo_tools_table.setColumnHidden(2, True) + self.ui.geo_tools_table.setColumnHidden(3, True) + self.ui.geo_tools_table.setColumnHidden(4, True) + self.ui.addtool_entry_lbl.hide() + self.ui.addtool_entry.hide() + self.ui.addtool_btn.hide() + self.ui.copytool_btn.hide() + self.ui.deltool_btn.hide() + self.ui.endzlabel.hide() + self.ui.gendz_entry.hide() + self.ui.fr_rapidlabel.hide() + self.ui.cncfeedrate_rapid_entry.hide() + self.ui.extracut_cb.hide() + self.ui.pdepth_label.hide() + self.ui.pdepth_entry.hide() + self.ui.feedrate_probe_label.hide() + self.ui.feedrate_probe_entry.hide() + else: + self.ui.level.setText('Advanced') + self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click) self.ui.paint_tool_button.clicked.connect(self.app.paint_tool.run) @@ -2832,8 +2934,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): self.ui.grid3.itemAt(i).widget().currentIndexChanged.disconnect() if isinstance(self.ui.grid3.itemAt(i).widget(), LengthEntry) or \ - isinstance(self.ui.grid3.itemAt(i), IntEntry) or \ - isinstance(self.ui.grid3.itemAt(i), FCEntry): + isinstance(self.ui.grid3.itemAt(i).widget(), IntEntry) or \ + isinstance(self.ui.grid3.itemAt(i).widget(), FCEntry): self.ui.grid3.itemAt(i).widget().editingFinished.disconnect() except: pass @@ -3544,6 +3646,17 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): self.app.report_usage("geometry_on_generatecnc_button") self.read_form() + + self.sel_tools = {} + + try: + if self.special_group: + self.app.inform.emit("[WARNING_NOTCL]This Geometry can't be processed because it is %s geometry." % + str(self.special_group)) + return + except AttributeError: + pass + # test to see if we have tools available in the tool table if self.ui.geo_tools_table.selectedItems(): for x in self.ui.geo_tools_table.selectedItems(): @@ -3565,8 +3678,19 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): tooluid: copy.deepcopy(tooluid_value) }) self.mtool_gen_cncjob() - self.ui.geo_tools_table.clearSelection() + + elif self.ui.geo_tools_table.rowCount() == 1: + tooluid = int(self.ui.geo_tools_table.item(0, 5).text()) + + for tooluid_key, tooluid_value in self.tools.items(): + if int(tooluid_key) == tooluid: + self.sel_tools.update({ + tooluid: copy.deepcopy(tooluid_value) + }) + self.mtool_gen_cncjob() + self.ui.geo_tools_table.clearSelection() + else: self.app.inform.emit("[ERROR_NOTCL] Failed. No tool selected in the tool table ...") @@ -3855,6 +3979,16 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): '[ERROR_NOTCL]Wrong value format for self.defaults["feedrate_probe"] ' 'or self.options["feedrate_probe"]') + # make sure that trying to make a CNCJob from an empty file is not creating an app crash + if not self.solid_geometry: + a = 0 + for tooluid_key in self.tools: + if self.tools[tooluid_key]['solid_geometry'] is None: + a += 1 + if a == len(self.tools): + self.app.inform.emit('[ERROR_NOTCL]Cancelled. Empty file, it has no geometry...') + return 'fail' + for tooluid_key in self.sel_tools: tool_cnt += 1 app_obj.progress.emit(20) @@ -4561,6 +4695,9 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): ''' self.exc_cnc_tools = {} + # flag to store if the CNCJob is part of a special group of CNCJob objects that can't be processed by the + # default engine of FlatCAM. They generated by some of tools and are special cases of CNCJob objects. + self. special_group = None # for now it show if the plot will be done for multi-tool CNCJob (True) or for single tool # (like the one in the TCL Command), False @@ -4736,6 +4873,13 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): # set the kind of geometries are plotted by default with plot2() from camlib.CNCJob self.ui.cncplot_method_combo.set_value(self.app.defaults["cncjob_plot_kind"]) + # Show/Hide Advanced Options + if self.app.defaults["global_app_level"] == 'b': + self.ui.level.setText('Basic') + + else: + self.ui.level.setText('Advanced') + self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click) self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click) self.ui.modify_gcode_button.clicked.connect(self.on_modifygcode_button_click) @@ -4803,13 +4947,24 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): preamble = str(self.ui.prepend_text.get_value()) postamble = str(self.ui.append_text.get_value()) - self.export_gcode(filename, preamble=preamble, postamble=postamble) + gc = self.export_gcode(filename, preamble=preamble, postamble=postamble) + if gc == 'fail': + return + self.app.file_saved.emit("gcode", filename) self.app.inform.emit("[success] Machine Code file saved to: %s" % filename) def on_modifygcode_button_click(self, *args): + preamble = str(self.ui.prepend_text.get_value()) + postamble = str(self.ui.append_text.get_value()) + gc = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True) + if gc == 'fail': + return + else: + self.app.gcode_edited = gc + # add the tab if it was closed - self.app.ui.plot_tab_area.addTab(self.app.ui.cncjob_tab, "CNC Code Editor") + self.app.ui.plot_tab_area.addTab(self.app.ui.cncjob_tab, "Code Editor") # delete the absolute and relative position and messages in the infobar self.app.ui.position_label.setText("") @@ -4818,10 +4973,6 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): # Switch plot_area to CNCJob tab self.app.ui.plot_tab_area.setCurrentWidget(self.app.ui.cncjob_tab) - preamble = str(self.ui.prepend_text.get_value()) - postamble = str(self.ui.append_text.get_value()) - self.app.gcode_edited = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True) - # print(self.app.gcode_edited) # first clear previous text in text editor (if any) self.app.ui.code_editor.clear() @@ -4935,6 +5086,14 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): roland = False hpgl = False + try: + if self.special_group: + self.app.inform.emit("[WARNING_NOTCL]This CNCJob object can't be processed because " + "it is a %s CNCJob object." % str(self.special_group)) + return 'fail' + except AttributeError: + pass + # detect if using Roland postprocessor try: for key in self.cnc_tools: diff --git a/FlatCAMPostProc.py b/FlatCAMPostProc.py index 41fd9f84..cc7a3d52 100644 --- a/FlatCAMPostProc.py +++ b/FlatCAMPostProc.py @@ -20,6 +20,7 @@ class ABCPostProcRegister(ABCMeta): postprocessors[newclass.__name__] = newclass() # here is your register function return newclass + class FlatCAMPostProc(object, metaclass=ABCPostProcRegister): @abstractmethod def start_code(self, p): @@ -65,6 +66,77 @@ class FlatCAMPostProc(object, metaclass=ABCPostProcRegister): def spindle_stop_code(self,p): pass + +class FlatCAMPostProc_Tools(object, metaclass=ABCPostProcRegister): + @abstractmethod + def start_code(self, p): + pass + + @abstractmethod + def lift_code(self, p): + pass + + @abstractmethod + def down_z_start_code(self, p): + pass + + @abstractmethod + def lift_z_dispense_code(self, p): + pass + + @abstractmethod + def down_z_stop_code(self, p): + pass + + @abstractmethod + def toolchange_code(self, p): + pass + + @abstractmethod + def rapid_code(self, p): + pass + + @abstractmethod + def linear_code(self, p): + pass + + @abstractmethod + def end_code(self, p): + pass + + @abstractmethod + def feedrate_xy_code(self, p): + pass + + @abstractmethod + def feedrate_z_code(self, p): + pass + + @abstractmethod + def feedrate_z_dispense_code(self,p): + pass + + @abstractmethod + def spindle_fwd_code(self,p): + pass + + @abstractmethod + def spindle_rev_code(self,p): + pass + + @abstractmethod + def spindle_off_code(self,p): + pass + + @abstractmethod + def dwell_fwd_code(self,p): + pass + + @abstractmethod + def dwell_rev_code(self,p): + pass + + def load_postprocessors(app): postprocessors_path_search = [os.path.join(app.data_path,'postprocessors','*.py'), os.path.join('postprocessors', '*.py')] diff --git a/GUIElements.py b/GUIElements.py index 6eaac17d..f14e4dcf 100644 --- a/GUIElements.py +++ b/GUIElements.py @@ -490,7 +490,8 @@ class FCComboBox(QtWidgets.QComboBox): class FCInputDialog(QtWidgets.QInputDialog): - def __init__(self, parent=None, ok=False, val=None, title=None, text=None, min=None, max=None, decimals=None): + def __init__(self, parent=None, ok=False, val=None, title=None, text=None, min=None, max=None, decimals=None, + init_val=None): super(FCInputDialog, self).__init__(parent) self.allow_empty = ok self.empty_val = val @@ -498,6 +499,8 @@ class FCInputDialog(QtWidgets.QInputDialog): self.val = 0.0 self.ok = '' + self.init_value = init_val if init_val else 0.0 + if title is None: self.title = 'title' else: @@ -521,7 +524,7 @@ class FCInputDialog(QtWidgets.QInputDialog): def get_value(self): self.val, self.ok = self.getDouble(self, self.title, self.text, min=self.min, - max=self.max, decimals=self.decimals) + max=self.max, decimals=self.decimals, value=self.init_value) return [self.val, self.ok] # "Transform", "Enter the Angle value:" diff --git a/ObjectCollection.py b/ObjectCollection.py index dc60edec..97dcb0fd 100644 --- a/ObjectCollection.py +++ b/ObjectCollection.py @@ -504,6 +504,8 @@ class ObjectCollection(QtCore.QAbstractItemModel): sel = len(self.view.selectedIndexes()) > 0 self.app.ui.menuprojectenable.setEnabled(sel) self.app.ui.menuprojectdisable.setEnabled(sel) + self.app.ui.menuprojectviewsource.setEnabled(sel) + self.app.ui.menuprojectcopy.setEnabled(sel) self.app.ui.menuprojectedit.setEnabled(sel) self.app.ui.menuprojectdelete.setEnabled(sel) @@ -514,14 +516,15 @@ class ObjectCollection(QtCore.QAbstractItemModel): self.app.ui.menuprojectgeneratecnc.setVisible(True) self.app.ui.menuprojectedit.setVisible(True) self.app.ui.menuprojectsave.setVisible(True) + self.app.ui.menuprojectviewsource.setVisible(True) for obj in self.get_selected(): if type(obj) != FlatCAMGeometry: self.app.ui.menuprojectgeneratecnc.setVisible(False) if type(obj) != FlatCAMGeometry and type(obj) != FlatCAMExcellon: self.app.ui.menuprojectedit.setVisible(False) - if type(obj) != FlatCAMGeometry and type(obj) != FlatCAMExcellon and type(obj) != FlatCAMCNCjob: - self.app.ui.menuprojectsave.setVisible(False) + if type(obj) != FlatCAMGerber and type(obj) != FlatCAMExcellon: + self.app.ui.menuprojectviewsource.setVisible(False) else: self.app.ui.menuprojectgeneratecnc.setVisible(False) @@ -918,7 +921,7 @@ class ObjectCollection(QtCore.QAbstractItemModel): except IndexError: FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)") - + self.app.inform.emit('') try: self.app.ui.selected_scroll_area.takeWidget() except: diff --git a/ObjectUI.py b/ObjectUI.py index 1dc9074e..f8d38493 100644 --- a/ObjectUI.py +++ b/ObjectUI.py @@ -32,6 +32,19 @@ class ObjectUI(QtWidgets.QWidget): self.title_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) self.title_box.addWidget(self.title_label, stretch=1) + ## App Level label + self.level = QtWidgets.QLabel("") + self.level.setToolTip( + "BASIC is suitable for a beginner. Many parameters\n" + "are hidden from the user in this mode.\n" + "ADVANCED mode will make available all parameters.\n\n" + "To change the application LEVEL, go to:\n" + "Edit -> Preferences -> General and check:\n" + "'APP. LEVEL' radio button." + ) + self.level.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.title_box.addWidget(self.level) + ## Box box for custom widgets # This gets populated in offspring implementations. self.custom_box = QtWidgets.QVBoxLayout() @@ -237,13 +250,13 @@ class GerberObjectUI(ObjectUI): grid1.addWidget(self.iso_overlap_entry, 2, 1) # Milling Type Radio Button - milling_type_label = QtWidgets.QLabel('Milling Type:') - milling_type_label.setToolTip( + self.milling_type_label = QtWidgets.QLabel('Milling Type:') + self.milling_type_label.setToolTip( "Milling type:\n" "- climb / best for precision milling and to reduce tool usage\n" "- conventional / useful when there is no backlash compensation" ) - grid1.addWidget(milling_type_label, 3, 0) + grid1.addWidget(self.milling_type_label, 3, 0) self.milling_type_radio = RadioSet([{'label': 'Climb', 'value': 'cl'}, {'label': 'Conv.', 'value': 'cv'}]) grid1.addWidget(self.milling_type_radio, 3, 1) @@ -256,13 +269,12 @@ class GerberObjectUI(ObjectUI): grid1.addWidget(self.combine_passes_cb, 4, 0) # generate follow - self.follow_cb = FCCheckBox(label='"Follow" Geo') + self.follow_cb = FCCheckBox(label='"Follow"') self.follow_cb.setToolTip( "Generate a 'Follow' geometry.\n" "This means that it will cut through\n" - "the middle of the trace.\n" - "Requires that the Gerber file to be\n" - "loaded with 'follow' parameter." + "the middle of the trace." + ) grid1.addWidget(self.follow_cb, 4, 1) @@ -380,16 +392,19 @@ class GerberObjectUI(ObjectUI): # Rounded corners self.noncopper_rounded_cb = FCCheckBox(label="Rounded corners") self.noncopper_rounded_cb.setToolTip( - "Creates a Geometry objects with polygons\n" - "covering the copper-free areas of the PCB." + "Resulting geometry will have rounded corners." ) - grid4.addWidget(self.noncopper_rounded_cb, 1, 0, 1, 2) + grid4.addWidget(self.noncopper_rounded_cb, 1, 0) - self.generate_noncopper_button = QtWidgets.QPushButton('Generate Geometry') - self.custom_box.addWidget(self.generate_noncopper_button) + self.generate_noncopper_button = QtWidgets.QPushButton('Generate Geo') + grid4.addWidget(self.generate_noncopper_button, 1, 1) ## Bounding box self.boundingbox_label = QtWidgets.QLabel('Bounding Box:') + self.boundingbox_label.setToolTip( + "Create a geometry surrounding the Gerber object.\n" + "Square shape." + ) self.custom_box.addWidget(self.boundingbox_label) grid5 = QtWidgets.QGridLayout() @@ -411,13 +426,13 @@ class GerberObjectUI(ObjectUI): "their radius is equal to\n" "the margin." ) - grid5.addWidget(self.bbrounded_cb, 1, 0, 1, 2) + grid5.addWidget(self.bbrounded_cb, 1, 0) - self.generate_bb_button = QtWidgets.QPushButton('Generate Geometry') + self.generate_bb_button = QtWidgets.QPushButton('Generate Geo') self.generate_bb_button.setToolTip( - "Genrate the Geometry object." + "Generate the Geometry object." ) - self.custom_box.addWidget(self.generate_bb_button) + grid5.addWidget(self.generate_bb_button, 1, 1) class ExcellonObjectUI(ObjectUI): @@ -485,7 +500,7 @@ class ExcellonObjectUI(ObjectUI): self.tools_box.addWidget(self.tools_table) self.tools_table.setColumnCount(6) - self.tools_table.setHorizontalHeaderLabels(['#', 'Diameter', 'Drills', 'Slots', 'Offset', 'P']) + self.tools_table.setHorizontalHeaderLabels(['#', 'Diameter', 'Drills', 'Slots', 'Offset Z', 'P']) self.tools_table.setSortingEnabled(False) self.tools_table.horizontalHeaderItem(0).setToolTip( @@ -562,22 +577,22 @@ class ExcellonObjectUI(ObjectUI): self.ois_tcz_e = OptionalInputSection(self.toolchange_cb, [self.toolchangez_entry]) # Start move Z: - startzlabel = QtWidgets.QLabel("Start move Z:") - startzlabel.setToolTip( + self.estartz_label = QtWidgets.QLabel("Start move Z:") + self.estartz_label.setToolTip( "Tool height just before starting the work.\n" "Delete the value if you don't need this feature." ) - grid1.addWidget(startzlabel, 4, 0) + grid1.addWidget(self.estartz_label, 4, 0) self.estartz_entry = FloatEntry() grid1.addWidget(self.estartz_entry, 4, 1) # End move Z: - endzlabel = QtWidgets.QLabel("End move Z:") - endzlabel.setToolTip( + self.eendz_label = QtWidgets.QLabel("End move Z:") + self.eendz_label.setToolTip( "Z-axis position (height) for\n" "the last move." ) - grid1.addWidget(endzlabel, 5, 0) + grid1.addWidget(self.eendz_label, 5, 0) self.eendz_entry = LengthEntry() grid1.addWidget(self.eendz_entry, 5, 1) @@ -593,13 +608,13 @@ class ExcellonObjectUI(ObjectUI): grid1.addWidget(self.feedrate_entry, 6, 1) # Excellon Rapid Feedrate - fr_rapid_label = QtWidgets.QLabel('Feedrate Rapids:') - fr_rapid_label.setToolTip( + self.feedrate_rapid_label = QtWidgets.QLabel('Feedrate Rapids:') + self.feedrate_rapid_label.setToolTip( "Tool speed while drilling\n" "(in units per minute).\n" "This is for the rapid move G00." ) - grid1.addWidget(fr_rapid_label, 7, 0) + grid1.addWidget(self.feedrate_rapid_label, 7, 0) self.feedrate_rapid_entry = LengthEntry() grid1.addWidget(self.feedrate_rapid_entry, 7, 1) @@ -976,7 +991,6 @@ class GeometryObjectUI(ObjectUI): ) self.grid3.addWidget(self.mpass_cb, 4, 0) - self.maxdepth_entry = LengthEntry() self.maxdepth_entry.setToolTip( "Depth of each pass (positive)." @@ -1026,12 +1040,12 @@ class GeometryObjectUI(ObjectUI): # self.grid3.addWidget(self.gstartz_entry, 8, 1) # The Z value for the end move - endzlabel = QtWidgets.QLabel('End move Z:') - endzlabel.setToolTip( + self.endzlabel = QtWidgets.QLabel('End move Z:') + self.endzlabel.setToolTip( "This is the height (Z) at which the CNC\n" "will go as the last move." ) - self.grid3.addWidget(endzlabel, 9, 0) + self.grid3.addWidget(self.endzlabel, 9, 0) self.gendz_entry = LengthEntry() self.grid3.addWidget(self.gendz_entry, 9, 1) @@ -1056,13 +1070,13 @@ class GeometryObjectUI(ObjectUI): self.grid3.addWidget(self.cncplunge_entry, 11, 1) # Feedrate rapids - fr_rapidlabel = QtWidgets.QLabel('Feed Rate Rapids:') - fr_rapidlabel.setToolTip( + self.fr_rapidlabel = QtWidgets.QLabel('Feed Rate Rapids:') + self.fr_rapidlabel.setToolTip( "Cutting speed in the XY\n" "plane in units per minute\n" "for the rapid movements" ) - self.grid3.addWidget(fr_rapidlabel, 12, 0) + self.grid3.addWidget(self.fr_rapidlabel, 12, 0) self.cncfeedrate_rapid_entry = LengthEntry() self.grid3.addWidget(self.cncfeedrate_rapid_entry, 12, 1) @@ -1338,9 +1352,9 @@ class CNCObjectUI(ObjectUI): self.custom_box.addLayout(h_lay) # Edit GCode Button - self.modify_gcode_button = QtWidgets.QPushButton('Edit CNC Code') + self.modify_gcode_button = QtWidgets.QPushButton('View CNC Code') self.modify_gcode_button.setToolTip( - "Opens TAB to modify/print G-Code\n" + "Opens TAB to view/modify/print G-Code\n" "file." ) diff --git a/PlotCanvas.py b/PlotCanvas.py index 56333568..6d66f3d7 100644 --- a/PlotCanvas.py +++ b/PlotCanvas.py @@ -198,6 +198,13 @@ class PlotCanvas(QtCore.QObject): except TypeError: pass + # adjust the view camera to be slightly bigger than the bounds so the shape colleaction can be seen clearly + # otherwise the shape collection boundary will have no border + rect.left *= 0.96 + rect.bottom *= 0.96 + rect.right *= 1.01 + rect.top *= 1.01 + self.vispy_canvas.view.camera.rect = rect self.shape_collection.unlock_updates() diff --git a/README.md b/README.md index 51f70fbd..14c57144 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,74 @@ CAD program, and create G-Code for Isolation routing. ================================================= +21.02.2019 + +- added protection against creating CNCJob from an empty Geometry object (with no geometry inside) +- changed the shortcut key for YouTube channel from F2 to key F4 +- changed the way APP LEVEL is showed both in Edit -> Preferences -> General tab and in each Selected Tab. Changed the ToolTips content for this. +- added the functions for GCode View and GCode Save in Tool SolderPaste +- some work in the Gcode generation function in Tool SolderPaste +- added protection against trying to create a CNCJob from a solder_paste dispenser geometry. This one is different than the default Geometry and can be handled only by SolderPaste Tool. +- ToolSolderPaste tools (nozzles) now have each it's own settings +- creating the camlib functions for the ToolSolderPaste gcode generation functions +- finished work in ToolSolderPaste + +20.02.2019 + +- finished added a Tool Table for Tool SolderPaste +- working on multi tool solder paste dispensing +- finished the Edit -> Preferences defaults section +- finished the UI, created the postprocessor file template +- finished the multi-tool solder paste dispensing: it will start using the biggest nozzle, fill the pads it can, and then go to the next smaller nozzle until there are no pads without solder. + +19.02.2019 + +- added the ability to compress the FlatCAM project on save with LZMA compression. There is a setting in Edit -> Preferences -> Compression Level between 0 and 9. 9 level yields best compression at the price of RAM usage and time spent. +- made FlatCAM able to load old type (uncompressed) FlatCAM projects +- fixed issue with not loading old projects that do not have certain information's required by the new versions of FlatCAM +- compacted a bit more the GUI for Gerber Object +- removed the Open Gerber with 'follow' menu entry and also the open_gerber Tcl Command attribute 'follow'. This is no longer required because now the follow_geometry is stored by default in a Gerber object attribute gerber_obj.follow_geometry +- added a new parameter for the Tcl CommandIsolate, named: 'follow'. When follow = 1 (True) the resulting geometry will follow the Gerber paths. +- added a new setting in Edit -> Preferences -> General that allow to select the type of saving for the FlatCAM project: either compressed or uncompressed. Compression introduce an time overhead to the saving/restoring of a FlatCAM project. +- started to work on Solder Paste Dispensing Tool +- fixed a bug in rotate from shortcut function +- finished generating the solder paste dispense geometry + +18.02.2019 + +- added protections again wrong values for the Buffer and Paint Tool in Geometry Editor +- the Paint Tool in Geometry Editor will load the default values from Tool Paint in Preferences +- when the Tools in Geometry Editor are activated, the notebook with the Tool Tab will be unhidden. After execution the notebook will hide again for the Buffer Tool. +- changed the font in Tool names +- added in Geometry Editor a new Tool: Transformation Tool. +- in Geometry Editor by selecting a shape with a selection shape, that object was added multiple times (one per each selection) to the selected list, which is not intended. Bug fixed. +- finished adding Transform Tool in Geometry Editor - everything is working as intended +- fixed a bug in Tool Transform that made the user to not be able to capture the click coordinates with SHIFT + LMB click combo +- added the ability to choose an App QStyle out of the offered choices (different for each OS) to be applied at the next app start (Preferences -> General -> Gui Pref -> Style Combobox) +- added support for FlatCAM usage with High DPI monitors (4k). It is applied on the next app startup after change in Preferences -> General -> Gui Settings -> HDPI Support Checkbox +- made the app not remember the window size if the app is maximized and remember in QSettings if it was maximized. This way we can restore the maximized state but restore the windows size unmaximized +- added a button to clear the GUI preferences in Preferences -> General -> Gui Settings -> Clear GUI Settings +- added key shortcuts for the shape transformations within Geometry Editor: X, Y keys for Flip(mirror), SHIFT+X, SHIFT+Y combo keys for Skew and ALT+X, ALT+Y combo keys for Offset +- adjusted the plotcanvas.zomm_fit() function so the objects are better fit into view (with a border around) +- modified the GUI in Objects Selected Tab to accommodate 2 different modes: basic and Advanced. In Basic mode, some of the functionality's are hidden from the user. +- added Tool Transform preferences in Edit -> Preferences and used them through out the app +- made the output of Panelization Tool a choice out of Gerber and Geometry type of objects. Useful for those who want to engrave multiple copies of the same design. + +17.02.2019 + +- changed some status bar messages +- New feature: added the capability to view the source code of the Gerber/Excellon file that was loaded into the app. The file is also stored as an object attribute for later use. The view option is in the project context menu and in Menu -> Options -> View Source +- Serialized the source_file of the Objects so it is saved in the FlatCAM project and restored. +- if there is a single tool in the tool list (Geometry , Excellon) and the user click the Generate GCode, use that tool even if it is not selected +- fixed issue where after loading a project, if the default kind of CNCjob view is only 'cuts' the plot will revert to the 'all' type +- in Editors, if the modifier key set in Preferences (CTRL or SHIFT key) is pressed at the end of one tool operation it will automatically continue to that action until the modifier is no longer pressed when Select tool will be automatically selected. +- in Geometry Editor, on entry the notebook is automatically hidden and restored on Geometry Editor exit. +- when pressing Escape in Geometry Editor it will automatically deselect any shape not only the currently selected tool. +- when deselecting an object in Project menu the status bar selection message is deleted +- added ability to save the Gerber file content that is stored in FlatCAM on Gerber file loading. It's useful to recover from saved FlatCAM projects when the source files are no longer available. +- fixed an issue where the function handler that changed the layout had a parameter changed accidentally by an index value passed by the 'activate' signal to which was connected +- fixed bug in paint function in Geometry Editor that didn't allow painting due of overlap value + 16.02.2019 - added the 'Save' menu entry to the Project context menu, for CNCJob: it will export the GCode. diff --git a/camlib.py b/camlib.py index bbe3de02..0686bc0a 100644 --- a/camlib.py +++ b/camlib.py @@ -92,8 +92,11 @@ class Geometry(object): # Final geometry: MultiPolygon or list (of geometry constructs) self.solid_geometry = None + # Final geometry: MultiLineString or list (of LineString or Points) + self.follow_geometry = None + # Attributes to be included in serialization - self.ser_attrs = ["units", 'solid_geometry'] + self.ser_attrs = ["units", 'solid_geometry', 'follow_geometry'] # Flattened geometry (list of paths only) self.flat_geometry = [] @@ -500,7 +503,7 @@ class Geometry(object): # # return self.flat_geometry, self.flat_geometry_rtree - def isolation_geometry(self, offset, iso_type=2, corner=None): + def isolation_geometry(self, offset, iso_type=2, corner=None, follow=None): """ Creates contours around geometry at a given offset distance. @@ -542,16 +545,24 @@ class Geometry(object): # the previously commented block is replaced with this block - regression - to solve the bug with multiple # isolation passes cutting from the copper features if offset == 0: - geo_iso = self.solid_geometry - else: - if corner is None: - geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4)) + if follow: + geo_iso = self.follow_geometry else: - geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4), join_style=corner) + geo_iso = self.solid_geometry + else: + if follow: + geo_iso = self.follow_geometry + else: + if corner is None: + geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4)) + else: + geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4), + join_style=corner) # end of replaced block - - if iso_type == 2: + if follow: + return geo_iso + elif iso_type == 2: return geo_iso elif iso_type == 0: return self.get_exteriors(geo_iso) @@ -1375,8 +1386,6 @@ class Geometry(object): except AttributeError: self.app.inform.emit("[ERROR_NOTCL] Failed to mirror. No object selected") - - def rotate(self, angle, point): """ Rotate an object by an angle (in degrees) around the provided coordinates. @@ -1891,8 +1900,12 @@ class Gerber (Geometry): # Initialize parent Geometry.__init__(self, geo_steps_per_circle=int(steps_per_circle)) + # will store the Gerber geometry's as solids self.solid_geometry = Polygon() + # will store the Gerber geometry's as paths + self.follow_geometry = [] + # Number format self.int_digits = 3 """Number of integer digits in Gerber numbers. Used during parsing.""" @@ -1913,11 +1926,13 @@ class Gerber (Geometry): # Aperture Macros self.aperture_macros = {} + self.source_file = '' + # Attributes to be included in serialization # Always append to it because it carries contents # from Geometry. self.ser_attrs += ['int_digits', 'frac_digits', 'apertures', - 'aperture_macros', 'solid_geometry'] + 'aperture_macros', 'solid_geometry', 'source_file'] #### Parser patterns #### # FS - Format Specification @@ -2113,10 +2128,10 @@ class Gerber (Geometry): yield line break - self.parse_lines(line_generator(), follow=follow) + self.parse_lines(line_generator()) #@profile - def parse_lines(self, glines, follow=False): + def parse_lines(self, glines): """ Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``, ``self.flashes``, ``self.regions`` and ``self.units``. @@ -2143,6 +2158,9 @@ class Gerber (Geometry): # applying a union for every new polygon. poly_buffer = [] + # store here the follow geometry + follow_buffer = [] + last_path_aperture = None current_aperture = None @@ -2185,6 +2203,8 @@ class Gerber (Geometry): for gline in glines: line_num += 1 + self.source_file += gline + '\n' + ### Cleanup gline = gline.strip(' \r\n') # log.debug("Line=%3s %s" % (line_num, gline)) @@ -2206,10 +2226,11 @@ class Gerber (Geometry): # --- Buffered ---- width = self.apertures[last_path_aperture]["size"] - if follow: - geo = LineString(path) - else: - geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) + geo = LineString(path) + if not geo.is_empty: + follow_buffer.append(geo) + + geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) if not geo.is_empty: poly_buffer.append(geo) @@ -2220,9 +2241,14 @@ class Gerber (Geometry): # TODO: Remove when bug fixed if len(poly_buffer) > 0: if current_polarity == 'D': + self.follow_geometry = self.solid_geometry.union(cascaded_union(follow_buffer)) self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer)) + else: - self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer)) + self.follow_geometry = self.solid_geometry.difference(cascaded_union(follow_buffer)) + self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer)) + + follow_buffer = [] poly_buffer = [] current_polarity = match.group(1) @@ -2366,8 +2392,6 @@ class Gerber (Geometry): log.debug("Bare op-code %d." % current_operation_code) # flash = Gerber.create_flash_geometry(Point(path[-1]), # self.apertures[current_aperture]) - if follow: - continue flash = Gerber.create_flash_geometry( Point(current_x, current_y), self.apertures[current_aperture], int(self.steps_per_circle)) @@ -2403,10 +2427,11 @@ class Gerber (Geometry): # --- Buffered ---- width = self.apertures[last_path_aperture]["size"] - if follow: - geo = LineString(path) - else: - geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) + geo = LineString(path) + if not geo.is_empty: + follow_buffer.append(geo) + + geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) if not geo.is_empty: poly_buffer.append(geo) @@ -2422,10 +2447,11 @@ class Gerber (Geometry): ## --- Buffered --- width = self.apertures[last_path_aperture]["size"] - if follow: - geo = LineString(path) - else: - geo = LineString(path).buffer(width/1.999, int(self.steps_per_circle / 4)) + geo = LineString(path) + if not geo.is_empty: + follow_buffer.append(geo) + + geo = LineString(path).buffer(width/1.999, int(self.steps_per_circle / 4)) if not geo.is_empty: poly_buffer.append(geo) @@ -2443,6 +2469,7 @@ class Gerber (Geometry): if current_operation_code == 2: if geo: if not geo.is_empty: + follow_buffer.append(geo) poly_buffer.append(geo) continue @@ -2462,15 +2489,14 @@ class Gerber (Geometry): # "aperture": last_path_aperture}) # --- Buffered --- - if follow: - region = Polygon() - else: - region = Polygon(path) + region = Polygon() + if not region.is_empty: + follow_buffer.append(region) + + region = Polygon(path) if not region.is_valid: - if not follow: - region = region.buffer(0, int(self.steps_per_circle / 4)) - + region = region.buffer(0, int(self.steps_per_circle / 4)) if not region.is_empty: poly_buffer.append(region) @@ -2527,7 +2553,7 @@ class Gerber (Geometry): if path[-1] != [linear_x, linear_y]: path.append([linear_x, linear_y]) - if follow == 0 and making_region is False: + if making_region is False: # if the aperture is rectangle then add a rectangular shape having as parameters the # coordinates of the start and end point and also the width and height # of the 'R' aperture @@ -2553,29 +2579,35 @@ class Gerber (Geometry): geo = None ## --- BUFFERED --- + # this treats the case when we are storing geometry as paths only if making_region: - if follow: - geo = Polygon() - else: - elem = [linear_x, linear_y] - if elem != path[-1]: - path.append([linear_x, linear_y]) - try: - geo = Polygon(path) - except ValueError: - log.warning("Problem %s %s" % (gline, line_num)) - self.app.inform.emit("[ERROR] Region does not have enough points. " - "File will be processed but there are parser errors. " - "Line number: %s" % str(line_num)) + geo = Polygon() + else: + geo = LineString(path) + try: + if self.apertures[last_path_aperture]["type"] != 'R': + if not geo.is_empty: + follow_buffer.append(geo) + except: + follow_buffer.append(geo) + + # this treats the case when we are storing geometry as solids + if making_region: + elem = [linear_x, linear_y] + if elem != path[-1]: + path.append([linear_x, linear_y]) + try: + geo = Polygon(path) + except ValueError: + log.warning("Problem %s %s" % (gline, line_num)) + self.app.inform.emit("[ERROR] Region does not have enough points. " + "File will be processed but there are parser errors. " + "Line number: %s" % str(line_num)) else: if last_path_aperture is None: log.warning("No aperture defined for curent path. (%d)" % line_num) width = self.apertures[last_path_aperture]["size"] # TODO: WARNING this should fail! - #log.debug("Line %d: Setting aperture to %s before buffering." % (line_num, last_path_aperture)) - if follow: - geo = LineString(path) - else: - geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) + geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) try: if self.apertures[last_path_aperture]["type"] != 'R': @@ -2598,19 +2630,23 @@ class Gerber (Geometry): # Create path draw so far. if len(path) > 1: # --- Buffered ---- + + # this treats the case when we are storing geometry as paths + geo = LineString(path) + if not geo.is_empty: + try: + if self.apertures[current_aperture]["type"] != 'R': + follow_buffer.append(geo) + except: + follow_buffer.append(geo) + + # this treats the case when we are storing geometry as solids width = self.apertures[last_path_aperture]["size"] - - if follow: - geo = LineString(path) - else: - geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) - + geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) if not geo.is_empty: try: if self.apertures[current_aperture]["type"] != 'R': poly_buffer.append(geo) - else: - pass except: poly_buffer.append(geo) @@ -2619,11 +2655,12 @@ class Gerber (Geometry): # --- BUFFERED --- # Draw the flash - if follow: - continue + # this treats the case when we are storing geometry as paths + follow_buffer.append(Point([linear_x, linear_y])) + + # this treats the case when we are storing geometry as solids flash = Gerber.create_flash_geometry( - Point( - [linear_x, linear_y]), + Point( [linear_x, linear_y]), self.apertures[current_aperture], int(self.steps_per_circle) ) @@ -2709,10 +2746,13 @@ class Gerber (Geometry): # --- BUFFERED --- width = self.apertures[last_path_aperture]["size"] - if follow: - buffered = LineString(path) - else: - buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle)) + # this treats the case when we are storing geometry as paths + geo = LineString(path) + if not geo.is_empty: + follow_buffer.append(geo) + + # this treats the case when we are storing geometry as solids + buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle)) if not buffered.is_empty: poly_buffer.append(buffered) @@ -2831,19 +2871,24 @@ class Gerber (Geometry): else: # EOF, create shapely LineString if something still in path ## --- Buffered --- + + # this treats the case when we are storing geometry as paths + geo = LineString(path) + if not geo.is_empty: + follow_buffer.append(geo) + + # this treats the case when we are storing geometry as solids width = self.apertures[last_path_aperture]["size"] - if follow: - geo = LineString(path) - else: - geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) + geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) if not geo.is_empty: poly_buffer.append(geo) # --- Apply buffer --- - if follow: - self.solid_geometry = poly_buffer - return + # this treats the case when we are storing geometry as paths + self.follow_geometry = follow_buffer + + # this treats the case when we are storing geometry as solids log.warning("Joining %d polygons." % len(poly_buffer)) if len(poly_buffer) == 0: @@ -3293,6 +3338,8 @@ class Excellon(Geometry): # self.slots (list) to store the slots; each is a dictionary self.slots = [] + self.source_file = '' + # it serve to flag if a start routing or a stop routing was encountered # if a stop is encounter and this flag is still 0 (so there is no stop for a previous start) issue error self.routing_flag = 1 @@ -3323,7 +3370,8 @@ class Excellon(Geometry): # Always append to it because it carries contents # from Geometry. self.ser_attrs += ['tools', 'drills', 'zeros', 'excellon_format_upper_mm', 'excellon_format_lower_mm', - 'excellon_format_upper_in', 'excellon_format_lower_in', 'excellon_units', 'slots'] + 'excellon_format_upper_in', 'excellon_format_lower_in', 'excellon_units', 'slots', + 'source_file'] #### Patterns #### # Regex basics: @@ -3469,6 +3517,8 @@ class Excellon(Geometry): line_num += 1 # log.debug("%3d %s" % (line_num, str(eline))) + self.source_file += eline + # Cleanup lines eline = eline.strip(' \r\n') @@ -3819,7 +3869,7 @@ class Excellon(Geometry): self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool}) repeat -= 1 repeating_x = repeating_y = 0 - log.debug("{:15} {:8} {:8}".format(eline, x, y)) + # log.debug("{:15} {:8} {:8}".format(eline, x, y)) continue ## Coordinates with period: Use literally. ## @@ -3901,7 +3951,7 @@ class Excellon(Geometry): self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool}) repeat -= 1 repeating_x = repeating_y = 0 - log.debug("{:15} {:8} {:8}".format(eline, x, y)) + # log.debug("{:15} {:8} {:8}".format(eline, x, y)) continue #### Header #### @@ -4004,7 +4054,6 @@ class Excellon(Geometry): # is finished since the tools definitions are spread in the Excellon body. We use as units the value # from self.defaults['excellon_units'] log.info("Zeros: %s, Units %s." % (self.zeros, self.units)) - except Exception as e: log.error("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline)) msg = "[ERROR_NOTCL] An internal error has ocurred. See shell.\n" @@ -4454,6 +4503,9 @@ class CNCjob(Geometry): self.pp_excellon_name = pp_excellon_name self.pp_excellon = self.app.postprocessors[self.pp_excellon_name] + self.pp_solderpaste_name = None + + # Controls if the move from Z_Toolchange to Z_Move is done fast with G0 or normally with G1 self.f_plunge = None @@ -4478,6 +4530,8 @@ class CNCjob(Geometry): self.oldx = None self.oldy = None + self.tool = 0.0 + # Attributes to be included in serialization # Always append to it because it carries contents # from Geometry. @@ -4977,7 +5031,7 @@ class CNCjob(Geometry): :param depthpercut: Maximum depth in each pass. :param extracut: Adds (or not) an extra cut at the end of each path overlapping the first point in path to ensure complete copper removal - :return: None + :return: GCode - string """ log.debug("Generate_from_multitool_geometry()") @@ -5103,7 +5157,9 @@ class CNCjob(Geometry): log.debug("Starting G-Code...") path_count = 0 current_pt = (0, 0) + pt, geo = storage.nearest(current_pt) + try: while True: path_count += 1 @@ -5392,13 +5448,162 @@ class CNCjob(Geometry): return self.gcode + def generate_gcode_from_solderpaste_geo(self, **kwargs): + """ + Algorithm to generate from multitool Geometry. + + Algorithm description: + ---------------------- + Uses RTree to find the nearest path to follow. + + :return: Gcode string + """ + + log.debug("Generate_from_solderpaste_geometry()") + + ## Index first and last points in paths + # What points to index. + def get_pts(o): + return [o.coords[0], o.coords[-1]] + + self.gcode = "" + + if not kwargs: + log.debug("camlib.generate_from_solderpaste_geo() --> No tool in the solderpaste geometry.") + self.app.inform.emit("[ERROR_NOTCL] There is no tool data in the SolderPaste geometry.") + + + # this is the tool diameter, it is used as such to accommodate the postprocessor who need the tool diameter + # given under the name 'toolC' + + self.postdata['z_start'] = kwargs['data']['tools_solderpaste_z_start'] + self.postdata['z_dispense'] = kwargs['data']['tools_solderpaste_z_dispense'] + self.postdata['z_stop'] = kwargs['data']['tools_solderpaste_z_stop'] + self.postdata['z_travel'] = kwargs['data']['tools_solderpaste_z_travel'] + self.postdata['z_toolchange'] = kwargs['data']['tools_solderpaste_z_toolchange'] + self.postdata['xy_toolchange'] = kwargs['data']['tools_solderpaste_xy_toolchange'] + self.postdata['frxy'] = kwargs['data']['tools_solderpaste_frxy'] + self.postdata['frz'] = kwargs['data']['tools_solderpaste_frz'] + self.postdata['frz_dispense'] = kwargs['data']['tools_solderpaste_frz_dispense'] + self.postdata['speedfwd'] = kwargs['data']['tools_solderpaste_speedfwd'] + self.postdata['dwellfwd'] = kwargs['data']['tools_solderpaste_dwellfwd'] + self.postdata['speedrev'] = kwargs['data']['tools_solderpaste_speedrev'] + self.postdata['dwellrev'] = kwargs['data']['tools_solderpaste_dwellrev'] + self.postdata['pp_solderpaste_name'] = kwargs['data']['tools_solderpaste_pp'] + + self.postdata['toolC'] = kwargs['tooldia'] + + self.pp_solderpaste_name = kwargs['data']['tools_solderpaste_pp'] if kwargs['data']['tools_solderpaste_pp'] \ + else self.app.defaults['tools_solderpaste_pp'] + p = self.app.postprocessors[self.pp_solderpaste_name] + + ## Flatten the geometry. Only linear elements (no polygons) remain. + flat_geometry = self.flatten(kwargs['solid_geometry'], pathonly=True) + log.debug("%d paths" % len(flat_geometry)) + + # Create the indexed storage. + storage = FlatCAMRTreeStorage() + storage.get_points = get_pts + + # Store the geometry + log.debug("Indexing geometry before generating G-Code...") + for shape in flat_geometry: + if shape is not None: + storage.insert(shape) + + # Initial G-Code + self.gcode = self.doformat(p.start_code) + self.gcode += self.doformat(p.spindle_off_code) + self.gcode += self.doformat(p.toolchange_code) + + ## Iterate over geometry paths getting the nearest each time. + log.debug("Starting SolderPaste G-Code...") + path_count = 0 + current_pt = (0, 0) + + pt, geo = storage.nearest(current_pt) + + try: + while True: + path_count += 1 + + # Remove before modifying, otherwise deletion will fail. + storage.remove(geo) + + # If last point in geometry is the nearest but prefer the first one if last point == first point + # then reverse coordinates. + if pt != geo.coords[0] and pt == geo.coords[-1]: + geo.coords = list(geo.coords)[::-1] + + self.gcode += self.create_soldepaste_gcode(geo, p=p) + current_pt = geo.coords[-1] + pt, geo = storage.nearest(current_pt) # Next + + except StopIteration: # Nothing found in storage. + pass + + log.debug("Finishing SolderPste G-Code... %s paths traced." % path_count) + + # Finish + self.gcode += self.doformat(p.lift_code) + self.gcode += self.doformat(p.end_code) + + return self.gcode + + def create_soldepaste_gcode(self, geometry, p): + gcode = '' + path = geometry.coords + + if type(geometry) == LineString or type(geometry) == LinearRing: + # Move fast to 1st point + gcode += self.doformat(p.rapid_code, x=path[0][0], y=path[0][1]) # Move to first point + + # Move down to cutting depth + gcode += self.doformat(p.feedrate_z_code) + gcode += self.doformat(p.down_z_start_code) + gcode += self.doformat(p.spindle_fwd_code) # Start dispensing + gcode += self.doformat(p.dwell_fwd_code) + gcode += self.doformat(p.feedrate_z_dispense_code) + gcode += self.doformat(p.lift_z_dispense_code) + gcode += self.doformat(p.feedrate_xy_code) + + # Cutting... + for pt in path[1:]: + gcode += self.doformat(p.linear_code, x=pt[0], y=pt[1]) # Linear motion to point + + # Up to travelling height. + gcode += self.doformat(p.spindle_off_code) # Stop dispensing + gcode += self.doformat(p.spindle_rev_code) + gcode += self.doformat(p.down_z_stop_code) + gcode += self.doformat(p.spindle_off_code) + gcode += self.doformat(p.dwell_rev_code) + gcode += self.doformat(p.feedrate_z_code) + gcode += self.doformat(p.lift_code) + elif type(geometry) == Point: + gcode += self.doformat(p.linear_code, x=path[0][0], y=path[0][1]) # Move to first point + + gcode += self.doformat(p.feedrate_z_dispense_code) + gcode += self.doformat(p.down_z_start_code) + gcode += self.doformat(p.spindle_fwd_code) # Start dispensing + gcode += self.doformat(p.dwell_fwd_code) + gcode += self.doformat(p.lift_z_dispense_code) + + gcode += self.doformat(p.spindle_off_code) # Stop dispensing + gcode += self.doformat(p.spindle_rev_code) + gcode += self.doformat(p.spindle_off_code) + gcode += self.doformat(p.down_z_stop_code) + gcode += self.doformat(p.dwell_rev_code) + gcode += self.doformat(p.feedrate_z_code) + gcode += self.doformat(p.lift_code) + return gcode + def create_gcode_single_pass(self, geometry, extracut, tolerance): # G-code. Note: self.linear2gcode() and self.point2gcode() will lower and raise the tool every time. gcode_single_pass = '' if type(geometry) == LineString or type(geometry) == LinearRing: if extracut is False: - gcode_single_pass = self.linear2gcode(geometry, tolerance=tolerance, ) + gcode_single_pass = self.linear2gcode(geometry, tolerance=tolerance) else: if geometry.is_ring: gcode_single_pass = self.linear2gcode_extra(geometry, tolerance=tolerance) @@ -5503,7 +5708,8 @@ class CNCjob(Geometry): else: command['Z'] = 0 - elif 'grbl_laser' in self.pp_excellon_name or 'grbl_laser' in self.pp_geometry_name: + elif 'grbl_laser' in self.pp_excellon_name or 'grbl_laser' in self.pp_geometry_name or \ + (self.pp_solderpaste_name is not None and 'Paste' in self.pp_solderpaste_name): match_lsr = re.search(r"X([\+-]?\d+.[\+-]?\d+)\s*Y([\+-]?\d+.[\+-]?\d+)", gline) if match_lsr: command['X'] = float(match_lsr.group(1).replace(" ", "")) @@ -5517,7 +5723,12 @@ class CNCjob(Geometry): command['Z'] = 1 else: command['Z'] = 0 - + elif self.pp_solderpaste is not None: + if 'Paste' in self.pp_solderpaste: + match_paste = re.search(r"X([\+-]?\d+.[\+-]?\d+)\s*Y([\+-]?\d+.[\+-]?\d+)", gline) + if match_paste: + command['X'] = float(match_paste.group(1).replace(" ", "")) + command['Y'] = float(match_paste.group(2).replace(" ", "")) else: match = re.search(r'^\s*([A-Z])\s*([\+\-\.\d\s]+)', gline) while match: diff --git a/flatcamTools/ToolCalculators.py b/flatcamTools/ToolCalculators.py index 98e03354..92460c78 100644 --- a/flatcamTools/ToolCalculators.py +++ b/flatcamTools/ToolCalculators.py @@ -18,7 +18,14 @@ class ToolCalculator(FlatCAMTool): self.app = app ## Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) ###################### diff --git a/flatcamTools/ToolCutOut.py b/flatcamTools/ToolCutOut.py index 2907887b..593368cc 100644 --- a/flatcamTools/ToolCutOut.py +++ b/flatcamTools/ToolCutOut.py @@ -1,14 +1,9 @@ from FlatCAMTool import FlatCAMTool -from copy import copy,deepcopy from ObjectCollection import * from FlatCAMApp import * -from PyQt5 import QtGui, QtCore, QtWidgets -from GUIElements import IntEntry, RadioSet, LengthEntry - -from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber -class ToolCutOut(FlatCAMTool): +class CutOut(FlatCAMTool): toolName = "Cutout PCB" @@ -16,7 +11,14 @@ class ToolCutOut(FlatCAMTool): FlatCAMTool.__init__(self, app) ## Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) ## Form Layout diff --git a/flatcamTools/ToolDblSided.py b/flatcamTools/ToolDblSided.py index 7654d55e..2e1db762 100644 --- a/flatcamTools/ToolDblSided.py +++ b/flatcamTools/ToolDblSided.py @@ -15,7 +15,14 @@ class DblSidedTool(FlatCAMTool): FlatCAMTool.__init__(self, app) ## Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + 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("") diff --git a/flatcamTools/ToolFilm.py b/flatcamTools/ToolFilm.py index 1c508233..6129b8d4 100644 --- a/flatcamTools/ToolFilm.py +++ b/flatcamTools/ToolFilm.py @@ -12,7 +12,14 @@ class Film(FlatCAMTool): FlatCAMTool.__init__(self, app) # Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) # Form Layout diff --git a/flatcamTools/ToolImage.py b/flatcamTools/ToolImage.py index e03a915f..e681dc8e 100644 --- a/flatcamTools/ToolImage.py +++ b/flatcamTools/ToolImage.py @@ -12,7 +12,14 @@ class ToolImage(FlatCAMTool): FlatCAMTool.__init__(self, app) # Title - title_label = QtWidgets.QLabel("IMAGE to PCB") + title_label = QtWidgets.QLabel("%s" % 'Image to PCB') + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) # Form Layout diff --git a/flatcamTools/ToolNonCopperClear.py b/flatcamTools/ToolNonCopperClear.py index e739f60c..bf42f8f7 100644 --- a/flatcamTools/ToolNonCopperClear.py +++ b/flatcamTools/ToolNonCopperClear.py @@ -1,7 +1,5 @@ from FlatCAMTool import FlatCAMTool from copy import copy,deepcopy -# from GUIElements import IntEntry, RadioSet, FCEntry -# from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber from ObjectCollection import * import time @@ -24,7 +22,14 @@ class NonCopperClear(FlatCAMTool, Gerber): self.tools_frame.setLayout(self.tools_box) ## Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.tools_box.addWidget(title_label) ## Form Layout diff --git a/flatcamTools/ToolPaint.py b/flatcamTools/ToolPaint.py index 945c6030..1f263c1f 100644 --- a/flatcamTools/ToolPaint.py +++ b/flatcamTools/ToolPaint.py @@ -14,7 +14,14 @@ class ToolPaint(FlatCAMTool, Gerber): Geometry.__init__(self, geo_steps_per_circle=self.app.defaults["geometry_circle_steps"]) ## Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) self.tools_frame = QtWidgets.QFrame() diff --git a/flatcamTools/ToolPanelize.py b/flatcamTools/ToolPanelize.py index 5ae48515..8cfef018 100644 --- a/flatcamTools/ToolPanelize.py +++ b/flatcamTools/ToolPanelize.py @@ -13,7 +13,14 @@ class Panelize(FlatCAMTool): self.app = app ## Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) ## Form Layout @@ -119,6 +126,18 @@ class Panelize(FlatCAMTool): ) form_layout.addRow(self.rows_label, self.rows) + ## Type of resulting Panel object + self.panel_type_radio = RadioSet([{'label': 'Gerber', 'value': 'gerber'}, + {'label': 'Geometry', 'value': 'geometry'}]) + self.panel_type_label = QtWidgets.QLabel("Panel Type:") + self.panel_type_label.setToolTip( + "Choose the type of object for the panel object:\n" + "- Geometry\n" + "- Gerber" + ) + form_layout.addRow(self.panel_type_label) + form_layout.addRow(self.panel_type_radio) + ## Constrains self.constrain_cb = FCCheckBox("Constrain panel within:") self.constrain_cb.setToolTip( @@ -149,7 +168,6 @@ class Panelize(FlatCAMTool): self.constrain_sel = OptionalInputSection( self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry]) - ## Buttons hlay_2 = QtWidgets.QHBoxLayout() self.layout.addLayout(hlay_2) @@ -225,6 +243,10 @@ class Panelize(FlatCAMTool): self.app.defaults["tools_panelize_constrainy"] else 0.0 self.y_height_entry.set_value(float(y_w)) + panel_type = self.app.defaults["tools_panelize_panel_type"] if \ + self.app.defaults["tools_panelize_panel_type"] else 'gerber' + self.panel_type_radio.set_value(panel_type) + 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())) @@ -337,6 +359,9 @@ class Panelize(FlatCAMTool): "use a number.") return + panel_type = str(self.panel_type_radio.get_value()) + + if 0 in {columns, rows}: self.app.inform.emit("[ERROR_NOTCL]Columns or Rows are zero value. Change them to a positive integer.") return "Columns or Rows are zero value. Change them to a positive integer." @@ -541,7 +566,8 @@ class Panelize(FlatCAMTool): self.app.new_object("excellon", self.outname, job_init_excellon, plot=True, autoselected=True) else: self.app.progress.emit(50) - self.app.new_object("geometry", self.outname, job_init_geometry, plot=True, autoselected=True) + self.app.new_object(panel_type, self.outname, job_init_geometry, + plot=True, autoselected=True) if self.constrain_flag is False: self.app.inform.emit("[success]Panel done...") diff --git a/flatcamTools/ToolProperties.py b/flatcamTools/ToolProperties.py index ffdbe136..5a7c6280 100644 --- a/flatcamTools/ToolProperties.py +++ b/flatcamTools/ToolProperties.py @@ -22,7 +22,14 @@ class Properties(FlatCAMTool): self.properties_frame.setLayout(self.properties_box) ## Title - title_label = QtWidgets.QLabel(" %s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.properties_box.addWidget(title_label) # self.layout.setMargin(0) # PyQt4 diff --git a/flatcamTools/ToolSolderPaste.py b/flatcamTools/ToolSolderPaste.py new file mode 100644 index 00000000..1316897d --- /dev/null +++ b/flatcamTools/ToolSolderPaste.py @@ -0,0 +1,1186 @@ +from FlatCAMTool import FlatCAMTool +from copy import copy,deepcopy +from ObjectCollection import * +from FlatCAMApp import * +from PyQt5 import QtGui, QtCore, QtWidgets +from GUIElements import IntEntry, RadioSet, LengthEntry +from FlatCAMCommon import LoudDict + +from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber + + +class SolderPaste(FlatCAMTool): + + toolName = "Solder Paste Tool" + + def __init__(self, app): + FlatCAMTool.__init__(self, app) + + ## Title + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) + self.layout.addWidget(title_label) + + ## Form Layout + obj_form_layout = QtWidgets.QFormLayout() + self.layout.addLayout(obj_form_layout) + + ## Gerber Object to be used for solderpaste dispensing + self.obj_combo = QtWidgets.QComboBox() + self.obj_combo.setModel(self.app.collection) + self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + self.obj_combo.setCurrentIndex(1) + + self.object_label = QtWidgets.QLabel("Gerber: ") + self.object_label.setToolTip( + "Gerber Solder paste object. " + ) + obj_form_layout.addRow(self.object_label, self.obj_combo) + + #### Tools #### + self.tools_table_label = QtWidgets.QLabel('Tools Table') + self.tools_table_label.setToolTip( + "Tools pool from which the algorithm\n" + "will pick the ones used for dispensing solder paste." + ) + self.layout.addWidget(self.tools_table_label) + + self.tools_table = FCTable() + self.layout.addWidget(self.tools_table) + + self.tools_table.setColumnCount(3) + self.tools_table.setHorizontalHeaderLabels(['#', 'Diameter', '']) + self.tools_table.setColumnHidden(2, True) + self.tools_table.setSortingEnabled(False) + # self.tools_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + + self.tools_table.horizontalHeaderItem(0).setToolTip( + "This is the Tool Number.\n" + "The solder dispensing will start with the tool with the biggest \n" + "diameter, continuing until there are no more Nozzle tools.\n" + "If there are no longer tools but there are still pads not covered\n " + "with solder paste, the app will issue a warning message box." + ) + self.tools_table.horizontalHeaderItem(1).setToolTip( + "Nozzle tool Diameter. It's value (in current FlatCAM units)\n" + "is the width of the solder paste dispensed.") + + self.empty_label = QtWidgets.QLabel('') + self.layout.addWidget(self.empty_label) + + #### Add a new Tool #### + hlay_tools = QtWidgets.QHBoxLayout() + self.layout.addLayout(hlay_tools) + + self.addtool_entry_lbl = QtWidgets.QLabel('New Nozzle Tool:') + self.addtool_entry_lbl.setToolTip( + "Diameter for the new Nozzle tool to add in the Tool Table" + ) + self.addtool_entry = FCEntry() + + # hlay.addWidget(self.addtool_label) + # hlay.addStretch() + hlay_tools.addWidget(self.addtool_entry_lbl) + hlay_tools.addWidget(self.addtool_entry) + + grid0 = QtWidgets.QGridLayout() + self.layout.addLayout(grid0) + + self.addtool_btn = QtWidgets.QPushButton('Add') + self.addtool_btn.setToolTip( + "Add a new nozzle tool to the Tool Table\n" + "with the diameter specified above." + ) + + self.deltool_btn = QtWidgets.QPushButton('Delete') + self.deltool_btn.setToolTip( + "Delete a selection of tools in the Tool Table\n" + "by first selecting a row(s) in the Tool Table." + ) + + self.soldergeo_btn = QtWidgets.QPushButton("Generate Geo") + self.soldergeo_btn.setToolTip( + "Generate solder paste dispensing geometry." + ) + + grid0.addWidget(self.addtool_btn, 0, 0) + # grid2.addWidget(self.copytool_btn, 0, 1) + grid0.addWidget(self.deltool_btn, 0, 2) + + ## Form Layout + geo_form_layout = QtWidgets.QFormLayout() + self.layout.addLayout(geo_form_layout) + + ## Geometry Object to be used for solderpaste dispensing + self.geo_obj_combo = QtWidgets.QComboBox() + self.geo_obj_combo.setModel(self.app.collection) + self.geo_obj_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex())) + self.geo_obj_combo.setCurrentIndex(1) + + self.geo_object_label = QtWidgets.QLabel("Geometry:") + self.geo_object_label.setToolTip( + "Geometry Solder paste object.\n" + "In order to enable the GCode generation section,\n" + "the name of the object has to end in:\n" + "'_solderpaste' as a protection." + ) + geo_form_layout.addRow(self.geo_object_label, self.geo_obj_combo) + + self.gcode_frame = QtWidgets.QFrame() + self.gcode_frame.setContentsMargins(0, 0, 0, 0) + self.layout.addWidget(self.gcode_frame) + self.gcode_box = QtWidgets.QVBoxLayout() + self.gcode_box.setContentsMargins(0, 0, 0, 0) + self.gcode_frame.setLayout(self.gcode_box) + + ## Form Layout + self.gcode_form_layout = QtWidgets.QFormLayout() + self.gcode_box.addLayout(self.gcode_form_layout) + + # Z dispense start + self.z_start_entry = FCEntry() + self.z_start_label = QtWidgets.QLabel("Z Dispense Start:") + self.z_start_label.setToolTip( + "The height (Z) when solder paste dispensing starts." + ) + self.gcode_form_layout.addRow(self.z_start_label, self.z_start_entry) + + # Z dispense + self.z_dispense_entry = FCEntry() + self.z_dispense_label = QtWidgets.QLabel("Z Dispense:") + self.z_dispense_label.setToolTip( + "The height (Z) when doing solder paste dispensing." + + ) + self.gcode_form_layout.addRow(self.z_dispense_label, self.z_dispense_entry) + + # Z dispense stop + self.z_stop_entry = FCEntry() + self.z_stop_label = QtWidgets.QLabel("Z Dispense Stop:") + self.z_stop_label.setToolTip( + "The height (Z) when solder paste dispensing stops." + ) + self.gcode_form_layout.addRow(self.z_stop_label, self.z_stop_entry) + + # Z travel + self.z_travel_entry = FCEntry() + self.z_travel_label = QtWidgets.QLabel("Z Travel:") + self.z_travel_label.setToolTip( + "The height (Z) for travel between pads\n" + "(without dispensing solder paste)." + ) + self.gcode_form_layout.addRow(self.z_travel_label, self.z_travel_entry) + + # Z toolchange location + self.z_toolchange_entry = FCEntry() + self.z_toolchange_label = QtWidgets.QLabel("Z Toolchange:") + self.z_toolchange_label.setToolTip( + "The height (Z) for tool (nozzle) change." + ) + self.gcode_form_layout.addRow(self.z_toolchange_label, self.z_toolchange_entry) + + # X,Y Toolchange location + self.xy_toolchange_entry = FCEntry() + self.xy_toolchange_label = QtWidgets.QLabel("XY Toolchange:") + self.xy_toolchange_label.setToolTip( + "The X,Y location for tool (nozzle) change.\n" + "The format is (x, y) where x and y are real numbers." + ) + self.gcode_form_layout.addRow(self.xy_toolchange_label, self.xy_toolchange_entry) + + # Feedrate X-Y + self.frxy_entry = FCEntry() + self.frxy_label = QtWidgets.QLabel("Feedrate X-Y:") + self.frxy_label.setToolTip( + "Feedrate (speed) while moving on the X-Y plane." + ) + self.gcode_form_layout.addRow(self.frxy_label, self.frxy_entry) + + # Feedrate Z + self.frz_entry = FCEntry() + self.frz_label = QtWidgets.QLabel("Feedrate Z:") + self.frz_label.setToolTip( + "Feedrate (speed) while moving vertically\n" + "(on Z plane)." + ) + self.gcode_form_layout.addRow(self.frz_label, self.frz_entry) + + # Feedrate Z Dispense + self.frz_dispense_entry = FCEntry() + self.frz_dispense_label = QtWidgets.QLabel("Feedrate Z Dispense:") + self.frz_dispense_label.setToolTip( + "Feedrate (speed) while moving up vertically\n" + " to Dispense position (on Z plane)." + ) + self.gcode_form_layout.addRow(self.frz_dispense_label, self.frz_dispense_entry) + + # Spindle Speed Forward + self.speedfwd_entry = FCEntry() + self.speedfwd_label = QtWidgets.QLabel("Spindle Speed FWD:") + self.speedfwd_label.setToolTip( + "The dispenser speed while pushing solder paste\n" + "through the dispenser nozzle." + ) + self.gcode_form_layout.addRow(self.speedfwd_label, self.speedfwd_entry) + + # Dwell Forward + self.dwellfwd_entry = FCEntry() + self.dwellfwd_label = QtWidgets.QLabel("Dwell FWD:") + self.dwellfwd_label.setToolTip( + "Pause after solder dispensing." + ) + self.gcode_form_layout.addRow(self.dwellfwd_label, self.dwellfwd_entry) + + # Spindle Speed Reverse + self.speedrev_entry = FCEntry() + self.speedrev_label = QtWidgets.QLabel("Spindle Speed REV:") + self.speedrev_label.setToolTip( + "The dispenser speed while retracting solder paste\n" + "through the dispenser nozzle." + ) + self.gcode_form_layout.addRow(self.speedrev_label, self.speedrev_entry) + + # Dwell Reverse + self.dwellrev_entry = FCEntry() + self.dwellrev_label = QtWidgets.QLabel("Dwell REV:") + self.dwellrev_label.setToolTip( + "Pause after solder paste dispenser retracted,\n" + "to allow pressure equilibrium." + ) + self.gcode_form_layout.addRow(self.dwellrev_label, self.dwellrev_entry) + + # Postprocessors + pp_label = QtWidgets.QLabel('PostProcessors:') + pp_label.setToolTip( + "Files that control the GCode generation." + ) + + self.pp_combo = FCComboBox() + self.pp_combo.setStyleSheet('background-color: rgb(255,255,255)') + self.gcode_form_layout.addRow(pp_label, self.pp_combo) + + ## Buttons + grid1 = QtWidgets.QGridLayout() + self.gcode_box.addLayout(grid1) + + self.solder_gcode_btn = QtWidgets.QPushButton("Generate GCode") + self.solder_gcode_btn.setToolTip( + "Generate GCode for Solder Paste dispensing\n" + "on PCB pads." + ) + + ## Form Layout + cnc_form_layout = QtWidgets.QFormLayout() + self.gcode_box.addLayout(cnc_form_layout) + + ## Gerber Object to be used for solderpaste dispensing + self.cnc_obj_combo = QtWidgets.QComboBox() + self.cnc_obj_combo.setModel(self.app.collection) + self.cnc_obj_combo.setRootModelIndex(self.app.collection.index(3, 0, QtCore.QModelIndex())) + self.cnc_obj_combo.setCurrentIndex(1) + + self.cnc_object_label = QtWidgets.QLabel("CNCJob: ") + self.cnc_object_label.setToolTip( + "CNCJob Solder paste object.\n" + "In order to enable the GCode save section,\n" + "the name of the object has to end in:\n" + "'_solderpaste' as a protection." + ) + cnc_form_layout.addRow(self.cnc_object_label, self.cnc_obj_combo) + + self.save_gcode_frame = QtWidgets.QFrame() + self.save_gcode_frame.setContentsMargins(0, 0, 0, 0) + self.layout.addWidget(self.save_gcode_frame) + self.save_gcode_box = QtWidgets.QVBoxLayout() + self.save_gcode_box.setContentsMargins(0, 0, 0, 0) + self.save_gcode_frame.setLayout(self.save_gcode_box) + + + ## Buttons + grid2 = QtWidgets.QGridLayout() + self.save_gcode_box.addLayout(grid2) + + step1_lbl = QtWidgets.QLabel("STEP 1:") + step1_lbl.setToolTip( + "First step is to select a number of nozzle tools for usage\n" + "and then create a solder paste dispensing geometry out of an\n" + "Solder Paste Mask Gerber file." + ) + grid2.addWidget(step1_lbl, 0, 0) + grid2.addWidget(self.soldergeo_btn, 0, 2) + + step2_lbl = QtWidgets.QLabel("STEP 2:") + step2_lbl.setToolTip( + "Second step is to select a solder paste dispensing geometry,\n" + "set the CAM parameters and then generate a CNCJob object which\n" + "will pe painted on canvas in blue color." + ) + + grid2.addWidget(step2_lbl, 1, 0) + grid2.addWidget(self.solder_gcode_btn, 1, 2) + + self.solder_gcode_view_btn = QtWidgets.QPushButton("View GCode") + self.solder_gcode_view_btn.setToolTip( + "View the generated GCode for Solder Paste dispensing\n" + "on PCB pads." + ) + + self.solder_gcode_save_btn = QtWidgets.QPushButton("Save GCode") + self.solder_gcode_save_btn.setToolTip( + "Save the generated GCode for Solder Paste dispensing\n" + "on PCB pads, to a file." + ) + + step3_lbl = QtWidgets.QLabel("STEP 3:") + step3_lbl.setToolTip( + "Third step (and last) is to select a CNCJob made from \n" + "a solder paste dispensing geometry, and then view/save it's GCode." + ) + + grid2.addWidget(step3_lbl, 2, 0) + grid2.addWidget(self.solder_gcode_view_btn, 2, 2) + grid2.addWidget(self.solder_gcode_save_btn, 3, 2) + + self.layout.addStretch() + + # self.gcode_frame.setDisabled(True) + # self.save_gcode_frame.setDisabled(True) + + self.tools = {} + self.tooluid = 0 + + self.options = LoudDict() + self.form_fields = {} + + self.units = '' + + ## Signals + self.addtool_btn.clicked.connect(self.on_tool_add) + self.deltool_btn.clicked.connect(self.on_tool_delete) + self.soldergeo_btn.clicked.connect(self.on_create_geo) + self.solder_gcode_btn.clicked.connect(self.on_create_gcode) + self.solder_gcode_view_btn.clicked.connect(self.on_view_gcode) + self.solder_gcode_save_btn.clicked.connect(self.on_save_gcode) + + self.geo_obj_combo.currentIndexChanged.connect(self.on_geo_select) + + self.cnc_obj_combo.currentIndexChanged.connect(self.on_cncjob_select) + + def run(self): + self.app.report_usage("ToolSolderPaste()") + + FlatCAMTool.run(self) + self.set_tool_ui() + self.build_ui() + + # if the splitter us hidden, display it + if self.app.ui.splitter.sizes()[0] == 0: + self.app.ui.splitter.setSizes([1, 1]) + self.app.ui.notebook.setTabText(2, "SolderPaste Tool") + + def install(self, icon=None, separator=None, **kwargs): + FlatCAMTool.install(self, icon, separator, shortcut='ALT+K', **kwargs) + + def set_tool_ui(self): + self.form_fields.update({ + "tools_solderpaste_new": self.addtool_entry, + "tools_solderpaste_z_start": self.z_start_entry, + "tools_solderpaste_z_dispense": self.z_dispense_entry, + "tools_solderpaste_z_stop": self.z_stop_entry, + "tools_solderpaste_z_travel": self.z_travel_entry, + "tools_solderpaste_z_toolchange": self.z_toolchange_entry, + "tools_solderpaste_xy_toolchange": self.xy_toolchange_entry, + "tools_solderpaste_frxy": self.frxy_entry, + "tools_solderpaste_frz": self.frz_entry, + "tools_solderpaste_frz_dispense": self.frz_dispense_entry, + "tools_solderpaste_speedfwd": self.speedfwd_entry, + "tools_solderpaste_dwellfwd": self.dwellfwd_entry, + "tools_solderpaste_speedrev": self.speedrev_entry, + "tools_solderpaste_dwellrev": self.dwellrev_entry, + "tools_solderpaste_pp": self.pp_combo + }) + self.set_form_from_defaults() + self.read_form_to_options() + + self.tools_table.setupContextMenu() + self.tools_table.addContextMenu( + "Add", lambda: self.on_tool_add(dia=None, muted=None), icon=QtGui.QIcon("share/plus16.png")) + self.tools_table.addContextMenu( + "Delete", lambda: + self.on_tool_delete(rows_to_delete=None, all=None), icon=QtGui.QIcon("share/delete32.png")) + + try: + dias = [float(eval(dia)) for dia in self.app.defaults["tools_solderpaste_tools"].split(",")] + except: + log.error("At least one Nozzle tool diameter needed. " + "Verify in Edit -> Preferences -> TOOLS -> Solder Paste Tools.") + return + + self.tooluid = 0 + + self.tools.clear() + for tool_dia in dias: + self.tooluid += 1 + self.tools.update({ + int(self.tooluid): { + 'tooldia': float('%.4f' % tool_dia), + 'data': deepcopy(self.options), + 'solid_geometry': [] + } + }) + + self.name = "" + self.obj = None + + self.units = self.app.general_options_form.general_app_group.units_radio.get_value().upper() + + for name in list(self.app.postprocessors.keys()): + # populate only with postprocessor files that start with 'Paste_' + if name.partition('_')[0] != 'Paste': + continue + self.pp_combo.addItem(name) + + self.reset_fields() + + def build_ui(self): + self.ui_disconnect() + + # updated units + self.units = self.app.general_options_form.general_app_group.units_radio.get_value().upper() + + sorted_tools = [] + for k, v in self.tools.items(): + sorted_tools.append(float('%.4f' % float(v['tooldia']))) + sorted_tools.sort(reverse=True) + + n = len(sorted_tools) + self.tools_table.setRowCount(n) + tool_id = 0 + + for tool_sorted in sorted_tools: + for tooluid_key, tooluid_value in self.tools.items(): + if float('%.4f' % tooluid_value['tooldia']) == tool_sorted: + tool_id += 1 + id = QtWidgets.QTableWidgetItem('%d' % int(tool_id)) + id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + row_no = tool_id - 1 + self.tools_table.setItem(row_no, 0, id) # Tool name/id + + # Make sure that the drill diameter when in MM is with no more than 2 decimals + # There are no drill bits in MM with more than 3 decimals diameter + # For INCH the decimals should be no more than 3. There are no drills under 10mils + if self.units == 'MM': + dia = QtWidgets.QTableWidgetItem('%.2f' % tooluid_value['tooldia']) + else: + dia = QtWidgets.QTableWidgetItem('%.3f' % tooluid_value['tooldia']) + + dia.setFlags(QtCore.Qt.ItemIsEnabled) + + tool_uid_item = QtWidgets.QTableWidgetItem(str(int(tooluid_key))) + + self.tools_table.setItem(row_no, 1, dia) # Diameter + + self.tools_table.setItem(row_no, 2, tool_uid_item) # Tool unique ID + + # make the diameter column editable + for row in range(tool_id): + self.tools_table.item(row, 1).setFlags( + QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + + # all the tools are selected by default + self.tools_table.selectColumn(0) + # + self.tools_table.resizeColumnsToContents() + self.tools_table.resizeRowsToContents() + + vertical_header = self.tools_table.verticalHeader() + vertical_header.hide() + self.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + + horizontal_header = self.tools_table.horizontalHeader() + horizontal_header.setMinimumSectionSize(10) + horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) + horizontal_header.resizeSection(0, 20) + horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) + + # self.tools_table.setSortingEnabled(True) + # sort by tool diameter + # self.tools_table.sortItems(1) + + self.tools_table.setMinimumHeight(self.tools_table.getHeight()) + self.tools_table.setMaximumHeight(self.tools_table.getHeight()) + + self.ui_connect() + + def update_ui(self, row=None): + self.ui_disconnect() + + if row is None: + try: + current_row = self.tools_table.currentRow() + except: + current_row = 0 + else: + current_row = row + + if current_row < 0: + current_row = 0 + + + # populate the form with the data from the tool associated with the row parameter + try: + tooluid = int(self.tools_table.item(current_row, 2).text()) + except Exception as e: + log.debug("Tool missing. Add a tool in Tool Table. %s" % str(e)) + return + + # update the form + try: + # set the form with data from the newly selected tool + for tooluid_key, tooluid_value in self.tools.items(): + if int(tooluid_key) == tooluid: + self.set_form(deepcopy(tooluid_value['data'])) + except Exception as e: + log.debug("FlatCAMObj ---> update_ui() " + str(e)) + + self.ui_connect() + + def on_row_selection_change(self): + self.update_ui() + + def ui_connect(self): + # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the + # changes in geometry UI + for i in range(self.gcode_form_layout.count()): + if isinstance(self.gcode_form_layout.itemAt(i).widget(), FCComboBox): + self.gcode_form_layout.itemAt(i).widget().currentIndexChanged.connect(self.read_form_to_tooldata) + if isinstance(self.gcode_form_layout.itemAt(i).widget(), FCEntry): + self.gcode_form_layout.itemAt(i).widget().editingFinished.connect(self.read_form_to_tooldata) + + self.tools_table.itemChanged.connect(self.on_tool_edit) + self.tools_table.currentItemChanged.connect(self.on_row_selection_change) + + def ui_disconnect(self): + # if connected, disconnect the signal from the slot on item_changed as it creates issues + + try: + for i in range(self.gcode_form_layout.count()): + if isinstance(self.gcode_form_layout.itemAt(i).widget(), FCComboBox): + self.gcode_form_layout.itemAt(i).widget().currentIndexChanged.disconnect() + if isinstance(self.gcode_form_layout.itemAt(i).widget(), FCEntry): + self.gcode_form_layout.itemAt(i).widget().editingFinished.disconnect() + except: + pass + try: + self.tools_table.itemChanged.disconnect(self.on_tool_edit) + except: + pass + + try: + self.tools_table.currentItemChanged.disconnect(self.on_row_selection_change) + except: + pass + + def read_form_to_options(self): + """ + Will read all the parameters from Solder Paste Tool UI and update the self.options dictionary + :return: + """ + + for key in self.form_fields: + self.options[key] = self.form_fields[key].get_value() + + def read_form_to_tooldata(self, tooluid=None): + + current_row = self.tools_table.currentRow() + uid = tooluid if tooluid else int(self.tools_table.item(current_row, 2).text()) + for key in self.form_fields: + self.tools[uid]['data'].update({ + key: self.form_fields[key].get_value() + }) + + def set_form_from_defaults(self): + """ + Will read all the parameters of Solder Paste Tool from the app self.defaults and update the UI + :return: + """ + for key in self.form_fields: + if key in self.app.defaults: + self.form_fields[key].set_value(self.app.defaults[key]) + + def set_form(self, val): + """ + Will read all the parameters of Solder Paste Tool from the provided val parameter and update the UI + :param val: dictionary with values to store in the form + :param_type: dictionary + :return: + """ + + if not isinstance(val, dict): + log.debug("ToolSoderPaste.set_form() --> parameter not a dict") + return + + for key in self.form_fields: + if key in val: + self.form_fields[key].set_value(val[key]) + + def on_tool_add(self, dia=None, muted=None): + + self.ui_disconnect() + + if dia: + tool_dia = dia + else: + try: + tool_dia = float(self.addtool_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + tool_dia = float(self.addtool_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + if tool_dia is None: + self.build_ui() + self.app.inform.emit("[WARNING_NOTCL] Please enter a tool diameter to add, in Float format.") + return + + if tool_dia == 0: + self.app.inform.emit("[WARNING_NOTCL] Please enter a tool diameter with non-zero value, in Float format.") + return + + # construct a list of all 'tooluid' in the self.tools + tool_uid_list = [] + for tooluid_key in self.tools: + tool_uid_item = int(tooluid_key) + tool_uid_list.append(tool_uid_item) + + # find maximum from the temp_uid, add 1 and this is the new 'tooluid' + if not tool_uid_list: + max_uid = 0 + else: + max_uid = max(tool_uid_list) + self.tooluid = int(max_uid + 1) + + tool_dias = [] + for k, v in self.tools.items(): + for tool_v in v.keys(): + if tool_v == 'tooldia': + tool_dias.append(float('%.4f' % v[tool_v])) + + if float('%.4f' % tool_dia) in tool_dias: + if muted is None: + self.app.inform.emit("[WARNING_NOTCL]Adding Nozzle tool cancelled. Tool already in Tool Table.") + self.tools_table.itemChanged.connect(self.on_tool_edit) + return + else: + if muted is None: + self.app.inform.emit("[success] New Nozzle tool added to Tool Table.") + self.tools.update({ + int(self.tooluid): { + 'tooldia': float('%.4f' % tool_dia), + 'data': deepcopy(self.options), + 'solid_geometry': [] + } + }) + + self.build_ui() + + def on_tool_edit(self): + self.ui_disconnect() + + tool_dias = [] + for k, v in self.tools.items(): + for tool_v in v.keys(): + if tool_v == 'tooldia': + tool_dias.append(float('%.4f' % v[tool_v])) + + for row in range(self.tools_table.rowCount()): + + try: + new_tool_dia = float(self.tools_table.item(row, 1).text()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + new_tool_dia = float(self.tools_table.item(row, 1).text().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + + tooluid = int(self.tools_table.item(row, 2).text()) + + # identify the tool that was edited and get it's tooluid + if new_tool_dia not in tool_dias: + self.tools[tooluid]['tooldia'] = new_tool_dia + self.app.inform.emit("[success] Nozzle tool from Tool Table was edited.") + self.build_ui() + return + else: + # identify the old tool_dia and restore the text in tool table + for k, v in self.tools.items(): + if k == tooluid: + old_tool_dia = v['tooldia'] + break + restore_dia_item = self.tools_table.item(row, 1) + restore_dia_item.setText(str(old_tool_dia)) + self.app.inform.emit("[WARNING_NOTCL] Edit cancelled. New diameter value is already in the Tool Table.") + self.build_ui() + + def on_tool_delete(self, rows_to_delete=None, all=None): + self.ui_disconnect() + + deleted_tools_list = [] + if all: + self.tools.clear() + self.build_ui() + return + + if rows_to_delete: + try: + for row in rows_to_delete: + tooluid_del = int(self.tools_table.item(row, 2).text()) + deleted_tools_list.append(tooluid_del) + except TypeError: + deleted_tools_list.append(rows_to_delete) + + for t in deleted_tools_list: + self.tools.pop(t, None) + self.build_ui() + return + + try: + if self.tools_table.selectedItems(): + for row_sel in self.tools_table.selectedItems(): + row = row_sel.row() + if row < 0: + continue + tooluid_del = int(self.tools_table.item(row, 2).text()) + deleted_tools_list.append(tooluid_del) + + for t in deleted_tools_list: + self.tools.pop(t, None) + + except AttributeError: + self.app.inform.emit("[WARNING_NOTCL] Delete failed. Select a Nozzle tool to delete.") + return + except Exception as e: + log.debug(str(e)) + + self.app.inform.emit("[success] Nozzle tool(s) deleted from Tool Table.") + self.build_ui() + + def on_geo_select(self): + # if self.geo_obj_combo.currentText().rpartition('_')[2] == 'solderpaste': + # self.gcode_frame.setDisabled(False) + # else: + # self.gcode_frame.setDisabled(True) + pass + + def on_cncjob_select(self): + # if self.cnc_obj_combo.currentText().rpartition('_')[2] == 'solderpaste': + # self.save_gcode_frame.setDisabled(False) + # else: + # self.save_gcode_frame.setDisabled(True) + pass + + @staticmethod + def distance(pt1, pt2): + return sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2) + + def on_create_geo(self): + + proc = self.app.proc_container.new("Creating Solder Paste dispensing geometry.") + name = self.obj_combo.currentText() + + if name == '': + self.app.inform.emit("[WARNING_NOTCL] No SolderPaste mask Gerber object loaded.") + return + + # update the self.options + self.read_form_to_options() + + obj = self.app.collection.get_by_name(name) + + if type(obj.solid_geometry) is not list and type(obj.solid_geometry) is not MultiPolygon: + obj.solid_geometry = [obj.solid_geometry] + + # Sort tools in descending order + sorted_tools = [] + for k, v in self.tools.items(): + # make sure that the tools diameter is more than zero and not zero + if float(v['tooldia']) > 0: + sorted_tools.append(float('%.4f' % float(v['tooldia']))) + sorted_tools.sort(reverse=True) + + def geo_init(geo_obj, app_obj): + geo_obj.options.update(self.options) + geo_obj.solid_geometry = [] + + geo_obj.tools = {} + geo_obj.multigeo = True + geo_obj.multitool = True + geo_obj.special_group = 'solder_paste_tool' + + def solder_line(p, offset): + + xmin, ymin, xmax, ymax = p.bounds + + min = [xmin, ymin] + max = [xmax, ymax] + min_r = [xmin, ymax] + max_r = [xmax, ymin] + + diagonal_1 = LineString([min, max]) + diagonal_2 = LineString([min_r, max_r]) + if self.units == 'MM': + round_diag_1 = round(diagonal_1.intersection(p).length, 1) + round_diag_2 = round(diagonal_2.intersection(p).length, 1) + else: + round_diag_1 = round(diagonal_1.intersection(p).length, 2) + round_diag_2 = round(diagonal_2.intersection(p).length, 2) + + if round_diag_1 == round_diag_2: + l = distance((xmin, ymin), (xmax, ymin)) + h = distance((xmin, ymin), (xmin, ymax)) + + if offset >= l /2 or offset >= h / 2: + return "fail" + if l > h: + h_half = h / 2 + start = [xmin, (ymin + h_half)] + stop = [(xmin + l), (ymin + h_half)] + else: + l_half = l / 2 + start = [(xmin + l_half), ymin] + stop = [(xmin + l_half), (ymin + h)] + geo = LineString([start, stop]) + elif round_diag_1 > round_diag_2: + geo = diagonal_1.intersection(p) + else: + geo = diagonal_2.intersection(p) + + offseted_poly = p.buffer(-offset) + geo = geo.intersection(offseted_poly) + return geo + + work_geo = obj.solid_geometry + rest_geo = [] + tooluid = 1 + + if not sorted_tools: + self.app.inform.emit("[WARNING_NOTCL] No Nozzle tools in the tool table.") + return 'fail' + + for tool in sorted_tools: + offset = tool / 2 + + for uid, v in self.tools.items(): + if float('%.4f' % float(v['tooldia'])) == tool: + tooluid = int(uid) + break + + geo_obj.tools[tooluid] = {} + geo_obj.tools[tooluid]['tooldia'] = tool + geo_obj.tools[tooluid]['data'] = self.tools[tooluid]['data'] + geo_obj.tools[tooluid]['solid_geometry'] = [] + geo_obj.tools[tooluid]['offset'] = 'Path' + geo_obj.tools[tooluid]['offset_value'] = 0.0 + geo_obj.tools[tooluid]['type'] = 'SolderPaste' + geo_obj.tools[tooluid]['tool_type'] = 'DN' + + for g in work_geo: + if type(g) == MultiPolygon: + for poly in g: + geom = solder_line(poly, offset=offset) + if geom != 'fail': + try: + geo_obj.tools[tooluid]['solid_geometry'].append(geom) + except Exception as e: + log.debug('ToolSoderPaste.on_create_geo() --> %s' % str(e)) + else: + rest_geo.append(poly) + elif type(g) == Polygon: + geom = solder_line(g, offset=offset) + if geom != 'fail': + try: + geo_obj.tools[tooluid]['solid_geometry'].append(geom) + except Exception as e: + log.debug('ToolSoderPaste.on_create_geo() --> %s' % str(e)) + else: + rest_geo.append(g) + + work_geo = deepcopy(rest_geo) + rest_geo[:] = [] + + if not work_geo: + app_obj.inform.emit("[success] Solder Paste geometry generated successfully...") + return + + # if we still have geometry not processed at the end of the tools then we failed + # some or all the pads are not covered with solder paste + if rest_geo: + app_obj.inform.emit("[WARNING_NOTCL] Some or all pads have no solder " + "due of inadequate nozzle diameters...") + return 'fail' + + def job_thread(app_obj): + try: + app_obj.new_object("geometry", name + "_solderpaste", geo_init) + except Exception as e: + proc.done() + traceback.print_stack() + return + proc.done() + + self.app.inform.emit("Generating Solder Paste dispensing geometry...") + # Promise object with the new name + self.app.collection.promise(name) + + # Background + self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) + # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) + + def on_view_gcode(self): + time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now()) + + # add the tab if it was closed + self.app.ui.plot_tab_area.addTab(self.app.ui.cncjob_tab, "Code Editor") + + # Switch plot_area to CNCJob tab + self.app.ui.plot_tab_area.setCurrentWidget(self.app.ui.cncjob_tab) + + name = self.cnc_obj_combo.currentText() + obj = self.app.collection.get_by_name(name) + + try: + if obj.special_group != 'solder_paste_tool': + self.app.inform.emit("[WARNING_NOTCL]This CNCJob object can't be processed. " + "NOT a solder_paste_tool CNCJob object.") + return + except AttributeError: + self.app.inform.emit("[WARNING_NOTCL]This CNCJob object can't be processed. " + "NOT a solder_paste_tool CNCJob object.") + return + + gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \ + (str(self.app.version), str(self.app.version_date)) + '\n' + + gcode += '(Name: ' + str(name) + ')\n' + gcode += '(Type: ' + "G-code from " + str(obj.options['type']) + " for Solder Paste dispenser" + ')\n' + + # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': + # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n' + + gcode += '(Units: ' + self.units.upper() + ')\n' + "\n" + gcode += '(Created on ' + time_str + ')\n' + '\n' + + for tool in obj.cnc_tools: + gcode += obj.cnc_tools[tool]['gcode'] + + # then append the text from GCode to the text editor + try: + lines = StringIO(gcode) + except: + self.app.inform.emit("[ERROR_NOTCL] No Gcode in the object...") + return + + try: + for line in lines: + proc_line = str(line).strip('\n') + self.app.ui.code_editor.append(proc_line) + except Exception as e: + log.debug('ToolSolderPaste.on_view_gcode() -->%s' % str(e)) + self.app.inform.emit('[ERROR]ToolSolderPaste.on_view_gcode() -->%s' % str(e)) + return + + self.app.ui.code_editor.moveCursor(QtGui.QTextCursor.Start) + + self.app.handleTextChanged() + self.app.ui.show() + + def on_save_gcode(self): + time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now()) + name = self.cnc_obj_combo.currentText() + obj = self.app.collection.get_by_name(name) + + if obj.special_group != 'solder_paste_tool': + self.app.inform.emit("[WARNING_NOTCL]This CNCJob object can't be processed. " + "NOT a solder_paste_tool CNCJob object.") + return + + _filter_ = "G-Code Files (*.nc);;G-Code Files (*.txt);;G-Code Files (*.tap);;G-Code Files (*.cnc);;" \ + "G-Code Files (*.g-code);;All Files (*.*)" + + try: + dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name) + filename, _ = QtWidgets.QFileDialog.getSaveFileName( + caption="Export GCode ...", + directory=dir_file_to_save, + filter=_filter_ + ) + except TypeError: + filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export Machine Code ...", filter=_filter_) + + if filename == '': + self.app.inform.emit("[WARNING_NOTCL]Export Machine Code cancelled ...") + return + + gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \ + (str(self.app.version), str(self.app.version_date)) + '\n' + + gcode += '(Name: ' + str(name) + ')\n' + gcode += '(Type: ' + "G-code from " + str(obj.options['type']) + " for Solder Paste dispenser" + ')\n' + + # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': + # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n' + + gcode += '(Units: ' + self.units.upper() + ')\n' + "\n" + gcode += '(Created on ' + time_str + ')\n' + '\n' + + for tool in obj.cnc_tools: + gcode += obj.cnc_tools[tool]['gcode'] + lines = StringIO(gcode) + + ## Write + if filename is not None: + try: + with open(filename, 'w') as f: + for line in lines: + f.write(line) + except FileNotFoundError: + self.app.inform.emit("[WARNING_NOTCL] No such file or directory") + return + + self.app.file_saved.emit("gcode", filename) + self.app.inform.emit("[success] Solder paste dispenser GCode file saved to: %s" % filename) + + def on_create_gcode(self, use_thread=True): + """ + Creates a multi-tool CNCJob out of this Geometry object. + :return: None + """ + + name = self.geo_obj_combo.currentText() + obj = self.app.collection.get_by_name(name) + + if obj.special_group != 'solder_paste_tool': + self.app.inform.emit("[WARNING_NOTCL]This Geometry can't be processed. NOT a solder_paste_tool geometry.") + return + + offset_str = '' + multitool_gcode = '' + + # use the name of the first tool selected in self.geo_tools_table which has the diameter passed as tool_dia + originar_name = obj.options['name'].rpartition('_')[0] + outname = "%s_%s" % (originar_name, '_cnc_solderpaste') + + try: + xmin = obj.options['xmin'] + ymin = obj.options['ymin'] + xmax = obj.options['xmax'] + ymax = obj.options['ymax'] + except Exception as e: + log.debug("FlatCAMObj.FlatCAMGeometry.mtool_gen_cncjob() --> %s\n" % str(e)) + msg = "[ERROR] An internal error has ocurred. See shell.\n" + msg += 'FlatCAMObj.FlatCAMGeometry.mtool_gen_cncjob() --> %s' % str(e) + msg += traceback.format_exc() + self.app.inform.emit(msg) + return + + + # Object initialization function for app.new_object() + # RUNNING ON SEPARATE THREAD! + def job_init(job_obj, app_obj): + assert isinstance(job_obj, FlatCAMCNCjob), \ + "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj) + + tool_cnc_dict = {} + + # this turn on the FlatCAMCNCJob plot for multiple tools + job_obj.multitool = True + job_obj.multigeo = True + job_obj.cnc_tools.clear() + job_obj.special_group = 'solder_paste_tool' + + job_obj.options['xmin'] = xmin + job_obj.options['ymin'] = ymin + job_obj.options['xmax'] = xmax + job_obj.options['ymax'] = ymax + + a = 0 + for tooluid_key in obj.tools: + if obj.tools[tooluid_key]['solid_geometry'] is None: + a += 1 + if a == len(obj.tools): + self.app.inform.emit('[ERROR_NOTCL]Cancelled. Empty file, it has no geometry...') + return 'fail' + + for tooluid_key, tooluid_value in obj.tools.items(): + app_obj.progress.emit(20) + + # find the tool_dia associated with the tooluid_key + tool_dia = tooluid_value['tooldia'] + tool_cnc_dict = deepcopy(tooluid_value) + + job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"] + job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"] + job_obj.tool = int(tooluid_key) + + # Propagate options + job_obj.options["tooldia"] = tool_dia + job_obj.options['tool_dia'] = tool_dia + + ### CREATE GCODE ### + res = job_obj.generate_gcode_from_solderpaste_geo(**tooluid_value) + + if res == 'fail': + log.debug("FlatCAMGeometry.mtool_gen_cncjob() --> generate_from_geometry2() failed") + return 'fail' + else: + tool_cnc_dict['gcode'] = res + + ### PARSE GCODE ### + tool_cnc_dict['gcode_parsed'] = job_obj.gcode_parse() + + # TODO this serve for bounding box creation only; should be optimized + tool_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in tool_cnc_dict['gcode_parsed']]) + + # tell gcode_parse from which point to start drawing the lines depending on what kind of + # object is the source of gcode + job_obj.toolchange_xy_type = "geometry" + app_obj.progress.emit(80) + + job_obj.cnc_tools.update({ + tooluid_key: deepcopy(tool_cnc_dict) + }) + tool_cnc_dict.clear() + + if use_thread: + # To be run in separate thread + # The idea is that if there is a solid_geometry in the file "root" then most likely thare are no + # separate solid_geometry in the self.tools dictionary + def job_thread(app_obj): + with self.app.proc_container.new("Generating CNC Code"): + if app_obj.new_object("cncjob", outname, job_init) != 'fail': + app_obj.inform.emit("[success]ToolSolderPaste CNCjob created: %s" % outname) + app_obj.progress.emit(100) + + # Create a promise with the name + self.app.collection.promise(outname) + # Send to worker + self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) + else: + self.app.new_object("cncjob", outname, job_init) + + def reset_fields(self): + self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + self.geo_obj_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex())) + self.cnc_obj_combo.setRootModelIndex(self.app.collection.index(3, 0, QtCore.QModelIndex())) diff --git a/flatcamTools/ToolTransform.py b/flatcamTools/ToolTransform.py index 77c6561e..2540f9bb 100644 --- a/flatcamTools/ToolTransform.py +++ b/flatcamTools/ToolTransform.py @@ -20,7 +20,14 @@ class ToolTransform(FlatCAMTool): self.transform_lay = QtWidgets.QVBoxLayout() self.layout.addLayout(self.transform_lay) ## Title - title_label = QtWidgets.QLabel("%s
" % self.toolName) + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.transform_lay.addWidget(title_label) self.empty_label = QtWidgets.QLabel("") @@ -368,18 +375,64 @@ class ToolTransform(FlatCAMTool): self.app.ui.notebook.setTabText(2, "Transform Tool") def install(self, icon=None, separator=None, **kwargs): - FlatCAMTool.install(self, icon, separator, shortcut='ALT+R', **kwargs) + FlatCAMTool.install(self, icon, separator, shortcut='ALT+T', **kwargs) def set_tool_ui(self): ## Initialize form - self.rotate_entry.set_value('0') - self.skewx_entry.set_value('0') - self.skewy_entry.set_value('0') - self.scalex_entry.set_value('1') - self.scaley_entry.set_value('1') - self.offx_entry.set_value('0') - self.offy_entry.set_value('0') - self.flip_ref_cb.setChecked(False) + if self.app.defaults["tools_transform_rotate"]: + self.rotate_entry.set_value(self.app.defaults["tools_transform_rotate"]) + else: + self.rotate_entry.set_value(0.0) + + if self.app.defaults["tools_transform_skew_x"]: + self.skewx_entry.set_value(self.app.defaults["tools_transform_skew_x"]) + else: + self.skewx_entry.set_value(0.0) + + if self.app.defaults["tools_transform_skew_y"]: + self.skewy_entry.set_value(self.app.defaults["tools_transform_skew_y"]) + else: + self.skewy_entry.set_value(0.0) + + if self.app.defaults["tools_transform_scale_x"]: + self.scalex_entry.set_value(self.app.defaults["tools_transform_scale_x"]) + else: + self.scalex_entry.set_value(1.0) + + if self.app.defaults["tools_transform_scale_y"]: + self.scaley_entry.set_value(self.app.defaults["tools_transform_scale_y"]) + else: + self.scaley_entry.set_value(1.0) + + if self.app.defaults["tools_transform_scale_link"]: + self.scale_link_cb.set_value(self.app.defaults["tools_transform_scale_link"]) + else: + self.scale_link_cb.set_value(True) + + if self.app.defaults["tools_transform_scale_reference"]: + self.scale_zero_ref_cb.set_value(self.app.defaults["tools_transform_scale_reference"]) + else: + self.scale_zero_ref_cb.set_value(True) + + if self.app.defaults["tools_transform_offset_x"]: + self.offx_entry.set_value(self.app.defaults["tools_transform_offset_x"]) + else: + self.offx_entry.set_value(0.0) + + if self.app.defaults["tools_transform_offset_y"]: + self.offy_entry.set_value(self.app.defaults["tools_transform_offset_y"]) + else: + self.offy_entry.set_value(0.0) + + if self.app.defaults["tools_transform_mirror_reference"]: + self.flip_ref_cb.set_value(self.app.defaults["tools_transform_mirror_reference"]) + else: + self.flip_ref_cb.set_value(False) + + if self.app.defaults["tools_transform_mirror_point"]: + self.flip_ref_entry.set_value(self.app.defaults["tools_transform_mirror_point"]) + else: + self.flip_ref_entry.set_value((0,0)) def on_rotate(self): try: @@ -412,7 +465,7 @@ class ToolTransform(FlatCAMTool): return def on_flip_add_coords(self): - val = self.app.defaults["global_point_clipboard_format"] % (self.app.pos[0], self.app.pos[1]) + val = self.app.clipboard.text() self.flip_ref_entry.set_value(val) def on_skewx(self): @@ -595,7 +648,7 @@ class ToolTransform(FlatCAMTool): # add information to the object that it was changed and how much sel_obj.options['rotate'] = num - self.app.inform.emit('Object(s) were rotated ...') + self.app.inform.emit('[success]Rotate done ...') self.app.progress.emit(100) except Exception as e: @@ -656,7 +709,7 @@ class ToolTransform(FlatCAMTool): else: obj.options['mirror_y'] = True obj.plot() - self.app.inform.emit('Flipped on the Y axis ...') + self.app.inform.emit('[success]Flip on the Y axis done ...') elif axis is 'Y': obj.mirror('Y', (px, py)) # add information to the object that it was changed and how much @@ -666,9 +719,8 @@ class ToolTransform(FlatCAMTool): else: obj.options['mirror_x'] = True obj.plot() - self.app.inform.emit('Flipped on the X axis ...') + self.app.inform.emit('[success]Flip on the X axis done ...') self.app.object_changed.emit(obj) - self.app.progress.emit(100) except Exception as e: @@ -715,7 +767,7 @@ class ToolTransform(FlatCAMTool): obj.options['skew_y'] = num obj.plot() self.app.object_changed.emit(obj) - self.app.inform.emit('Object(s) were skewed on %s axis ...' % str(axis)) + self.app.inform.emit('[success]Skew on the %s axis done ...' % str(axis)) self.app.progress.emit(100) except Exception as e: @@ -771,7 +823,7 @@ class ToolTransform(FlatCAMTool): obj.options['scale_y'] = yfactor obj.plot() self.app.object_changed.emit(obj) - self.app.inform.emit('Object(s) were scaled on %s axis ...' % str(axis)) + self.app.inform.emit('[success]Scale on the %s axis done ...' % str(axis)) self.app.progress.emit(100) except Exception as e: self.app.inform.emit("[ERROR_NOTCL] Due of %s, Scale action was not executed." % str(e)) @@ -816,7 +868,7 @@ class ToolTransform(FlatCAMTool): obj.options['offset_y'] = num obj.plot() self.app.object_changed.emit(obj) - self.app.inform.emit('Object(s) were offseted on %s axis ...' % str(axis)) + self.app.inform.emit('[success]Offset on the %s axis done ...' % str(axis)) self.app.progress.emit(100) except Exception as e: diff --git a/flatcamTools/__init__.py b/flatcamTools/__init__.py index e5e1e84f..cd9dd56c 100644 --- a/flatcamTools/__init__.py +++ b/flatcamTools/__init__.py @@ -6,12 +6,13 @@ from flatcamTools.ToolFilm import Film from flatcamTools.ToolMove import ToolMove from flatcamTools.ToolDblSided import DblSidedTool -from flatcamTools.ToolCutOut import ToolCutOut +from flatcamTools.ToolCutOut import CutOut from flatcamTools.ToolCalculators import ToolCalculator from flatcamTools.ToolProperties import Properties from flatcamTools.ToolImage import ToolImage from flatcamTools.ToolPaint import ToolPaint from flatcamTools.ToolNonCopperClear import NonCopperClear from flatcamTools.ToolTransform import ToolTransform +from flatcamTools.ToolSolderPaste import SolderPaste from flatcamTools.ToolShell import FCShell diff --git a/make_win.py b/make_win.py index fac62161..812130b2 100644 --- a/make_win.py +++ b/make_win.py @@ -73,7 +73,7 @@ else: excludes=['scipy', 'pytz'], # packages=['OpenGL','numpy','vispy','ortools','google'] # packages=['numpy', 'rasterio'] # works for Python 3.7 - packages = ['opengl', 'numpy', 'google', 'rasterio'] # works for Python 3.6.5 and Python 3.7.1 + packages = ['opengl', 'numpy', 'rasterio'] # works for Python 3.6.5 and Python 3.7.1 ) diff --git a/postprocessors/Paste_1.py b/postprocessors/Paste_1.py new file mode 100644 index 00000000..f9e7edd0 --- /dev/null +++ b/postprocessors/Paste_1.py @@ -0,0 +1,151 @@ +from FlatCAMPostProc import * + + +class Paste_1(FlatCAMPostProc_Tools): + + coordinate_format = "%.*f" + feedrate_format = '%.*f' + + def start_code(self, p): + units = ' ' + str(p['units']).lower() + coords_xy = [float(eval(a)) for a in p['xy_toolchange'].split(",")] + + gcode = '' + + xmin = '%.*f' % (p.coords_decimals, p['options']['xmin']) + xmax = '%.*f' % (p.coords_decimals, p['options']['xmax']) + ymin = '%.*f' % (p.coords_decimals, p['options']['ymin']) + ymax = '%.*f' % (p.coords_decimals, p['options']['ymax']) + + gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n' + gcode += '(Feedrate_XY: ' + str(p['frxy']) + units + '/min' + ')\n' + gcode += '(Feedrate_Z: ' + str(p['frz']) + units + '/min' + ')\n' + gcode += '(Feedrate_Z_Dispense: ' + str(p['frz_dispense']) + units + '/min' + ')\n' + + gcode += '(Z_Dispense_Start: ' + str(p['z_start']) + units + ')\n' + gcode += '(Z_Dispense: ' + str(p['z_dispense']) + units + ')\n' + gcode += '(Z_Dispense_Stop: ' + str(p['z_stop']) + units + ')\n' + gcode += '(Z_Travel: ' + str(p['z_travel']) + units + ')\n' + gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n' + + gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n' + + if 'Paste' in p.pp_solderpaste_name: + gcode += '(Postprocessor SolderPaste Dispensing Geometry: ' + str(p.pp_solderpaste_name) + ')\n' + '\n' + + gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n' + gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n' + + gcode += '(Spindle Speed FWD: %s RPM)\n' % str(p['speedfwd']) + gcode += '(Spindle Speed REV: %s RPM)\n' % str(p['speedrev']) + gcode += '(Dwell FWD: %s RPM)\n' % str(p['dwellfwd']) + gcode += '(Dwell REV: %s RPM)\n' % str(p['dwellrev']) + + gcode += ('G20\n' if p.units.upper() == 'IN' else 'G21\n') + gcode += 'G90\n' + gcode += 'G94\n' + return gcode + + def lift_code(self, p): + return 'G00 Z' + self.coordinate_format%(p.coords_decimals, float(p['z_travel'])) + + def down_z_start_code(self, p): + return 'G01 Z' + self.coordinate_format%(p.coords_decimals, float(p['z_start'])) + + def lift_z_dispense_code(self, p): + return 'G01 Z' + self.coordinate_format%(p.coords_decimals, float(p['z_dispense'])) + + def down_z_stop_code(self, p): + return 'G01 Z' + self.coordinate_format%(p.coords_decimals, float(p['z_stop'])) + + def toolchange_code(self, p): + toolchangez = float(p['z_toolchange']) + toolchangexy = [float(eval(a)) for a in p['xy_toolchange'].split(",")] + gcode = '' + + if toolchangexy is not None: + toolchangex = toolchangexy[0] + toolchangey = toolchangexy[1] + + if p.units.upper() == 'MM': + toolC_formatted = format(float(p['toolC']), '.2f') + else: + toolC_formatted = format(float(p['toolC']), '.4f') + + if toolchangexy is not None: + gcode = """ +G00 Z{toolchangez} +G00 X{toolchangex} Y{toolchangey} +T{tool} +M6 +(MSG, Change to Tool with Nozzle Dia = {toolC}) +M0 +""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex), + toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), + toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + tool=int(int(p.tool)), + toolC=toolC_formatted) + + else: + gcode = """ +G00 Z{toolchangez} +T{tool} +M6 +(MSG, Change to Tool with Nozzle Dia = {toolC}) +M0 +""".format(toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + tool=int(int(p.tool)), + toolC=toolC_formatted) + + return gcode + + def position_code(self, p): + return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \ + (p.coords_decimals, p.x, p.coords_decimals, p.y) + + def rapid_code(self, p): + return ('G00 ' + self.position_code(p)).format(**p) + '\nG00 Z' + \ + self.coordinate_format%(p.coords_decimals, float(p['z_travel'])) + + def linear_code(self, p): + return ('G01 ' + self.position_code(p)).format(**p) + + def end_code(self, p): + coords_xy = [float(eval(a)) for a in p['xy_toolchange'].split(",")] + gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, float(p['z_toolchange'])) + "\n") + + if coords_xy is not None: + gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n" + return gcode + + def feedrate_xy_code(self, p): + return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, float(p['frxy']))) + + def feedrate_z_code(self, p): + return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, float(p['frz']))) + + def feedrate_z_dispense_code(self, p): + return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, float(p['frz_dispense']))) + + def spindle_fwd_code(self, p): + if p.spindlespeed: + return 'M03 S' + str(float(p['speedfwd'])) + else: + return 'M03' + + def spindle_rev_code(self, p): + if p.spindlespeed: + return 'M04 S' + str(float(p['speedrev'])) + else: + return 'M04' + + def spindle_off_code(self,p): + return 'M05' + + def dwell_fwd_code(self, p): + if p.dwelltime: + return 'G4 P' + str(float(p['dwellfwd'])) + + def dwell_rev_code(self, p): + if p.dwelltime: + return 'G4 P' + str(float(p['dwellrev'])) diff --git a/setup_ubuntu.sh b/setup_ubuntu.sh index 636c7a23..fe04bb9a 100644 --- a/setup_ubuntu.sh +++ b/setup_ubuntu.sh @@ -14,7 +14,6 @@ apt-get install python3-tk apt-get install libspatialindex-dev apt-get install python3-gdal apt-get install python3-lxml -apt-get install python3-ezdxf easy_install3 -U distribute pip3 install --upgrade dill pip3 install --upgrade Shapely diff --git a/share/offsetx32.png b/share/offsetx32.png new file mode 100644 index 00000000..29242ee2 Binary files /dev/null and b/share/offsetx32.png differ diff --git a/share/offsety32.png b/share/offsety32.png new file mode 100644 index 00000000..04ef1387 Binary files /dev/null and b/share/offsety32.png differ diff --git a/share/solderpaste32.png b/share/solderpaste32.png new file mode 100644 index 00000000..80766f96 Binary files /dev/null and b/share/solderpaste32.png differ diff --git a/share/solderpastebis32.png b/share/solderpastebis32.png new file mode 100644 index 00000000..541d58df Binary files /dev/null and b/share/solderpastebis32.png differ diff --git a/share/source32.png b/share/source32.png new file mode 100644 index 00000000..9f6db60a Binary files /dev/null and b/share/source32.png differ diff --git a/tclCommands/TclCommandGeoCutout.py b/tclCommands/TclCommandGeoCutout.py index db49be02..749bce35 100644 --- a/tclCommands/TclCommandGeoCutout.py +++ b/tclCommands/TclCommandGeoCutout.py @@ -225,7 +225,7 @@ class TclCommandGeoCutout(TclCommandSignaled): def geo_init(geo_obj, app_obj): try: - geo = cutout_obj.isolation_geometry((dia / 2), iso_type=0, corner=2) + geo = cutout_obj.isolation_geometry((dia / 2), iso_type=0, corner=2, follow=None) except Exception as e: log.debug("TclCommandGeoCutout.execute() --> %s" % str(e)) return 'fail' diff --git a/tclCommands/TclCommandIsolate.py b/tclCommands/TclCommandIsolate.py index 93ad41d0..7293f506 100644 --- a/tclCommands/TclCommandIsolate.py +++ b/tclCommands/TclCommandIsolate.py @@ -28,7 +28,9 @@ class TclCommandIsolate(TclCommandSignaled): ('passes', int), ('overlap', float), ('combine', int), - ('outname', str) + ('outname', str), + ('follow', str) + ]) # array of mandatory options for current Tcl command: required = {'name','outname'} @@ -43,7 +45,8 @@ class TclCommandIsolate(TclCommandSignaled): ('passes', 'Passes of tool width.'), ('overlap', 'Fraction of tool diameter to overlap passes.'), ('combine', 'Combine all passes into one geometry.'), - ('outname', 'Name of the resulting Geometry object.') + ('outname', 'Name of the resulting Geometry object.'), + ('follow', 'Create a Geometry that follows the Gerber path.') ]), 'examples': [] } @@ -68,6 +71,9 @@ class TclCommandIsolate(TclCommandSignaled): else: timeout = 10000 + if 'follow' not in args: + args['follow'] = None + obj = self.app.collection.get_by_name(name) if obj is None: self.raise_tcl_error("Object not found: %s" % name) diff --git a/tclCommands/TclCommandOpenGerber.py b/tclCommands/TclCommandOpenGerber.py index 9472aa3e..2d4b5b48 100644 --- a/tclCommands/TclCommandOpenGerber.py +++ b/tclCommands/TclCommandOpenGerber.py @@ -17,7 +17,6 @@ class TclCommandOpenGerber(TclCommandSignaled): # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value option_types = collections.OrderedDict([ - ('follow', str), ('outname', str) ]) @@ -29,7 +28,6 @@ class TclCommandOpenGerber(TclCommandSignaled): 'main': "Opens a Gerber file.", 'args': collections.OrderedDict([ ('filename', 'Path to file to open.'), - ('follow', 'N If 1, does not create polygons, just follows the gerber path.'), ('outname', 'Name of the resulting Gerber object.') ]), 'examples': [] @@ -54,7 +52,7 @@ class TclCommandOpenGerber(TclCommandSignaled): # Opening the file happens here self.app.progress.emit(30) try: - gerber_obj.parse_file(filename, follow=follow) + gerber_obj.parse_file(filename) except IOError: app_obj.inform.emit("[ERROR_NOTCL] Failed to open file: %s " % filename) @@ -77,9 +75,8 @@ class TclCommandOpenGerber(TclCommandSignaled): else: outname = filename.split('/')[-1].split('\\')[-1] - follow = None if 'follow' in args: - follow = args['follow'] + self.raise_tcl_error("The 'follow' parameter is obsolete. To create 'follow' geometry use the 'follow' parameter for the Tcl Command isolate()") with self.app.proc_container.new("Opening Gerber"):