From b8fb64a1430f5cffd768a43c73c4103992c4fc56 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Thu, 23 Jul 2020 00:44:33 +0300 Subject: [PATCH] - 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) --- CHANGELOG.md | 5 ++ appEditors/AppGeoEditor.py | 142 +++++++++++++++++------------------ appEditors/appGCodeEditor.py | 59 +++++++++++++-- appObjects/AppObject.py | 30 +++++++- app_Main.py | 19 ++++- camlib.py | 26 ++++++- preprocessors/Z_laser.py | 111 +++++++++++++++++++++++++++ 7 files changed, 308 insertions(+), 84 deletions(-) create mode 100644 preprocessors/Z_laser.py diff --git a/CHANGELOG.md b/CHANGELOG.md index eb38c754..7864d6b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ CHANGELOG for FlatCAM beta - 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) 21.07.2020 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/appGCodeEditor.py b/appEditors/appGCodeEditor.py index af186ca0..51a8893e 100644 --- a/appEditors/appGCodeEditor.py +++ b/appEditors/appGCodeEditor.py @@ -36,14 +36,15 @@ class AppGCodeEditor(QtCore.QObject): self.ui = AppGCodeEditorUI(app=self.app) - # ################################################################################# - # ################### SIGNALS ##################################################### - # ################################################################################# - self.gcode_obj = None self.code_edited = '' def set_ui(self): + """ + + :return: + :rtype: + """ # ############################################################################################################# # ############# ADD a new TAB in the PLot Tab Area # ############################################################################################################# @@ -73,9 +74,18 @@ class AppGCodeEditor(QtCore.QObject): self.ui.append_text.set_value(self.app.defaults["cncjob_append"]) self.ui.prepend_text.set_value(self.app.defaults["cncjob_prepend"]) - self.ui.exit_editor_button.buttonSave.clicked.connect(self.update_fcgcode) + # ################################################################################# + # ################### 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 @@ -84,12 +94,27 @@ class AppGCodeEditor(QtCore.QObject): 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) @@ -97,7 +122,22 @@ class AppGCodeEditor(QtCore.QObject): 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 @@ -111,6 +151,11 @@ class AppGCodeEditor(QtCore.QObject): 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() @@ -120,7 +165,11 @@ class AppGCodeEditor(QtCore.QObject): 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 (*.*)" 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/app_Main.py b/app_Main.py index ebc62447..45a73f77 100644 --- a/app_Main.py +++ b/app_Main.py @@ -2193,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 @@ -8780,9 +8781,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: @@ -8833,7 +8840,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..ecf81922 100644 --- a/camlib.py +++ b/camlib.py @@ -1058,6 +1058,7 @@ 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 = self.flatten(geos, reset=True, pathonly=True) geos = linemerge(geos) # Add to object @@ -1081,12 +1082,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,6 +1123,7 @@ class Geometry(object): geos = getdxfgeo(dxf) # trying to optimize the resulting geometry by merging contiguous lines + geos = self.flatten(geos, reset=True, pathonly=True) geos = linemerge(geos) # Add to object @@ -5176,7 +5197,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'