diff --git a/CHANGELOG.md b/CHANGELOG.md index f925ca41..2ff35813 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ CHANGELOG for FlatCAM Evo beta - in Geometry Editor added a new feature. While drawing a 'Path' now the user can project a direction by moving the mouse cursor in a certain direction and after that by typing a number or an arithmetic simple expression, a line segment will be drawn in that direction with the specified length from the last point - in Geometry Editor for the Path tool but only when using the 3D engine graphic mode, the mouse cursor is followed by position data - in Geometry Editor for the Path tool fixed an issue with path projection when changing the grid size while the Path tool is active +- in Geometry Editor, for Path tool, added UI that close on end of the Path tool action; it displays the projected length which now is kept for as long as it is wanted, allowing for path automation in case of repetitive lengths 13.04.2022 diff --git a/appEditors/AppGeoEditor.py b/appEditors/AppGeoEditor.py index 4a25ae82..39a6f12d 100644 --- a/appEditors/AppGeoEditor.py +++ b/appEditors/AppGeoEditor.py @@ -22,6 +22,7 @@ from appEditors.plugins.GeoBufferPlugin import BufferSelectionTool from appEditors.plugins.GeoPaintPlugin import PaintOptionsTool from appEditors.plugins.GeoTextPlugin import TextInputTool from appEditors.plugins.GeoTransformationPlugin import TransformEditorTool +from appEditors.plugins.GeoPathPlugin import PathEditorTool from vispy.geometry import Rect @@ -939,8 +940,8 @@ class FCPath(FCPolygon): def __init__(self, draw_app): FCPolygon.__init__(self, draw_app) self.draw_app = draw_app - self.interpolate_length = '' self.name = 'path' + self.app = self.draw_app.app try: QtGui.QGuiApplication.restoreOverrideCursor() @@ -952,6 +953,12 @@ class FCPath(FCPolygon): self.draw_app.app.plotcanvas.view.camera.zoom_callback = self.draw_cursor_data self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x)) + self.path_tool = PathEditorTool(self.app, self.draw_app) + self.path_tool.run() + self.app.ui.notebook.setTabText(2, _("Path")) + if self.draw_app.app.ui.splitter.sizes()[0] == 0: + self.draw_app.app.ui.splitter.setSizes([1, 1]) + def make(self): self.geometry = DrawToolShape(LineString(self.points)) self.name = 'path' @@ -963,7 +970,6 @@ class FCPath(FCPolygon): self.draw_app.in_action = False self.complete = True - self.interpolate_length = '' self.draw_app.app.jump_signal.disconnect() self.geometry.data['type'] = _('Path') @@ -1050,20 +1056,31 @@ class FCPath(FCPolygon): QtCore.Qt.Key.Key_Slash, QtCore.Qt.Key.Key_Asterisk]: try: # VisPy keys - self.interpolate_length += str(key.name) + if self.path_tool.length == 0: + self.path_tool.length = str(key.name) + else: + self.path_tool.length = str(self.path_tool.length) + str(key.name) except AttributeError: # Qt keys - self.interpolate_length += chr(key) + if self.path_tool.length == 0: + self.path_tool.length = chr(key) + else: + self.path_tool.length = str(self.path_tool.length) + chr(key) if key == 'Enter' or key == QtCore.Qt.Key.Key_Return or key == QtCore.Qt.Key.Key_Enter: - if self.interpolate_length != '': - target_length = self.interpolate_length.replace(',', '.') - try: - target_length = eval(target_length) - except SyntaxError as err: - ret = '%s: %s' % (str(err).capitalize(), self.interpolate_length) - self.interpolate_length = '' - return ret + if self.path_tool.length != 0: + # target_length = self.interpolate_length.replace(',', '.') + # try: + # target_length = eval(target_length) + # except SyntaxError as err: + # ret = '%s: %s' % (str(err).capitalize(), self.interpolate_length) + # self.interpolate_length = '' + # return ret + + target_length = self.path_tool.length + if target_length is None: + self.path_tool.length = 0.0 + return _("Failed.") first_pt = self.points[-1] last_pt = self.draw_app.app.mouse @@ -1077,15 +1094,16 @@ class FCPath(FCPolygon): self.clean_up() return '%s %s' % (_("Failed."), str(err)) - self.points.append((new_x, new_y)) - self.draw_app.app.on_jump_to(custom_location=(new_x, new_y), fit_center=False) - if len(self.points) > 0: - msg = '%s: %s. %s' % ( - _("Projected"), str(self.interpolate_length), - _("Click on next Point or click right mouse button to complete ...")) - self.draw_app.app.inform.emit(msg) - self.interpolate_length = '' - # return "Click on next point or hit ENTER to complete ..." + if self.points[-1] != (new_x, new_y): + self.points.append((new_x, new_y)) + self.draw_app.app.on_jump_to(custom_location=(new_x, new_y), fit_center=False) + if len(self.points) > 0: + msg = '%s: %s. %s' % ( + _("Projected"), str(self.path_tool.length), + _("Click on next Point or click right mouse button to complete ...")) + self.draw_app.app.inform.emit(msg) + # self.interpolate_length = '' + # return "Click on next point or hit ENTER to complete ..." def clean_up(self): self.draw_app.selected = [] @@ -1100,6 +1118,7 @@ class FCPath(FCPolygon): self.draw_app.app.jump_signal.disconnect() except (TypeError, AttributeError): pass + self.path_tool.on_tab_close() class FCSelect(DrawTool): @@ -1121,6 +1140,12 @@ class FCSelect(DrawTool): if self.draw_app.app.plotcanvas.text_cursor.parent: self.draw_app.app.plotcanvas.text_cursor.parent = None + # make sure that the Tools tab is removed + try: + self.draw_app.app.ui.notebook.removeTab(2) + except Exception: + pass + def click_release(self, point): """ @@ -1609,7 +1634,7 @@ class FCBuffer(FCShapeTool): self.origin = (0, 0) self.buff_tool = BufferSelectionTool(self.app, self.draw_app) self.buff_tool.run() - self.app.ui.notebook.setTabText(2, _("Buffer Tool")) + self.app.ui.notebook.setTabText(2, _("Buffer")) if self.draw_app.app.ui.splitter.sizes()[0] == 0: self.draw_app.app.ui.splitter.setSizes([1, 1]) self.activate() @@ -1620,19 +1645,19 @@ class FCBuffer(FCShapeTool): return try: - buffer_distance = float(self.buff_tool.buffer_distance_entry.get_value()) + buffer_distance = float(self.buff_tool.ui.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) + buffer_distance = float(self.buff_tool.ui.buffer_distance_entry.get_value().replace(',', '.')) + self.buff_tool.ui.buffer_distance_entry.set_value(buffer_distance) except ValueError: self.app.inform.emit('[WARNING_NOTCL] %s' % _("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 + join_style = self.buff_tool.ui.buffer_corner_cb.currentIndex() + 1 ret_val = self.draw_app.buffer(buffer_distance, join_style) self.deactivate() @@ -1646,19 +1671,19 @@ class FCBuffer(FCShapeTool): return try: - buffer_distance = float(self.buff_tool.buffer_distance_entry.get_value()) + buffer_distance = float(self.buff_tool.ui.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) + buffer_distance = float(self.buff_tool.ui.buffer_distance_entry.get_value().replace(',', '.')) + self.buff_tool.ui.buffer_distance_entry.set_value(buffer_distance) except ValueError: self.app.inform.emit('[WARNING_NOTCL] %s' % _("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 + join_style = self.buff_tool.ui.buffer_corner_cb.currentIndex() + 1 ret_val = self.draw_app.buffer_int(buffer_distance, join_style) self.deactivate() @@ -1672,19 +1697,19 @@ class FCBuffer(FCShapeTool): return try: - buffer_distance = float(self.buff_tool.buffer_distance_entry.get_value()) + buffer_distance = float(self.buff_tool.ui.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) + buffer_distance = float(self.buff_tool.ui.buffer_distance_entry.get_value().replace(',', '.')) + self.buff_tool.ui.buffer_distance_entry.set_value(buffer_distance) except ValueError: self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' % _("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 + join_style = self.buff_tool.ui.buffer_corner_cb.currentIndex() + 1 ret_val = 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]) diff --git a/appEditors/plugins/GeoBufferPlugin.py b/appEditors/plugins/GeoBufferPlugin.py index 8d8f24a7..de12d47c 100644 --- a/appEditors/plugins/GeoBufferPlugin.py +++ b/appEditors/plugins/GeoBufferPlugin.py @@ -61,7 +61,7 @@ class BufferSelectionTool(AppTool): # self.app.ui.notebook.callback_on_close = self.on_tab_close - self.app.ui.notebook.setTabText(2, _("Buffer Tool")) + self.app.ui.notebook.setTabText(2, _("Buffer")) def set_tool_ui(self): # Init appGUI @@ -375,7 +375,7 @@ class BufferEditorUI: # Buffer distance self.buffer_distance_entry = FCDoubleSpinner() self.buffer_distance_entry.set_precision(self.decimals) - self.buffer_distance_entry.set_range(0.0000, 9910000.0000) + self.buffer_distance_entry.set_range(0.0000, 10000.0000) grid_buffer.addWidget(FCLabel('%s:' % _("Buffer distance")), 0, 0) grid_buffer.addWidget(self.buffer_distance_entry, 0, 1) diff --git a/appEditors/plugins/GeoPathPlugin.py b/appEditors/plugins/GeoPathPlugin.py new file mode 100644 index 00000000..60206662 --- /dev/null +++ b/appEditors/plugins/GeoPathPlugin.py @@ -0,0 +1,152 @@ + +from appTool import * + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext + + +class PathEditorTool(AppTool): + """ + Simple input for buffer distance. + """ + + def __init__(self, app, draw_app): + AppTool.__init__(self, app) + + self.draw_app = draw_app + self.decimals = app.decimals + + self.ui = PathEditorUI(layout=self.layout, path_class=self) + + self.connect_signals_at_init() + self.set_tool_ui() + + def connect_signals_at_init(self): + # Signals + self.ui.clear_btn.clicked.connect(self.on_clear) + + def disconnect_signals(self): + # Signals + try: + self.ui.clear_btn.clicked.disconnect() + except (TypeError, AttributeError): + pass + + def run(self): + self.app.defaults.report_usage("Geo Editor ToolPath()") + AppTool.run(self) + + # if the splitter us hidden, display it + if self.app.ui.splitter.sizes()[0] == 0: + self.app.ui.splitter.setSizes([1, 1]) + + # if the Tool Tab is hidden display it, else hide it but only if the objectName is the same + found_idx = None + for idx in range(self.app.ui.notebook.count()): + if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab": + found_idx = idx + break + # show the Tab + if not found_idx: + try: + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + except RuntimeError: + self.app.ui.plugin_tab = QtWidgets.QWidget() + self.app.ui.plugin_tab.setObjectName("plugin_tab") + self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab) + self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2) + + self.app.ui.plugin_scroll_area = VerticalScrollArea() + self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area) + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + + # focus on Tool Tab + self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) + + # self.app.ui.notebook.callback_on_close = self.on_tab_close + + self.app.ui.notebook.setTabText(2, _("Path")) + + def set_tool_ui(self): + # Init appGUI + self.length = 0.0 + + def on_tab_close(self): + self.disconnect_signals() + self.hide_tool() + # self.app.ui.notebook.callback_on_close = lambda: None + + def on_clear(self): + self.set_tool_ui() + + @property + def length(self): + return self.ui.project_line_entry.get_value() + + @length.setter + def length(self, val): + self.ui.project_line_entry.set_value(val) + + def hide_tool(self): + self.ui.path_tool_frame.hide() + self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab) + if self.draw_app.active_tool.name != 'select': + self.draw_app.select_tool("select") + + +class PathEditorUI: + pluginName = _("Path") + + def __init__(self, layout, path_class): + self.path_class = path_class + self.decimals = self.path_class.app.decimals + self.layout = layout + + # Title + title_label = FCLabel("%s" % ('Editor ' + self.pluginName)) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) + self.layout.addWidget(title_label) + + # this way I can hide/show the frame + self.path_tool_frame = QtWidgets.QFrame() + self.path_tool_frame.setContentsMargins(0, 0, 0, 0) + self.layout.addWidget(self.path_tool_frame) + self.path_tool_box = QtWidgets.QVBoxLayout() + self.path_tool_box.setContentsMargins(0, 0, 0, 0) + self.path_tool_frame.setLayout(self.path_tool_box) + + # Grid Layout + grid_path = GLay(v_spacing=5, h_spacing=3) + self.path_tool_box.addLayout(grid_path) + + # Project distance + self.project_line_lbl = FCLabel('%s:' % _("Length")) + self.project_line_entry = NumericalEvalEntry(border_color='#0069A9') + grid_path.addWidget(self.project_line_lbl, 0, 0) + grid_path.addWidget(self.project_line_entry, 0, 1) + + # self.buffer_corner_lbl = FCLabel('%s:' % _("Buffer corner")) + # self.buffer_corner_lbl.setToolTip( + # _("There are 3 types of corners:\n" + # " - 'Round': the corner is rounded for exterior buffer.\n" + # " - 'Square': the corner is met in a sharp angle for exterior buffer.\n" + # " - 'Beveled': the corner is a line that directly connects the features meeting in the corner") + # ) + # self.buffer_corner_cb = FCComboBox() + # self.buffer_corner_cb.addItem(_("Round")) + # self.buffer_corner_cb.addItem(_("Square")) + # self.buffer_corner_cb.addItem(_("Beveled")) + # grid_path.addWidget(self.buffer_corner_lbl, 2, 0) + # grid_path.addWidget(self.buffer_corner_cb, 2, 1) + + self.clear_btn = FCButton(_("Clear")) + grid_path.addWidget(self.clear_btn, 4, 0, 1, 2) + + self.layout.addStretch(1) diff --git a/appGUI/MainGUI.py b/appGUI/MainGUI.py index d5cc1b55..44f80bc3 100644 --- a/appGUI/MainGUI.py +++ b/appGUI/MainGUI.py @@ -3482,20 +3482,21 @@ class MainGUI(QtWidgets.QMainWindow): # automatically make the selection tool active after completing current action self.app.geo_editor.select_tool('select') return - elif self.app.geo_editor.active_tool.name == 'path': - if self.app.geo_editor.active_tool.interpolate_length != '': - pass else: - self.app.geo_editor.active_tool.click( - self.app.geo_editor.snap(self.app.geo_editor.x, self.app.geo_editor.y)) + if self.app.geo_editor.active_tool.name == 'path' and \ + self.app.geo_editor.active_tool.path_tool.length != 0.0: + pass + else: + self.app.geo_editor.active_tool.click( + self.app.geo_editor.snap(self.app.geo_editor.x, self.app.geo_editor.y)) - self.app.geo_editor.active_tool.make() + self.app.geo_editor.active_tool.make() - if self.app.geo_editor.active_tool.complete: - self.app.geo_editor.on_shape_complete() - self.app.inform.emit('[success] %s' % _("Done.")) - # automatically make the selection tool active after completing current action - self.app.geo_editor.select_tool('select') + if self.app.geo_editor.active_tool.complete: + self.app.geo_editor.on_shape_complete() + self.app.inform.emit('[success] %s' % _("Done.")) + # automatically make the selection tool active after completing current action + self.app.geo_editor.select_tool('select') # Abort the current action if key == QtCore.Qt.Key.Key_Escape or key == 'Escape':