diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce1572c..90443124 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,17 @@ CHANGELOG for FlatCAM beta - Tools Tab is hidden when entering into a Editor and showed on exit (this needs to be remade such that the toolbars state should be restored to whatever it was before entering in the Editor) +22.07.2020 + +- working on a proper GCode Editor +- wip in the GCode Editor +- added a Laser preprocessor named 'Z_laser' which will change the Z to the Travel Z on each ToolChange event allowing therefore control of the dot size +- by default now a new blank Geometry object created by FlatCAM is of type multigeo +- made sure that optimizations of lines when importing SVG or DXF as lines will not encounter polygons but only LinesStrings or LinearRings, otherwise having crashes +- fixed the import SVG and import DXF, when importing as Geometry to be imported as multigeo tool +- fixed the import SVG and import DXF, the source files will be saved as loaded into the source_file attribute of the resulting object (be it Geometry or Gerber) +- in import SVG and import DXF methods made sure that any polygons that are imported as polygons will survive and only the lines are optimized (changed the behavior of the above made modification) + 21.07.2020 - updated the FCRadio class with a method that allow disabling certain options diff --git a/appEditors/AppExcEditor.py b/appEditors/AppExcEditor.py index 7640d406..77d1479f 100644 --- a/appEditors/AppExcEditor.py +++ b/appEditors/AppExcEditor.py @@ -2569,7 +2569,7 @@ class AppExcEditor(QtCore.QObject): self.set_ui() - # now that we hava data, create the appGUI interface and add it to the Tool Tab + # now that we have data, create the appGUI interface and add it to the Tool Tab self.build_ui(first_run=True) # we activate this after the initial build as we don't need to see the tool been populated diff --git a/appEditors/AppGeoEditor.py b/appEditors/AppGeoEditor.py index 5a0160ef..1a20d580 100644 --- a/appEditors/AppGeoEditor.py +++ b/appEditors/AppGeoEditor.py @@ -3979,77 +3979,6 @@ class AppGeoEditor(QtCore.QObject): # self.storage = AppGeoEditor.make_storage() self.replot() - def edit_fcgeometry(self, fcgeometry, multigeo_tool=None): - """ - Imports the geometry from the given FlatCAM Geometry object - into the editor. - - :param fcgeometry: GeometryObject - :param multigeo_tool: A tool for the case of the edited geometry being of type 'multigeo' - :return: None - """ - assert isinstance(fcgeometry, Geometry), "Expected a Geometry, got %s" % type(fcgeometry) - - self.deactivate() - self.activate() - - self.set_ui() - - # Hide original geometry - self.fcgeometry = fcgeometry - fcgeometry.visible = False - - # Set selection tolerance - DrawToolShape.tolerance = fcgeometry.drawing_tolerance * 10 - - self.select_tool("select") - - if self.app.defaults['geometry_spindledir'] == 'CW': - if self.app.defaults['geometry_editor_milling_type'] == 'cl': - milling_type = 1 # CCW motion = climb milling (spindle is rotating CW) - else: - milling_type = -1 # CW motion = conventional milling (spindle is rotating CW) - else: - if self.app.defaults['geometry_editor_milling_type'] == 'cl': - milling_type = -1 # CCW motion = climb milling (spindle is rotating CCW) - else: - milling_type = 1 # CW motion = conventional milling (spindle is rotating CCW) - - # Link shapes into editor. - if multigeo_tool: - self.multigeo_tool = multigeo_tool - geo_to_edit = self.flatten(geometry=fcgeometry.tools[self.multigeo_tool]['solid_geometry'], - orient_val=milling_type) - self.app.inform.emit( - '[WARNING_NOTCL] %s: %s %s: %s' % ( - _("Editing MultiGeo Geometry, tool"), - str(self.multigeo_tool), - _("with diameter"), - str(fcgeometry.tools[self.multigeo_tool]['tooldia']) - ) - ) - else: - geo_to_edit = self.flatten(geometry=fcgeometry.solid_geometry, orient_val=milling_type) - - for shape in geo_to_edit: - if shape is not None: - if type(shape) == Polygon: - self.add_shape(DrawToolShape(shape.exterior)) - for inter in shape.interiors: - self.add_shape(DrawToolShape(inter)) - else: - self.add_shape(DrawToolShape(shape)) - - self.replot() - - # updated units - self.units = self.app.defaults['units'].upper() - self.decimals = self.app.decimals - - # start with GRID toolbar activated - if self.app.ui.grid_snap_btn.isChecked() is False: - self.app.ui.grid_snap_btn.trigger() - def on_buffer_tool(self): buff_tool = BufferSelectionTool(self.app, self) buff_tool.run() @@ -4700,6 +4629,77 @@ class AppGeoEditor(QtCore.QObject): return snap_x, snap_y + def edit_fcgeometry(self, fcgeometry, multigeo_tool=None): + """ + Imports the geometry from the given FlatCAM Geometry object + into the editor. + + :param fcgeometry: GeometryObject + :param multigeo_tool: A tool for the case of the edited geometry being of type 'multigeo' + :return: None + """ + assert isinstance(fcgeometry, Geometry), "Expected a Geometry, got %s" % type(fcgeometry) + + self.deactivate() + self.activate() + + self.set_ui() + + # Hide original geometry + self.fcgeometry = fcgeometry + fcgeometry.visible = False + + # Set selection tolerance + DrawToolShape.tolerance = fcgeometry.drawing_tolerance * 10 + + self.select_tool("select") + + if self.app.defaults['geometry_spindledir'] == 'CW': + if self.app.defaults['geometry_editor_milling_type'] == 'cl': + milling_type = 1 # CCW motion = climb milling (spindle is rotating CW) + else: + milling_type = -1 # CW motion = conventional milling (spindle is rotating CW) + else: + if self.app.defaults['geometry_editor_milling_type'] == 'cl': + milling_type = -1 # CCW motion = climb milling (spindle is rotating CCW) + else: + milling_type = 1 # CW motion = conventional milling (spindle is rotating CCW) + + # Link shapes into editor. + if multigeo_tool: + self.multigeo_tool = multigeo_tool + geo_to_edit = self.flatten(geometry=fcgeometry.tools[self.multigeo_tool]['solid_geometry'], + orient_val=milling_type) + self.app.inform.emit( + '[WARNING_NOTCL] %s: %s %s: %s' % ( + _("Editing MultiGeo Geometry, tool"), + str(self.multigeo_tool), + _("with diameter"), + str(fcgeometry.tools[self.multigeo_tool]['tooldia']) + ) + ) + else: + geo_to_edit = self.flatten(geometry=fcgeometry.solid_geometry, orient_val=milling_type) + + for shape in geo_to_edit: + if shape is not None: + if type(shape) == Polygon: + self.add_shape(DrawToolShape(shape.exterior)) + for inter in shape.interiors: + self.add_shape(DrawToolShape(inter)) + else: + self.add_shape(DrawToolShape(shape)) + + self.replot() + + # updated units + self.units = self.app.defaults['units'].upper() + self.decimals = self.app.decimals + + # start with GRID toolbar activated + if self.app.ui.grid_snap_btn.isChecked() is False: + self.app.ui.grid_snap_btn.trigger() + def update_fcgeometry(self, fcgeometry): """ Transfers the geometry tool shape buffer to the selected geometry diff --git a/appEditors/AppTextEditor.py b/appEditors/AppTextEditor.py index e9726c98..4efe000d 100644 --- a/appEditors/AppTextEditor.py +++ b/appEditors/AppTextEditor.py @@ -5,7 +5,7 @@ # MIT Licence # # ########################################################## -from appGUI.GUIElements import FCFileSaveDialog, FCEntry, FCTextAreaExtended, FCTextAreaLineNumber +from appGUI.GUIElements import FCFileSaveDialog, FCEntry, FCTextAreaExtended, FCTextAreaLineNumber, FCButton from PyQt5 import QtPrintSupport, QtWidgets, QtCore, QtGui from reportlab.platypus import SimpleDocTemplate, Paragraph @@ -30,6 +30,7 @@ class AppTextEditor(QtWidgets.QWidget): self.app = app self.plain_text = plain_text + self.callback = lambda x: None self.setSizePolicy( QtWidgets.QSizePolicy.MinimumExpanding, @@ -71,17 +72,17 @@ class AppTextEditor(QtWidgets.QWidget): if text: self.code_editor.setPlainText(text) - self.buttonPreview = QtWidgets.QPushButton(_('Print Preview')) + self.buttonPreview = FCButton(_('Print Preview')) self.buttonPreview.setIcon(QtGui.QIcon(self.app.resource_location + '/preview32.png')) self.buttonPreview.setToolTip(_("Open a OS standard Preview Print window.")) self.buttonPreview.setMinimumWidth(100) - self.buttonPrint = QtWidgets.QPushButton(_('Print Code')) + self.buttonPrint = FCButton(_('Print Code')) self.buttonPrint.setIcon(QtGui.QIcon(self.app.resource_location + '/printer32.png')) self.buttonPrint.setToolTip(_("Open a OS standard Print window.")) self.buttonPrint.setMinimumWidth(100) - self.buttonFind = QtWidgets.QPushButton(_('Find in Code')) + self.buttonFind = FCButton(_('Find in Code')) self.buttonFind.setIcon(QtGui.QIcon(self.app.resource_location + '/find32.png')) self.buttonFind.setToolTip(_("Will search and highlight in yellow the string in the Find box.")) self.buttonFind.setMinimumWidth(100) @@ -89,7 +90,7 @@ class AppTextEditor(QtWidgets.QWidget): self.entryFind = FCEntry() self.entryFind.setToolTip(_("Find box. Enter here the strings to be searched in the text.")) - self.buttonReplace = QtWidgets.QPushButton(_('Replace With')) + self.buttonReplace = FCButton(_('Replace With')) self.buttonReplace.setIcon(QtGui.QIcon(self.app.resource_location + '/replace32.png')) self.buttonReplace.setToolTip(_("Will replace the string from the Find box with the one in the Replace box.")) self.buttonReplace.setMinimumWidth(100) @@ -101,22 +102,22 @@ class AppTextEditor(QtWidgets.QWidget): self.sel_all_cb.setToolTip(_("When checked it will replace all instances in the 'Find' box\n" "with the text in the 'Replace' box..")) - self.button_copy_all = QtWidgets.QPushButton(_('Copy All')) + self.button_copy_all = FCButton(_('Copy All')) self.button_copy_all.setIcon(QtGui.QIcon(self.app.resource_location + '/copy_file32.png')) self.button_copy_all.setToolTip(_("Will copy all the text in the Code Editor to the clipboard.")) self.button_copy_all.setMinimumWidth(100) - self.buttonOpen = QtWidgets.QPushButton(_('Open Code')) + self.buttonOpen = FCButton(_('Open Code')) self.buttonOpen.setIcon(QtGui.QIcon(self.app.resource_location + '/folder32_bis.png')) self.buttonOpen.setToolTip(_("Will open a text file in the editor.")) self.buttonOpen.setMinimumWidth(100) - self.buttonSave = QtWidgets.QPushButton(_('Save Code')) + self.buttonSave = FCButton(_('Save Code')) self.buttonSave.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png')) self.buttonSave.setToolTip(_("Will save the text in the editor into a file.")) self.buttonSave.setMinimumWidth(100) - self.buttonRun = QtWidgets.QPushButton(_('Run Code')) + self.buttonRun = FCButton(_('Run Code')) self.buttonRun.setToolTip(_("Will run the TCL commands found in the text file, one by one.")) self.buttonRun.setMinimumWidth(100) @@ -162,6 +163,9 @@ class AppTextEditor(QtWidgets.QWidget): self.code_edited = '' + def set_callback(self, callback): + self.callback = callback + def handlePrint(self): self.app.defaults.report_usage("handlePrint()") diff --git a/appEditors/appGCodeEditor.py b/appEditors/appGCodeEditor.py new file mode 100644 index 00000000..51a8893e --- /dev/null +++ b/appEditors/appGCodeEditor.py @@ -0,0 +1,322 @@ +# ########################################################## +# FlatCAM: 2D Post-processing for Manufacturing # +# File Author: Marius Adrian Stanciu (c) # +# Date: 07/22/2020 # +# MIT Licence # +# ########################################################## + +from appEditors.AppTextEditor import AppTextEditor +from appObjects.FlatCAMCNCJob import CNCJobObject +from appGUI.GUIElements import FCTextArea, FCEntry, FCButton +from PyQt5 import QtWidgets, QtCore, QtGui + +# from io import StringIO + +import logging + +import gettext +import appTranslation as fcTranslate +import builtins + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext + +log = logging.getLogger('base') + + +class AppGCodeEditor(QtCore.QObject): + + def __init__(self, app, parent=None): + super().__init__(parent=parent) + + self.app = app + self.plain_text = '' + self.callback = lambda x: None + + self.ui = AppGCodeEditorUI(app=self.app) + + self.gcode_obj = None + self.code_edited = '' + + def set_ui(self): + """ + + :return: + :rtype: + """ + # ############################################################################################################# + # ############# ADD a new TAB in the PLot Tab Area + # ############################################################################################################# + self.ui.gcode_editor_tab = AppTextEditor(app=self.app, plain_text=True) + + # add the tab if it was closed + self.app.ui.plot_tab_area.addTab(self.ui.gcode_editor_tab, '%s' % _("Code Editor")) + self.ui.gcode_editor_tab.setObjectName('code_editor_tab') + + # delete the absolute and relative position and messages in the infobar + self.app.ui.position_label.setText("") + self.app.ui.rel_position_label.setText("") + + self.ui.gcode_editor_tab.code_editor.completer_enable = False + self.ui.gcode_editor_tab.buttonRun.hide() + + # Switch plot_area to CNCJob tab + self.app.ui.plot_tab_area.setCurrentWidget(self.ui.gcode_editor_tab) + + self.ui.gcode_editor_tab.t_frame.hide() + + self.ui.gcode_editor_tab.t_frame.show() + self.app.proc_container.view.set_idle() + # ############################################################################################################# + # ############################################################################################################# + + self.ui.append_text.set_value(self.app.defaults["cncjob_append"]) + self.ui.prepend_text.set_value(self.app.defaults["cncjob_prepend"]) + + # ################################################################################# + # ################### SIGNALS ##################################################### + # ################################################################################# + self.ui.update_gcode_button.clicked.connect(self.insert_gcode) + self.ui.exit_editor_button.clicked.connect(self.update_fcgcode) + + def build_ui(self): + """ + + :return: + :rtype: + """ + # Remove anything else in the GUI Selected Tab + self.app.ui.selected_scroll_area.takeWidget() + # Put ourselves in the GUI Selected Tab + self.app.ui.selected_scroll_area.setWidget(self.ui.edit_widget) + # Switch notebook to Selected page + self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab) + + def ui_connect(self): + """ + + :return: + :rtype: + """ + pass + + def ui_disconnect(self): + """ + + :return: + :rtype: + """ + pass + + def handleTextChanged(self): + """ + + :return: + :rtype: + """ + # enable = not self.ui.code_editor.document().isEmpty() + # self.ui.buttonPrint.setEnabled(enable) + # self.ui.buttonPreview.setEnabled(enable) + + self.buttonSave.setStyleSheet("QPushButton {color: red;}") + self.buttonSave.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as_red.png')) + + def insert_gcode(self): + """ + + :return: + :rtype: + """ + pass + + def edit_fcgcode(self, cnc_obj): + """ + + :param cnc_obj: + :type cnc_obj: + :return: + :rtype: + """ + assert isinstance(cnc_obj, CNCJobObject) + self.gcode_obj = cnc_obj + + gcode_text = self.gcode_obj.source_file + + self.set_ui() + self.build_ui() + + # then append the text from GCode to the text editor + self.ui.gcode_editor_tab.load_text(gcode_text, move_to_start=True, clear_text=True) + self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Editor')) + + def update_fcgcode(self): + """ + + :return: + :rtype: + """ + preamble = str(self.ui.prepend_text.get_value()) + postamble = str(self.ui.append_text.get_value()) + my_gcode = self.ui.gcode_editor_tab.code_editor.toPlainText() + self.gcode_obj.source_file = my_gcode + + self.ui.gcode_editor_tab.buttonSave.setStyleSheet("") + self.ui.gcode_editor_tab.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png')) + + def on_open_gcode(self): + """ + + :return: + :rtype: + """ + _filter_ = "G-Code Files (*.nc);; G-Code Files (*.txt);; G-Code Files (*.tap);; G-Code Files (*.cnc);; " \ + "All Files (*.*)" + + path, _f = QtWidgets.QFileDialog.getOpenFileName( + caption=_('Open file'), directory=self.app.get_last_folder(), filter=_filter_) + + if path: + file = QtCore.QFile(path) + if file.open(QtCore.QIODevice.ReadOnly): + stream = QtCore.QTextStream(file) + self.code_edited = stream.readAll() + self.ui.gcode_editor_tab.load_text(self.code_edited, move_to_start=True, clear_text=True) + file.close() + + +class AppGCodeEditorUI: + def __init__(self, app): + self.app = app + + # Number of decimals used by tools in this class + self.decimals = self.app.decimals + + # ## Current application units in Upper Case + self.units = self.app.defaults['units'].upper() + + # self.setSizePolicy( + # QtWidgets.QSizePolicy.MinimumExpanding, + # QtWidgets.QSizePolicy.MinimumExpanding + # ) + + self.gcode_editor_tab = None + + self.edit_widget = QtWidgets.QWidget() + # ## Box for custom widgets + # This gets populated in offspring implementations. + layout = QtWidgets.QVBoxLayout() + self.edit_widget.setLayout(layout) + + # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Drills widgets + # this way I can hide/show the frame + self.edit_frame = QtWidgets.QFrame() + self.edit_frame.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.edit_frame) + self.edit_box = QtWidgets.QVBoxLayout() + self.edit_box.setContentsMargins(0, 0, 0, 0) + self.edit_frame.setLayout(self.edit_box) + + # ## Page Title box (spacing between children) + self.title_box = QtWidgets.QHBoxLayout() + self.edit_box.addLayout(self.title_box) + + # ## Page Title icon + pixmap = QtGui.QPixmap(self.app.resource_location + '/flatcam_icon32.png') + self.icon = QtWidgets.QLabel() + self.icon.setPixmap(pixmap) + self.title_box.addWidget(self.icon, stretch=0) + + # ## Title label + self.title_label = QtWidgets.QLabel("%s" % _('GCode Editor')) + self.title_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) + self.title_box.addWidget(self.title_label, stretch=1) + + # ## Object name + self.name_box = QtWidgets.QHBoxLayout() + self.edit_box.addLayout(self.name_box) + name_label = QtWidgets.QLabel(_("Name:")) + self.name_box.addWidget(name_label) + self.name_entry = FCEntry() + self.name_box.addWidget(self.name_entry) + + # Prepend text to GCode + prependlabel = QtWidgets.QLabel('%s:' % _('Prepend to CNC Code')) + prependlabel.setToolTip( + _("Type here any G-Code commands you would\n" + "like to add at the beginning of the G-Code file.") + ) + self.edit_box.addWidget(prependlabel) + + self.prepend_text = FCTextArea() + self.prepend_text.setPlaceholderText( + _("Type here any G-Code commands you would\n" + "like to add at the beginning of the G-Code file.") + ) + self.edit_box.addWidget(self.prepend_text) + + # Append text to GCode + appendlabel = QtWidgets.QLabel('%s:' % _('Append to CNC Code')) + appendlabel.setToolTip( + _("Type here any G-Code commands you would\n" + "like to append to the generated file.\n" + "I.e.: M2 (End of program)") + ) + self.edit_box.addWidget(appendlabel) + + self.append_text = FCTextArea() + self.append_text.setPlaceholderText( + _("Type here any G-Code commands you would\n" + "like to append to the generated file.\n" + "I.e.: M2 (End of program)") + ) + self.edit_box.addWidget(self.append_text) + + h_lay = QtWidgets.QHBoxLayout() + h_lay.setAlignment(QtCore.Qt.AlignVCenter) + self.edit_box.addLayout(h_lay) + + # GO Button + self.update_gcode_button = FCButton(_('Update Code')) + # self.update_gcode_button.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png')) + self.update_gcode_button.setToolTip( + _("Update the Gcode in the Editor with the values\n" + "in the 'Prepend' and 'Append' text boxes.") + ) + + h_lay.addWidget(self.update_gcode_button) + + layout.addStretch() + + # Editor + self.exit_editor_button = FCButton(_('Exit Editor')) + self.exit_editor_button.setIcon(QtGui.QIcon(self.app.resource_location + '/power16.png')) + self.exit_editor_button.setToolTip( + _("Exit from Editor.") + ) + self.exit_editor_button.setStyleSheet(""" + QPushButton + { + font-weight: bold; + } + """) + layout.addWidget(self.exit_editor_button) + # ############################ FINSIHED GUI ################################################################## + # ############################################################################################################# + + def confirmation_message(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"), + self.decimals, + minval, + self.decimals, + maxval), False) + else: + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) + + def confirmation_message_int(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' % + (_("Edited value is out of range"), minval, maxval), False) + else: + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) diff --git a/appGUI/GUIElements.py b/appGUI/GUIElements.py index 3039d10b..6cbf10c6 100644 --- a/appGUI/GUIElements.py +++ b/appGUI/GUIElements.py @@ -1557,8 +1557,13 @@ class FCInputDialog(QtWidgets.QInputDialog): class FCButton(QtWidgets.QPushButton): - def __init__(self, parent=None): - super(FCButton, self).__init__(parent) + def __init__(self, text=None, checkable=None, click_callback=None, parent=None): + super(FCButton, self).__init__(text, parent) + if not checkable is None: + self.setCheckable(checkable) + + if not click_callback is None: + self.clicked.connect(click_callback) def get_value(self): return self.isChecked() diff --git a/appGUI/ObjectUI.py b/appGUI/ObjectUI.py index 7ea12e96..da1859d1 100644 --- a/appGUI/ObjectUI.py +++ b/appGUI/ObjectUI.py @@ -1870,7 +1870,7 @@ class CNCObjectUI(ObjectUI): self.custom_box.addWidget(self.updateplot_button) # Editor - self.editor_button = QtWidgets.QPushButton(_('GCode Editor')) + self.editor_button = FCButton(_('GCode Editor')) self.editor_button.setIcon(QtGui.QIcon(self.app.resource_location + '/edit_file32.png')) self.editor_button.setToolTip( diff --git a/appObjects/AppObject.py b/appObjects/AppObject.py index 9387384b..e485cce1 100644 --- a/appObjects/AppObject.py +++ b/appObjects/AppObject.py @@ -223,11 +223,37 @@ class AppObject(QtCore.QObject): :return: None """ + outname = 'new_geo' def initialize(obj, app): - obj.multitool = False + obj.multitool = True + obj.multigeo = True + # store here the default data for Geometry Data + default_data = {} - self.new_object('geometry', 'new_geo', initialize, plot=False) + for opt_key, opt_val in app.options.items(): + if opt_key.find('geometry' + "_") == 0: + oname = opt_key[len('geometry') + 1:] + default_data[oname] = self.app.options[opt_key] + if opt_key.find('tools_mill' + "_") == 0: + oname = opt_key[len('tools_mill') + 1:] + default_data[oname] = self.app.options[opt_key] + + obj.tools = {} + obj.tools.update({ + 1: { + 'tooldia': float(app.defaults["geometry_cnctooldia"]), + 'offset': 'Path', + 'offset_value': 0.0, + 'type': _('Rough'), + 'tool_type': 'C1', + 'data': deepcopy(default_data), + 'solid_geometry': [] + } + }) + obj.tools[1]['data']['name'] = outname + + self.new_object('geometry', outname, initialize, plot=False) def new_gerber_object(self): """ diff --git a/appObjects/FlatCAMCNCJob.py b/appObjects/FlatCAMCNCJob.py index c434b056..9fd26b27 100644 --- a/appObjects/FlatCAMCNCJob.py +++ b/appObjects/FlatCAMCNCJob.py @@ -149,6 +149,7 @@ class CNCJobObject(FlatCAMObj, CNCjob): self.gcode_editor_tab = None + self.source_file = '' self.units_found = self.app.defaults['units'] def build_ui(self): @@ -538,10 +539,14 @@ class CNCJobObject(FlatCAMObj, CNCjob): self.ui.name_entry.set_value(new_name) self.on_name_activate(silent=True) - preamble = str(self.ui.prepend_text.get_value()) - postamble = str(self.ui.append_text.get_value()) + try: + preamble = str(self.ui.prepend_text.get_value()) + postamble = str(self.ui.append_text.get_value()) + gc = self.export_gcode(filename, preamble=preamble, postamble=postamble) + except Exception as err: + log.debug("CNCJobObject.export_gcode_handler() --> %s" % str(err)) + gc = self.export_gcode(filename) - gc = self.export_gcode(filename, preamble=preamble, postamble=postamble) if gc == 'fail': return @@ -590,15 +595,19 @@ class CNCJobObject(FlatCAMObj, CNCjob): try: self.gcode_editor_tab.load_text(self.app.gcode_edited.getvalue(), move_to_start=True, clear_text=True) except Exception as e: - log.debug('FlatCAMCNNJob.on_edit_code_click() -->%s' % str(e)) - self.app.inform.emit('[ERROR] %s %s' % ('FlatCAMCNNJob.on_edit_code_click() -->', str(e))) + log.debug('FlatCAMCNCJob.on_edit_code_click() -->%s' % str(e)) return self.gcode_editor_tab.t_frame.show() self.app.proc_container.view.set_idle() + self.gcode_editor_tab.buttonSave.clicked.connect(self.on_update_source_file) + self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Editor')) + def on_update_source_file(self): + self.source_file = self.gcode_editor_tab.code_editor.toPlainText() + def gcode_header(self, comment_start_symbol=None, comment_stop_symbol=None): """ Will create a header to be added to all GCode files generated by FlatCAM @@ -735,6 +744,11 @@ class CNCJobObject(FlatCAMObj, CNCjob): include_header = True + if preamble == '': + preamble = self.app.defaults["cncjob_prepend"] + if postamble == '': + preamble = self.app.defaults["cncjob_append"] + try: if self.special_group: self.app.inform.emit('[WARNING_NOTCL] %s %s %s.' % diff --git a/appObjects/FlatCAMGeometry.py b/appObjects/FlatCAMGeometry.py index 76395ebd..7c85b45b 100644 --- a/appObjects/FlatCAMGeometry.py +++ b/appObjects/FlatCAMGeometry.py @@ -1881,6 +1881,7 @@ class GeometryObject(FlatCAMObj, Geometry): job_obj.z_pdepth = float(self.app.defaults["geometry_z_pdepth"]) job_obj.feedrate_probe = float(self.app.defaults["geometry_feedrate_probe"]) + total_gcode = '' for tooluid_key in list(tools_dict.keys()): tool_cnt += 1 @@ -1970,6 +1971,8 @@ class GeometryObject(FlatCAMObj, Geometry): else: dia_cnc_dict['gcode'] = res + total_gcode += res + # 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" @@ -1993,6 +1996,8 @@ class GeometryObject(FlatCAMObj, Geometry): }) dia_cnc_dict.clear() + job_obj.source_file = total_gcode + # Object initialization function for app.app_obj.new_object() # RUNNING ON SEPARATE THREAD! def job_init_multi_geometry(job_obj, app_obj): @@ -2031,6 +2036,7 @@ class GeometryObject(FlatCAMObj, Geometry): self.app.inform.emit('[ERROR_NOTCL] %s...' % _('Cancelled. Empty file, it has no geometry')) return 'fail' + total_gcode = '' for tooluid_key in list(tools_dict.keys()): tool_cnt += 1 dia_cnc_dict = deepcopy(tools_dict[tooluid_key]) @@ -2123,6 +2129,7 @@ class GeometryObject(FlatCAMObj, Geometry): return 'fail' else: dia_cnc_dict['gcode'] = res + total_gcode += res self.app.inform.emit('[success] %s' % _("G-Code parsing in progress...")) dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse() @@ -2149,6 +2156,8 @@ class GeometryObject(FlatCAMObj, Geometry): }) dia_cnc_dict.clear() + job_obj.source_file = total_gcode + if use_thread: # To be run in separate thread def job_thread(a_obj): @@ -2288,17 +2297,18 @@ class GeometryObject(FlatCAMObj, Geometry): # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially # to a value of 0.0005 which is 20 times less than 0.01 tol = float(self.app.defaults['global_tolerance']) / 20 - job_obj.generate_from_geometry_2( - self, tooldia=tooldia, offset=offset, tolerance=tol, - z_cut=z_cut, z_move=z_move, - feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, - spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime, - multidepth=multidepth, depthpercut=depthperpass, - toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy, - extracut=extracut, extracut_length=extracut_length, startz=startz, endz=endz, endxy=endxy, - pp_geometry_name=ppname_g + res = job_obj.generate_from_geometry_2(self, tooldia=tooldia, offset=offset, tolerance=tol, + z_cut=z_cut, z_move=z_move, feedrate=feedrate, + feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, + spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime, + multidepth=multidepth, depthpercut=depthperpass, + toolchange=toolchange, toolchangez=toolchangez, + toolchangexy=toolchangexy, + extracut=extracut, extracut_length=extracut_length, + startz=startz, endz=endz, endxy=endxy, + pp_geometry_name=ppname_g ) - + job_obj.source_file = res # 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" diff --git a/appTools/ToolDrilling.py b/appTools/ToolDrilling.py index 142570d6..b62cca2c 100644 --- a/appTools/ToolDrilling.py +++ b/appTools/ToolDrilling.py @@ -1797,6 +1797,7 @@ class ToolDrilling(AppTool, Excellon): self.total_gcode_parsed += tool_gcode_parsed job_obj.gcode = self.total_gcode + job_obj.source_file = self.total_gcode job_obj.gcode_parsed = self.total_gcode_parsed if job_obj.gcode == 'fail': return 'fail' diff --git a/appTools/ToolSolderPaste.py b/appTools/ToolSolderPaste.py index e0565ba7..59e567be 100644 --- a/appTools/ToolSolderPaste.py +++ b/appTools/ToolSolderPaste.py @@ -913,6 +913,7 @@ class SolderPaste(AppTool): job_obj.options['xmax'] = xmax job_obj.options['ymax'] = ymax + total_gcode = '' for tooluid_key, tooluid_value in obj.tools.items(): # find the tool_dia associated with the tooluid_key tool_dia = tooluid_value['tooldia'] @@ -934,6 +935,7 @@ class SolderPaste(AppTool): return 'fail' else: tool_cnc_dict['gcode'] = res + total_gcode += res # ## PARSE GCODE # ## tool_cnc_dict['gcode_parsed'] = job_obj.gcode_parse() @@ -949,6 +951,8 @@ class SolderPaste(AppTool): }) tool_cnc_dict.clear() + job_obj.source_file = total_gcode + if use_thread: # To be run in separate thread def job_thread(app_obj): diff --git a/app_Main.py b/app_Main.py index f116dabf..e00c29d4 100644 --- a/app_Main.py +++ b/app_Main.py @@ -83,6 +83,7 @@ from appEditors.AppGeoEditor import AppGeoEditor from appEditors.AppExcEditor import AppExcEditor from appEditors.AppGerberEditor import AppGerberEditor from appEditors.AppTextEditor import AppTextEditor +from appEditors.appGCodeEditor import AppGCodeEditor from appParsers.ParseHPGL2 import HPGL2 # FlatCAM Workers @@ -1574,6 +1575,12 @@ class App(QtCore.QObject): self.grb_editor = AppGerberEditor(self) except Exception as es: log.debug("app_Main.__init__() --> Gerber Editor Error: %s" % str(es)) + + try: + self.gcode_editor = AppGCodeEditor(self) + except Exception as es: + log.debug("app_Main.__init__() --> GCode Editor Error: %s" % str(es)) + self.log.debug("Finished adding FlatCAM Editor's.") self.set_ui_title(name=_("New Project - Not saved")) @@ -2186,9 +2193,10 @@ class App(QtCore.QObject): if edited_object.tools[tool]['tooldia'] == selected_tooldia: multi_tool = tool break - + log.debug("Editing MultiGeo Geometry with tool diameter: %s" % str(multi_tool)) self.geo_editor.edit_fcgeometry(edited_object, multigeo_tool=multi_tool) else: + log.debug("Editing SingleGeo Geometry with tool diameter.") self.geo_editor.edit_fcgeometry(edited_object) # set call source to the Editor we go into @@ -2226,7 +2234,10 @@ class App(QtCore.QObject): if self.ui.splitter.sizes()[0] == 0: self.ui.splitter.setSizes([1, 1]) - edited_object.on_edit_code_click() + # set call source to the Editor we go into + self.call_source = 'gcode_editor' + + self.gcode_editor.edit_fcgcode(edited_object) return # make sure that we can't select another object while in Editor Mode: @@ -7762,10 +7773,7 @@ class App(QtCore.QObject): # then append the text from GCode to the text editor if obj.kind == 'cncjob': try: - file = obj.export_gcode( - preamble=self.defaults["cncjob_prepend"], - postamble=self.defaults["cncjob_append"], - to_file=True) + file = obj.export_gcode(to_file=True) if file == 'fail': return 'fail' except AttributeError: @@ -8682,14 +8690,14 @@ class App(QtCore.QObject): def job_thread_grb(app_obj): ret = make_gerber() if ret == 'fail': - self.inform.emit('[ERROR_NOTCL] %s' % _('Could not export Gerber file.')) + self.inform.emit('[ERROR_NOTCL] %s' % _('Could not export file.')) return self.worker_task.emit({'fcn': job_thread_grb, 'params': [self]}) else: gret = make_gerber() if gret == 'fail': - self.inform.emit('[ERROR_NOTCL] %s' % _('Could not export Gerber file.')) + self.inform.emit('[ERROR_NOTCL] %s' % _('Could not export file.')) return 'fail' if local_use is not None: return gret @@ -8790,9 +8798,15 @@ class App(QtCore.QObject): units = self.defaults['units'].upper() def obj_init(geo_obj, app_obj): - geo_obj.import_svg(filename, obj_type, units=units) - geo_obj.multigeo = False - geo_obj.source_file = self.export_gerber(obj_name=name, filename=None, local_use=geo_obj, use_thread=False) + if obj_type == "geometry": + geo_obj.import_svg(filename, obj_type, units=units) + elif obj_type == "gerber": + geo_obj.import_svg(filename, obj_type, units=units) + + geo_obj.multigeo = True + with open(filename) as f: + file_content = f.read() + geo_obj.source_file = file_content with self.proc_container.new(_("Importing SVG")) as proc: @@ -8843,7 +8857,11 @@ class App(QtCore.QObject): geo_obj.import_dxf_as_gerber(filename, units=units) else: return "fail" + geo_obj.multigeo = True + with open(filename) as f: + file_content = f.read() + geo_obj.source_file = file_content with self.proc_container.new(_("Importing DXF")): diff --git a/camlib.py b/camlib.py index 14cb1fb2..84baeb4e 100644 --- a/camlib.py +++ b/camlib.py @@ -1058,7 +1058,19 @@ class Geometry(object): geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos] # trying to optimize the resulting geometry by merging contiguous lines - geos = linemerge(geos) + geos = list(self.flatten_list(geos)) + geos_polys = [] + geos_lines = [] + for g in geos: + if isinstance(g, Polygon): + geos_polys.append(g) + else: + geos_lines.append(g) + + merged_lines = linemerge(geos_lines) + geos = geos_polys + for l in merged_lines: + geos.append(l) # Add to object if self.solid_geometry is None: @@ -1081,12 +1093,31 @@ class Geometry(object): if flip: # Change origin to bottom left for i in geos_text: - _, minimy, _, maximy = i.bounds + __, minimy, __, maximy = i.bounds h2 = (maximy - minimy) * 0.5 geos_text_f.append(translate(scale(i, 1.0, -1.0, origin=(0, 0)), yoff=(h + h2))) if geos_text_f: self.solid_geometry = self.solid_geometry + geos_text_f + tooldia = float(self.app.defaults["geometry_cnctooldia"]) + tooldia = float('%.*f' % (self.decimals, tooldia)) + + new_data = {k: v for k, v in self.options.items()} + + self.tools.update({ + 1: { + 'tooldia': tooldia, + 'offset': 'Path', + 'offset_value': 0.0, + 'type': _('Rough'), + 'tool_type': 'C1', + 'data': deepcopy(new_data), + 'solid_geometry': self.solid_geometry + } + }) + + self.tools[1]['data']['name'] = self.options['name'] + def import_dxf_as_geo(self, filename, units='MM'): """ Imports shapes from an DXF file into the object's geometry. @@ -1103,7 +1134,19 @@ class Geometry(object): geos = getdxfgeo(dxf) # trying to optimize the resulting geometry by merging contiguous lines - geos = linemerge(geos) + geos = list(self.flatten_list(geos)) + geos_polys = [] + geos_lines = [] + for g in geos: + if isinstance(g, Polygon): + geos_polys.append(g) + else: + geos_lines.append(g) + + merged_lines = linemerge(geos_lines) + geos = geos_polys + for l in merged_lines: + geos.append(l) # Add to object if self.solid_geometry is None: @@ -5176,7 +5219,8 @@ class CNCjob(Geometry): geo_storage = {} for geo in temp_solid_geometry: - geo_storage[geo.coords[0]] = geo + if not geo is None: + geo_storage[geo.coords[0]] = geo locations = list(geo_storage.keys()) if opt_type == 'M': diff --git a/preprocessors/Z_laser.py b/preprocessors/Z_laser.py new file mode 100644 index 00000000..73e8db4d --- /dev/null +++ b/preprocessors/Z_laser.py @@ -0,0 +1,111 @@ +# ########################################################## +# FlatCAM: 2D Post-processing for Manufacturing # +# http://flatcam.org # +# File Author: Matthieu Berthomé # +# Date: 5/26/2017 # +# MIT Licence # +# ########################################################## + +from appPreProcessor import * + +# This post processor is configured to output code that +# is compatible with almost any version of Grbl. + + +class Z_laser(PreProc): + + include_header = True + coordinate_format = "%.*f" + feedrate_format = '%.*f' + + def start_code(self, p): + units = ' ' + str(p['units']).lower() + gcode = '(This preprocessor is used with a motion controller loaded with GRBL firmware. )\n' + gcode += '(It is for the case when it is used together with a LASER connected on the SPINDLE connector.)\n' + gcode += '(On toolchange event the laser will move to a defined Z height to change the laser dot size.)\n\n' + + 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 += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n' + gcode += '(Feedrate rapids: ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n' + + gcode += '(Z Focus: ' + str(p['z_move']) + units + ')\n' + + gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n' + + if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': + gcode += '(Preprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' + else: + gcode += '(Preprocessor Geometry: ' + str(p['pp_geometry_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 += '(Laser Power (Spindle Speed): ' + str(p['spindlespeed']) + ')\n\n' + + gcode += ('G20' if p.units.upper() == 'IN' else 'G21') + "\n" + gcode += 'G90\n' + gcode += 'G17\n' + gcode += 'G94' + + return gcode + + def startz_code(self, p): + return '' + + def lift_code(self, p): + return 'M5' + + def down_code(self, p): + sdir = {'CW': 'M03', 'CCW': 'M04'}[p.spindledir] + if p.spindlespeed: + return '%s S%s' % (sdir, str(p.spindlespeed)) + else: + return sdir + + def toolchange_code(self, p): + return 'G00 Z' + self.coordinate_format % (p.coords_decimals, p.z_move) + + def up_to_zero_code(self, p): + return 'M5' + + 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) + + def linear_code(self, p): + return ('G01 ' + self.position_code(p)).format(**p) + \ + ' F' + str(self.feedrate_format % (p.fr_decimals, p.feedrate)) + + def end_code(self, p): + coords_xy = p['xy_end'] + gcode = ('G00 Z' + self.feedrate_format % (p.fr_decimals, p.z_end) + "\n") + + if coords_xy and coords_xy != '': + gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n" + return gcode + + def feedrate_code(self, p): + return 'G01 F' + str(self.feedrate_format % (p.fr_decimals, p.feedrate)) + + def z_feedrate_code(self, p): + return 'G01 F' + str(self.feedrate_format % (p.fr_decimals, p.z_feedrate)) + + def spindle_code(self, p): + sdir = {'CW': 'M03', 'CCW': 'M04'}[p.spindledir] + if p.spindlespeed: + return '%s S%s' % (sdir, str(p.spindlespeed)) + else: + return sdir + + def dwell_code(self, p): + return '' + + def spindle_stop_code(self, p): + return 'M5' diff --git a/tclCommands/TclCommandDrillcncjob.py b/tclCommands/TclCommandDrillcncjob.py index 8940a697..e6a07e0a 100644 --- a/tclCommands/TclCommandDrillcncjob.py +++ b/tclCommands/TclCommandDrillcncjob.py @@ -332,6 +332,7 @@ class TclCommandDrillcncjob(TclCommandSignaled): job_obj.excellon_optimization_type = opt_type ret_val = job_obj.generate_from_excellon_by_tool(obj, tools, use_ui=False) + job_obj.source_file = ret_val if ret_val == 'fail': return 'fail'