From 0f91d4dff0da9ab867ff279ee2f3d1c37b7269b1 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Thu, 26 Sep 2019 17:46:25 +0300 Subject: [PATCH] - added a Copy All button in the Code Editor, clicking this button will copy all text in the editor to the clipboard - added a 'Milling Type' radio button in Geometry Editor Preferences to contorl the type of geometry will be generated in the Geo Editor (for conventional milling or for the climb milling) - added the functionality to allow climb/conventional milling selection for the geometry created in the Geometry Editor - now any Geometry that is edited in Geometry editor will have coordinates ordered such that the resulting Gcode will allow the selected milling type in the 'Milling Type' radio button in Geometry Editor Preferences (which depends also of the spindle direction) - some strings update - French Google-translation at 100% - German Google-translation update to 100% --- FlatCAMApp.py | 8 + FlatCAMObj.py | 15 +- README.md | 10 + flatcamEditors/FlatCAMGeoEditor.py | 108 +- flatcamGUI/FlatCAMGUI.py | 7 + flatcamGUI/PlotCanvasLegacy.py | 16 +- flatcamGUI/PreferencesUI.py | 12 + flatcamTools/ToolMove.py | 6 +- flatcamTools/ToolNonCopperClear.py | 2 +- flatcamTools/ToolPaint.py | 4 +- flatcamTools/ToolSolderPaste.py | 2 +- locale/de/LC_MESSAGES/strings.mo | Bin 252539 -> 285547 bytes locale/de/LC_MESSAGES/strings.po | 3216 +++++++++++-------------- locale/fr/LC_MESSAGES/strings.mo | Bin 190271 -> 287965 bytes locale/fr/LC_MESSAGES/strings.po | 3603 +++++++++++++++++----------- 15 files changed, 3751 insertions(+), 3258 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index cb2e7c9f..b8afe5e4 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -657,6 +657,7 @@ class App(QtCore.QObject): # Geometry Editor "geometry_editor_sel_limit": self.ui.geometry_defaults_form.geometry_editor_group.sel_limit_entry, + "geometry_editor_milling_type": self.ui.geometry_defaults_form.geometry_editor_group.milling_type_radio, # CNCJob General "cncjob_plot": self.ui.cncjob_defaults_form.cncjob_gen_group.plot_cb, @@ -1064,6 +1065,7 @@ class App(QtCore.QObject): # Geometry Editor "geometry_editor_sel_limit": 30, + "geometry_editor_milling_type": "cl", # CNC Job General "cncjob_plot": True, @@ -1976,6 +1978,7 @@ class App(QtCore.QObject): self.ui.buttonPreview.clicked.connect(self.handlePreview) self.ui.buttonFind.clicked.connect(self.handleFindGCode) self.ui.buttonReplace.clicked.connect(self.handleReplaceGCode) + self.ui.button_copy_all.clicked.connect(self.handleCopyAll) # portability changed signal self.ui.general_defaults_form.general_app_group.portability_cb.stateChanged.connect(self.on_portable_checked) @@ -6548,6 +6551,11 @@ class App(QtCore.QObject): # Mark end of undo block cursor.endEditBlock() + def handleCopyAll(self): + text = self.ui.code_editor.toPlainText() + self.clipboard.setText(text) + self.inform.emit(_("Code Editor content copied to clipboard ...")) + def handleRunCode(self): # trying to run a Tcl command without having the Shell open will create some warnings because the Tcl Shell # tries to print on a hidden widget, therefore show the dock if hidden diff --git a/FlatCAMObj.py b/FlatCAMObj.py index c34f6638..f88e8a24 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -5918,20 +5918,23 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): if "toolchange_probe" in ppg.lower(): probe_pp = True break - except Exception as e: - log.debug("FlatCAMCNCJob.gcode_header() error: --> %s" % str(e)) + except KeyError: + # log.debug("FlatCAMCNCJob.gcode_header() error: --> %s" % str(e)) + pass try: if self.options['ppname_e'] == 'marlin' or self.options['ppname_e'] == 'Repetier': marlin = True - except Exception as e: - log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e)) + except KeyError: + # log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e)) + pass try: if "toolchange_probe" in self.options['ppname_e'].lower(): probe_pp = True - except Exception as e: - log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e)) + except KeyError: + # log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e)) + pass if marlin is True: gcode = ';Marlin(Repetier) G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s\n' % \ diff --git a/README.md b/README.md index 8c617a86..2b0eaca5 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,16 @@ CAD program, and create G-Code for Isolation routing. 25.09.2019 +- added a Copy All button in the Code Editor, clicking this button will copy all text in the editor to the clipboard +- added a 'Milling Type' radio button in Geometry Editor Preferences to contorl the type of geometry will be generated in the Geo Editor (for conventional milling or for the climb milling) +- added the functionality to allow climb/conventional milling selection for the geometry created in the Geometry Editor +- now any Geometry that is edited in Geometry editor will have coordinates ordered such that the resulting Gcode will allow the selected milling type in the 'Milling Type' radio button in Geometry Editor Preferences (which depends also of the spindle direction) +- some strings update +- French Google-translation at 100% +- German Google-translation update to 100% + +25.09.2019 + - French translation at 33% - fixed the 'Jump To' function to work in legacy graphic engine - in legacy graphic engine fixed the mouse cursor shape when grid snapping is ON, such that it fits with the shape from the OpenGL graphic engine diff --git a/flatcamEditors/FlatCAMGeoEditor.py b/flatcamEditors/FlatCAMGeoEditor.py index bc94be7b..276230aa 100644 --- a/flatcamEditors/FlatCAMGeoEditor.py +++ b/flatcamEditors/FlatCAMGeoEditor.py @@ -17,10 +17,11 @@ from camlib import * from FlatCAMTool import FlatCAMTool from flatcamGUI.ObjectUI import LengthEntry, RadioSet -from shapely.geometry import LineString, LinearRing, MultiLineString +from shapely.geometry import LineString, LinearRing, MultiLineString, Polygon, MultiPolygon # from shapely.geometry import mapping from shapely.ops import cascaded_union, unary_union import shapely.affinity as affinity +from shapely.geometry.polygon import orient from numpy import arctan2, Inf, array, sqrt, sign, dot from numpy.linalg import norm as numpy_norm @@ -3562,19 +3563,33 @@ class FlatCAMGeoEditor(QtCore.QObject): 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 = fcgeometry.flatten(geometry=fcgeometry.tools[self.multigeo_tool]['solid_geometry']) - 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']) - ) - ) + 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 = fcgeometry.flatten() + geo_to_edit = self.flatten(geometry=fcgeometry.solid_geometry, + orient_val=milling_type) for shape in geo_to_edit: if shape is not None: # TODO: Make flatten never create a None @@ -3672,7 +3687,6 @@ class FlatCAMGeoEditor(QtCore.QObject): 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 == 1: @@ -4055,8 +4069,37 @@ class FlatCAMGeoEditor(QtCore.QObject): def on_shape_complete(self): self.app.log.debug("on_shape_complete()") + geom = self.active_tool.geometry.geo + + if self.app.defaults['geometry_editor_milling_type'] == 'cl': + # reverse the geometry coordinates direction to allow creation of Gcode for climb milling + try: + pl = [] + for p in geom: + if p is not None: + if isinstance(p, Polygon): + pl.append(Polygon(p.exterior.coords[::-1], p.interiors)) + elif isinstance(p, LinearRing): + pl.append(Polygon(p.coords[::-1])) + # elif isinstance(p, LineString): + # pl.append(LineString(p.coords[::-1])) + geom = MultiPolygon(pl) + except TypeError: + if isinstance(geom, Polygon) and geom is not None: + geom = Polygon(geom.exterior.coords[::-1], geom.interiors) + elif isinstance(geom, LinearRing) and geom is not None: + geom = Polygon(geom.coords[::-1]) + elif isinstance(geom, LineString) and geom is not None: + geom = LineString(geom.coords[::-1]) + else: + log.debug("FlatCAMGeoEditor.on_shape_complete() Error --> Unexpected Geometry %s" % + type(geom)) + except Exception as e: + log.debug("FlatCAMGeoEditor.on_shape_complete() Error --> %s" % str(e)) + return 'fail' + # Add shape - self.add_shape(self.active_tool.geometry) + self.add_shape(DrawToolShape(geom)) # Remove any utility shapes self.delete_utility_geometry() @@ -4641,6 +4684,47 @@ class FlatCAMGeoEditor(QtCore.QObject): '[success] %s' % _("Paint done.")) self.replot() + def flatten(self, geometry, orient_val=1, reset=True, pathonly=False): + """ + Creates a list of non-iterable linear geometry objects. + Polygons are expanded into its exterior and interiors if specified. + + Results are placed in self.flat_geometry + + :param geometry: Shapely type or list or list of list of such. + :param orient_val: will orient the exterior coordinates CW if 1 and CCW for else (whatever else means ...) + https://shapely.readthedocs.io/en/stable/manual.html#polygons + :param reset: Clears the contents of self.flat_geometry. + :param pathonly: Expands polygons into linear elements. + """ + + if reset: + self.flat_geo = [] + + # ## If iterable, expand recursively. + try: + for geo in geometry: + if geo is not None: + self.flatten(geometry=geo, + orient_val=orient_val, + reset=False, + pathonly=pathonly) + + # ## Not iterable, do the actual indexing and add. + except TypeError: + if type(geometry) == Polygon: + geometry = orient(geometry, orient_val) + + if pathonly and type(geometry) == Polygon: + self.flat_geo.append(geometry.exterior) + self.flatten(geometry=geometry.interiors, + reset=False, + pathonly=True) + else: + self.flat_geo.append(geometry) + + return self.flat_geo + def distance(pt1, pt2): return sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2) diff --git a/flatcamGUI/FlatCAMGUI.py b/flatcamGUI/FlatCAMGUI.py index e9f63392..dd01e608 100644 --- a/flatcamGUI/FlatCAMGUI.py +++ b/flatcamGUI/FlatCAMGUI.py @@ -1844,6 +1844,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.buttonReplace.setToolTip(_("Will replace the string from the Find box with the one in the Replace box.")) self.buttonReplace.setMinimumWidth(100) + self.entryReplace = FCEntry() self.entryReplace.setToolTip(_("String to replace the one in the Find box throughout the text.")) @@ -1851,6 +1852,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow): 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.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.setToolTip(_("Will open a text file in the editor.")) @@ -1870,6 +1876,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): cnc_tab_lay_1.addWidget(self.buttonReplace) cnc_tab_lay_1.addWidget(self.entryReplace) cnc_tab_lay_1.addWidget(self.sel_all_cb) + cnc_tab_lay_1.addWidget(self.button_copy_all) self.cncjob_tab_layout.addLayout(cnc_tab_lay_1, 1, 0, 1, 5) cnc_tab_lay_3 = QtWidgets.QHBoxLayout() diff --git a/flatcamGUI/PlotCanvasLegacy.py b/flatcamGUI/PlotCanvasLegacy.py index dac0f8bd..3e3d8cd3 100644 --- a/flatcamGUI/PlotCanvasLegacy.py +++ b/flatcamGUI/PlotCanvasLegacy.py @@ -982,14 +982,18 @@ class ShapeCollectionLegacy: log.debug("ShapeCollectionLegacy.redraw() --> %s" % str(e)) else: if isinstance(local_shapes[element]['shape'], Polygon): - x, y = local_shapes[element]['shape'].exterior.xy - self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-') - for ints in local_shapes[element]['shape'].interiors: - x, y = ints.coords.xy + ext_shape = local_shapes[element]['shape'].exterior + if ext_shape is not None: + x, y = ext_shape.xy self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-') + for ints in local_shapes[element]['shape'].interiors: + if ints is not None: + x, y = ints.coords.xy + self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-') else: - x, y = local_shapes[element]['shape'].coords.xy - self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-') + if local_shapes[element]['shape'] is not None: + x, y = local_shapes[element]['shape'].coords.xy + self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-') self.app.plotcanvas.auto_adjust_axes() diff --git a/flatcamGUI/PreferencesUI.py b/flatcamGUI/PreferencesUI.py index a3e6ba9c..e8a00f2b 100644 --- a/flatcamGUI/PreferencesUI.py +++ b/flatcamGUI/PreferencesUI.py @@ -2954,6 +2954,18 @@ class GeometryEditorPrefGroupUI(OptionsGroupUI): grid0.addWidget(self.sel_limit_label, 0, 0) grid0.addWidget(self.sel_limit_entry, 0, 1) + # Milling Type + milling_type_label = QtWidgets.QLabel('%s:' % _('Milling Type')) + 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") + ) + self.milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'}, + {'label': _('Conv.'), 'value': 'cv'}]) + grid0.addWidget(milling_type_label, 1, 0) + grid0.addWidget(self.milling_type_radio, 1, 1) + self.layout.addStretch() diff --git a/flatcamTools/ToolMove.py b/flatcamTools/ToolMove.py index 2e19a447..ce3e3d0f 100644 --- a/flatcamTools/ToolMove.py +++ b/flatcamTools/ToolMove.py @@ -177,7 +177,7 @@ class ToolMove(FlatCAMTool): self.replot_signal.emit(obj_list) except Exception as e: proc.done() - self.app.inform.emit('[ERROR_NOTCL] %s --> %s' % (_('ToolMove.on_left_click()'), str(e))) + self.app.inform.emit('[ERROR_NOTCL] %s --> %s' % ('ToolMove.on_left_click()', str(e))) return "fail" proc.done() @@ -194,8 +194,8 @@ class ToolMove(FlatCAMTool): except TypeError as e: log.debug("ToolMove.on_left_click() --> %s" % str(e)) - self.app.inform.emit('[ERROR_NOTCL] %s' % - _('ToolMove.on_left_click() --> Error when mouse left click.')) + self.app.inform.emit('[ERROR_NOTCL] ToolMove.on_left_click() --> %s' % + _('Error when mouse left click.')) return self.clicked_move = 1 diff --git a/flatcamTools/ToolNonCopperClear.py b/flatcamTools/ToolNonCopperClear.py index 23817445..be3e4283 100644 --- a/flatcamTools/ToolNonCopperClear.py +++ b/flatcamTools/ToolNonCopperClear.py @@ -1065,7 +1065,7 @@ class NonCopperClear(FlatCAMTool, Gerber): # init values for the next usage self.reset_usage() - self.app.report_usage(_("on_paint_button_click")) + self.app.report_usage("on_paint_button_click") try: self.overlap = float(self.ncc_overlap_entry.get_value()) diff --git a/flatcamTools/ToolPaint.py b/flatcamTools/ToolPaint.py index 82e4d1ea..cc02d796 100644 --- a/flatcamTools/ToolPaint.py +++ b/flatcamTools/ToolPaint.py @@ -917,7 +917,7 @@ class ToolPaint(FlatCAMTool, Gerber): # init values for the next usage self.reset_usage() - self.app.report_usage(_("on_paint_button_click")) + self.app.report_usage("on_paint_button_click") # self.app.call_source = 'paint' # ##################################################### @@ -1490,7 +1490,7 @@ class ToolPaint(FlatCAMTool, Gerber): except Exception as e: proc.done() self.app.inform.emit('[ERROR_NOTCL] %s --> %s' % - (_('PaintTool.paint_poly()'), + ('PaintTool.paint_poly()', str(e))) return proc.done() diff --git a/flatcamTools/ToolSolderPaste.py b/flatcamTools/ToolSolderPaste.py index 902657ef..8c1999b0 100644 --- a/flatcamTools/ToolSolderPaste.py +++ b/flatcamTools/ToolSolderPaste.py @@ -1355,7 +1355,7 @@ class SolderPaste(FlatCAMTool): except Exception as e: log.debug('ToolSolderPaste.on_view_gcode() -->%s' % str(e)) self.app.inform.emit('[ERROR] %s --> %s' % - (_('ToolSolderPaste.on_view_gcode()'), str(e))) + ('ToolSolderPaste.on_view_gcode()', str(e))) return self.app.ui.code_editor.moveCursor(QtGui.QTextCursor.Start) diff --git a/locale/de/LC_MESSAGES/strings.mo b/locale/de/LC_MESSAGES/strings.mo index 22c14f0fe0c9e887333cf4a8cfb5841926d0ab68..4a0093d71c56e99e3bf8d4679358bc2fa2916d71 100644 GIT binary patch delta 76836 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